@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
JavaScript
"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