net-services v0.13.1
Net⬄Services
A type-safe asynchronous RPC Service facility for connecting your apps to the network.
Introduction
Net-Services provides a simple and intuitive toolkit that makes connecting your app to the network easy. You can use Net-Services to transform your application into a network connected Service App. You can connect to your Service App, from the same process or another process, and call methods on it using a type-safe Service API.
Features
- A type-safe API facility: code completion, parameter types, and return types.
- Return values and Errors are marshalled back to the caller.
- Infinite property nesting; you can use a Service API to call nested properties on a Service App at any depth.
- Bi-directional asynchronous RPC over TCP.
- Security can be implemented using the native Node TLS module (i.e., TLS and Client Certificate Authentication).
- A configurable communication protocol. You can marshal your messages however you choose (e.g., JSON, binary, etc.), or use the default minimalist JSON communication protocol.
- Extend Net-Services using the native
stream.Duplex
interface.
Table of Contents
- Concepts
- Usage
- API
- Type Safety
- Examples
- Communication Protocol
- Extend Net-Services
- Security
- Best Practices
- Create a TypeScript interface for your Service API.
- Set a timeout in ServiceAPIOptions.
- Impose property path restrictions.
- Ensure your
stream.Duplex
(e.g., anet.Socket
) is ready for use. - If you create a stream (e.g., a
net.Socket
), set anerror
handler on it. - Close and dereference streams in order to prevent memory leaks.
Concepts
Net-Services features an intuitive API that can be most easily understood by looking at an example or common usage. There are three important concepts that comprise the API, a Service, a Service App, and a Service API.
Service
A Service instance coordinates bi-directional communication over a stream.Duplex (e.g., a net.Socket). Once a Service is instantiated it can be used in order to create a Service App or a Service API. You can create a Service using the createService
helper function.
Service App
A Service App is a user defined object instance (i.e., an instance of your application) that is connected to the network. You can use the Service.createServiceApp<T>
helper function, with your app as its argument, in order to create a Service App. Once a Service App is connected to the network, its methods can be called using a Service API instantiated in the same or different process.
Service API
A Service API is a type safe representation of your remote Service. You can create a Service API using the Service.createServiceAPI<T>
helper function. Service.createServiceAPI<T>
will return a Proxy that is type coerced in order to make the methods that comprise <T>
suitable for making asynchronous networked function calls. You can call methods on the Service API object much how you would call methods on an instance of <T>
itself.
Usage
Using Net-Services involves creating a Service App and calling its methods over a stream (e.g., net.Socket
) using a Service API. In this example you will create a Greeter
Service and call its Greeter.greet
method over a net.Socket
using an asynchronous Service API of type Greeter
.
How to create a Hello World Greeter Service.
Import the node:net
module and the createService
helper function.
import * as net from "node:net";
import { createService } from 'net-services';
Create a Greeter Application.
class Greeter {
greet(kind: string) {
return `Hello, ${kind} world!`;
}
}
const greeter = new Greeter();
Create a Server and create a Greeter Service App that is connected to the network.
const server = net.createServer().listen({ port: 3000, host: '127.0.0.1' });
server.on('connection', (socket: net.Socket) => {
const service = createService(socket);
service.createServiceApp(greeter); // Greeter.greet can now be called over the Socket using a Service API.
});
Connect to the Server and create a ServiceAPI of type Greeter
.
Use the greeter
Service API in order to call the remote Service App's methods and log the greeting.
const socket = net.connect({ port: 3000, host: '127.0.0.1' });
socket.on('ready', async () => {
const service = createService(socket);
const greeter = service.createServiceAPI<Greeter>(); // Create a Service API of type Greeter using the Socket named `socket`.
const greeting = await greeter.greet('happy');
// ^
// The `greeter` object facilitates code completion, parameter types and return types.
console.log(greeting); // Hello, happy world!
});
Please see the Hello World example for a working implementation.
In the Hello World example communication is uni-directional (i.e., a request-response architecture). Please see the bi-directional type safe APIs example for an example of bi-directional communication.
API
net-services.createService(stream, options)
stream
<stream.Duplex>
Astream.Duplex
(e.g., anet.Socket
) connecting twonet.Servers
.options
<IMuxOptions>
ingressBufferLimit
<number>
An optional ingress buffer size limit in bytes. This argument specifies the limit on buffered data that may accumulate from calls from the remote Service API and return values from the remote Service App. If the size of the ingress buffer exceeds this value, the stream will emit aBufferLimitError
and close. Default:undefined
(i.e., no limit).egressBufferLimit
<number>
An optional egress buffer size limit in bytes. This argument specifies the limit on buffered data that may accumulate from calls to the remote Service App and return values to the remote Service API. If the size of the egress buffer exceeds this value, aBufferLimitError
will be thrown and the stream will close. Default:undefined
(i.e., no limit).serializeMessage
<(message: ResultMessage | CallMessage) => Buffer>
An optional handler for serializing messages. If undefined, Net-Services will use the minimalist default JSON communication protocol. Default:undefined
deserializeMessage
<(data: Buffer) => ResultMessage | CallMessage>
An optional handler for deserializing messages. If undefined, Net-Services will use the minimalist default JSON communication protocol. Default:undefined
- Returns:
<Service>
Service.createServiceApp\<T>(app, options)
app
<object>
An instance of your application.options
<ServiceAppOptions<T>>
paths
<Array<PropPath<Async<T>>>>
AnArray
of property paths (i.e., dot-pathstring
s). If defined, only property paths in this list may be called on the Service App. Each element of the Array is aPropPath
and aPropPath
is simply a dot-pathstring
representation of a property path. Please see the Nested Method example for a working example. Default:undefined
.
Returns:
<ServiceApp<T>>
Service.createServiceAPI\<T>(options)
options
<ServiceAPIOptions>
timeout
<number>
Optional argument in milliseconds that specifies thetimeout
for function calls. Default:undefined
(i.e., no timeout).
Returns:
<Async<T>>
AProxy
of type<T>
that consists of asynchronous analogues of methods in<T>
.
The
Service.createServiceAPI<T>
helper function returns a JavaScriptProxy
object coerced to type<Async<T>>
.Service.createServiceAPI<T>
filters and transforms the function types that comprise<T>
into their asynchronous analogues i.e., if a function type isn't already defined as returning aPromise
, it will be transformed to return aPromise
- otherwise its return type will be left unchanged. This transformation is necessary because all function calls over astream.Duplex
(e.g., anet.Socket
) are asynchronous. Please see the Bi-directional Type Safe APIs example for how to easily consume a<Async<T>>
in your application.
The following Errors may arise when a Service API method is called:
- Errors:
- If the remote Service method throws an
Error
, theError
will be marshalled back from the Service and thePromise
will reject with theError
as its reason. - If a call exceeds the
egressBufferLimit
, thePromise
will reject withBufferLimitError
as its reason and the stream will close. - If an
error
event occurs on thestream.Duplex
, thePromise
will reject with the given reason. - If the
stream.Duplex
closes, thePromise
will reject withStreamClosedError
as its reason. - If the
paths
Array is defined in the remoteServiceAppOptions<T>
and a method is called that is not a registered property path, thePromise
will reject withPropertyPathError
as its reason. - If a property is invoked that is not a function on the remote Service, the
Promise
will reject withTypeError
as its reason. - If the call fails to resolve or reject prior to the
timeout
specified inServiceAPIOptions
, thePromise
will reject withCallTimeoutError
as its reason.
- If the remote Service method throws an
NB The Service API and type safety is not enforced at runtime. Please see the
paths
property of theServiceAppOptions<T>
object for runtime checks.
Type Safety
Net-Services provides a facility for building a type safe network API. The type safe API facility is realized through use of JavaScript's Proxy object and TypeScript's type variables. A Proxy interface is created by passing your app's public interface to the type parameter of the Service.createServiceAPI<T>
helper function. The type safe Proxy interface facilitates code completion, parameter types, and return types; it helps safeguard the integrity of your API.
Please see the Bi-directional Type Safe APIs example for a working implementation.
Examples
An instance of Hello, World! (example)
Please see the Usage section above or the Hello, World! example for a working implementation.
Use Net-Services to create bi-directional type safe APIs. (example)
Please see the Bi-directional Type Safe APIs example for a working implementation.
Use Net-Services with TLS Encryption and Client Certificate Authentication. (example)
Please see the TLS Encryption and Client Authentication example for a working implementation.
Use Net-Services to create an API with a nested method. (example)
Please see the Nested Method example for a working implementation.
Communication Protocol
Net-Services provides a default minimalist JSON message protocol. However, you can marshal your messages however you choose by implementing theserializeMessage
and deserializeMessage
handlers and passing them in the ServiceOptions
when you create your Service.
Default JSON Communication Protocol
Net-Services provides a concise default JSON Communication Protocol. The communication protocol is guided by parsimony; it includes just what is needed to make a function call and return a result or throw an Error
.
Marshalling
Arguments, return values, and Errors are serialized using JavaScript's JSON.stringify
. The choice of using JSON.stringify
has important and certain ramifications that should be understood. Please see the rules for serialization.
Type Definitions
The type definitions for the default JSON communication protocol:
The Call Message
A CallMessageList
consists of a numeric message type, the call identifier, the property path to the called function, and its arguments.
type CallMessageList = [
0, // The message type; 0 = Call.
number, // The call identifier.
Array<string>, // The elements of the property path to the called method.
...Array<unknown> // The arguments to be passed to the function.
];
The Result Message
A ResultMessageList
consists of a numeric message type, the call identifier, and a return value or Error
.
type ResultMessageList = [
1 | 2, // The message type; 1 = Error, 2 = Result.
number, // The call identifier.
unknown // The return value or Error.
];
Extend Net-Services
Net-Services is modeled around communication over net.Sockets
; however, it can be used in order to communicate over any resource that implements the stream.Duplex
interface. The net-services.createService
helper function takes a stream.Duplex
as its first argument. This means that if you can model your bi-directional resource as a stream.Duplex
, it should work with Net-Services. Just implement a stream.Duplex and pass it into the net-services.createService
helper function.
Security
Security is a complex and multifaceted endeavor.
Use TLS encryption.
TLS Encryption may be implemented using native Node.js TLS Encryption. Please see the TLS Encryption and Client Authentication example for a working implementation.
Use TLS client certificate authentication.
TLS Client Certificate Authentication may be implemented using native Node.js TLS Client Authentication. Please see the TLS Encryption and Client Authentication example for a working implementation.
Restrict API calls at runtime.
The Service API and type safety are not enforced at runtime. You can restrict API calls to specified Service App methods by providing an Array of property paths to the paths
property of the ServiceAppOptions<T>
object. If the paths
Array is defined in ServiceAppOptions<T>
and a method is called that is not a registered property path, the awaited Promise
will reject with PropertyPathError
as its reason.
Specify an ingressBufferLimit
and egressBufferLimit
in the Service options.
Net-Services respects backpressure; however, it is advisable to specify how much data may be buffered in order to ensure your application can respond to adverse network phenomena. If the stream peer reads data at a rate that is slower than the rate that data is written to the stream, data may buffer until memory is exhausted. This is a vulnerability that is inherent in streams, which can be mitigated by preventing internal buffers from growing too large.
You can specify a hard limit on ingress and egress buffers in the Service options
. ingressBufferLimit
specifies a limit on incoming data i.e., data returned from the remote Service or calls from the remote Service API. egressBufferLimit
specifies a limit on outgoing data i.e., data returned to the remote Service API or calls to the remote Service. If an ingress or egress buffer exceeds the specified limit, the respective stream will error and close. Net-Services will immediately tear down its internal buffers in order to free memory - dereference the stream, and GC will sweep.
Best Practices
Create a TypeScript interface for your Service API.
You can pass your application's class as a type variable argument to the Service.createServiceAPI<T>
helper function; however, it's advisable to define a public interface instead. You can publish your public interface to be consumed separately or export it from your application.
For example, for the Greeter
class in the "Hello, World!" example, the interface:
interface IGreeter {
greet(kind: string): string
}
Set a timeout in ServiceAPIOptions
.
Calling a method on a remote Service App using a Service API may take too long to resolve or reject - or may never resolve at all. This effect can be caused by a long running operation in the remote Service App or a congested network. If the call fails to resolve or reject prior to the timeout
specified in ServiceAPIOptions
, the Promise
will reject with CallTimeoutError
as its reason.
Impose property path restrictions.
Unless you control the definition of both the Service API and the Service, you should specify which methods may be called on your Service using the paths
property of ServiceAppOptions<T>
.
Ensure your stream.Duplex
(e.g., a net.Socket
) is ready for use.
Net-Services assumes that the stream.Duplex
passed to net-services.createService
is ready to use; this assumption and separation of concern is an intentional design decision. A stream.Duplex
implementation (e.g., a net.Socket
) may include an event (e.g., something like 'ready'
or 'connect'
) that will indicate it is ready for use. Please await this event, if available, prior to passing the stream.Duplex
to the createService
helper function.
If you create a stream (e.g., a net.Socket
), set an error
handler on it.
A stream.Duplex
may error before becoming ready; hence, as usual, you should synchronously set an error handler on a new stream instance.
Close and dereference streams in order to prevent memory leaks.
The object graph of a Net-Services instance is rooted on its stream. It will begin decomposition immediately upon stream closure. However, in order to fully dispose of a Net-Services instance, simply destroy and dereference its stream; GC will sweep buffers and other liminal resources.
4 months ago
4 months ago
4 months ago
4 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago