UNPKG

@temporalio/common

Version:

Common library for code that's used across the Client, Worker, and/or Workflow

151 lines (139 loc) 4.83 kB
import type { WalkEnv } from '@temporalio/proto/lib/payload-visitor.generated'; import { sequential, type ConcurrencyLimit } from '../concurrency/limit'; import type { Payload } from '../interfaces'; /** * Called for each singular (or map-value) payload-bearing field. One payload in, one out. * * @internal * @experimental */ export type PayloadTransform<Ctx> = (payload: Payload, context: Ctx, abortSignal?: AbortSignal) => Promise<Payload>; /** * Called for each payload-bearing field that may contain multiple payloads (e.g. Payloads or repeated fields). * May return any number of payloads, including zero. * * @internal * @experimental */ export type PayloadsTransform<Ctx> = ( payloads: Payload[], context: Ctx, abortSignal?: AbortSignal ) => Promise<Payload[]>; /** * Called on entering each message and returns the context for its children. * * @internal * @experimental */ export type ContextDeriver<Ctx> = (message: object, typeName: string, context: Ctx) => Ctx; /** * Two transform functions are required because some fields require a single (non-null) payload while others * are simply lists. * * @internal * @experimental */ export interface VisitOptions<Ctx> { transformPayload: PayloadTransform<Ctx>; transformPayloads: PayloadsTransform<Ctx>; deriveContext?: ContextDeriver<Ctx>; /** Context in scope before any message is entered. */ initialContext?: Ctx; /** * Optional concurrency limit applied to every transform call. Share one limit across visits for a * global cap (e.g. a payload store's total budget), or nest limits to compose a per-visit cap under * a global one. Omit to run transforms one at a time (sequential). */ limit?: ConcurrencyLimit; skipHeaders?: boolean; skipSearchAttributes?: boolean; /** Aborts the walk; composed with the internal cancel-on-error signal and handed to the transform. */ abortSignal?: AbortSignal; } /** * Awaits every promise a walk produced, then throws the first rejection in traversal order. * `allSettled` guarantees no in-flight transform is left running on the error path. */ async function drain(pending: Promise<unknown>[]): Promise<void> { const results = await Promise.allSettled(pending); for (const result of results) { if (result.status === 'rejected') { throw result.reason; } } } /** * Runs a recursive walk from the VisitOptions. Runs each transform through the concurrency limit, * then wraps them with the per-visit cancel-on-error signal and assigns them to {@link WalkEnv}. * * @internal * @experimental */ async function runVisit<Ctx>( options: VisitOptions<Ctx>, walk: (env: WalkEnv<Ctx>, context: Ctx) => Promise<unknown>[] ): Promise<void> { const { transformPayload, transformPayloads, deriveContext, initialContext, limit = sequential(), skipHeaders = false, skipSearchAttributes = false, abortSignal, } = options; const failure = new AbortController(); let removeListener: (() => void) | undefined; if (abortSignal) { if (abortSignal.aborted) { failure.abort(abortSignal.reason); } else { const onAbort = () => failure.abort(abortSignal.reason); abortSignal.addEventListener('abort', onAbort, { once: true }); removeListener = () => abortSignal.removeEventListener('abort', onAbort); } } const runTransform = <T>(call: (signal: AbortSignal) => Promise<T>): Promise<T> => { failure.signal.throwIfAborted(); return limit(async () => { failure.signal.throwIfAborted(); try { return await call(failure.signal); } catch (reason) { failure.abort(reason); throw reason; } }); }; const env: WalkEnv<Ctx> = { transformPayload: (payload, context) => runTransform((signal) => transformPayload(payload, context, signal)), transformPayloads: (payloads, context) => runTransform((signal) => transformPayloads(payloads, context, signal)), deriveContext, skipHeaders, skipSearchAttributes, }; try { await drain(walk(env, initialContext as Ctx)); } finally { removeListener?.(); } } /** * Applies the payload transforms to every {@link Payload} in `root`, mutating it in place. Pass the * generated `walk*` function for the root's message type (all are re-exported below). * * @internal * @experimental */ export async function visit<Root, Ctx = void>( root: Root, walk: (root: Root, env: WalkEnv<Ctx>, context: Ctx) => Promise<unknown>[], options: VisitOptions<Ctx> ): Promise<void> { return runVisit(options, (env, context) => walk(root, env, context)); } // Re-export every generated walker so consumers pair any of them with `visit` without a deep import // into the generated file. export * from '@temporalio/proto/lib/payload-visitor.generated';