UNPKG

@vgerbot/web-rpc

Version:

A TypeScript library that provides type-safe Remote Procedure Call (RPC) communication between different JavaScript contexts using various transport mechanisms

649 lines (628 loc) 20.1 kB
"use strict"; var WebRPCLib = (() => { var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { BrowserExtensionTransport: () => BrowserExtensionTransport, PostMessageTransport: () => PostMessageTransport, SendFunctionTransport: () => SendFunctionTransport, WebRPC: () => WebRPC, WindowPostMessageTransport: () => WindowPostMessageTransport, default: () => index_default }); // src/common/isFunction.ts function isFunction(value) { return typeof value === "function"; } // src/common/isPlainObject.ts function isPlainObject(obj) { if (typeof obj !== "object" || obj === null) { return false; } const proto = Object.getPrototypeOf(obj); return proto === null || proto === Object.prototype; } // src/protocol/Message.ts function isRPCMessage(data) { return isPlainObject(data) && typeof data.invocationId === "object" && typeof data.action === "string" && typeof data.timestamp === "number" && (data.action === "method-call" || data.action === "method-return"); } // src/core/SendFunctionTransport.ts var SendFunctionTransport = class { constructor(send) { this.send = send; } onMessage(callback) { return () => { }; } close() { } }; // src/common/Defer.ts var Defer = class { _resolve; _reject; _promise; constructor() { this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); } get promise() { return this._promise; } resolve(value) { this._resolve(value); } reject(reason) { this._reject(reason); } }; // src/protocol/Callback.ts var CALLBACK_FLAG = "$$WEB-RPC-CALLBACK"; function isCallback(data) { return typeof data === "object" && data !== null && "flag" in data && data.flag === CALLBACK_FLAG; } // src/protocol/Getter.ts var GETTER_FLAG = "$$WEB-RPC-GETTER"; function isGetter(data) { return typeof data === "object" && data !== null && "flag" in data && data.flag === GETTER_FLAG; } // src/common/deserializeRequestData.ts function deserializeRequestData(data, invokeCallback) { const handled = /* @__PURE__ */ new Map(); const transform = (value) => { if (handled.has(value)) { return handled.get(value); } if (isCallback(value)) { const callback = (...args) => { return invokeCallback(value.id, args); }; handled.set(value, callback); return callback; } if (Array.isArray(value)) { const result = []; handled.set(value, result); const arr = value.map((it) => { if (handled.has(it)) { return handled.get(it); } const result2 = transform(it); handled.set(it, result2); return result2; }); result.push(...arr); return result; } else if (isPlainObject(value)) { const object = {}; handled.set(value, object); const descriptors = Object.getOwnPropertyDescriptors(value); for (const key in descriptors) { const originalValue = value[key]; if (isGetter(originalValue)) { const descriptor = { get: () => { return invokeCallback(originalValue.id, []); } }; Object.defineProperty(object, key, descriptor); } else { object[key] = transform(originalValue); } } return object; } return value; }; return data.map(transform); } // src/common/isTransferable.ts function isTransferable(value) { return value instanceof ArrayBuffer || value instanceof MessagePort || globalThis.OffscreenCanvas && value instanceof globalThis.OffscreenCanvas || globalThis.ReadableStream && value instanceof globalThis.ReadableStream || globalThis.WritableStream && value instanceof globalThis.WritableStream || globalThis.TransformStream && value instanceof globalThis.TransformStream || globalThis.VideoFrame && value instanceof globalThis.VideoFrame || globalThis.ImageBitmap && value instanceof globalThis.ImageBitmap; } // src/common/isTypedArray.ts function isTypedArray(value) { return value instanceof Uint8Array || value instanceof Uint16Array || value instanceof Uint32Array || value instanceof Int8Array || value instanceof Int16Array || value instanceof Int32Array || value instanceof Uint8ClampedArray; } // src/common/uid.ts function uid(template = "********") { return template.replace(/\*/g, () => { const character = Math.floor(random() * 16).toString(16); if (random() >= 0.5) { return character; } else { return character.toUpperCase(); } }); } var cryptoArray = new Uint32Array(1); function random() { if (typeof crypto === "undefined") { return Math.random(); } const value = crypto.getRandomValues(cryptoArray)[0]; if (typeof value === "undefined") { return Math.random(); } return value / 4294967295; } // src/common/serializeRequestData.ts function serializeRequestData(contextId, data) { const functions = /* @__PURE__ */ new Map(); const processedItems = /* @__PURE__ */ new Set(); const transferables = /* @__PURE__ */ new Set(); const createFunctionCallback = (func) => { const functionId = uid("func-******"); functions.set(functionId, func); return { flag: CALLBACK_FLAG, contextId, id: functionId }; }; const createGetterObject = (getter) => { const functionId = uid("func-******"); functions.set(functionId, getter); return { flag: GETTER_FLAG, contextId, id: functionId }; }; const processPropertyDescriptor = (targetObject, key, descriptor, originalValue) => { if (descriptor.get) { const getterObject = createGetterObject(descriptor.get); const newDescriptor = { configurable: descriptor.configurable, enumerable: descriptor.enumerable, writable: descriptor.writable, value: getterObject }; Object.defineProperty(targetObject, key, newDescriptor); } else if (typeof originalValue === "function") { const newDescriptor = { configurable: descriptor.configurable, enumerable: descriptor.enumerable, writable: descriptor.writable, value: processValue(originalValue) }; Object.defineProperty(targetObject, key, newDescriptor); } else { Object.defineProperty(targetObject, key, descriptor); } }; const processPlainObject = (obj) => { const descriptors = Object.getOwnPropertyDescriptors(obj); const transformedObject = {}; for (const key in descriptors) { const descriptor = descriptors[key]; processPropertyDescriptor(transformedObject, key, descriptor, obj[key]); } return transformedObject; }; const collectTransferables = (value) => { if (isTypedArray(value)) { transferables.add(value.buffer); } else if (isTransferable(value)) { transferables.add(value); } }; const processValue = (value) => { if (processedItems.has(value)) { return value; } processedItems.add(value); if (Array.isArray(value)) { return value.map((item) => processValue(item)); } if (isPlainObject(value)) { return processPlainObject(value); } if (typeof value === "function") { return createFunctionCallback(value); } collectTransferables(value); return value; }; const processedData = data.map((item) => processValue(item)); return { functions, transferables, data: processedData }; } // src/common/isPromiseLike.ts function isPromiseLike(value) { return typeof value === "object" && value !== null && "then" in value; } // src/core/WebRPCPort.ts var WebRPCPort = class { constructor(clientId, portId, localInstance, transport) { this.clientId = clientId; this.portId = portId; this.localInstance = localInstance; this.transport = transport; } callbacks = /* @__PURE__ */ new Map(); invocationDefers = /* @__PURE__ */ new Map(); remoteImplementation = new Proxy( {}, { has(property) { switch (property) { case "then": return false; } return typeof property === "string"; }, get: (target, property) => { if (typeof property === "symbol") { return void 0; } if (property === "then") { return void 0; } return (...args) => { return this.invokeRemoteMethod(property, args); }; } } ); receive(message) { switch (message.action) { case "method-call": this.handleMethodCall(message); break; case "method-return": this.handleMethodReturn(message); break; } } async invokeRemoteMethod(methodName, args) { const invocationId = uid("invocation-********"); const { data, transferables, functions } = serializeRequestData(this.portId, args); const defer = new Defer(); functions.forEach((func, functionId) => { this.callbacks.set(functionId, func); }); const req = { invocationId: { clientId: this.clientId, portId: this.portId, method: methodName, id: invocationId }, action: "method-call", method: methodName, args: data, timestamp: Date.now() }; this.transport.send(req, Array.from(transferables)); this.invocationDefers.set(invocationId, defer); return defer.promise; } handleMethodCall(message) { const invocationId = message.invocationId; const args = deserializeRequestData(message.args, (callbackId, args2) => { return this.invokeRemoteMethod(callbackId, args2); }); let method; if (Object.prototype.hasOwnProperty.call(this.localInstance, message.method)) { method = this.localInstance[message.method].bind(this.localInstance); } else { method = this.callbacks.get(message.method); } if (!isFunction(method)) { throw new Error(`Method not found: ${message.method}`); } this.invokeLocalMethod(invocationId, method, args); } invokeLocalMethod(invocationId, method, args) { const handleSuccess = (result) => { const { data, transferables, functions } = serializeRequestData(this.portId, [result]); functions.forEach((func, functionId) => { this.callbacks.set(functionId, func); }); const response = { invocationId, action: "method-return", status: "success", result: data[0], timestamp: Date.now() }; this.transport.send(response, Array.from(transferables)); }; const handleError = (reason) => { const ret = { invocationId, action: "method-return", status: "error", error: { message: reason instanceof Error ? reason.message : String(reason), stack: reason instanceof Error ? reason.stack : void 0 }, timestamp: Date.now() }; this.transport.send(ret, []); }; try { const result = method(...args); if (isPromiseLike(result)) { result.then(handleSuccess, handleError); } else { handleSuccess(result); } } catch (e) { handleError(e); } } handleMethodReturn(message) { const defer = this.invocationDefers.get(message.invocationId.id); if (defer) { if (message.status === "success") { const result = deserializeRequestData([message.result], (callbackId, callbackArgs) => { return this.invokeRemoteMethod(callbackId, callbackArgs); })[0]; defer.resolve(result); } else { defer.reject(message.error); } } this.invocationDefers.delete(message.invocationId.id); } }; // src/core/WebRPC.ts var WebRPC = class { /** * Creates a new WebRPC instance. * * @param clientId - A unique identifier for this client instance * @param transport - Either a Transport object or a function that sends data to the remote endpoint */ constructor(clientId, transport) { this.clientId = clientId; if (isFunction(transport)) { this.transport = new SendFunctionTransport(transport); } else { this.transport = transport; } this.transport.onMessage((data) => { this.receive(data); }); } ports = /* @__PURE__ */ new Map(); transport; receive(data) { if (!isRPCMessage(data)) { return; } if (data.invocationId.clientId !== this.clientId) { return; } const portId = data.invocationId.portId; const port = this.ports.get(portId); if (port) { port.receive(data); } } /** * Registers a service instance that can be called remotely. * * @param id - Unique identifier for the service * @param instance - Object containing methods to be exposed remotely * * @example * ```typescript * webRPC.register('calculator', { * add: (a: number, b: number) => a + b, * subtract: (a: number, b: number) => a - b * }); * ``` */ register(id, instance) { const port = new WebRPCPort(this.clientId, id, instance, this.transport); this.ports.set(id, port); } /** * Gets a proxy object for calling remote methods on a registered service. * * @param id - Identifier of the remote service * @returns A proxy object that allows calling remote methods as if they were local * * @example * ```typescript * const remoteCalc = webRPC.get<{ * add: (a: number, b: number) => Promise<number>; * subtract: (a: number, b: number) => Promise<number>; * }>('calculator'); * * const result = await remoteCalc.add(10, 5); // Returns 15 * ``` */ get(id) { if (!this.ports.has(id)) { const port2 = new WebRPCPort(this.clientId, id, {}, this.transport); this.ports.set(id, port2); } const port = this.ports.get(id); return port.remoteImplementation; } /** * Closes the WebRPC connection and cleans up resources. * * @example * ```typescript * webRPC.close(); * ``` */ close() { this.transport.close(); } }; // src/transports/index.ts var transports_exports = {}; __export(transports_exports, { BrowserExtensionTransport: () => BrowserExtensionTransport, PostMessageTransport: () => PostMessageTransport, WindowPostMessageTransport: () => WindowPostMessageTransport }); // src/transports/BrowserExtensionTransport.ts var BrowserExtensionTransport = class { port; listener; constructor(options) { this.port = options.port; } send(data, transfer) { if (transfer?.length) { console.warn("BrowserExtensionTransport does not support transferable objects."); } this.port.postMessage(data); } onMessage(callback) { this.listener = (message) => { callback(message); }; this.port.onMessage.addListener(this.listener); return () => { if (this.listener) { this.port.onMessage.removeListener(this.listener); this.listener = void 0; } }; } close() { if (this.listener) { this.port.onMessage.removeListener(this.listener); this.listener = void 0; } this.port.disconnect(); } }; // src/transports/PostMessageTransport.ts var PostMessageTransport = class { constructor(sender) { this.sender = sender; if (!isPostMessageTarget(sender)) { throw new Error(`Invalid post message target: ${typeof sender} is not a valid post message target`); } if (isMessagePort(this.sender)) { this.sender.start(); } } cleanup = []; send(data, transfer) { const sender = this.sender; if (isBroadcastChannel(sender)) { sender.postMessage(data); } else { sender.postMessage(data, transfer ?? []); } } onMessage(callback) { const target = this.sender; const listener = (event) => { if (event instanceof MessageEvent) { callback(event.data); } }; target.addEventListener("message", listener); this.cleanup.push(() => { target.removeEventListener("message", listener); }); return () => { target.removeEventListener("message", listener); }; } close() { this.cleanup.forEach((cleanup) => cleanup()); this.cleanup.length = 0; } }; function isMessagePort(target) { return target instanceof MessagePort; } function isBroadcastChannel(target) { return target instanceof BroadcastChannel; } function isServiceWorker(target) { return target instanceof ServiceWorker; } function isWorker(target) { return target instanceof Worker; } function isDedicatedWorkerGlobalScope(target) { if (typeof DedicatedWorkerGlobalScope === "undefined") { return false; } return target instanceof DedicatedWorkerGlobalScope; } function isPostMessageTarget(target) { return isMessagePort(target) || isBroadcastChannel(target) || isServiceWorker(target) || isWorker(target) || isDedicatedWorkerGlobalScope(target); } // src/transports/WindowPostMessageTransport.ts var WindowPostMessageTransport = class { remote; origin; source; listener; constructor(options) { this.remote = options.remote; this.origin = options.origin ?? "*"; this.source = options.source ?? window; } send(data, transfer) { this.remote.postMessage(data, this.origin, transfer); } onMessage(callback) { this.listener = (event) => { if (this.origin !== "*" && event.origin !== this.origin) { return; } if (event.source !== this.remote) { return; } callback(event.data); }; this.source.addEventListener("message", this.listener); return () => { if (this.listener) { this.source.removeEventListener("message", this.listener); this.listener = void 0; } }; } close() { if (this.listener) { this.source.removeEventListener("message", this.listener); this.listener = void 0; } } }; // src/index.ts Object.assign(WebRPC, transports_exports, { WebRPC }); var index_default = WebRPC; return __toCommonJS(index_exports); })(); globalThis.WebRPC = WebRPCLib.WebRPC; //# sourceMappingURL=index.global.js.map