@ch1/rpc
Version:
JavaScript Remote Procedure Call (RPC)
258 lines • 9.29 kB
JavaScript
/**
* Low level functions that respond to remote procedure calls:
*/
import { safeCall } from './remote';
import { createErrorFromRPCError } from './rpc-error';
import { createEvent, createErrorEvent } from './events';
import { defer, isDefer, isRPCErrorPayload, isRPCInvocationPayload, isRPCReturnPayload, rangeError, throwIfNotRPCEvent, typeError, isFunction, } from './utils';
import { RPCEventType, RPCAsyncType, } from './interfaces';
const fnReturn = (c, payload, id, callbacks) => returnPayload(c, payload, callbacks, id);
export 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;
}
export function create(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,
};
});
}
export 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,
};
}
export 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;
}
export 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');
}
}
export function sendAck(c, id) {
c.emit(createEvent(RPCEventType.ack, { result: [id] }, c.uid()));
}
export 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));
}
}
export 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;
}
}
export 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');
}
}
export 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);
}
export 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);
}
}
export 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);
}
});
}
export 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;
}
//# sourceMappingURL=response.js.map