UNPKG

@ch1/rpc

Version:

JavaScript Remote Procedure Call (RPC)

853 lines (840 loc) 25.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * Project interfaces */ /** * tl;dr these are used in `RemoteDesc` to describe given API functions * * js-rpc uses these strings to identify types of "real world" functions that * it can interface with. * * These types are actually used by consumers of the library since they are the * specific strings enforced by `RemoteDesc` * * - ### "nodeCallback" * nodeCallback refers to JavaScript callback functions that strictly follow * two rules: * * - the _last_ parameter passed to a function is the callback * - a "Nullable Error" is the _first_ parameter passed to the callback * * Example: * * ```js * function add(a, b, callback) { * setTimeout(() => { * if (isNumber(a) && (isNumber(b)) { * callback(null, result); * } else { * callback(new TypeError('add is for numbers')); * } * }, 0); * } * * add(1, 2, (err, result) => expect(result).toBe(3); * add(1, 2, (err, result) => expect(error).toBeFalsey(); * add('1', 2), (erro, result) => expect(error instanceof Error).toBe(true); * ``` * * - ### "nodeEvent"/"nodeEventInternal" * nodeEvents are a _limited_ interface to * * [node EventEmitters](https://nodejs.org/api/events.html "docs") * * - ### "observable" * In this case observables refer to * [RxJS5](https://github.com/ReactiveX/rxjs, "RxJS") * * - ### "promise" * promises are es6 promises or any A+ promises that implement `.then` and * `.catch` along with the global `Promise` */ // export type RPCAsyncType = // | 'nodeCallback' // | 'nodeEvent' // | 'nodeEventInternal' // | 'observable' // | 'promise'; const RPCAsyncType = { observable: 100, promise: 200, }; /** * The Different Types of RPC Events */ // export type RPCEventType = // | 'ack' // | 'addEventListener' // | 'create' // | 'createReturn' // | 'destroy' // | 'destroyReturn' // | 'fnReturn' // | 'invoke' // | 'nodeCallback' // | 'nodeOn' // | 'nodeRemoveListener' // | 'browserRemoveListener' // | 'observe' // | 'promise' // | 'subscribe' // | 'subscribeReturn' // | 'unSubscribeReturn'; const RPCEventType = Object.freeze({ ack: 1, create: 2, createReturn: 3, destroy: 4, destroyReturn: 5, fnReturn: 6, invoke: 7, promise: 9, subscribe: 10, unsubscribe: 11, }); /** * Project wide constants */ /** * Dfault type of async function */ const DEFAULT_ASYNC_TYPE = RPCAsyncType.promise; /** * Default message used across postMessage */ const DEFAULT_MESSAGE = 'js-rpc-message'; /** * ms to wait before retrying *first* create signal */ const DEFAULT_CREATE_RETRY = 10; /** * multiplier used to "curve" `DEFAULT_CREATE_RETRY` */ const DEFAULT_CREATE_RETRY_CURVE = 1.2; /** * ms to wait before giving up on create exchange */ const DEFAULT_CREATE_WAIT = 30000; const isRPCDefaultAsync = arg => { if (arg === DEFAULT_ASYNC_TYPE) { return true; } return false; }; function isDefer(thing) { if (!thing) { return false; } if (!isFunction(thing.resolve)) { return false; } if (!isFunction(thing.reject)) { return false; } if (!isPromise(thing.promise)) { return false; } return true; } function isError(err) { return err instanceof Error; } function isFunction(fn) { return typeof fn === 'function'; } function isObject(obj) { if (!obj) { return false; } return typeof obj === 'object'; } function isDictionary(dict) { return isObject(dict); } function isPromise(promise) { if (!promise) { return false; } if (!isFunction(promise.then)) { return false; } if (!isFunction(promise.catch)) { return false; } return true; } function isRPCEvent(event) { if (!event) { return false; } if (typeof event.uid !== 'string') { return false; } if (typeof event.type !== 'number') { return false; } if (isRPCErrorPayload(event.payload) || isRPCInvocationPayload(event.payload) || isRPCReturnPayload(event.payload)) { return true; } return false; } function isRPCError(error) { if (!error) { return false; } if (!error.message) { return false; } return true; } function isRPCErrorPayload(payload) { if (!payload) { return false; } return isRPCError(payload.error); } function isRPCInvocationPayload(payload) { if (!payload) { return false; } if (!Array.isArray(payload.args)) { return false; } if (typeof payload.fn !== 'string') { return false; } return true; } function isRPCReturnPayload(payload) { if (!payload) { return false; } return Array.isArray(payload.result); } function noop() { } const pnoop = () => new Promise(resolve => resolve()); function createUidGenerator() { let uidCount = 0; const rpcId = Math.floor(Math.random() * 100000).toString(32); return () => { // increment the counter uidCount += 1; // return a uid return [rpcId, uidCount.toString(32)].join('-'); }; } function defer() { let pass = noop; let fail = noop; const promise = new Promise((resolve, reject) => { pass = resolve; fail = reject; }); return { promise, reject: fail, resolve: pass, }; } function createRangeError(message) { return new RangeError('js-rpc: ' + message); } function createTypeError(message) { return new TypeError('js-rpc: ' + message); } function rangeError(message) { throw createRangeError(message); } function safeInvoke(fn, args) { try { return fn.apply(null, args); } catch (err) { return err; } } function throwIfNotDefer(d, message) { if (!isDefer(d)) { typeError(message || 'given value is not a defer'); } } function throwIfNotError(err, message) { if (!isError(err)) { typeError(message || 'given value is not an Error'); } } function throwIfNotFunction(fn, message) { if (!isFunction(fn)) { typeError(message || 'given value is not a function'); } } function throwIfNotRPCEvent(event, message) { if (!isRPCEvent(event)) { typeError(message || 'given value is not an RPCEvent'); } } function typeError(message) { throw createTypeError(message); } function createRPCError(c, error) { throwIfNotError(error); let stack = ''; const err = error.constructor; const errorType = err.name || 'Error'; if (c.enableStackTrace) { stack = error.stack; } return { code: error.code, message: error.message, stack, type: errorType, }; } // prefix a's stack with b's function prefixStackWith(a, b) { a.stack = b.stack + a.stack; } function createErrorFromRPCError(c, error) { let err; switch (error.type) { case 'EvalError': err = new EvalError(error.message); break; case 'RangeError': err = new RangeError(error.message); break; case 'ReferenceError': err = new ReferenceError(error.message); break; case 'SyntaxError': err = new SyntaxError(error.message); break; case 'TypeError': err = new TypeError(error.message); break; case 'URIError': err = new URIError(error.message); break; default: err = new Error(error.message); break; } prefixStackWith(err, error); if (error.code) { err.code = error.code; } return err; } /** * Event functions */ /** * create an event object */ function createEvent(type, payload, givenUid) { return { payload, type, uid: givenUid, }; } /** * create an error event object */ function createErrorEvent(c, type, error, id) { return createEvent(type, { error: createRPCError(c, error) }, id); } /** * Functions for executing remote procedures and returning their results */ function validateRegistration(callbacks, asyncFn, type, uid) { if (callbacks[uid]) { rangeError('Remote Procedure: async uid already exists!'); } switch (type) { case RPCAsyncType.promise: throwIfNotDefer(asyncFn); break; // case 'nodeCallback': // throwIfNotFunction(asyncFn); // break; // case 'nodeEvent': // throwIfNotFunction(asyncFn); // break; } } function registerAsync(callbacks, callback, type, uid) { validateRegistration(callbacks, callback, type, uid); callbacks[uid] = { async: callback, type, }; } function doPost(uid, postMethod, type, remoteFunction, args) { const event = createEvent(type, { args, fn: remoteFunction, }, uid()); postMethod(event); return event; } // export function callbackRemote( // callbacks: RPCAsyncContainerDictionary, // postMethod: ConfiguredRPCEmit, // type: RPCAsyncType, // remoteFunction: string, // args, // ) { // if (args.length === 0) { // typeError('RPC: Invalid Invocation: Callback required!'); // } // const cb = args.pop(); // const event = doPost(postMethod, type, remoteFunction, args); // registerAsync(callbacks, cb, type, event.uid); // } function promiseRemote(uid, callbacks, postMethod, eventType, asyncType, remoteFunction, args) { const d = defer(); const event = doPost(uid, postMethod, eventType, remoteFunction, args); registerAsync(callbacks, d, asyncType, event.uid); return d.promise; } function create(c, callbacks, fullFnName, fnType) { switch (fnType) { case RPCAsyncType.promise: return (...args) => promiseRemote(c.uid, callbacks, c.emit, RPCEventType.promise, RPCAsyncType.promise, fullFnName, args); // case 'nodeCallback': // return (...args) => // callbackRemote(callbacks, c.emit, 'nodeCallback', fullFnName, args); // case 'nodeEvent': // return (...args) => // callbackRemote(callbacks, c.emit, 'nodeEvent', fullFnName, args); default: if (fnType) { throw new Error('remote-procedure: Unsupported function type ' + fnType); } console.log('return default'); return create(c, callbacks, fullFnName, c.defaultAsyncType); } } /** * Functions related to creating and using remote objects */ function getFinalFunction(remote, fnName) { const names = fnName.split('.').filter(Boolean); // invalid function name case if (names.length === 0) { return createTypeError(`getFinalFunction: ${fnName} not found on remote`); } // "first level" case if (names.length === 1) { if (isFunction(remote[names[0]])) { return remote[names[0]]; } return createTypeError(`getFinalFunction: ${fnName} is not a function: ` + typeof remote[names[0]]); } // invalid sub object case if (!remote[names[0]]) { return createTypeError(`getFinalFunction: ${names[0]} is not a ` + 'namespace (sub-object) on remote'); } // sub object case if (names.length === 2) { return getFinalFunction(remote[names[0]], names[1]); } // deeper object case return getFinalFunction(remote[names.shift()], names.join('.')); } /** * Safely finds and executes a proxy for a remote procedure */ function safeCall(c, fnName, args) { const fn = getFinalFunction(c.remote, fnName); if (!Array.isArray(args)) { args = []; } if (fn instanceof Error) { return fn; } return safeInvoke(fn, args); } function create$1(c, callbacks, remoteDesc, remote, prefix) { remoteDesc = remoteDesc || Object.create(null); remote = remote || Object.create(null); return Object.keys(remoteDesc).reduce((state, prop) => { const desc = remoteDesc[prop]; prefix = prefix || ''; if (isDictionary(desc)) { state[prop] = create$1(c, callbacks, desc, remote[prop], prop + '.'); } else if (isRPCDefaultAsync(desc)) { state[prop] = create(c, callbacks, prefix + prop, desc); } return state; }, remote); } function createRemoteDescFrom(c, remote, remoteDesc) { remote = remote || Object.create(null); remoteDesc = remoteDesc || Object.create(null); const newRemoteDesc = Object.create(null); /* tslint:disable forin */ for (let prop in remote) { if (isObject(remote[prop])) { let newDesc; if (isDictionary(remoteDesc[prop])) { newDesc = remoteDesc[prop]; } else { newDesc = Object.create(null); } newRemoteDesc[prop] = createRemoteDescFrom(c, remote[prop], newDesc); } if (isFunction(remote[prop])) { const type = remoteDesc[prop] || c.defaultAsyncType; newRemoteDesc[prop] = type; } } return newRemoteDesc; } /** * Low level functions that respond to remote procedure calls: */ const fnReturn = (c, payload, id, callbacks) => returnPayload(c, payload, callbacks, id); function createFunctionalState() { return { responders: { // [RPCEventType.ack]: nodeOn, // [RPCEventType.ack]: nodeRemoveListener, // [RPCEventType.ack]: nodeCallback, }, successHandlers: {}, errorHandlers: {}, }; } function register(dictionary, key, value) { if (dictionary[key]) { return; } dictionary[key] = value; } function create$2(config, callbacks, remoteDesc) { bootstrap(config.functionalState); const id = config.uid(); const initState = createInitializationState(config, remoteDesc, id); return initialize(config, initState).then(localRemoteDesc => { const off = on(sendAck, config, callbacks, id); /** @todo implement RPC destroy here */ return { off: () => new Promise(resolve => resolve(off())), remoteDesc: localRemoteDesc, }; }); } function createInitializationState(config, remoteDesc, id) { const d = defer(); const readTimeout = setTimeout(() => d.reject(new Error('RPC initialization failed, maximum delay of ' + `${config.defaultCreateWait}ms exceeded`)), config.defaultCreateWait); let delay = config.defaultCreateRetry; let createTimeout = setTimeout(fireCreate, delay); function fireCreate() { config.emit(createEvent(RPCEventType.create, { result: [remoteDesc] }, config.uid())); delay *= config.defaultCreateRetryCurve; createTimeout = setTimeout(fireCreate, delay); } function clean() { clearTimeout(readTimeout); stopCreateSpam(); } function stopCreateSpam() { clearTimeout(createTimeout); } return { clean, defer: d, id, isCreated: false, hasCreated: false, localRemoteDesc: null, stopCreateSpam, }; } function initialize(config, initState) { const off = config.on((event) => { const { payload } = event; if (isRPCErrorPayload(payload)) { throw createErrorFromRPCError(config, payload.error); } if (!isRPCReturnPayload(payload)) { rangeError('unexpected payload received during initialization ' + JSON.stringify(event)); } if (isRPCReturnPayload(payload)) { if (event.type === RPCEventType.create) { if (initState.hasCreated) { return; } // create local remote initState.localRemoteDesc = payload.result[0]; initState.hasCreated = true; config.emit(createEvent(RPCEventType.createReturn, { result: [initState.id] }, config.uid())); } else if (event.type === RPCEventType.createReturn) { if (initState.isCreated) { return; } initState.stopCreateSpam(); initState.isCreated = true; } else { rangeError('unexpected event received during initialization: ' + event.type); } } if (initState.isCreated && initState.hasCreated) { initState.clean(); off(); initState.defer.resolve(initState.localRemoteDesc); } }); return initState.defer.promise; } function ack(c, event) { if (!c.useAcks) { typeError('ack even received but useAcks is disabled'); } const payload = event.payload; if (isRPCReturnPayload(payload)) { const id = payload.result[0]; if (!c.useAcks[id]) { typeError(`ack expecting to find ack timeout for: ${id}`); } clearTimeout(c.useAcks[id]); delete c.useAcks[id]; } else { typeError('ack received invalid payload'); } } function sendAck(c, id) { c.emit(createEvent(RPCEventType.ack, { result: [id] }, c.uid())); } function invoke(c, payload, id) { if (isRPCInvocationPayload(payload)) { const result = safeCall(c, payload.fn, payload.args); if (result instanceof Error) { c.emit(createErrorEvent(c, RPCEventType.fnReturn, result, id)); return; } c.emit(createEvent(RPCEventType.fnReturn, { result: [result] }, id)); } else { c.emit(createErrorEvent(c, RPCEventType.fnReturn, new TypeError('invoke: invalidPayload'), id)); } } function fireError(c, payload, asyncReturn) { const error = createErrorFromRPCError(c, payload.error); const asyncFn = asyncReturn.async; if (c.functionalState.errorHandlers[asyncReturn.type]) { c.functionalState.errorHandlers[asyncReturn.type](asyncFn, error); } else { throw error; } } function fireSuccess(c, payload, asyncReturn) { const asyncFn = asyncReturn.async; if (c.functionalState.successHandlers[asyncReturn.type]) { c.functionalState.successHandlers[asyncReturn.type](asyncFn, payload.result); } else { rangeError('fireSuccess: no async handler'); } } function returnPayload(c, payload, callbacks, id) { if (!callbacks[id]) { rangeError(`invokeReturn: no matching callback for ${id}`); } if (isRPCErrorPayload(payload)) { fireError(c, payload, callbacks[id]); delete callbacks[id]; return; } else if (isRPCReturnPayload(payload)) { fireSuccess(c, payload, callbacks[id]); delete callbacks[id]; return; } typeError('returnPayload: unexpected payload for event: ' + event.type); } function promise(c, payload, id) { if (isRPCInvocationPayload(payload)) { const result = safeCall(c, payload.fn, payload.args); if (result instanceof Error) { c.emit(createErrorEvent(c, RPCEventType.fnReturn, result, id)); return; } if (isFunction(result.then)) { result .then((...args) => c.emit(createEvent(RPCEventType.fnReturn, { result: args }, id))) .catch(err => c.emit(createErrorEvent(c, RPCEventType.fnReturn, err, id))); } else { // support direct invocation fallback c.emit(createEvent(RPCEventType.fnReturn, { result: [result] }, id)); } } else { c.emit(createErrorEvent(c, RPCEventType.fnReturn, new TypeError('promise: invalidPayload'), id)); } } function tryHandler(fn, args, event) { try { return fn.apply(null, args); } catch (err) { throw new Error(`RPC: No registered handler for event: ${event.type}: ` + err.message); } } function on(sendAckFn, c, callbacks, id) { return c.on(event => { throwIfNotRPCEvent(event, `expecting an RPCEvent: Received: ${typeof event} `); tryHandler(c.functionalState.responders[event.type], [c, event.payload, event.uid, callbacks, id], event); if (event.useAcks && event.type !== RPCEventType.ack) { sendAckFn(c, event.uid); } }); } function bootstrap(functionalState) { const registerResponder = register.bind(null, functionalState.responders); const registerSuccessHandler = register.bind(null, functionalState.successHandlers); const registerErrorHandler = register.bind(null, functionalState.errorHandlers); /** Bootstrap the inbuilt respoonders */ registerResponder(RPCEventType.ack, ack); registerResponder(RPCEventType.invoke, invoke); registerResponder(RPCEventType.fnReturn, fnReturn); registerResponder(RPCEventType.promise, promise); registerErrorHandler(RPCAsyncType.promise, (asyncFn, error) => { if (isDefer(asyncFn)) { asyncFn.reject(error); } else { throw new RangeError('registerErrorHandler: incorrect function type'); } }); // case 'nodeCallback': // if (isRPCNodeCallback<any>(asyncFn)) { // asyncFn(error); // return; // } // break; registerSuccessHandler(RPCAsyncType.promise, (asyncFn, payload) => { if (isDefer(asyncFn)) { asyncFn.resolve.apply(asyncFn.resolve, payload); } else { throw new RangeError('registerSuccessHandler: incorrect function type'); } }); // case 'nodeCallback': // if (isRPCNodeCallback(asyncFn)) { // asyncFn.apply(null, [null].concat(payload.result)); // return; // } // break; // case 'nodeEvent': // if (isRPCNotify(asyncFn)) { // asyncFn.apply(null, [null].concat(payload.result)); // return; // } // break; return functionalState; } /** * Main module "really ties the project together" */ /** * Where `RemoteType` is the description of the interface you _expect_ to be * exposed *by the "other side"* * @param config * @param remote optional remote to expose *on the "other side"* */ function create$3(config, remote, remoteDesc) { remote = validateRemote(remote); config = validateConfig(config, remote); const onDestroyDict = Object.create(null); const local = Object.create(null); const { callbacks } = config; const combinedDesc = createRemoteDescFrom(config, remote, remoteDesc); let destroy = pnoop; const isReady = create$2(config, callbacks, combinedDesc) .then(network => { create$1(config, callbacks, network.remoteDesc, local); destroy = network.off; }); return { config, destroy: (reason = '') => { Object.keys(local).forEach(key => delete local[key]); flushCallbacks(callbacks, config.functionalState.errorHandlers, reason); return destroy().then(() => { destroy = pnoop; Object.keys(onDestroyDict).forEach(key => { safeInvoke(onDestroyDict[key], [reason]); delete onDestroyDict[key]; }); }); }, onDestroy: (callback) => { const id = Date.now().toString(32) + Math.floor(Math.random() * 50000).toString(32); onDestroyDict[id] = callback; return () => { delete onDestroyDict[id]; }; }, ready: isReady, remote: local, }; } function flushCallbacks(callbacks, errorHandlers, reason = '') { Object.keys(callbacks).forEach(id => { const cb = callbacks[id]; errorHandlers[cb.type] ? errorHandlers[cb.type](cb.async, new Error('rpc flush ' + reason)) : console.warn('rpc: flush: no error handler registered for ' + cb.type); delete callbacks[id]; }); } function validateRemote(r) { r = r || Object.create(null); if (!isObject(r)) { typeError('validateRemote: remote must be an object'); } return r; } function validateConfig(c, remote) { throwIfNotFunction(c.on, 'validateConfig: config requires an on method'); throwIfNotFunction(c.emit, 'validateConfig: config requires an emit method'); c.defaultAsyncType = c.defaultAsyncType || DEFAULT_ASYNC_TYPE; c.defaultCreateRetry = c.defaultCreateRetry || DEFAULT_CREATE_RETRY; c.defaultCreateRetryCurve = c.defaultCreateRetryCurve || DEFAULT_CREATE_RETRY_CURVE; c.defaultCreateWait = c.defaultCreateWait || DEFAULT_CREATE_WAIT; c.enableStackTrace = c.enableStackTrace || false; c.functionalState = createFunctionalState(); c.callbacks = Object.create(null); c.maxAckDelay = c.maxAckDelay || 5000; c.message = c.message || DEFAULT_MESSAGE; c.uid = createUidGenerator(); c.useAcks = c.useAcks ? Object.create(null) : null; c.remote = remote; return Object.freeze(c); } /** * Program Entry Point */ exports.DEFAULT_MESSAGE = DEFAULT_MESSAGE; exports.create = create$3; //# sourceMappingURL=index.cjs.js.map