UNPKG

@darlean/core

Version:

Darlean core functionality for creating applications that define, expose and host actors

235 lines (234 loc) 10.8 kB
"use strict"; 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;