UNPKG

@temporalio/worker

Version:
309 lines 12.7 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.NexusHandler = void 0; exports.constructNexusOperationContext = constructNexusOperationContext; const nexus = __importStar(require("nexus-rpc")); const common_1 = require("@temporalio/common"); const context_1 = require("@temporalio/nexus/lib/context"); const internal_non_workflow_1 = require("@temporalio/common/lib/internal-non-workflow"); const type_helpers_1 = require("@temporalio/common/lib/type-helpers"); const interceptors_1 = require("@temporalio/common/lib/interceptors"); const conversions_1 = require("./conversions"); const UNINITIALIZED = Symbol(); class NexusHandler { taskToken; namespace; taskQueue; context; client; abortController; serviceRegistry; dataConverter; /** * Logger bound to `sdkComponent: worker`, with metadata from this Nexus task. * This is the logger to use for all log messages emitted by the Nexus * worker. Note this is not exactly the same thing as the Nexus context * logger, which is bound to `sdkComponent: nexus`. */ logger; /** * Metric Meter with tags from this Nexus task, including tags from interceptors. */ metricMeter; /** * List of interceptors for this Nexus task. */ interceptors; constructor(taskToken, namespace, taskQueue, context, client, abortController, serviceRegistry, dataConverter, workerLogger, workerMetricMeter, interceptors) { this.taskToken = taskToken; this.namespace = namespace; this.taskQueue = taskQueue; this.context = context; this.client = client; this.abortController = abortController; this.serviceRegistry = serviceRegistry; this.dataConverter = dataConverter; this.logger = common_1.LoggerWithComposedMetadata.compose(workerLogger, this.getLogAttributes.bind(this)); this.metricMeter = common_1.MetricMeterWithComposedTags.compose(workerMetricMeter, this.getMetricTags.bind(this)); this.interceptors = { inbound: [], outbound: [] }; interceptors .map((factory) => factory(this.context)) .forEach(({ inbound, outbound }) => { if (inbound) this.interceptors.inbound.push(inbound); if (outbound) this.interceptors.outbound.push(outbound); }); } getLogAttributes() { const logAttributes = { namespace: this.namespace, taskQueue: this.taskQueue, service: this.context.service, operation: this.context.operation, }; // In case some interceptor uses the logger while initializing... if (this.interceptors == null) return logAttributes; return (0, interceptors_1.composeInterceptors)(this.interceptors.outbound, 'getLogAttributes', (a) => a)(logAttributes); } getMetricTags() { const baseTags = { namespace: this.namespace, taskQueue: this.taskQueue, service: this.context.service, operation: this.context.operation, }; // In case some interceptors use the metric meter while initializing... if (this.interceptors == null) return baseTags; return (0, interceptors_1.composeInterceptors)(this.interceptors.outbound, 'getMetricTags', (a) => a)(baseTags); } async startOperation(ctx, payload) { try { const input = await (0, conversions_1.decodePayloadIntoLazyValue)(this.dataConverter, payload); const result = await this.invokeUserCode('startOperation', this.serviceRegistry.start.bind(this.serviceRegistry, ctx, input)); if (result.isAsync) { return { taskToken: this.taskToken, completed: { startOperation: { asyncSuccess: { operationToken: result.token, links: ctx.outboundLinks.map(nexusLinkToProtoLink), }, }, }, }; } else { return { taskToken: this.taskToken, completed: { startOperation: { syncSuccess: { payload: await (0, internal_non_workflow_1.encodeToPayload)(this.dataConverter, result.value), links: ctx.outboundLinks.map(nexusLinkToProtoLink), }, }, }, }; } } catch (err) { if (err instanceof nexus.OperationError) { return { taskToken: this.taskToken, completed: { startOperation: { operationError: await (0, conversions_1.operationErrorToProto)(this.dataConverter, err), }, }, }; } return { taskToken: this.taskToken, error: await (0, conversions_1.handlerErrorToProto)(this.dataConverter, (0, conversions_1.coerceToHandlerError)(err)), }; } } async cancelOperation(ctx, token) { try { await this.invokeUserCode('cancelOperation', this.serviceRegistry.cancel.bind(this.serviceRegistry, ctx, token)); return { taskToken: this.taskToken, completed: { cancelOperation: {}, }, }; } catch (err) { return { taskToken: this.taskToken, error: await (0, conversions_1.handlerErrorToProto)(this.dataConverter, (0, conversions_1.coerceToHandlerError)(err)), }; } } async invokeUserCode(method, fn) { let error = UNINITIALIZED; // In case someone decides to throw undefined... const startTime = process.hrtime.bigint(); this.logger.debug('Nexus handler started', { method }); try { return await fn(); } catch (err) { error = err; throw err; } finally { const durationNanos = process.hrtime.bigint() - startTime; const durationMs = Number(durationNanos / 1000000n); if (error === UNINITIALIZED) { this.logger.debug('Nexus handler invocation completed', { method, durationMs }); } else if ((error instanceof common_1.CancelledFailure || (0, type_helpers_1.isAbortError)(error)) && this.abortController.signal.aborted) { this.logger.debug('Nexus handler invocation completed as cancelled', { method, durationMs }); } else { this.logger.warn('Nexus handler invocation failed', { method, error, durationMs }); } } } /** * Actually executes the Operation. * * Any call up to this function and including this one will be trimmed out of stack traces. */ async execute(task) { if (task.request?.startOperation != null) { const variant = task.request?.startOperation; return await this.startOperation({ ...this.context, requestId: variant.requestId ?? undefined, inboundLinks: (variant.links ?? []).map(protoLinkToNexusLink), callbackUrl: variant.callback ?? undefined, callbackHeaders: variant.callbackHeader ?? undefined, outboundLinks: [], }, variant.payload ?? undefined); } else if (task.request?.cancelOperation != null) { const variant = task.request?.cancelOperation; if (variant.operationToken == null) { throw new nexus.HandlerError('BAD_REQUEST', 'Request missing Operation token'); } return await this.cancelOperation({ ...this.context, }, variant.operationToken); } else { throw new nexus.HandlerError('NOT_IMPLEMENTED', 'Request method not implemented'); } } async run(task) { // Ensure that client calls made with the worker's client in this handler's context are tied to the abort signal. // TODO: Actually support canceling requests backed by NativeConnection. Once it does, this functionality should be tested. return await this.client.withAbortSignal(this.abortController.signal, async () => { return await context_1.asyncLocalStorage.run({ client: this.client, namespace: this.namespace, taskQueue: this.taskQueue, log: common_1.LoggerWithComposedMetadata.compose(this.logger, { sdkComponent: common_1.SdkComponent.nexus }), metrics: this.metricMeter, }, this.execute.bind(this, task)); }); } } exports.NexusHandler = NexusHandler; function constructNexusOperationContext(request, abortSignal) { const base = { abortSignal, headers: headersProxy(request?.header), }; if (request?.startOperation != null) { const op = request.startOperation; if (op?.service == null) { throw new common_1.IllegalStateError('expected request service to not be empty'); } if (op?.operation == null) { throw new common_1.IllegalStateError('expected request service to not be empty'); } return { ...base, service: op.service, operation: op.operation }; } if (request?.cancelOperation != null) { const op = request.cancelOperation; if (op?.service == null) { throw new common_1.IllegalStateError('expected request service to not be empty'); } if (op?.operation == null) { throw new common_1.IllegalStateError('expected request service to not be empty'); } return { ...base, service: op.service, operation: op.operation }; } throw new nexus.HandlerError('NOT_IMPLEMENTED', 'Request method not implemented'); } // TODO: That utility should be moved to the nexus-rpc package. function headersProxy(initializer) { const headers = initializer ? Object.fromEntries(Object.entries(initializer).map(([k, v]) => [k.toLowerCase(), v])) : {}; return new Proxy(headers, { get(target, p) { if (typeof p !== 'string') { throw new TypeError('header keys must be strings'); } return target[p.toLowerCase()]; }, set(target, p, newValue) { if (typeof p !== 'string') { throw new TypeError('header keys must be strings'); } if (typeof newValue !== 'string') { throw new TypeError('header values must be strings'); } target[p.toLowerCase()] = newValue; return true; }, }); } function protoLinkToNexusLink(plink) { if (!plink.url) { throw new nexus.HandlerError('BAD_REQUEST', 'empty link URL'); } if (!plink.type) { throw new nexus.HandlerError('BAD_REQUEST', 'empty link type'); } return { url: new URL(plink.url), type: plink.type, }; } function nexusLinkToProtoLink(nlink) { return { url: nlink.url.toString(), type: nlink.type, }; } //# sourceMappingURL=index.js.map