@temporalio/activity
Version:
Temporal.io SDK Activity sub-package
431 lines • 19.8 kB
JavaScript
;
/**
* This package's main export is {@link Context}. Get the current Activity's context with
* {@link Context.current | `Context.current()`}:
*
* ```ts
* import { Context } from '@temporalio/activity';
*
* export async function myActivity() {
* const context = Context.current();
* }
* ```
*
* Any function can be used as an Activity as long as its parameters and return value are serializable using a
* {@link https://docs.temporal.io/concepts/what-is-a-data-converter/ | DataConverter}.
*
* ### Cancellation
*
* Activity Cancellation:
*
* - lets the Activity know it doesn't need to keep doing work, and
* - gives the Activity time to clean up any resources it has created.
*
* Activities can only receive Cancellation if they {@link Context.heartbeat | emit heartbeats} or are Local Activities
* (which can't heartbeat but receive Cancellation anyway).
*
* An Activity may receive Cancellation if:
*
* - The Workflow scope containing the Activity call was requested to be Cancelled and
* {@link ActivityOptions.cancellationType} was **not** set to {@link ActivityCancellationType.ABANDON}. The scope can
* be cancelled in either of the following ways:
* - The entire Workflow was Cancelled (via {@link WorkflowHandle.cancel}).
* - Calling {@link CancellationScope.cancel}) from inside a Workflow.
* - The Worker has started to shut down. Shutdown is initiated by either:
* - One of the {@link RuntimeOptions.shutdownSignals} was sent to the process.
* - {@link Worker.shutdown | `Worker.shutdown()`} was called.
* - The Activity was considered failed by the Server because any of the Activity timeouts have triggered (for example,
* the Server didn't receive a heartbeat within the {@link ActivityOptions.heartbeatTimeout}). The
* {@link CancelledFailure} will have `message: 'TIMED_OUT'`.
* - An Activity sends a heartbeat with `Context.current().heartbeat()` and the heartbeat details can't be converted by
* the Worker's configured {@link DataConverter}.
* - The Workflow Run reached a {@link https://docs.temporal.io/workflows#status | Closed state}, in which case the
* {@link CancelledFailure} will have `message: 'NOT_FOUND'`.
*
* The reason for the Cancellation is available at {@link CancelledFailure.message} or
* {@link Context#cancellationSignal | Context.cancellationSignal.reason}.
*
* Activity implementations should opt-in and subscribe to cancellation using one of the following methods:
*
* 1. `await` on {@link Context.cancelled | `Context.current().cancelled`} or
* {@link Context.sleep | `Context.current().sleep()`}, which each throw a {@link CancelledFailure}.
* 2. Pass the context's {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} at
* {@link Context.cancellationSignal | `Context.current().cancellationSignal`} to a library that supports it.
*
* ### Examples
*
* #### An Activity that sends progress heartbeats and can be Cancelled
*
* <!--SNIPSTART typescript-activity-fake-progress-->
* <!--SNIPEND-->
*
* #### An Activity that makes a cancellable HTTP request
*
* It passes the `AbortSignal` to {@link https://github.com/node-fetch/node-fetch#api | `fetch`}: `fetch(url, { signal:
* Context.current().cancellationSignal })`.
*
* <!--SNIPSTART typescript-activity-cancellable-fetch-->
* <!--SNIPEND-->
*
* @module
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.metricMeter = exports.log = exports.Context = exports.asyncLocalStorage = exports.CompleteAsyncError = exports.CancelledFailure = exports.ApplicationFailure = void 0;
exports.activityInfo = activityInfo;
exports.sleep = sleep;
exports.heartbeat = heartbeat;
exports.cancelled = cancelled;
exports.cancellationDetails = cancellationDetails;
exports.cancellationSignal = cancellationSignal;
exports.getClient = getClient;
const node_async_hooks_1 = require("node:async_hooks");
const common_1 = require("@temporalio/common");
const time_1 = require("@temporalio/common/lib/time");
const type_helpers_1 = require("@temporalio/common/lib/type-helpers");
var common_2 = require("@temporalio/common");
Object.defineProperty(exports, "ApplicationFailure", { enumerable: true, get: function () { return common_2.ApplicationFailure; } });
Object.defineProperty(exports, "CancelledFailure", { enumerable: true, get: function () { return common_2.CancelledFailure; } });
/**
* Throw this error from an Activity in order to make the Worker forget about this Activity.
*
* The Activity can then be completed asynchronously (from anywhere—usually outside the Worker) using
* {@link Client.activity}.
*
* @example
*
* ```ts
*import { CompleteAsyncError } from '@temporalio/activity';
*
*export async function myActivity(): Promise<never> {
* // ...
* throw new CompleteAsyncError();
*}
* ```
*/
let CompleteAsyncError = class CompleteAsyncError extends Error {
};
exports.CompleteAsyncError = CompleteAsyncError;
exports.CompleteAsyncError = CompleteAsyncError = __decorate([
(0, type_helpers_1.SymbolBasedInstanceOfError)('CompleteAsyncError')
], CompleteAsyncError);
// Make it safe to use @temporalio/activity with multiple versions installed.
const asyncLocalStorageSymbol = Symbol.for('__temporal_activity_context_storage__');
if (!globalThis[asyncLocalStorageSymbol]) {
globalThis[asyncLocalStorageSymbol] = new node_async_hooks_1.AsyncLocalStorage();
}
exports.asyncLocalStorage = globalThis[asyncLocalStorageSymbol];
/**
* Activity Context, used to:
*
* - Get {@link Info} about the current Activity Execution
* - Send {@link https://docs.temporal.io/concepts/what-is-an-activity-heartbeat | heartbeats}
* - Get notified of Activity cancellation
* - Sleep (cancellation-aware)
*
* Call `Context.current()` from Activity code in order to get the current Activity's Context.
*/
class Context {
info;
cancelled;
cancellationSignal;
heartbeatFn;
_client;
log;
metricMeter;
_cancellationDetails;
/**
* Gets the context of the current Activity.
*
* Uses {@link https://nodejs.org/docs/latest-v16.x/api/async_context.html#class-asynclocalstorage | AsyncLocalStorage} under the hood to make it accessible in nested callbacks and promises.
*/
static current() {
const store = exports.asyncLocalStorage.getStore();
if (store === undefined) {
throw new Error('Activity context not initialized');
}
return store;
}
/**
* **Not** meant to instantiated by Activity code, used by the worker.
*
* @ignore
*/
constructor(
/**
* Holds information about the current executing Activity.
*/
info,
/**
* A Promise that fails with a {@link CancelledFailure} when cancellation of this activity is requested. The promise
* is guaranteed to never successfully resolve. Await this promise in an Activity to get notified of cancellation.
*
* Note that to get notified of cancellation, an activity must _also_ {@link Context.heartbeat}.
*
* @see [Cancellation](/api/namespaces/activity#cancellation)
*/
cancelled,
/**
* An {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} that can be used to react to
* Activity cancellation.
*
* This can be passed in to libraries such as
* {@link https://www.npmjs.com/package/node-fetch#request-cancellation-with-abortsignal | fetch} to abort an
* in-progress request and
* {@link https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options child_process}
* to abort a child process, as well as other built-in node modules and modules found on npm.
*
* Note that to get notified of cancellation, an activity must _also_ {@link Context.heartbeat}.
*
* @see [Cancellation](/api/namespaces/activity#cancellation)
*/
cancellationSignal,
/**
* The heartbeat implementation, injected via the constructor.
*/
heartbeatFn,
/**
* The Worker's client, passed down through Activity context.
*/
_client,
/**
* The logger for this Activity.
*
* This defaults to the `Runtime`'s Logger (see {@link Runtime.logger}). Attributes from the current Activity context
* are automatically included as metadata on every log entries. An extra `sdkComponent` metadata attribute is also
* added, with value `activity`; this can be used for fine-grained filtering of log entries further downstream.
*
* To customize log attributes, register a {@link ActivityOutboundCallsInterceptor} that intercepts the
* `getLogAttributes()` method.
*
* Modifying the context logger (eg. `context.log = myCustomLogger` or by an {@link ActivityInboundLogInterceptor}
* with a custom logger as argument) is deprecated. Doing so will prevent automatic inclusion of custom log attributes
* through the `getLogAttributes()` interceptor. To customize _where_ log messages are sent, set the
* {@link Runtime.logger} property instead.
*/
log,
/**
* Get the metric meter for this activity with activity-specific tags.
*
* To add custom tags, register a {@link ActivityOutboundCallsInterceptor} that
* intercepts the `getMetricTags()` method.
*/
metricMeter,
/**
* Holder object for activity cancellation details
*/
_cancellationDetails) {
this.info = info;
this.cancelled = cancelled;
this.cancellationSignal = cancellationSignal;
this.heartbeatFn = heartbeatFn;
this._client = _client;
this.log = log;
this.metricMeter = metricMeter;
this._cancellationDetails = _cancellationDetails;
}
/**
* Send a {@link https://docs.temporal.io/concepts/what-is-an-activity-heartbeat | heartbeat} from an Activity.
*
* If an Activity times out, then during the next retry, the last value of `details` is available at
* {@link Info.heartbeatDetails}. This acts as a periodic checkpoint mechanism for the progress of an Activity.
*
* If an Activity times out on the final retry (relevant in cases in which {@link RetryPolicy.maximumAttempts} is
* set), the Activity function call in the Workflow code will throw an {@link ActivityFailure} with the `cause`
* attribute set to a {@link TimeoutFailure}, which has the last value of `details` available at
* {@link TimeoutFailure.lastHeartbeatDetails}.
*
* Calling `heartbeat()` from a Local Activity has no effect.
*
* The SDK automatically throttles heartbeat calls to the server with a duration of 80% of the specified activity
* heartbeat timeout. Throttling behavior may be customized with the `{@link maxHeartbeatThrottleInterval | https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#maxheartbeatthrottleinterval} and {@link defaultHeartbeatThrottleInterval | https://typescript.temporal.io/api/interfaces/worker.WorkerOptions#defaultheartbeatthrottleinterval} worker options.
*
* Activities must heartbeat in order to receive Cancellation (unless they're Local Activities, which don't need to).
*
* :warning: Cancellation is not propagated from this function, use {@link cancelled} or {@link cancellationSignal} to
* subscribe to cancellation notifications.
*/
heartbeat = (details) => {
this.heartbeatFn(details);
};
/**
* A Temporal Client, bound to the same Temporal Namespace as the Worker executing this Activity.
*
* May throw an {@link IllegalStateError} if the Activity is running inside a `MockActivityEnvironment`
* that was created without a Client.
*
* @experimental Client support over `NativeConnection` is experimental. Error handling may be
* incomplete or different from what would be observed using a {@link Connection}
* instead. Client doesn't support cancellation through a Signal.
*/
get client() {
if (this._client === undefined) {
throw new common_1.IllegalStateError('No Client available. This may be a MockActivityEnvironment that was created without a Client.');
}
return this._client;
}
/**
* Helper function for sleeping in an Activity.
* @param ms Sleep duration: number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}
* @returns A Promise that either resolves when `ms` is reached or rejects when the Activity is cancelled
*/
sleep = (ms) => {
let handle;
const timer = new Promise((resolve) => {
handle = setTimeout(resolve, (0, time_1.msToNumber)(ms));
});
return Promise.race([this.cancelled.finally(() => clearTimeout(handle)), timer]);
};
/**
* Return the cancellation details for this activity, if any.
* @returns an object with boolean properties that describes the reason for cancellation, or undefined if not cancelled.
*
* @experimental Activity cancellation details include usage of experimental features such as activity pause, and may be subject to change.
*/
get cancellationDetails() {
return this._cancellationDetails.details;
}
}
exports.Context = Context;
/**
* The current Activity's context.
*/
function activityInfo() {
// For consistency with workflow.workflowInfo(), we want activityInfo() to be a function, rather than a const object.
return Context.current().info;
}
/**
* The logger for this Activity.
*
* This is a shortcut for `Context.current().log` (see {@link Context.log}).
*/
exports.log = {
// Context.current().log may legitimately change during the lifetime of an Activity, so we can't
// just initialize that field to the value of Context.current().log and move on. Hence this indirection.
log(level, message, meta) {
return Context.current().log.log(level, message, meta);
},
trace(message, meta) {
return Context.current().log.trace(message, meta);
},
debug(message, meta) {
return Context.current().log.debug(message, meta);
},
info(message, meta) {
return Context.current().log.info(message, meta);
},
warn(message, meta) {
return Context.current().log.warn(message, meta);
},
error(message, meta) {
return Context.current().log.error(message, meta);
},
};
/**
* Helper function for sleeping in an Activity.
*
* This is a shortcut for `Context.current().sleep(ms)` (see {@link Context.sleep}).
*
* @param ms Sleep duration: number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}
* @returns A Promise that either resolves when `ms` is reached or rejects when the Activity is cancelled
*/
function sleep(ms) {
return Context.current().sleep(ms);
}
/**
* Send a {@link https://docs.temporal.io/concepts/what-is-an-activity-heartbeat | heartbeat} from an Activity.
*
* If an Activity times out, then during the next retry, the last value of `details` is available at
* {@link Info.heartbeatDetails}. This acts as a periodic checkpoint mechanism for the progress of an Activity.
*
* If an Activity times out on the final retry (relevant in cases in which {@link RetryPolicy.maximumAttempts} is
* set), the Activity function call in the Workflow code will throw an {@link ActivityFailure} with the `cause`
* attribute set to a {@link TimeoutFailure}, which has the last value of `details` available at
* {@link TimeoutFailure.lastHeartbeatDetails}.
*
* This is a shortcut for `Context.current().heatbeat(ms)` (see {@link Context.heartbeat}).
*/
function heartbeat(details) {
Context.current().heartbeat(details);
}
/**
* Return a Promise that fails with a {@link CancelledFailure} when cancellation of this activity is requested. The
* promise is guaranteed to never successfully resolve. Await this promise in an Activity to get notified of
* cancellation.
*
* Note that to get notified of cancellation, an activity must _also_ do {@link Context.heartbeat}.
*
* This is a shortcut for `Context.current().cancelled` (see {@link Context.cancelled}).
*/
function cancelled() {
return Context.current().cancelled;
}
/**
* Return the cancellation details for this activity, if any.
* @returns an object with boolean properties that describes the reason for cancellation, or undefined if not cancelled.
*
* @experimental Activity cancellation details include usage of experimental features such as activity pause, and may be subject to change.
*/
function cancellationDetails() {
return Context.current().cancellationDetails;
}
/**
* Return an {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal | `AbortSignal`} that can be used to
* react to Activity cancellation.
*
* This can be passed in to libraries such as
* {@link https://www.npmjs.com/package/node-fetch#request-cancellation-with-abortsignal | fetch} to abort an
* in-progress request and
* {@link https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options child_process}
* to abort a child process, as well as other built-in node modules and modules found on npm.
*
* Note that to get notified of cancellation, an activity must _also_ do {@link Context.heartbeat}.
*
* This is a shortcut for `Context.current().cancellationSignal` (see {@link Context.cancellationSignal}).
*/
function cancellationSignal() {
return Context.current().cancellationSignal;
}
/**
* A Temporal Client, bound to the same Temporal Namespace as the Worker executing this Activity.
*
* May throw an {@link IllegalStateError} if the Activity is running inside a `MockActivityEnvironment`
* that was created without a Client.
*
* This is a shortcut for `Context.current().client` (see {@link Context.client}).
*
* @experimental Client support over `NativeConnection` is experimental. Error handling may be
* incomplete or different from what would be observed using a {@link Connection}
* instead. Client doesn't support cancellation through a Signal.
*/
function getClient() {
return Context.current().client;
}
/**
* Get the metric meter for the current activity, with activity-specific tags.
*
* To add custom tags, register a {@link ActivityOutboundCallsInterceptor} that
* intercepts the `getMetricTags()` method.
*
* This is a shortcut for `Context.current().metricMeter` (see {@link Context.metricMeter}).
*/
exports.metricMeter = {
createCounter(name, unit, description) {
return Context.current().metricMeter.createCounter(name, unit, description);
},
createHistogram(name, valueType = 'int', unit, description) {
return Context.current().metricMeter.createHistogram(name, valueType, unit, description);
},
createGauge(name, valueType = 'int', unit, description) {
return Context.current().metricMeter.createGauge(name, valueType, unit, description);
},
withTags(tags) {
return Context.current().metricMeter.withTags(tags);
},
};
//# sourceMappingURL=index.js.map