@darlean/core
Version:
Darlean core functionality for creating applications that define, expose and host actors
235 lines (234 loc) • 10.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransportRemote = exports.TRANSPORT_ERROR_PARAMETER_MESSAGE = exports.TRANSPORT_ERROR_TRANSPORT_CALL_INTERRUPTED = exports.TRANSPORT_ERROR_TRANSPORT_CALL_TIMEOUT = exports.TRANSPORT_ERROR_TRANSPORT_ERROR = void 0;
const utils_1 = require("@darlean/utils");
const uuid = __importStar(require("uuid"));
const instances_1 = require("./instances");
exports.TRANSPORT_ERROR_TRANSPORT_ERROR = 'TRANSPORT_ERROR';
exports.TRANSPORT_ERROR_TRANSPORT_CALL_TIMEOUT = 'TRANSPORT_CALL_TIMEOUT';
exports.TRANSPORT_ERROR_TRANSPORT_CALL_INTERRUPTED = 'TRANSPORT_CALL_INTERRUPTED';
exports.TRANSPORT_ERROR_PARAMETER_MESSAGE = 'Message';
const DEBUG_PENDING_CALLS = true;
const ABORT_PENDING_CALLS = true;
class TransportRemote {
/**
* Creates a new TransportRemote.
* @param appId The id of the current application with which the remote makes itself known to the transport
* @param transport The transport mechanism used to send/receive messages
* @param container The multi-type instance container that this remote can dispatch incoming action requests to
*/
constructor(appId, transport, container) {
this.appId = appId;
this.transport = transport;
this.pendingCalls = new Map();
this.instanceContainer = container;
}
async init() {
this.session = await this.transport.connect(this.appId, (tags) => this.handleMessage(tags));
}
async finalize() {
await this.session?.finalize();
this.session = undefined;
const pending = this.pendingCalls;
this.pendingCalls = new Map();
if (pending.size > 0) {
console.log('THERE ARE STILL', pending.size, 'PENDING CALLS');
for (const p of pending.values()) {
if (p.options) {
console.log('PENDING CALL', JSON.stringify(p.options));
}
}
// The following code cancels aLL pending calls which effectively helps to
// prevent the application from hanging at exit, but doing so is a sign that
// somewhere else things are not cleaned up properly.
if (ABORT_PENDING_CALLS) {
for (const p of pending.values()) {
clearTimeout(p.timeout);
p.reject(exports.TRANSPORT_ERROR_TRANSPORT_CALL_INTERRUPTED);
}
}
}
}
invoke(options) {
return (0, utils_1.deeper)('io.darlean.remotetransport.invoke', options.destination).perform(() => this.invokeImpl(options));
}
invokeImpl(options) {
const callId = uuid.v4();
const tags = {
transport_receiver: options.destination,
transport_return: this.appId,
remotecall_kind: 'call',
remotecall_id: callId
};
return new Promise((resolve, reject) => {
const call = {
resolve,
reject,
timeout: setTimeout(() => {
this.pendingCalls.delete(callId);
resolve({
errorCode: exports.TRANSPORT_ERROR_TRANSPORT_CALL_TIMEOUT
});
}, 60 * 1000),
options: DEBUG_PENDING_CALLS ? options : undefined
};
this.pendingCalls.set(callId, call);
if (options.aborter) {
options.aborter.handle(() => {
this.pendingCalls.delete(callId);
clearTimeout(call.timeout);
resolve({
errorCode: exports.TRANSPORT_ERROR_TRANSPORT_CALL_INTERRUPTED
});
});
}
if (this.session) {
this.session.send(tags, this.toTransportRequest(options.content));
}
else {
clearTimeout(call.timeout);
resolve({
errorCode: exports.TRANSPORT_ERROR_TRANSPORT_ERROR,
errorParameters: {
[exports.TRANSPORT_ERROR_PARAMETER_MESSAGE]: 'Transport not ready'
}
});
}
});
}
toTransportRequest(content) {
return content;
}
fromTransportRequest(content) {
return content;
}
toTransportResponse(content) {
return {
result: content.result,
error: content.error ? this.errorToTransportError(content.error) : undefined
};
}
errorToTransportError(error) {
// Explicitly copy over all fields. The default BSON implementation skips some fields.
return {
code: error.code,
kind: error.kind,
message: error.message,
nested: error.nested?.map((x) => this.errorToTransportError(x)) ?? undefined,
parameters: error.parameters,
stack: error.stack,
template: error.template
};
}
fromTransportResponse(content) {
return content;
}
handleMessage(tags) {
function makeReturnTags(rec) {
const e1 = {
transport_receiver: rec,
remotecall_id: tags?.remotecall_id,
remotecall_kind: 'return'
};
return e1;
}
if (tags) {
if (tags.remotecall_kind === 'return') {
// Handling of a return message that contains the response (or error result) of a previous outgoing call
const call = this.pendingCalls.get(tags.remotecall_id);
if (call !== undefined) {
clearTimeout(call.timeout);
this.pendingCalls.delete(tags.remotecall_id);
if (tags.code) {
call.resolve({
errorCode: exports.TRANSPORT_ERROR_TRANSPORT_ERROR,
errorParameters: {
[exports.TRANSPORT_ERROR_PARAMETER_MESSAGE]: tags.message
}
});
}
else {
const result = this.fromTransportResponse(tags);
call.resolve({
content: result
});
}
}
}
else {
// Handle a new message that is to be sent to a local actor
setImmediate(() => {
const request = this.fromTransportRequest(tags);
(0, utils_1.deeper)('io.darlean.remotetransport.incoming-action', `${request.actorType}::${request.actorId}::${request.actionName}`).perform(async () => {
try {
const wrapper = this.instanceContainer.wrapper(request.actorType, request.actorId, request.lazy ?? false);
try {
const result = await wrapper.invoke(request.actionName, request.arguments);
const response = {
result
};
if (tags.transport_return) {
this.session?.send(makeReturnTags(tags.transport_return), this.toTransportResponse(response));
}
}
catch (e) {
const err = (0, instances_1.toActionError)(e);
const msg = `in ${request.actorType}::${request.actionName} (application: ${this.appId})`;
if (err.stack) {
const lines = err.stack.split('\n');
const lines2 = [lines[0], ' ' + msg, ...lines.slice(1)];
err.stack = lines2.join('\n');
}
else {
err.stack = msg;
}
// The proxy already catches application errors and properly encapsulates those
// within an ApplicationError. Also, when framework errors occur, they are
// delivered as FrameworkError. So, we just have to make sure here that anything
// unexpected that passed through is nicely converted.
const response = {
error: err
};
if (tags.transport_return) {
this.session?.send(makeReturnTags(tags.transport_return), this.toTransportResponse(response));
}
}
}
catch (e) {
const response = {
error: (0, instances_1.toFrameworkError)(e)
};
if (tags.transport_return) {
this.session?.send(makeReturnTags(tags.transport_return), this.toTransportResponse(response));
}
}
});
});
}
}
}
}
exports.TransportRemote = TransportRemote;