Powering distributed systems with simplicity and speed.
Service
Service
is a powerful, lightweight framework designed to simplify the development of efficient, reliable, and scalable applications. Whether you're developing a monolithic system or a suite of interconnected microservices, Service
simplifies the process with minimal configuration.
Features
- Hypertext Transfer Protocol (HTTP): Define and manage routes for handling web requests with support for dynamic and wildcard paths.
- Service Communication Protocol (SCP): Create robust inter-service communication with remote functions, broadcasts, and workflows.
- Service Discovery Protocol (SDP): Dynamically discover and link services for seamless interaction in distributed systems.
Installation
npm install @iprokit/service --save
Quick Start
Here’s how to get a basic service running:
import Service from '@iprokit/service';
// Create a service instance.
const service = new Service('MyService');
// Listen for service events.
service.on('start', () => console.log('Service has started successfully.'));
service.on('stop', () => console.log('Service has stopped successfully.'));
// Start the service.
await service.start(3000, 6000, 5000, '224.0.0.2');
console.log('Service is running: HTTP Port: 3000, SCP Port: 6000, SDP Port: 5000, Multicast Address: 224.0.0.2.');
HTTP (Hypertext Transfer Protocol)
The HTTP module provides the tools to define, expose, and manage routes for handling client requests efficiently.
Routes
Create HTTP routes to handle client requests with ease. The framework supports all standard HTTP methods and offers flexible options for advanced routing.
Get
Handle GET
requests to retrieve resources:
service.get('/products', (request, response) => {
response.end('Retrieve all products');
});
Post
Handle POST
requests to create resources:
service.post('/products', (request, response) => {
response.end('Create a new product');
});
Put
Handle PUT
requests to update resources entirely:
service.put('/products/:id', (request, response) => {
response.end(`Update product with ID: ${request.params.id}`);
});
Patch
Handle PATCH
requests to update resources partially:
service.patch('/products/:id', (request, response) => {
response.end(`Partially update product with ID: ${request.params.id}`);
});
Delete
Handle DELETE
requests to delete resources:
service.delete('/products/:id', (request, response) => {
response.end(`Delete product with ID: ${request.params.id}`);
});
All
Handle any HTTP method for a specific route:
service.all('/products', (request, response) => {
response.end('Handle all methods for /products');
});
Dynamic Parameters
Capture and use dynamic URL parameters to create flexible routes:
service.get('/products/:id', (request, response) => {
const { id } = request.params;
response.end(`Retrieve product with ID: ${id}`);
});
Query Parameters
Extract and use query parameters from the request URL:
service.get('/products/search', (request, response) => {
const { category, price } = request.query;
response.end(`Search products in category: ${category} with price: ${price}`);
});
Multiple Handlers
Chain multiple handlers for modular request processing. Use next()
to pass control:
const validateRequest = (request, response, next) => {
console.log('Validating request');
next();
};
const processRequest = (request, response) => {
response.end('Request processed successfully');
};
service.get('/products/process', validateRequest, processRequest);
Router
Organize and manage related routes with the Router
class.
Create and Mount Routers
Create a router and mount it to the service:
import { Router } from '@iprokit/service';
const router = new Router();
router.get('/users', (request, response) => {
response.end('Get all users');
});
service.mount('/', router);
Mount Routes on the Same Path
Mount multiple routers on the same path to handle different functionalities:
const userRouter = new Router();
const productRouter = new Router();
userRouter.get('/users', (request, response) => {
response.end('User route');
});
productRouter.get('/products', (request, response) => {
response.end('Product route');
});
service.mount('/', userRouter, productRouter);
Mount Routes on a Parent Router
Nest routes under a parent router for hierarchical organization:
const apiRouter = new Router();
const userRouter = new Router();
userRouter.get('/profile', (request, response) => {
response.end('User profile');
});
apiRouter.mount('/users', userRouter);
service.mount('/api', apiRouter);
Wildcard Paths
Use wildcard routes to match dynamic patterns.
Match Routes
Catch-all handler for unmatched routes:
service.get('*', (request, response) => {
response.end(`No matching route for: ${request.url}`);
});
Match Nested Routes
Match nested paths under a specific route:
service.get('/categories/*', (request, response) => {
response.end(`Nested path: ${request.url}`);
});
Match Prefix-Based Routes
Match routes starting with a specific prefix:
service.get('/prod*', (request, response) => {
response.end(`Matched prefix route: ${request.url}`);
});
SDP (Service Discovery Protocol)
The SDP module allows services to automatically discover and connect to each other within a network. This simplifies the process of linking services and ensures seamless communication between them.
Linking Services
Discoverable Target Service
A service can be configured to become discoverable on the network, allowing other services to link to it.
import Service from '@iprokit/service';
// Create a discoverable service instance.
const serviceA = new Service('ServiceA');
// Start the service to make it discoverable.
await serviceA.start(3000, 6000, 5000, '224.0.0.2');
console.log('ServiceA is discoverable on SDP (Port: 5000, Address: 224.0.0.2).');
Linking to a Target Service
A service can link to a discoverable target service using the RemoteService
class. This establishes a connection during startup, enabling communication between the two services.
import Service, { RemoteService } from '@iprokit/service';
// Create a service instance.
const serviceB = new Service('ServiceB');
// Link to ServiceA using the RemoteService class.
const remoteToA = new RemoteService('RemoteToA');
serviceB.link('ServiceA', remoteToA);
// Start the service.
await serviceB.start(4000, 7000, 5000, '224.0.0.2');
console.log('ServiceB is running and linked to ServiceA.');
SCP (Service Communication Protocol)
The SCP module enables seamless interaction between services by defining remote functions, broadcasting messages, and coordinating complex workflows. SCP supports various communication modes, making it easy to tailor inter-service interactions to your system’s requirements.
Broadcast
Broadcast messages to notify multiple services simultaneously. This is useful for system-wide updates, alerts, or notifications.
Send a broadcast to all subscribed services:
await serviceA.broadcast('Catalog.updated', { id: 'P12345', name: 'Wireless Headphones' });
Subscribe to broadcast events and handle incoming messages:
remoteToA.on('Catalog.updated', (product) => {
console.log(`Product Details: ID - ${product.id}, Name - ${product.name}`);
});
Execution (Remote Functions)
SCP allows services to expose and call remote functions for direct, targeted communication.
Message/Reply
The message-reply model enables direct communication between two services. This pattern is particularly suited for synchronous operations where the caller needs an immediate reply from the callee.
Expose a reply function to fetch product details:
serviceA.reply('getProduct', (productId) => {
return { id: productId, name: 'Wireless Headphones', price: 99.99, stock: 25 };
});
Call a remote function to retrieve product details:
const product = await remoteToA.message('getProduct', 'P12345');
console.log(`Product Details:`, product);
Conduct/Conductor
The conduct-conductor model facilitates multi-step workflows across services. It enables coordinated operations with support for signaling (e.g., COMMIT, ROLLBACK) to ensure consistency in distributed workflows.
Expose a conductor function to handle multi-step workflows:
serviceA.conductor('createOrder', (conductor, orderDetails) => {
console.log(`Processing order:`, orderDetails);
conductor.on('signal', (event, tags) => {
console.log(`${event} signal received.`);
conductor.signal(event, tags);
});
conductor.on('end', () => conductor.end());
});
Coordinate conductors across multiple services:
import { Coordinator } from '@iprokit/service';
const coordinator = new Coordinator();
try {
console.log('Starting workflow.');
// Conduct operations across services.
await remoteToA.conduct('createOrder', coordinator, { orderId: 'O123' });
console.log('Order validated.');
await remoteToB.conduct('processPayment', coordinator, { paymentId: 'P456' });
console.log('Payment processed.');
console.log('Sending COMMIT signal.');
await coordinator.signal('COMMIT');
} catch (error) {
console.error('Error occurred. Sending ROLLBACK signal.', error);
await coordinator.signal('ROLLBACK');
} finally {
console.log('Ending coordinator.');
await coordinator.end();
}
Omni
The Omni mode acts as a catch-all handler for operations that don’t match a specific function. It is particularly useful for handling undefined or broad operation patterns in a flexible and generic way.
serviceA.omni('order', (incoming, outgoing) => {
outgoing.end(`Operation '${incoming.operation}' completed.`);
});
Executor
The Executor class organizes and manages remote functions within a service. Executors are ideal for creating modular, reusable logic for handling various operations.
Create and Attach Executors
Create an executor and attach it to a service to handle specific operations:
const executor = new Executor();
executor.reply('get', (userId) => {
return { id: userId, name: 'John Doe', email: 'johndoe@example.com' };
});
serviceA.attach('User', executor);
Attach Executions on the Same Operation
The Omni mode allows multiple handlers to process the same operation in steps. Use proceed()
to pass control from one handler to the next.
Processing an Order in Steps:
executor.omni('processOrder', (incoming, outgoing, proceed) => {
console.log('Step 1: Validating order');
proceed(); // Pass control to the next handler
});
executor.omni('processOrder', (incoming, outgoing, proceed) => {
console.log('Step 2: Checking inventory');
proceed(); // Pass control to the next handler
});
executor.omni('processOrder', (incoming, outgoing) => {
console.log('Step 3: Completing the order');
outgoing.end('Order processed successfully.');
});
Note: The Omni mode is the only SCP mode that supports proceed()
and allows multiple handlers for the same operation. Each handler in Omni mode shares the same incoming
and outgoing
objects, enabling step-by-step processing within the same execution context. Other modes (e.g., Reply, Conductor) do not support proceed()
or multi-handler execution.
Wildcard Operations
Wildcard operations provide dynamic handling for undefined or broad operation patterns. This is useful for logging, debugging, or fallback logic.
Match All Unmatched Operations
Catch all unmatched operations using a wildcard handler:
executor.omni('*', (incoming, outgoing) => {
outgoing.end(`No handler defined for operation: ${incoming.operation}`);
});
Match Operations with Specific Prefixes
Match operations dynamically based on a prefix to group related tasks:
executor.omni('inventory*', (incoming, outgoing) => {
outgoing.end(`Handled operation: ${incoming.operation}`);
});
Contributing
We welcome contributions to @iprokit/service
! Whether it's reporting bugs, suggesting enhancements, or submitting pull requests — your help is appreciated.
To contribute:
- Fork the repository.
- Create a new branch for your feature or fix.
- Submit a pull request with a clear description of your changes.
Please ensure your code is clean, well-documented, and covered by tests where appropriate.
For major changes or questions, feel free to open an issue to discuss your ideas first.
License
This project is licensed under the Apache License 2.0 – see the LICENSE and NOTICE files for details.