nope-js-browser
Version:
NoPE Runtime for the Browser. For nodejs please use nope-js-node
339 lines (338 loc) • 17.1 kB
JavaScript
import { plugin } from "./plugin";
import { generateId } from "../helpers/index.browser";
import { DEBUG } from "../logger/index.browser";
import { NopePromise } from "../promise";
export const extend = plugin([
"dispatcher.rpcManager.NopeRpcManager",
"dispatcher.connectivityManager.NopeConnectivityManager",
], (clNopeRpcManager, clConnectivityManager) => {
class NopeRpcManager extends clNopeRpcManager {
constructor() {
super(...arguments);
/**
* Flag to keep callbacks open. Defaults to 1 Hour.
* The Value is given in *ms*
*/
this.defaultKeepAlive = 60 * 60 * 1000;
}
_performCall(serviceName, params, options = {}) {
var _a;
// Get a Call Id
const _taskId = generateId();
const _this = this;
const _options = {
resultSink: this._getServiceName(serviceName, "response"),
callbackOptions: [],
timeToLifeAfterCall: this.defaultKeepAlive,
calledOnce: [],
...options,
};
const _registeredCallbacks = [];
const clear = () => {
var _a;
// Remove the task:
if (_this._runningInternalRequestedTasks.has(_taskId)) {
if ((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(DEBUG)) {
_this._logger.debug(`Clearing Callbacks from ${_taskId}`);
}
// Delete all Callbacks.
_registeredCallbacks.map((id) => _this.unregisterService(id));
const task = _this._runningInternalRequestedTasks.get(_taskId);
// Remove the Timeout.
if (task.timeout) {
clearTimeout(task.timeout);
}
// Remove the Task itself
_this._runningInternalRequestedTasks.delete(_taskId);
}
};
if ((_a = _this._logger) === null || _a === void 0 ? void 0 : _a.enabledFor(DEBUG)) {
_this._logger.debug(`Dispatcher "${this._id}" requesting externally Function "${serviceName}" with task: "${_taskId}"`);
}
// Define a Callback-Function, which will expect the Task.
const ret = new NopePromise(async (resolve, reject) => {
var _a, _b, _c;
try {
const taskRequest = {
resolve,
reject,
clear,
serviceName,
timeout: null,
target: null,
};
// Register the Handlers,
_this._runningInternalRequestedTasks.set(_taskId, taskRequest);
// Define a Task-Request
const packet = {
functionId: serviceName,
params: [],
taskId: _taskId,
resultSink: _options.resultSink,
requestedBy: _this._id,
callbacks: [],
};
// Assign the callbackoptions.
const callbackOptions = {};
for (const item of _options.callbackOptions) {
callbackOptions[item.idx] = item;
}
// Iterate over all Parameters and
// Determin Callbacks. Based on the Parameter-
// Type assign it either to packet.params (
// for parsable Parameters) and packet.callbacks
// (for callback Parameters)
for (const [idx, contentOfParameter] of params.entries()) {
// Test if the parameter is a Function
if (typeof contentOfParameter !== "function") {
packet.params.push({
idx,
data: contentOfParameter,
});
}
else {
let timeToLifeAfterCall = (_a = callbackOptions[idx]) === null || _a === void 0 ? void 0 : _a.timeToLifeAfterCall;
timeToLifeAfterCall =
typeof timeToLifeAfterCall === "number"
? timeToLifeAfterCall
: _options.timeToLifeAfterCall;
let calledOnce = (_b = callbackOptions[idx]) === null || _b === void 0 ? void 0 : _b.calledOnce;
calledOnce =
typeof calledOnce === "boolean"
? calledOnce
: _options.calledOnce.includes(idx);
let id = "";
let timeout = null;
const removeCallback = () => {
this.unregisterService(id).catch((e) => {
this._logger.error("Failed to unregister a dynamic callback");
this._logger.error(e);
});
};
let cb = async (...args) => {
if (timeout) {
clearTimeout(timeout);
}
if (timeToLifeAfterCall > 0) {
timeout = setTimeout(removeCallback, timeToLifeAfterCall);
}
return await contentOfParameter(...args);
};
if (calledOnce) {
cb = async (...args) => {
const res = await contentOfParameter(...args);
await this.unregisterService(id);
return res;
};
}
else if (timeToLifeAfterCall) {
timeout = setTimeout(removeCallback, timeToLifeAfterCall);
}
// The Parameter is a Callback => store a
// Description of the Callback and register
// the callback inside of the Dispatcher
const _func = await _this.registerService(cb, {
schema: {},
id: generateId({ prestring: "callback" }),
});
id = _func["id"];
_registeredCallbacks.push(id);
// Register the Callback
packet.callbacks.push({
id,
idx,
});
}
}
if (!_this.serviceExists(serviceName)) {
// Create an Error:
const error = new Error(`No Service Provider known for "${serviceName}"`);
if (_this._logger) {
_this._logger.error(`No Service Provider known for "${serviceName}"`);
_this._logger.error(error);
}
throw error;
}
if (_this.options.forceUsingSelectors ||
this.services.amountOf.get(serviceName) > 1) {
if (typeof options.target !== "string") {
taskRequest.target = options.target;
}
else if (typeof (options === null || options === void 0 ? void 0 : options.selector) === "function") {
const dispatcherToUse = await options.selector({
rpcManager: this,
serviceName,
});
// Assign the Selector:
taskRequest.target = dispatcherToUse;
}
else {
const dispatcherToUse = await this._defaultSelector({
rpcManager: this,
serviceName,
});
// Assign the Selector:
taskRequest.target = dispatcherToUse;
}
packet.target = taskRequest.target;
}
else {
taskRequest.target = Array.from(this.services.keyMappingReverse.get(serviceName))[0];
}
// Send the Message to the specific element:
await _this._communicator.emit("rpcRequest", packet);
if ((_c = _this._logger) === null || _c === void 0 ? void 0 : _c.enabledFor(DEBUG)) {
_this._logger.debug(`Dispatcher "${this._id}" putting task "${_taskId}" on: "${_this._getServiceName(packet.functionId, "request")}"`);
}
// If there is a timeout =>
if (options.timeout > 0) {
taskRequest.timeout = setTimeout(() => {
_this.cancelTask(_taskId, new Error(`TIMEOUT. The Service allowed execution time of ${options.timeout.toString()}[ms] has been excided`), false);
}, options.timeout);
}
}
catch (e) {
// Clear all Elements of the Function:
clear();
// Throw the error.
reject(e);
}
});
ret.taskId = _taskId;
ret.cancel = (reason) => {
_this.cancelTask(_taskId, reason);
};
return ret;
}
async _handleExternalRequest(data, _function = null) {
var _a, _b;
try {
// Try to get the function if not provided:
if (typeof _function !== "function") {
_function = (_a = this._registeredServices.get(data.functionId)) === null || _a === void 0 ? void 0 : _a.func;
}
if ((_b = this._logger) === null || _b === void 0 ? void 0 : _b.enabledFor(DEBUG)) {
// If there is a Logger:
this._logger.debug(`Dispatcher "${this._id}" received request: "${data.functionId}" -> task: "${data.taskId}"`);
}
const _this = this;
if (typeof _function === "function") {
// Now we check, if we have to perform test, whether
// we are allowed to execute the task:
if (data.target && data.target !== this._id) {
return;
}
// Callbacks
// Callbacks
const cbs = [];
const observer = _this.onCancelTask.subscribe((cancelEvent) => {
if (cancelEvent.taskId == data.taskId) {
// Call Every Callback.
cbs.map((cb) => {
return cb(cancelEvent.reason);
});
// Although we are allowed to Cancel the Subscription
observer.unsubscribe();
}
});
// Only if the Function is present extract the arguments etc.
const args = [];
// First extract the basic arguments
data.params.map((item) => (args[item.idx] = item.data));
// Add the Callbacks. Therefore create a function which will
// trigger the remote.
data.callbacks.map((optionsOfCallback) => (args[optionsOfCallback.idx] = async (..._args) => {
// And Create the Task and its Promise.
const servicePromise = _this.performCall(optionsOfCallback.id, _args, optionsOfCallback);
const cancelCallback = (reason) => {
// The Main Task has been canceled =>
// We are allowed to canel the Subtask as well.
servicePromise.cancel(reason);
// this.cancelTask()
};
cbs.push(cancelCallback);
// Await the Result. If an Task is canceled => The Error is Thrown.
const result = await servicePromise;
// Remove the Index
cbs.splice(cbs.indexOf(cancelCallback), 1);
return result;
}));
// Perform the Task it self.
const _resultPromise = _function(...args);
if (typeof (_resultPromise === null || _resultPromise === void 0 ? void 0 : _resultPromise.cancel) ===
"function") {
// Push the Callback to the Result.
cbs.push((reason) => {
return _resultPromise.cancel(reason);
});
}
// Store, who has requested the task.
_this._runningExternalRequestedTasks.set(data.taskId, data.requestedBy);
let _result = null;
try {
// Wait for the Result to finish.
_result = await _resultPromise;
// Unsubscribe from Task-Cancelation
observer.unsubscribe();
}
catch (error) {
// Unsubscribe from Task-Cancelation
observer.unsubscribe();
// Now throw the Error again.
throw error;
}
// Define the Result message
const result = {
result: typeof _result !== "undefined" ? _result : null,
taskId: data.taskId,
sink: data.resultSink,
};
// Use the communicator to publish the result.
await this._communicator.emit("rpcResponse", result);
}
}
catch (error) {
if (this._logger) {
// If there is a Logger:
this._logger.error(`Dispatcher "${this._id}" failed with request: "${data.taskId}"`);
this._logger.error(error);
}
// Remove the requested task.
this._runningExternalRequestedTasks.delete(data.taskId);
// An Error occourd => Forward the Error.
const result = {
error: {
error,
msg: error.toString(),
},
taskId: data.taskId,
};
// Send the Error via the communicator to the remote.
await this._communicator.emit("rpcResponse", result);
}
}
}
class NopeConnectivityManager extends clConnectivityManager {
/**
* Add our Plugin to the Status Message.
* @returns We now enlist our Plugin.
*/
_info() {
const ret = super._info();
ret.plugins.push("rpcCallbacks");
return ret;
}
}
return [
{
adapted: NopeRpcManager,
name: "NopeRpcManager",
path: "dispatcher.rpcManager.NopeRpcManager",
},
{
adapted: NopeConnectivityManager,
name: "NopeConnectivityManager",
path: "dispatcher.connectivityManager.NopeConnectivityManager",
},
];
}, "rpcCallbacks");