node-hediet-json-rpc-0.5.1/0000755000000000000000000000000015072021641012317 5ustar node-hediet-json-rpc-0.5.1/README.md0000644000000000000000000001131115072021641013573 0ustar # Typed RPC Library based on JSON RPC [![](https://img.shields.io/twitter/follow/hediet_dev.svg?style=social)](https://twitter.com/intent/follow?screen_name=hediet_dev) A strongly typed remote procedure call library for typescript. Is compatible with JSON RPC 2.0. However, only object arguments are supported at the moment. Warning: This readme is slightly out of date. # Install To install the base library: ``` yarn add @hediet/json-rpc ``` For websocket clients/servers: ``` yarn add @hediet/json-rpc-websocket ``` For webworker communication: ``` yarn add @hediet/json-rpc-browser ``` For process stdio communication: ``` yarn add @hediet/json-rpc-node ``` # Features - Runtime checked types through `zod` - Supports Request/Response and Notification semantics - Supports Error and Success Responses - Provides a lot of abstractions (see Architecture) - Provides RPC over various mediums - Websockets - NodeJS Streams (Stdio) - Webworker # Usage ## Defining Contracts It is recommended to use the contracts API to specify RPC interfaces. Servers and clients are handled symmetrically. ```ts import { contract, requestType, notificationType, } from "@hediet/json-rpc"; const myRpcContract = contract({ name: "MyRpcContract", // the interface for clients to interact with servers server: { calculate: requestType({ // io-ts is used to specify types. // This might change in future major versions. params: s.sObject({ data: s.sString(), }), result: s.sString(), }), }, // the interface for servers to interact with clients client: { progress: notificationType({ params: s.sObject({ progress: s.sNumber(), }), }), }, }); ``` ## RPC over Websockets Server: ```ts import { Contract, ConsoleRpcLogger } from "@hediet/json-rpc"; import { startWebSocketServer } from "@hediet/json-rpc-websocket-server"; const clients = new Set(); startWebSocketServer({ port: 12345 }, async (stream) => { // `stream` provides methods to send and receive messages const { client } = Contract.registerServerToStream( myRpcContract, stream, // You can specify a logger to make debugging easier. new ConsoleRpcLogger(), { calculate: async ({ data }) => { // call the client await client.progress({ progress: 0.5 }); return data; }, } ); clients.add(client); await stream.onClosed; clients.delete(client); }); ``` Client: ```ts import { Contract, ConsoleRpcLogger } from "@hediet/json-rpc"; import { WebSocketStream } from "@hediet/json-rpc-websocket"; const { server } = Contract.getServerFromStream( myRpcContract, await WebSocketStream.connectTo({ address: "ws://localhost:12345" }), new ConsoleRpcLogger(), { progress: ({ progress }) => console.log("progress", progress), } ); const result = await server.calculate({ data: "some data" }); ``` ## RPC over Standard Input/Output Main process: ```ts import { Contract } from "@hediet/json-rpc"; import { NodeJsMessageStream } from "@hediet/json-rpc-streams"; const proc = spawn("node", [join(__dirname, "echo-process")]); const { server } = Contract.getServerFromStream( myRpcContract, NodeJsMessageStream.connectToProcess(proc), logger, { progress: ({ progress }) => console.log("progress", progress), } ); const result = await server.calculate({ data: "some data" }); ``` Child process: ```ts import { Contract } from "@hediet/json-rpc"; import { NodeJsMessageStream } from "@hediet/json-rpc-streams"; const { server } = Contract.getServerFromStream( myRpcContract, NodeJsMessageStream.connectToThisProcess(), logger, { calculate: async ({ data }) => { await client.progress({ progress: 0.5 }); return data; }, } ); ``` ## RPC over Web-Workers Main window: ```ts import { Contract } from "@hediet/json-rpc"; import { connectToWorker } from "@hediet/json-rpc-browser"; const worker = new Worker("./browser-worker.ts"); const { server } = Contract.getServerFromStream( myRpcContract, connectToWorker(worker), { logger }, { progress: ({ progress }) => console.log("progress", progress), } ); const result = await server.calculate({ data: "some data" }); ``` Web-Worker: ```ts import { Contract } from "@hediet/json-rpc"; import { connectWorkerToParent } from "@hediet/json-rpc-browser"; const { server } = Contract.getServerFromStream( myRpcContract, connectWorkerToParent(), { logger }, { calculate: async ({ data }) => { await client.progress({ progress: 0.5 }); return data; }, } ); ``` # Architecture of `@hediet/json-rpc` ![classes](docs/exported/main/Main.svg) Extend `BaseMessageStream` if you want to implement custom message transport. Implement `Channel` or `ChannelFactory` if you already provide a request/response channel. node-hediet-json-rpc-0.5.1/json-rpc/0000755000000000000000000000000015072021641014052 5ustar node-hediet-json-rpc-0.5.1/json-rpc/package.json0000644000000000000000000000116115072021641016337 0ustar { "name": "@hediet/json-rpc", "description": "A strongly typed JSON RPC implementation.", "version": "0.5.0", "author": { "name": "Henning Dieterichs", "email": "henning.dieterichs@live.de" }, "license": "MIT", "main": "./dist/index", "types": "./dist/index.d.ts", "files": [ "dist", "src" ], "scripts": { "dev": "tsc --watch", "build": "tsc" }, "dependencies": {}, "optionalDependencies": {}, "devDependencies": { "typescript": "^5.8.3" }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" } }node-hediet-json-rpc-0.5.1/json-rpc/src/0000755000000000000000000000000015072021641014641 5ustar node-hediet-json-rpc-0.5.1/json-rpc/src/Channel.ts0000644000000000000000000001065015072021641016563 0ustar import { constValue, IValueWithChangeEvent } from "./common"; import { RequestId, ErrorObject, JSONValue, JSONArray, JSONObject, } from "./JsonRpcTypes"; import { ConnectionState } from "./MessageTransport"; /** * Once a channel is constructed, it processes all incoming messages. */ export class Channel { constructor( public readonly connect: (listener: IRequestHandler | undefined) => IRequestSender ) { } public mapContext(map: (context: TContext) => TNewContext): Channel { return new Channel((listener) => this.connect(listener ? mapRequestHandlerContext(listener, map) : undefined) ); } } /** * A message sender has methods to send requests and notifications. * A request expects a response, a notification does not. */ export interface IRequestSender { /** * Sends a request. * @param request - The request to send. * @param messageIdCallback - An optional callback that is called before sending the request. * The passed request id can be used to track the request. * @return A promise of the response. Fails if the request could not be delivered or if a response could not be received. */ sendRequest(request: RequestObject, context: TContext, messageIdCallback?: (id: RequestId) => void): Promise; /** * Sends a notification. * @return A promise that is fulfilled as soon as the notification has been sent successfully. * Fails if the notification could not be sent. */ sendNotification(notification: RequestObject, context: TContext): Promise; /** * Returns human readable information of this channel. */ toString(): string; get state(): IValueWithChangeEvent; } /** * A request handler can handle requests and notifications. * Implementations must respond to all requests. */ export interface IRequestHandler { /** * Handles an incoming request. */ handleRequest(request: RequestObject, requestId: RequestId, context: TContext): Promise; handleNotification(request: RequestObject, context: TContext): Promise; } /** * Represents a request. * The `method` property should be used for deciding what to do with it. */ export interface RequestObject { method: string; // Compared to RequestMessage, params must be set, but can be null. // This is designed so that null can be handled by the deserializer // which undefined cannot. params: JSONArray | JSONObject | null; } /** * The result of a request. */ export type ResponseObject = | { result: JSONValue } | { error: ErrorObject }; /** * Implements a channel that directly forwards the requests to the given request handler. */ export class LoopbackChannel implements IRequestSender { private id: number = 0; constructor(private readonly requestHandler: IRequestHandler) { } public sendRequest(request: RequestObject, context: TContext, messageIdCallback?: (id: RequestId) => void): Promise { const curId = this.id++; if (messageIdCallback) { messageIdCallback(curId); } return this.requestHandler.handleRequest(request, curId, context); } public sendNotification(notification: RequestObject, context: TContext): Promise { return this.requestHandler.handleNotification(notification, context); } public toString(): string { return "Loopback"; } public readonly state = constValue({ state: "open" }); } export function mapMessageSenderContext( messageSender: IRequestSender, map: (context: TNewContext) => TContext ): IRequestSender { return { sendNotification: (notification, context) => messageSender.sendNotification(notification, map(context)), sendRequest: (request, context, messageIdCallback) => messageSender.sendRequest(request, map(context), messageIdCallback), toString: () => messageSender.toString(), state: messageSender.state, }; } export function mapRequestHandlerContext( messageHandler: IRequestHandler, map: (context: TNewContext) => TContext ): IRequestHandler { return { handleNotification: (request, context) => messageHandler.handleNotification(request, map(context)), handleRequest: (request, requestId, context) => messageHandler.handleRequest(request, requestId, map(context)), }; } node-hediet-json-rpc-0.5.1/json-rpc/src/JsonRpcTypes.ts0000644000000000000000000000702115072021641017614 0ustar // These types model the JSON RPC 2.0 Specification (see https://www.jsonrpc.org/specification). export function isRequestOrNotification(msg: Message): msg is IRequestMessage { return (msg as any).method !== undefined; } export type JSONObject = { [key: string]: JSONValue | undefined }; export interface JSONArray extends Array { } export type JSONValue = | string | number | boolean | null | JSONObject | JSONArray; export type Message = IRequestMessage | IResponseMessage; /** * Represents a request or a notification. */ export interface IRequestMessage { jsonrpc: "2.0"; /** must not match `rpc\..*` */ method: string; params?: JSONArray | JSONObject; /** Is not set if and only if the request is a notification. */ id?: RequestId; /** Requests don't have a result. */ result?: never; } export type RequestId = number | string; /** * Either result or error is set. */ export interface IResponseMessage { jsonrpc: "2.0"; /** * This member is REQUIRED on success. * This member MUST NOT exist if there was an error invoking the method. * The value of this member is determined by the method invoked on the Server. */ result?: JSONValue; /** * This member is REQUIRED on error. * This member MUST NOT exist if there was no error triggered during invocation. */ error?: ErrorObject; /** * If there was an error in detecting the id in the Request object * (e.g. Parse error/Invalid Request), it MUST be Null. */ id: RequestId | null; method?: never; } export interface ErrorObject { /** A Number that indicates the error type that occurred. */ code: ErrorCode; /** The message SHOULD be limited to a concise single sentence. */ message: string; /** * A Primitive or Structured value that contains additional information about the error. * This may be omitted. * The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.). */ data?: JSONValue; } export namespace ErrorObject { export function create(obj: ErrorObject): ErrorObject { return obj; } } export interface ErrorCode extends Number { } export namespace ErrorCode { /** * Invalid JSON was received by the server. * An error occurred on the server while parsing the JSON text. */ export const parseError = -32700 as ErrorCode; /** * The JSON sent is not a valid Request object. */ export const invalidRequest = -32600 as ErrorCode; /** * The method does not exist/is not available. */ export const methodNotFound = -32601 as ErrorCode; /** * Invalid method parameter(s). */ export const invalidParams = -32602 as ErrorCode; /** * Internal JSON-RPC error. */ export const internalError = -32603 as ErrorCode; /** * implementation-defined server-errors. */ export function isServerError(code: number): boolean { return -32099 <= code && code <= -32000; } /** * implementation-defined server-errors. */ export function serverError(code: number): ErrorCode { if (!isServerError(code)) { throw new Error("Invalid range for a server error."); } return code as ErrorCode; } /** * Non-spec. */ export const unexpectedServerError = -32000 as ErrorCode; export function isApplicationError(code: number): boolean { // todo implement proper checks return true; } export function applicationError(code: number): ErrorCode { if (!isApplicationError(code)) { throw new Error("Invalid range for an application error."); } return code as ErrorCode; } /** * Non-spec. */ export const genericApplicationError = -320100 as ErrorCode; } node-hediet-json-rpc-0.5.1/json-rpc/src/Logger.ts0000644000000000000000000000214115072021641016426 0ustar import { Message } from "./JsonRpcTypes"; /** * Represents a log entry used by `RpcLogger`. */ export interface LogEntry { /** * A human readable description of the log entry. */ text: string; /** * Data regarding this log entry. */ data?: object; /** * The json message the log entry is about, if any. */ message?: Message; /** * The exception the log entry is about, if any. */ exception?: unknown; } export interface RpcLogger { /** * Logs an event that ideally should not happen. */ warn(logEntry: LogEntry): void; /** * Logs an event that might be unusual. */ debug(logEntry: LogEntry): void; /** * Traces all communication. */ trace(logEntry: LogEntry): void; } /** * A `RpcLogger` that prints everything to `console`. */ export class ConsoleRpcLogger implements RpcLogger { public debug(logEntry: LogEntry): void { console.log(logEntry.text, logEntry.exception); } public warn(logEntry: LogEntry): void { console.warn(logEntry.text, logEntry.exception); } public trace(logEntry: LogEntry): void { console.log(logEntry.text, logEntry.exception); } } node-hediet-json-rpc-0.5.1/json-rpc/src/MessageTransport.ts0000644000000000000000000001111715072021641020513 0ustar import { IValueWithChangeEvent, ValueWithChangeEvent } from "./common"; import { Message } from "./JsonRpcTypes"; import { RpcLogger } from "./Logger"; /** * Represents a mechanism to send and receive messages. */ export interface IMessageTransport { get state(): IValueWithChangeEvent; send(message: Message): Promise; /** * Sets a listener for received messages. * The listener might be called multiple times before this function returns. * The method allows reentrancy. */ setListener(listener: MessageListener | undefined): void; /** * Returns a human readable representation of this stream. */ toString(): string; } export type ConnectionState = { state: "connecting" } | { state: "open" } | { state: "closed", error: Error | undefined }; export type MessageListener = (message: Message) => void; /** * Base class for implementing a MessageStream. * Provides an unreadMessage queue. */ export abstract class BaseMessageTransport implements IMessageTransport { private static id = 0; private readonly _unprocessedMessages: Message[] = []; private _messageListener: MessageListener | undefined; protected readonly id = BaseMessageTransport.id++; private readonly _state = new ValueWithChangeEvent({ state: "open" }); public readonly state = this._state; /** * Sets a callback for incoming messages. */ public setListener(listener: MessageListener | undefined): void { this._messageListener = listener; if (!listener) { return; } // _messageListener might change! while (this._unprocessedMessages.length > 0 && this._messageListener !== undefined) { const msg = this._unprocessedMessages.shift()!; this._messageListener(msg); } } /** * Writes a message to the stream. */ public send(message: Message): Promise { return this._sendImpl(message); } protected abstract _sendImpl(message: Message): Promise; /** * Returns human readable information of this message stream. */ public abstract toString(): string; /** * Call this in derived classes to signal a new message. */ protected _dispatchReceivedMessage(message: Message): void { const hasReadAllQueuedMessages = this._unprocessedMessages.length === 0; if (hasReadAllQueuedMessages && this._messageListener) { this._messageListener(message); } else { this._unprocessedMessages.push(message); } } /** * Call this in derived classes to signal that the connection closed. */ protected _onConnectionClosed(): void { this._state.value = { state: 'closed', error: undefined }; } public log(logger?: IMessageLogger): IMessageTransport { return new StreamLogger(this, logger ?? new ConsoleMessageLogger()); } } /** * Used by `StreamLogger` to log messages. */ export interface IMessageLogger { log(stream: IMessageTransport, type: "incoming" | "outgoing", message: Message): void; } /** * Intercepts a stream for logging. */ export class StreamLogger implements IMessageTransport { constructor( private readonly baseStream: IMessageTransport, private readonly logger: IMessageLogger ) { } public get state(): IValueWithChangeEvent { return this.baseStream.state; } public setListener(listener: ((readMessage: Message) => void) | undefined) { if (listener === undefined) { this.baseStream.setListener(undefined); return; } this.baseStream.setListener((readMessage) => { this.logger.log(this.baseStream, "incoming", readMessage); listener(readMessage); }); } public send(message: Message): Promise { this.logger.log(this.baseStream, "outgoing", message); return this.baseStream.send(message); } public toString(): string { return `StreamLogger/${this.baseStream.toString()}`; } } /** * Logs messages to a `RpcLogger`. */ export class RpcStreamLogger extends StreamLogger { constructor(baseStream: IMessageTransport, rpcLogger: RpcLogger) { super(baseStream, { log: (stream, type, message) => { const char = type === "incoming" ? "<-" : "->"; rpcLogger.trace({ text: `${char} [${stream.toString()}] ${JSON.stringify( message )}`, message: message, }); }, }); } } /** * Logs messages to `console`. */ export class ConsoleStreamLogger extends StreamLogger { constructor(baseStream: IMessageTransport) { super(baseStream, new ConsoleMessageLogger()); } } export class ConsoleMessageLogger implements IMessageLogger { log(stream: IMessageTransport, type: "incoming" | "outgoing", message: Message): void { const char = type === "incoming" ? "<-" : "->"; console.log(`${char} [${stream.toString()}] ${JSON.stringify(message)}`); } } node-hediet-json-rpc-0.5.1/json-rpc/src/StreamBasedChannel.ts0000644000000000000000000001307615072021641020703 0ustar import { Channel, IRequestHandler, IRequestSender, RequestObject, ResponseObject } from "./Channel"; import { ErrorCode, IRequestMessage, IResponseMessage, Message, RequestId, isRequestOrNotification } from "./JsonRpcTypes"; import { RpcLogger } from "./Logger"; import { ConnectionState, IMessageTransport } from "./MessageTransport"; import { IValueWithChangeEvent } from "./common"; /** * Implements a channel through a stream and an optional request handler to handle incoming requests. */ export class StreamBasedChannel implements IRequestSender { /** * Creates a channel factory from a given stream and logger. * This allows to delay specifying a `RequestHandler`. * Once the channel is created, it processes incoming messages. */ public static createChannel(stream: IMessageTransport, logger: RpcLogger | undefined): Channel { let constructed = false; return new Channel((listener) => { if (constructed) { throw new Error( `A channel to the stream ${stream} was already constructed!` ); } else { constructed = true; } return new StreamBasedChannel(stream, listener, logger); }); } private readonly _unprocessedResponses = new Map void>(); private _lastUsedRequestId = 0; constructor( private readonly _stream: IMessageTransport, private readonly _listener: IRequestHandler | undefined, private readonly _logger: RpcLogger | undefined ) { this._stream.setListener((message) => { if (isRequestOrNotification(message)) { if (message.id === undefined) { this._processNotification(message); } else { this._processRequest(message); } } else { this._processResponse(message); } }); } get state(): IValueWithChangeEvent { return this._stream.state; } private async _processNotification(message: IRequestMessage): Promise { if (message.id !== undefined) { throw new Error(); } if (!this._listener) { if (this._logger) { this._logger.debug({ text: "Notification ignored", message, }); } return; } try { await this._listener.handleNotification({ method: message.method, params: message.params || null, }); } catch (exception) { if (this._logger) { this._logger.warn({ text: `Exception was thrown while handling notification: ${exception}`, exception, message, }); } } } private async _processRequest(message: IRequestMessage): Promise { if (message.id === undefined) { throw new Error(); } let result: ResponseObject; if (this._listener) { try { result = await this._listener.handleRequest( { method: message.method, params: message.params || null, }, message.id ); } catch (exception) { if (this._logger) { this._logger.warn({ text: `Exception was thrown while handling request: ${exception}`, message, exception, }); } // do not leak exception details to client as it could contain sensitive information. result = { error: { code: ErrorCode.internalError, message: "An unexpected exception was thrown.", data: undefined, }, }; } } else { if (this._logger) { this._logger.debug({ text: "Received request even though not listening for requests", message, }); } result = { error: { code: ErrorCode.methodNotFound, message: "This endpoint does not listen for requests or notifications.", data: undefined, }, }; } let responseMsg: IResponseMessage; if ("result" in result) { responseMsg = { jsonrpc: "2.0", id: message.id, result: result.result, }; } else { responseMsg = { jsonrpc: "2.0", id: message.id, error: result.error, }; } await this._stream.send(responseMsg); } private _processResponse(message: IResponseMessage): void { const strId = "" + message.id; const callback = this._unprocessedResponses.get(strId); if (!callback) { if (this._logger) { this._logger.debug({ text: "Got an unexpected response message", message, }); } return; } this._unprocessedResponses.delete(strId); callback(message); } private _newRequestId(): RequestId { return this._lastUsedRequestId++; } public sendRequest(request: RequestObject, _context: void, messageIdCallback?: (requestId: RequestId) => void): Promise { const message: Message = { jsonrpc: "2.0", id: this._newRequestId(), method: request.method, params: request.params || undefined, }; if (messageIdCallback) { messageIdCallback(message.id!); } return new Promise((resolve, reject) => { const strId = "" + message.id; this._unprocessedResponses.set(strId, (response) => { if ("result" in response) { resolve({ result: response.result! }); } else { if (!response.error) { reject( new Error( "Response had neither 'result' nor 'error' field set." ) ); } resolve({ error: response.error! }); } }); this._stream.send(message).then(undefined, (reason) => { // Assuming no response will ever be sent as sending failed. this._unprocessedResponses.delete(strId); reject(reason); }); }); } public sendNotification(notification: RequestObject, context: void): Promise { const msg: Message = { jsonrpc: "2.0", id: undefined, method: notification.method, params: notification.params || undefined, }; return this._stream.send(msg); } public toString(): string { return "StreamChannel/" + this._stream.toString(); } } node-hediet-json-rpc-0.5.1/json-rpc/src/common.ts0000644000000000000000000000616615072021641016512 0ustar export interface IDisposable { dispose(): void; } export type IEvent = (listener: (e: T) => void) => IDisposable; export class EventEmitter { private listeners: Set<(e: T) => void> = new Set(); readonly event: IEvent = (listener: (e: T) => void) => { this.listeners.add(listener); return { dispose: () => { this.listeners.delete(listener); } }; }; fire(args: T): void { this.listeners.forEach(listener => listener(args)); } } export interface IValueWithChangeEvent { get value(): T; get onChange(): IEvent; } export function constValue(value: T): IValueWithChangeEvent { const eventEmitter = new EventEmitter(); return { get value(): T { return value; }, get onChange(): IEvent { return eventEmitter.event; } }; } export class ValueWithChangeEvent implements IValueWithChangeEvent { private _value: T; private eventEmitter: EventEmitter; constructor(initialValue: T) { this._value = initialValue; this.eventEmitter = new EventEmitter(); } get value(): T { return this._value; } set value(newValue: T) { if (this._value !== newValue) { this._value = newValue; this.eventEmitter.fire(newValue); } } get onChange(): IEvent { return this.eventEmitter.event; } } export function createTimeout(delay: number, callback: () => void): IDisposable { const handle = setTimeout(callback, delay); return { dispose: () => clearTimeout(handle) }; } export function setAndDeleteOnDispose(set: Set, item: T): IDisposable; export function setAndDeleteOnDispose(set: Map, key: TKey, item: TValue): IDisposable; export function setAndDeleteOnDispose( set: Set | Map, keyOrItem: any, item?: any): IDisposable { if (set instanceof Set) { set.add(keyOrItem); return { dispose: () => set.delete(keyOrItem) }; } else { set.set(keyOrItem, item); return { dispose: () => set.delete(keyOrItem) }; } } export class Deferred { private _state: "none" | "resolved" | "rejected" = "none"; public readonly promise: Promise; public resolve: (value: T | PromiseLike) => void = () => { }; public reject: (reason?: any) => void = () => { }; constructor() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } get state(): "none" | "resolved" | "rejected" { return this._state; } } export class Barrier { private deferred: Deferred = new Deferred(); public readonly unlock: (value: T | PromiseLike) => void = this.deferred.resolve; public readonly reject: (reason?: any) => void = this.deferred.reject; public readonly onUnlocked: Promise = this.deferred.promise; constructor() { } get state(): "none" | "resolved" | "rejected" { return this.deferred.state; } } node-hediet-json-rpc-0.5.1/json-rpc/src/index.ts0000644000000000000000000000075715072021641016331 0ustar export * from "./JsonRpcTypes"; export * from "./MessageTransport"; export * from "./Channel"; export * from "./StreamBasedChannel"; export * from "./typedChannel/TypedChannel"; export * from "./typedChannel/TypedChannelContracts"; export * from "./Logger"; export { ISerializer, setMapper } from "./schema"; export { EventEmitter, IDisposable, IEvent, IValueWithChangeEvent, ValueWithChangeEvent, constValue, Barrier, } from "./common"; import "./typedChannel/serializerMapping"; node-hediet-json-rpc-0.5.1/json-rpc/src/schema.ts0000644000000000000000000000657315072021641016464 0ustar import type { z } from 'zod'; export namespace Serializers { export function sAny(): ISerializer { return { deserializeFromJson: input => ({ hasErrors: false, value: input }), serializeToJson: input => input, } } export function sEmptyObject(): ISerializer<{}> { return { deserializeFromJson: input => ({ hasErrors: false, value: {} }), serializeToJson: input => ({}), }; } export function sVoidFromNull(): ISerializer { return { deserializeFromJson: input => ({ hasErrors: false, value: undefined }), serializeToJson: input => null, }; } } interface JsonRpcSerializerMapperLocal extends JsonRpcSerializerMapper { zod(val: T): T extends ({ parse: any, safeParse: any }) ? ISerializer> : undefined; serializer(val: T): T extends ISerializer ? T : undefined; } let globalMapper: Partial> = { serializer: val => { if (!isSerializer(val)) { return undefined; } return val; }, zod: val => { if (!isZodType(val)) { return undefined; } return { serializeToJson: input => val.parse(input), deserializeFromJson: input => { const result = val.safeParse(input); if (result.success) { return { hasErrors: false, value: result.data }; } else { return { hasErrors: true, value: undefined, errorMessage: result.error.message }; } } } as ISerializer; } }; function isZodType(val: any): val is z.ZodType { return 'parse' in val && 'safeParse' in val; } function isSerializer(val: any): val is ISerializer { return 'serializeToJson' in val && 'deserializeFromJson' in val; } export function setMapper(mapper: Partial>): void { Object.assign(globalMapper, mapper); } export type UnionValue = T[keyof T]; export type RemoveUndefined = T extends undefined ? never : T; export type SerializerOf = RemoveUndefined]: ReturnType[TKey]> }>>; export type SerializerTAny = SerializerOf extends never ? 'Error:UnknownSerializer' : SerializerOf extends ISerializer ? U : never; export type SerializerT> = T extends ISerializer ? U : never; export function convertSerializer(val: T): SerializerOf { for (const [key, value] of Object.entries(globalMapper)) { const result = value(val); if (result) { return result as SerializerOf; } } throw new Error(`No serializer found for value: ${val}`); } export interface ISerializer { serializeToJson(input: T): JSONValue; deserializeFromJson(input: JSONValue): DeserializationResult; }; export type DeserializationResult = { hasErrors: false; value: T; } | { hasErrors: true; value: undefined; errorMessage: string; } export type JSONObject = { [key: string]: JSONValue | undefined }; export interface JSONArray extends Array { } export type JSONValue = | string | number | boolean | null | JSONObject | JSONArray; node-hediet-json-rpc-0.5.1/json-rpc/src/typedChannel/0000755000000000000000000000000015072021641017257 5ustar node-hediet-json-rpc-0.5.1/json-rpc/src/typedChannel/TypedChannel.ts0000644000000000000000000005064415072021641022216 0ustar import { IRequestSender, Channel, RequestObject, ResponseObject } from "../Channel"; import { RequestId, ErrorCode, JSONValue, JSONArray, JSONObject } from "../JsonRpcTypes"; import { RpcLogger } from "../Logger"; import { IMessageTransport } from "../MessageTransport"; import { StreamBasedChannel } from "../StreamBasedChannel"; import { createTimeout, Deferred, EventEmitter, IDisposable, setAndDeleteOnDispose } from "../common"; import { convertSerializer, ISerializer, Serializers, SerializerTAny } from "../schema"; export const OptionalMethodNotFound = Symbol("OptionalMethodNotFound"); export type OptionalMethodNotFound = typeof OptionalMethodNotFound; export abstract class TypedChannelBase { public abstract request( requestType: RequestType, args: TParams, context: TSendContext ): Promise; public abstract notify( notificationType: NotificationType, params: TParams, context: TSendContext ): void; public abstract registerNotificationHandler( type: NotificationType, handler: NotificationHandlerFunc ): IDisposable; public abstract registerRequestHandler( requestType: RequestType, handler: RequestHandlerFunc ): IDisposable; public contextualize(args: { getNewContext: (context: TContext) => Promise | TNewContext; getSendContext: (newSendContext: TNewSendContext) => Promise | TSendContext; }): TypedChannelBase { return new ContextualizedTypedChannel(this, args); } } class ContextualizedTypedChannel< TNewContext, TNewSendContext, TContext, TSendContext > extends TypedChannelBase { constructor( private readonly underylingTypedChannel: TypedChannelBase< TContext, TSendContext >, private readonly converters: { getNewContext: ( context: TContext ) => Promise | TNewContext; getSendContext: ( newSendContext: TNewSendContext ) => Promise | TSendContext; } ) { super(); } public async request( requestType: RequestType, args: TParams, newContext: TNewSendContext ): Promise { const context = await this.converters.getSendContext(newContext); return this.underylingTypedChannel.request(requestType, args, context); } public async notify( notificationType: NotificationType, params: TParams, newContext: TNewSendContext ): Promise { const context = await this.converters.getSendContext(newContext); return this.underylingTypedChannel.notify( notificationType, params, context ); } public registerNotificationHandler( type: NotificationType, handler: NotificationHandlerFunc ): IDisposable { return this.underylingTypedChannel.registerNotificationHandler( type, async (arg, context) => { const newContext = await this.converters.getNewContext(context); return await handler(arg, newContext); } ); } public registerRequestHandler( requestType: RequestType, handler: RequestHandlerFunc ): IDisposable { return this.underylingTypedChannel.registerRequestHandler( requestType, async (arg, requestId, context) => { const newContext = await this.converters.getNewContext(context); return await handler(arg, requestId, newContext); } ); } } export interface TypedChannelOptions { logger?: RpcLogger; /** * If true, any sent or received unexpected properties are ignored. */ ignoreUnexpectedPropertiesInResponses?: boolean; sendExceptionDetails?: boolean; } /** * Represents a typed channel. * Call `startListen` to create the underlying channel * and to start processing all incoming messages. * At this point, all request and notification handlers should be registered. */ export class TypedChannel extends TypedChannelBase { public static fromTransport(stream: IMessageTransport, options: TypedChannelOptions = {}): TypedChannel { const channelFactory = StreamBasedChannel.createChannel(stream, options.logger); return new TypedChannel(channelFactory, options); } private _requestSender: IRequestSender | undefined = undefined; private readonly _handler = new Map< string, | RegisteredRequestHandler | RegisteredNotificationHandler >(); private readonly _unknownNotificationHandler = new Set< (notification: RequestObject) => void >(); private _timeout: IDisposable | undefined; public sendExceptionDetails: boolean = false; private readonly _logger: RpcLogger | undefined; constructor( private readonly channelCtor: Channel, options: TypedChannelOptions = {} ) { super(); this._logger = options.logger; this.sendExceptionDetails = !!options.sendExceptionDetails; this._timeout = createTimeout(1000, () => { if (!this._requestSender) { console.warn( `"${this.startListen.name}" has not been called within 1 second after construction of this channel. ` + `Did you forget to call it?`, this ); } }); } private listeningDeferred = new Deferred(); public onListening: Promise = this.listeningDeferred.promise; private readonly _requestDidErrorEventEmitter = new EventEmitter<{ error: RequestHandlingError; }>(); public readonly onRequestDidError = this._requestDidErrorEventEmitter.event; /** * This method must be called to forward messages from the stream to this channel. * This is not done automatically on construction so that this instance * can be setup properly before handling messages. */ public startListen(): void { if (this._requestSender) { throw new Error( `"${this.startListen.name}" can be called only once, but it already has been called.` ); } if (this._timeout) { this._timeout.dispose(); this._timeout = undefined; } this._requestSender = this.channelCtor.connect({ handleRequest: (req, id, context) => this.handleRequest(req, id, context), handleNotification: (req, context) => this.handleNotification(req, context), }); this.listeningDeferred.resolve(); } private checkChannel( channel: IRequestSender | undefined ): channel is IRequestSender { if (!channel) { throw new Error( `"${this.startListen.name}" must be called before any messages can be sent or received.` ); } return true; } private async handleRequest( request: RequestObject, requestId: RequestId, context: TContext ): Promise { const handler = this._handler.get(request.method) as | RegisteredRequestHandler< { brand: "params" }, { brand: "result" }, { brand: "error" }, TContext > | RegisteredNotificationHandler<{ brand: "params" }, TContext> | undefined; if (!handler) { if (this._logger) { this._logger.debug({ text: `No request handler for "${request.method}".`, data: { requestObject: request }, }); } return { error: { code: ErrorCode.methodNotFound, message: `No request handler for "${request.method}".`, data: { method: request.method }, }, }; } if (handler.kind != "request") { const message = `"${request.method}" is registered as notification, but was sent as request.`; if (this._logger) { this._logger.debug({ text: message, data: { requestObject: request }, }); } return { error: { code: ErrorCode.invalidRequest, message: message, data: { method: request.method }, }, }; } const decodeResult = handler.requestType.paramsSerializer.deserializeFromJson(request.params); if (decodeResult.hasErrors) { const message = `Got invalid params: ${decodeResult.errorMessage}`; if (this._logger) { this._logger.debug({ text: message, data: { requestObject: request, errorMessage: decodeResult.errorMessage, }, }); } return { error: { code: ErrorCode.invalidParams, message, data: { errors: decodeResult.errorMessage, }, }, }; } else { const args = decodeResult.value; let response: ResponseObject; try { const result = await handler.handler(args, requestId, context); if ("error" in result || "errorMessage" in result) { const errorData = result.error ? (handler.requestType.errorSerializer.serializeToJson(result.error)) : undefined; const code = result.errorCode || ErrorCode.genericApplicationError; const message = result.errorMessage || "An error was returned"; response = { error: { code, message, data: errorData } }; } else { const val = handler.requestType.resultSerializer.serializeToJson(result.ok); response = { result: val }; } } catch (exception) { if (exception instanceof RequestHandlingError) { // TODO: Introduce a better custom error // What about data? // Maybe default error data should be unknown response = { error: { code: exception.code, message: exception.message, }, }; } else { if (this._logger) { this._logger.warn({ text: `An exception was thrown while handling a request: ${exception}.`, exception, data: { requestObject: request }, }); } response = { error: { code: ErrorCode.unexpectedServerError, message: this.sendExceptionDetails ? `An exception was thrown while handling a request: ${exception}.` : "Server has thrown an unexpected exception", }, }; } } return response; } } private async handleNotification( request: RequestObject, context: TContext ): Promise { const handler = this._handler.get(request.method); if (!handler) { for (const h of this._unknownNotificationHandler) { h(request); } if (this._unknownNotificationHandler.size === 0) { if (this._logger) { this._logger.debug({ text: `Unhandled notification "${request.method}"`, data: { requestObject: request }, }); } } return; } if (handler.kind != "notification") { if (this._logger) { this._logger.debug({ text: `"${request.method}" is registered as request, but was sent as notification.`, data: { requestObject: request }, }); } // dont send a response back as we are handling a notification. return; } const decodeResult = handler.notificationType.paramsSerializer.deserializeFromJson(request.params); if (decodeResult.hasErrors) { if (this._logger) { this._logger.debug({ text: `Got invalid params: ${decodeResult}`, data: { requestObject: request, errorMessage: decodeResult.errorMessage, }, }); } // dont send a response back as we are handling a notification. return; } const val = decodeResult.value; for (const handlerFunc of handler.handlers) { try { handlerFunc(val, context); } catch (exception) { if (this._logger) { this._logger.warn({ text: `An exception was thrown while handling a notification: ${exception}.`, exception, data: { requestObject: request }, }); } } } } public registerUnknownNotificationHandler( handler: (notification: RequestObject) => void ): IDisposable { return setAndDeleteOnDispose(this._unknownNotificationHandler, handler); } public registerRequestHandler( requestType: RequestType, handler: RequestHandlerFunc ): IDisposable { const registeredHandler = this._handler.get(requestType.method); if (registeredHandler) { throw new Error( `Handler with method "${requestType.method}" already registered.` ); } return setAndDeleteOnDispose(this._handler, requestType.method, { kind: "request", requestType, handler, }); } public registerNotificationHandler( type: NotificationType, handler: NotificationHandlerFunc ): IDisposable { let registeredHandler = this._handler.get(type.method); if (!registeredHandler) { registeredHandler = { kind: "notification", notificationType: type, handlers: new Set(), }; this._handler.set(type.method, registeredHandler); } else { if (registeredHandler.kind !== "notification") { throw new Error( `Method "${type.method}" was already registered as request handler.` ); } if (registeredHandler.notificationType !== type) { throw new Error( `Method "${type.method}" was registered for a different type.` ); } } return setAndDeleteOnDispose(registeredHandler.handlers, handler); } public getRegisteredTypes(): Array { const result = []; for (const h of this._handler.values()) { if (h.kind === "notification") { result.push(h.notificationType); } else if (h.kind === "request") { result.push(h.requestType); } } return result; } public async request( requestType: RequestType, args: TParams, context: TSendContext ): Promise { if (!this.checkChannel(this._requestSender)) { throw new Error("Impossible"); } const params = requestType.paramsSerializer.serializeToJson(args); assertObjectArrayOrNull(params); const response = await this._requestSender.sendRequest( { method: requestType.method, params, }, context ); if ("error" in response) { if (requestType.isOptional && response.error.code === ErrorCode.methodNotFound) { return OptionalMethodNotFound as any; } let errorData; if (response.error.data !== undefined) { const deserializationResult = requestType.errorSerializer.deserializeFromJson( response.error.data ); if (deserializationResult.hasErrors) { throw new Error(deserializationResult.errorMessage); } errorData = deserializationResult.value; } else { errorData = undefined; } const error = new RequestHandlingError(response.error.message, errorData, response.error.code); this._requestDidErrorEventEmitter.fire({ error }); throw error; } else { const result = requestType.resultSerializer.deserializeFromJson(response.result); if (result.hasErrors) { throw new Error('Could not deserialize response: ' + result.errorMessage + `\n\n${JSON.stringify(response, null, 2)}`); } else { return result.value; } } } public async notify( notificationType: NotificationType, params: TParams, context: TSendContext ): Promise { if (!this.checkChannel(this._requestSender)) { throw new Error(); } const encodedParams = notificationType.paramsSerializer.serializeToJson(params); assertObjectArrayOrNull(encodedParams); this._requestSender.sendNotification({ method: notificationType.method, params: encodedParams }, context); } /*public requestAndCatchError(connection: Connection, body: TRequest): Promise> { }*/ } function assertObjectArrayOrNull( val: JSONValue ): asserts val is JSONObject | JSONArray | null { if (val !== null && Array.isArray(val) && typeof val !== "object") { throw new Error( "Invalid value! Only null, array and object is allowed." ); } } export type Result = OkResult | ErrorResult; export interface OkResult { ok: TOk; } export type ErrorResult = | { error: TError; errorMessage?: string; errorCode?: ErrorCode; } | { error?: TError; errorMessage: string; errorCode?: ErrorCode; }; export type RequestHandlerFunc = ( arg: TArg, requestId: RequestId, context: TContext ) => Promise>; export type NotificationHandlerFunc = ( arg: TArg, context: TContext ) => void; interface RegisteredRequestHandler { readonly kind: "request"; readonly requestType: RequestType; readonly handler: RequestHandlerFunc; } interface RegisteredNotificationHandler { readonly kind: "notification"; readonly notificationType: NotificationType; readonly handlers: Set>; } /** * Is thrown when handling the request raised an error. */ export class RequestHandlingError extends Error { constructor( message: string, public readonly data?: T, public readonly code: ErrorCode = ErrorCode.genericApplicationError ) { super(message); Object.setPrototypeOf(this, RequestHandlingError.prototype); } } /** * Describes a request type. */ export class RequestType< TParams = unknown, TResponse = unknown, TError = unknown, TMethod extends string | undefined = string, TOptional extends boolean = false, > { public readonly kind: "request" = "request"; constructor( public readonly method: TMethod, public readonly paramsSerializer: ISerializer, public readonly resultSerializer: ISerializer, public readonly errorSerializer: ISerializer, public readonly isOptional: TOptional = false as TOptional, ) { } public withMethod( method: string ): RequestType { return new RequestType( method, this.paramsSerializer, this.resultSerializer, this.errorSerializer ); } public optional(): RequestType { return new RequestType( this.method, this.paramsSerializer, this.resultSerializer, this.errorSerializer, true as true ); } } /** * Describes a notification type. */ export class NotificationType { public readonly kind: "notification" = "notification"; constructor( public readonly method: TMethod, public readonly paramsSerializer: ISerializer ) { } public withMethod(method: string): NotificationType { return new NotificationType(method, this.paramsSerializer); } } /** * Describes a notification type without static type validation. */ export function rawNotification( method: string ): NotificationType { return new NotificationType(method, Serializers.sAny()); } /** * Describes a request type as part of a `Contract`. */ export function requestType< TMethod extends string | undefined = undefined, TParams = ISerializer<{}>, TResult = ISerializer, TError = ISerializer >(request: { method?: TMethod; params?: TParams; result?: TResult; error?: TError; }): RequestType, SerializerTAny, SerializerTAny, TMethod> { return new RequestType( request.method!, request.params ? convertSerializer(request.params) as any : Serializers.sEmptyObject(), request.result ? convertSerializer(request.result) as any : Serializers.sVoidFromNull(), request.error ? convertSerializer(request.error) as any : Serializers.sVoidFromNull() ); } // TODO remove TMethod export function unverifiedRequest< TParams = {}, TResult = void, TError = void, TMethod extends string | undefined = string >(request?: { method?: TMethod; }): RequestType { return new RequestType((request || {}).method!, Serializers.sAny(), Serializers.sAny(), Serializers.sAny()); } /** * Describes a notification type as part of a `Contract`. */ export function notificationType< TMethod extends string | undefined = undefined, TParams = ISerializer<{}> >(notification: { method?: TMethod; params?: TParams; }): NotificationType, TMethod> { return new NotificationType( notification.method!, notification.params ? convertSerializer(notification.params) as any : Serializers.sEmptyObject() ); } // TODO remove TMethod export function unverifiedNotification (request?: { method?: TMethod } ): NotificationType { return new NotificationType((request || {}).method!, Serializers.sAny()); } node-hediet-json-rpc-0.5.1/json-rpc/src/typedChannel/TypedChannelContracts.ts0000644000000000000000000003065715072021641024101 0ustar import { RequestType, NotificationType, TypedChannel, ErrorResult, RequestHandlerFunc, TypedChannelBase, TypedChannelOptions, OptionalMethodNotFound } from "./TypedChannel"; import { RequestId, ErrorCode } from "../JsonRpcTypes"; import { IMessageTransport } from "../MessageTransport"; import { SerializerT } from "../schema"; import { IDisposable } from "../common"; /** * Describes a request type as part of a `Contract`. * The method is inferred from its position in the contract if not provided. */ export type ContractRequestType< TParams = unknown, TResult = unknown, TError = unknown, TOptional extends boolean = boolean > = RequestType; export type AnyRequestContract = ContractRequestType; export type AsRequestContract = T; /** * Describes a notification type as part of a `Contract`. * The method is inferred from its position in the contract if not provided. */ export type ContractNotificationType = NotificationType; export type AsNotificationContract = T; /** * Describes one side of a contract. */ export type OneSideContract = Record>; export type AsOneSideContract = T; export type ContractToRequest = { [TRequest in keyof TRequestMap]: TRequestMap[TRequest] extends AnyRequestContract ? RequestType< SerializerT, SerializerT, SerializerT > : NotificationType>; }; export type EmptyObjectToVoid = {} extends T ? void | T : T; export type ContractInterfaceOf< TRequestMap extends OneSideContract, TContext > = { [TRequest in keyof TRequestMap]: ( arg: EmptyObjectToVoid>, context: TContext, ) => TRequestMap[TRequest] extends AnyRequestContract ? Promise> | (TRequestMap[TRequest]['isOptional'] extends true ? OptionalMethodNotFound : never) : void; }; /** Marks a type as error wrapper. */ export const IsErrorWrapper = Symbol(); /** * Wraps an error so that it can be distinguished from a successfully returned result. */ export class ErrorWrapper { public static factory = (error: ErrorResult) => { return new ErrorWrapper(error); }; [IsErrorWrapper]!: true; constructor(public readonly error: ErrorResult) { } } export type ContractHandlerOf< TRequestMap extends OneSideContract, TCounterPartRequestMap extends OneSideContract, TContext, TOtherContext > = ObjectWithOptional<{ [TKey in keyof TRequestMap]: TRequestMap[TKey] extends AnyRequestContract ? { optional: TRequestMap[TKey]['isOptional'], type: ( arg: SerializerT, info: RequestHandlerInfo< SerializerT, ContractInterfaceOf, TContext > ) => Promise< | SerializerT | ErrorWrapper> > } : { optional: true, type: ( arg: SerializerT, info: HandlerInfo< ContractInterfaceOf, TContext > ) => void; } }>; export type ObjectWithOptional> = ( { [K in keyof T]?: T[K]["type"] } & { [K in keyof T as T[K]["optional"] extends false ? K : never]-?: T[K]["type"] } ) extends infer O ? { [K in keyof O]: O[K] } : never /** * Describes a contract. */ export interface ContractObject { server: OneSideContract; client: OneSideContract; } /** * Describes a new contract. */ export function contract< TServer extends OneSideContract, TClient extends OneSideContract, TTags extends string = never >(contractObj: { name: string; tags?: TTags[]; server: TServer; client: TClient; }): Contract { const server = transform(contractObj["server"]); const client = transform(contractObj["client"]); return new Contract(contractObj.tags || [], server as any, client as any); } function transform( requestMap: OneSideContract ): Record { const result: Record = {}; for (const [key, req] of Object.entries(requestMap)) { const method = req.method ? req.method : key; result[key] = req.withMethod(method); } return result; } /** * Provides additional information when handling a request or a notification. */ export interface HandlerInfo { context: TListenerContext; /** * The implementation of the other contract. */ counterpart: TCounterPart; } /** * Provides additional information when handling a request. */ export interface RequestHandlerInfo extends HandlerInfo { /** * Creates a new error object that can be returned in request handlers. */ newErr(error: ErrorResult): ErrorWrapper; /** * The id of the current request. */ requestId: RequestId; } export class Contract< TTags extends string, TContractObject extends ContractObject, TContext, TSendContext > { protected _onlyDesignTime() { return new Error("This property is not meant to be accessed at runtime"); } public get TContractObject(): TContractObject { throw this._onlyDesignTime(); } public get TClientInterface(): ContractInterfaceOf { throw this._onlyDesignTime(); } public get TServerInterface(): ContractInterfaceOf { throw this._onlyDesignTime(); } public get TClientHandler(): ContractHandlerOf { throw this._onlyDesignTime(); } public get TServerHandler(): ContractHandlerOf { throw this._onlyDesignTime(); } public get TTags(): TTags { throw this._onlyDesignTime(); } constructor( public readonly tags: TTags[] = [], public readonly server: ContractToRequest, public readonly client: ContractToRequest ) { } protected getInterface( typedChannel: TypedChannelBase, myContract: Record< string, NotificationType | RequestType >, otherContract: Record< string, NotificationType | RequestType >, myInterface: Record ): { counterpart: Record } & IDisposable { const counterpart = this.buildCounterpart(typedChannel, otherContract); const disposable = this.registerHandlers( typedChannel, myContract, myInterface, counterpart ); return { counterpart, dispose: () => disposable.dispose() }; } private buildCounterpart( typedChannel: TypedChannelBase, otherContract: Record< string, NotificationType | RequestType > ): Record { const counterpart: Record = {}; for (const [key, req] of Object.entries(otherContract)) { let method; if (req.kind === "request") { if (req.isOptional) { method = async (args: any, context: TSendContext) => { if (args === undefined) { args = {}; } try { return await typedChannel.request(req, args, context); } catch (error: any) { // TODO use proper types if (error && error.code === ErrorCode.methodNotFound) { return OptionalMethodNotFound; } throw error; } }; } else { method = (args: any, context: TSendContext) => { if (args === undefined) { args = {}; } return typedChannel.request(req, args, context); }; } } else { method = (args: any, context: TSendContext) => { if (args === undefined) { args = {}; } return typedChannel.notify(req, args, context); }; } counterpart[key] = method; } return counterpart; } private registerHandlers( typedChannel: TypedChannelBase, myContract: Record< string, NotificationType | RequestType >, myInterface: Record, counterpart: Record ): IDisposable { const disposables: IDisposable[] = []; for (const [key, req] of Object.entries(myContract)) { if (req.kind === "request") { let method = myInterface[key]; if (!method) { continue; } const handler = this.createRequestHandler(counterpart, method); disposables.push(typedChannel.registerRequestHandler(req, handler)); } else { const method = myInterface[key]; if (method) { disposables.push( typedChannel.registerNotificationHandler(req, (args, context) => { const notificationInfo: HandlerInfo = { context, counterpart }; // TODO maybe await and log errors? method(args, notificationInfo); }) ); } } } return { dispose: () => disposables.forEach(d => d.dispose()) }; } private createRequestHandler( counterpart: Record, method: Function ): RequestHandlerFunc { return async (args, requestId, listenerContext) => { const requestInfo: RequestHandlerInfo = { context: listenerContext, counterpart, newErr: ErrorWrapper.factory, requestId, }; const result = await method(args, requestInfo); if (result instanceof ErrorWrapper) { return result.error; } return { ok: result }; }; } /** * Gets a server object directly from a stream by constructing a new `TypedChannel`. * It also registers the client implementation to the stream. * The channel starts listening immediately. */ public static getServerFromStream< TContract extends Contract >( contract: TContract, stream: IMessageTransport, options: TypedChannelOptions, clientImplementation: TContract["TClientHandler"] ): { server: TContract["TServerInterface"]; channel: TypedChannel; } { const channel = TypedChannel.fromTransport(stream, options); const { server } = contract.getServer(channel, clientImplementation); channel.startListen(); return { channel, server }; } /** * Gets a client object directly from a stream by constructing a new `TypedChannel`. * It also registers the server implementation to the stream. * The channel starts listening immediately. */ public static registerServerToStream< TContract extends Contract >( contract: TContract, stream: IMessageTransport, options: TypedChannelOptions, serverImplementation: TContract["TServerHandler"] ): { client: TContract["TClientInterface"]; channel: TypedChannel; } { const channel = TypedChannel.fromTransport(stream, options); const { client } = contract.registerServer(channel, serverImplementation); channel.startListen(); return { channel, client }; } public getServer( typedChannel: TypedChannelBase, clientImplementation: this["TClientHandler"] ): { server: ContractInterfaceOf; } & IDisposable { const { counterpart, dispose } = this.getInterface(typedChannel, this.client, this.server, clientImplementation); return { server: counterpart as any, dispose }; } public registerServer( typedChannel: TypedChannelBase, serverImplementation: this["TServerHandler"] ): { client: ContractInterfaceOf; } & IDisposable { const { counterpart, dispose } = this.getInterface(typedChannel, this.server, this.client, serverImplementation); return { client: counterpart as any, dispose }; } public withContext(): Contract< TTags, TContractObject, TNewContext, TNewSendContext > { return new Contract(this.tags, this.server, this.client); } } node-hediet-json-rpc-0.5.1/json-rpc/src/typedChannel/serializerMapping.ts0000644000000000000000000000033215072021641023312 0ustar import { ISerializer } from "../schema"; const _x: ISerializer = null!; // keep the import declare global { // Used to extend JsonRpcSerializerMapperLocal. interface JsonRpcSerializerMapper { } } node-hediet-json-rpc-0.5.1/json-rpc/tsconfig.json0000644000000000000000000000021115072021641016553 0ustar { "extends": "../tsconfig.settings.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "skipLibCheck": true } } node-hediet-json-rpc-0.5.1/json-rpc-browser/0000755000000000000000000000000015072021641015533 5ustar node-hediet-json-rpc-0.5.1/json-rpc-browser/package.json0000644000000000000000000000106015072021641020016 0ustar { "name": "@hediet/json-rpc-browser", "description": "Provides JSON RPC through window.postMessage.", "version": "0.5.1", "author": { "name": "Henning Dieterichs", "email": "henning.dieterichs@live.de" }, "license": "MIT", "main": "./dist/index", "types": "./dist/index.d.ts", "files": [ "dist", "src" ], "scripts": { "dev": "tsc --watch", "build": "tsc" }, "dependencies": { "@hediet/json-rpc": "^0.5.0" }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" } }node-hediet-json-rpc-0.5.1/json-rpc-browser/src/0000755000000000000000000000000015072021641016322 5ustar node-hediet-json-rpc-0.5.1/json-rpc-browser/src/WindowLikeStream.ts0000644000000000000000000000306615072021641022127 0ustar import { BaseMessageTransport, Message } from "@hediet/json-rpc"; export interface WindowLike { postMessage(data: any, ...misc: any[]): void; addEventListener(eventName: "message", handler: MessageEventListener): void; removeEventListener(eventName: "message", handler: MessageEventListener): void; } export type MessageEventListener = (ev: { data: any; source: WindowLike | undefined }) => void; export class WindowLikeTransport extends BaseMessageTransport { private _disposed = false; constructor( private readonly _windowLike: WindowLike, private readonly _source: WindowLike | undefined = undefined, private readonly _loadingState: | { loaded: boolean; onLoaded: Promise } | undefined = undefined ) { super(); this._windowLike.addEventListener("message", this._messageHandler); } private readonly _messageHandler: MessageEventListener = ({ data, source }) => { if (this._source && source !== this._source) { return; } if (typeof data === "object" && data) { this._dispatchReceivedMessage(data); } }; protected override async _sendImpl(message: Message): Promise { if (this._disposed) { throw new Error("Transport is disposed"); } if (this._loadingState && !this._loadingState.loaded) { await this._loadingState.onLoaded; } this._windowLike.postMessage(message); } public toString(): string { return `${this.id}@${this._windowLike}`; } public dispose(): void { if (this._disposed) { return; } this._disposed = true; this._windowLike.removeEventListener("message", this._messageHandler); } } node-hediet-json-rpc-0.5.1/json-rpc-browser/src/iframe.ts0000644000000000000000000000175515072021641020145 0ustar import { BaseMessageTransport } from "@hediet/json-rpc"; import { WindowLikeTransport } from "./WindowLikeStream"; /** * Gets a stream that uses `self.postMessage` to write * and `self.addEventListener` to read messages. */ export function createTransportFromIFrameToParent(): BaseMessageTransport { if (window.self === window.top) { throw new Error(`Call this function from an iframe!`); } return new WindowLikeTransport(self, parent.window); } /** * Gets a stream that uses `worker.postMessage` to write * and `worker.addEventListener` to read messages. */ export function createTransportToIFrame(iframe: HTMLIFrameElement): BaseMessageTransport { if (typeof window === "undefined") { throw new Error(`call this function from the main browser thread`); } return new WindowLikeTransport(iframe.contentWindow!, iframe.contentWindow!, { loaded: window.document.readyState === "complete", onLoaded: new Promise((res) => { window.addEventListener("load", () => res()); }), }); } node-hediet-json-rpc-0.5.1/json-rpc-browser/src/index.ts0000644000000000000000000000012615072021641020000 0ustar export * from "./worker"; export * from "./iframe"; export * from "@hediet/json-rpc"; node-hediet-json-rpc-0.5.1/json-rpc-browser/src/worker.ts0000644000000000000000000000150615072021641020205 0ustar import { BaseMessageTransport } from "@hediet/json-rpc"; import { WindowLikeTransport } from "./WindowLikeStream"; /** * Gets a stream that uses `self.postMessage` to write * and `self.addEventListener` to read messages. */ export function createTransportFromWorkerToParent(): BaseMessageTransport { if (typeof self === "undefined" || typeof importScripts === "undefined") { throw new Error(`Call this function from a worker script`); } return new WindowLikeTransport(self); } /** * Gets a stream that uses `worker.postMessage` to write * and `worker.addEventListener` to read messages. */ export function createTransportToWorker(worker: Worker): BaseMessageTransport { if (typeof window === "undefined") { throw new Error(`call this function from the main browser thread`); } return new WindowLikeTransport(worker); } node-hediet-json-rpc-0.5.1/json-rpc-browser/tsconfig.json0000644000000000000000000000023415072021641020241 0ustar { "extends": "../tsconfig.settings.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "include": ["./src/**/*", "./test/**/*"] } node-hediet-json-rpc-0.5.1/json-rpc-websocket/0000755000000000000000000000000015072021641016036 5ustar node-hediet-json-rpc-0.5.1/json-rpc-websocket/package.json0000644000000000000000000000134715072021641020331 0ustar { "name": "@hediet/json-rpc-websocket", "description": "Provides a WebSocket to MessageStream adapter. Enables RPC calls to websocket server.", "version": "0.5.1", "author": { "name": "Henning Dieterichs", "email": "henning.dieterichs@live.de" }, "license": "MIT", "main": "./dist/index", "exports": { "./server": "./dist/server.js", ".": "./dist/index.js" }, "types": "./dist/index.d.ts", "files": [ "dist", "src" ], "scripts": { "dev": "tsc --watch", "build": "tsc" }, "dependencies": { "@hediet/json-rpc": "^0.5.0", "@types/ws": "^6.0.4", "isomorphic-ws": "^5.0.0" }, "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" } }node-hediet-json-rpc-0.5.1/json-rpc-websocket/src/0000755000000000000000000000000015072021641016625 5ustar node-hediet-json-rpc-0.5.1/json-rpc-websocket/src/index.ts0000644000000000000000000000535515072021641020314 0ustar import { Message, BaseMessageTransport, EventEmitter } from "@hediet/json-rpc"; import WebSocket from "isomorphic-ws"; export * from "@hediet/json-rpc"; export type NormalizedWebSocketOptions = { address: string; }; export type WebSocketOptions = | NormalizedWebSocketOptions | { host: string; port: number; forceTls?: boolean; }; /** * Normalizes the given options to `NormalizedWebSocketOptions`. * It builds the address from a given host and port. */ export function normalizeWebSocketOptions( options: WebSocketOptions ): NormalizedWebSocketOptions { if ("host" in options) { const useTls = options.forceTls!!; return { address: `${useTls ? "wss" : "ws"}://${options.host}:${options.port }`, }; } else { return options; } } /** * Represents a stream through a web socket. * Use the static `connectTo` method to get a stream to a web socket server. */ export class WebSocketTransport extends BaseMessageTransport { public static connectTo(options: WebSocketOptions): Promise { const normalizedOptions = normalizeWebSocketOptions(options); const ws = new WebSocket(normalizedOptions.address); return new Promise((res, rej) => { // don't use `on` as it does not exist for browsers ws.onerror = (err) => { rej(err); }; ws.onopen = () => { res(new WebSocketTransport(ws)); }; }); } // We use unknown here, because we don't want to pull in the ws types publically. public static fromWebSocket(socket: unknown): WebSocketTransport { return new WebSocketTransport(socket as WebSocket); } private readonly errorEmitter = new EventEmitter<{ error: unknown }>(); public readonly onError = this.errorEmitter; private constructor( private readonly socket: WebSocket ) { super(); socket.onmessage = (msg) => { // If we throw an exception here, the entire process will crash. try { const data = msg.data; if (typeof data === "string") { const json = JSON.parse(data) as Message; // TODO check type of json this._dispatchReceivedMessage(json); } else { throw new Error("Not supported"); // TODO test } } catch (error: unknown) { this.errorEmitter.fire({ error }); } }; socket.onclose = (_event) => { this._onConnectionClosed(); }; } /** * Closes the underlying socket. */ public close(): void { this.socket.close(); } /** * Same as `close`. */ public dispose(): void { this.close(); } public override _sendImpl(message: Message): Promise { const str = JSON.stringify(message); return new Promise((res, rej) => { this.socket.send(str, (err) => { if (err) { rej(err); } else { res(); } }); }); } public toString(): string { return `${this.id}@${this.socket.url}`; } } node-hediet-json-rpc-0.5.1/json-rpc-websocket/src/server.ts0000644000000000000000000000436615072021641020514 0ustar import * as WebSocket from "ws"; import { WebSocketTransport } from "./index"; import * as http from "http"; import * as https from "https"; import { EventEmitter, IEvent, Barrier } from "@hediet/json-rpc"; export interface StartWebSocketServerOptions { /** * Where to listen on. * Use `port` to listen on a specific or random port. * Use `server` to listen on an existing server. */ listenOn: | { port: number | "random" } | { server: http.Server | https.Server }; } /** * Starts a new web socket server. * Use `handleConnection` to handle the streams of incoming connections. */ export function startWebSocketServer( options: StartWebSocketServerOptions, handleConnection: (stream: WebSocketTransport) => void ): WebSocketServer { const opts: WebSocket.ServerOptions = {}; const l = options.listenOn; if ("port" in l) { opts.port = l.port === "random" ? 0 : l.port; } else if ("server" in l) { opts.server = l.server; } const wss = new WebSocket.Server(opts); wss.on("connection", ws => { const stream = WebSocketTransport.fromWebSocket(ws); handleConnection(stream); }); return new WebSocketServer(wss); } /** * Wraps a websocket server. */ export class WebSocketServer { constructor(private readonly server: WebSocket.Server) { const listeningEventEmitter = new Barrier(); this.onListening = listeningEventEmitter.onUnlocked; let state: "None" | "Listening" | "Error" = "None"; server.on("listening", () => { listeningEventEmitter.unlock(); state = "Listening"; }); const errorEventEmitter = new EventEmitter(); this.onError = errorEventEmitter.event; server.on("error", error => { if (state == "None") { state = "Error"; listeningEventEmitter.reject(error); } errorEventEmitter.fire(error); }); } /** * Is resolved once the socket is listening. */ public readonly onListening: Promise; public readonly onError: IEvent; /** * Gets the port the websocket server is running on. */ public get port(): number { const addrInfo = this.server.address() as WebSocket.AddressInfo; return addrInfo.port; } /** * Closes the server. */ public close(): void { this.server.close(); } /** * Same as `close`. */ public dispose(): void { this.close(); } } node-hediet-json-rpc-0.5.1/json-rpc-websocket/tsconfig.json0000644000000000000000000000026115072021641020544 0ustar { "extends": "../tsconfig.settings.json", "compilerOptions": { "outDir": "./dist", "rootDir": "./src", "skipLibCheck": true, "allowSyntheticDefaultImports": true } } node-hediet-json-rpc-0.5.1/tsconfig.settings.json0000644000000000000000000000050115072021641016661 0ustar { "compilerOptions": { "module": "esnext", "target": "esnext", "moduleResolution": "node", "strict": true, "declaration": true, "composite": true, "sourceMap": true, "declarationMap": true, "outDir": "./dist", "forceConsistentCasingInFileNames": true, "skipLibCheck": true //"baseUrl": ".", } }