@temporalio/worker
Version:
Temporal.io SDK Worker sub-package
309 lines • 12.7 kB
JavaScript
"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