@ch1/rpc
Version:
JavaScript Remote Procedure Call (RPC)
853 lines (840 loc) • 25.4 kB
JavaScript
;
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