Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@iprokit/service

iprokit70Apache-2.01.0.0TypeScript support: included

Powering distributed systems with simplicity and speed.

http, rest-api, scp, rpc, remote-procedure-call, inter-service-communication, distributed-messaging, broadcast, remote-calls, remote-functions, sdp, service-discovery, service-registry, microservices, service-framework, distributed-systems

readme

Service Logo Powering distributed systems with simplicity and speed.

npm version npm Node.js TypeScript License: Apache-2.0 Maintenance Made with love

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:

  1. Fork the repository.
  2. Create a new branch for your feature or fix.
  3. 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.

changelog

Changelog

[1.0.0]

Initial

  • First release of Service.