import { WorkflowHandleService } from '../services/durable/handle'; import { LogLevel } from './logger'; import { ProviderConfig, ProvidersConfig } from './provider'; import { StringAnyType, StringStringType } from './serializer'; import { StreamData, StreamError } from './stream'; /** * Type definition for workflow configuration. */ type WorkflowConfig = { /** * Backoff coefficient for retry mechanism. * @default 5 (HMSH_DURABLE_EXP_BACKOFF) */ backoffCoefficient?: number; /** * Initial interval before the first retry attempt. * Formula: initialInterval * backoffCoefficient^retryCount, clamped by maximumInterval. * @default '1s' (HMSH_DURABLE_INITIAL_INTERVAL) */ initialInterval?: string; /** * Maximum number of attempts for retries. * @default 50 (HMSH_DURABLE_MAX_ATTEMPTS) */ maximumAttempts?: number; /** * Maximum interval between retries. * @default 120s (HMSH_DURABLE_MAX_INTERVAL) */ maximumInterval?: string; /** * Whether to throw an error on final failure after retries are exhausted * or return the error object as a standard response containing error-related * fields like `stack`, `code`, `message`. * @default true */ throwOnError?: boolean; }; type WorkflowContext = { /** * can the workflow be retried if an error occurs */ canRetry: boolean; COUNTER: { /** * the reentrant semaphore parent counter object for object reference during increment */ counter: number; }; /** * the reentrant semaphore, incremented in real-time as idempotent statements are re-traversed upon reentry. Indicates the current semaphore count. */ counter: number; /** * number as string for the replay cursor */ cursor: string; /** * the replay hash of name/value pairs representing prior executions */ replay: StringStringType; /** * the HotMesh App namespace * @default durable */ namespace: string; /** * holds list of interruption payloads; if list is longer than 1 when the error is thrown, a `collator` subflow will be used */ interruptionRegistry: any[]; /** * entry point ancestor flow; might be the parent; will never be self */ originJobId: string; /** * the workflow/job ID */ workflowId: string; /** * the dimensional isolation for the reentrant hook, expressed in the format `0,0`, `0,1`, etc */ workflowDimension: string; /** * the task queue name (stream_name in worker_streams) */ taskQueue: string; /** * a concatenation of the task queue and workflow name (e.g., `${taskQueueName}-${workflowName}`); * used for engine-internal routing (graph.subscribes) */ workflowTopic: string; /** * the open telemetry trace context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink. */ workflowTrace: string; /** * the open telemetry span context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink. */ workflowSpan: string; /** * the native HotMesh message that encapsulates the arguments, metadata, and raw data for the workflow */ raw: StreamData; /** * the HotMesh connection configuration */ connection: Connection; /** * if present, the workflow will delay expiration for the specified number of seconds */ expire?: number; }; /** * Context available inside an executing activity function via * `Durable.activity.getContext()`. Populated by the activity worker * using `activityAsyncLocalStorage`. */ type DurableActivityContext = { /** The name of the activity function being executed */ activityName: string; /** The arguments passed to the activity */ arguments: any[]; /** Optional metadata provided via `proxyActivities({ headers })` */ headers: Record; /** The workflow ID of the parent workflow that dispatched this activity */ workflowId: string; /** The workflow topic of the parent workflow */ workflowTopic: string; }; /** * The schema for the full-text-search * @deprecated */ export type WorkflowSearchSchema = Record; type WorkflowSearchOptions = { /** FT index name (myapp:myindex) */ index?: string; /** FT prefixes (['myapp:myindex:prefix1', 'myapp:myindex:prefix2']) */ prefix?: string[]; /** * Schema mapping each field. Each field is a key-value pair where the key is the field name * and the value is a record of field options. If the fieldName is provided, * it will be used as the indexed field name. If not provided * key will be used as the indexed field name with an underscore prefix. * */ schema?: WorkflowSearchSchema; /** Additional data as a key-value record */ data?: StringStringType; }; type SearchResults = { /** * the total number of results */ count: number; /** * the raw FT.SEARCH query string */ query: string; /** * the raw FT.SEARCH results as an array of objects */ data: StringStringType[]; }; type WorkflowOptions = { /** * the namespace for the workflow * @default durable */ namespace?: string; /** * the task queue for the workflow; optional if entity is provided */ taskQueue?: string; /** * input arguments to pass in */ args: any[]; /** * the job id */ workflowId?: string; /** * if invoking a workflow, passing 'entity' will apply the value as the workflowName, taskQueue, and prefix, ensuring the FT.SEARCH index is properly scoped. This is a convenience method but limits options. */ entity?: string; /** * the name of the user's workflow function; optional if 'entity' is provided */ workflowName?: string; /** * the parent workflow id; adjacent ancestor ID */ parentWorkflowId?: string; /** * the entry point workflow id */ originJobId?: string; /** * OpenTelemetry trace context for the workflow */ workflowTrace?: string; /** * OpenTelemetry span context for the workflow */ workflowSpan?: string; /** * the full-text-search */ search?: WorkflowSearchOptions; /** * marker data (begins with a -) */ marker?: StringStringType; /** * the workflow configuration object */ config?: WorkflowConfig; /** * sets the number of seconds a workflow may exist after completion. The default policy is to expire the job hash as soon as it completes. */ expire?: number; /** * system flag to indicate that the flow should remain open beyond main method completion while still emitting the 'job done' event */ persistent?: boolean; /** * default is true; set to false to optimize workflows that do not require a `signal in` */ signalIn?: boolean; /** * default is true; if false, will not await the execution */ await?: boolean; /** * If provided, the job will initialize in a pending state, reserving * only the job ID (HSETNX) and persisting search and marker (if provided). * If a `resume` signal is sent before the specified number of seconds, * the job will resume as normal, transitioning to the adjacent children * of the trigger. If the job is not resumed within the number * of seconds specified, the job will be scrubbed. No dependencies * are added for a job in a pending state; however, dependencies * will be added after the job is resumed if relevant. */ pending?: number; /** * Provide to set the engine name. This MUST be unique, so do not * provide unless it is guaranteed to be a unique engine/worker guid * when identifying the point of presence within the mesh. */ guid?: string; }; /** * Options for setting up a hook. * 'durable' is the default namespace if not provided; * similar to setting `appid` in the YAML */ type HookOptions = { /** Optional namespace under which the hook function will be grouped */ namespace?: string; /** Optional task queue, needed unless 'entity' is provided */ taskQueue?: string; /** Input arguments to pass into the hook */ args: any[]; /** * Optional entity name. If provided, applies as the workflowName, * taskQueue, and prefix. This scopes the FT.SEARCH index appropriately. * This is a convenience method but limits options. */ entity?: string; /** Execution ID, also known as the job ID to hook into */ workflowId?: string; /** The name of the user's hook function */ workflowName?: string; /** Bind additional search terms immediately before hook reentry */ search?: WorkflowSearchOptions; /** Hook function constraints (backoffCoefficient, maximumAttempts, maximumInterval) */ config?: WorkflowConfig; }; /** * Options for sending signals in a workflow. */ type SignalOptions = { /** * Task queue associated with the workflow */ taskQueue: string; /** * Input data for the signal (any serializable object) */ data: StringAnyType; /** * Execution ID, also known as the job ID */ workflowId: string; /** * Optional name of the user's workflow function */ workflowName?: string; }; type ActivityWorkflowDataType = { activityName: string; arguments: any[]; headers?: Record; startToCloseTimeout?: number; workflowId: string; workflowTopic: string; }; type WorkflowDataType = { arguments: any[]; workflowId: string; workflowTopic: string; workflowDimension?: string; originJobId?: string; canRetry?: boolean; expire?: number; continueGeneration?: number; }; type Connection = ProviderConfig | ProvidersConfig; type ClientConfig = { connection: Connection; /** * Optional system-event sink. When set, `client.escalations.*` operations * call `events.publish` post-commit from the invoking process. Wires the * same hook as `HotMeshConfig.events` for direct-client callers. */ events?: import('./system_events').EventsConfig; }; type Registry = { [key: string]: Function; }; type WorkerConfig = { /** Connection configuration for the worker */ connection: Connection; /** * Namespace used in the app configuration, denoted as `appid` in the YAML * @default durable */ namespace?: string; /** Task queue name, denoted as `subscribes` in the YAML (e.g., 'hello-world') */ taskQueue: string; /** Target function or a record type with a name (string) and reference function */ workflow: Function | Record; /** * Optional activity functions to register with this worker's task queue. * When provided, these activities are registered and served on `{taskQueue}-activity`. * Workflows can then call them via `proxyActivities()` without passing activities inline. * * Workflows can then call them via `proxyActivities()` without passing activities inline. */ activities?: Record; /** Additional options for configuring the worker */ options?: WorkerOptions; /** Search options for workflow execution details */ search?: WorkflowSearchOptions; /** * Provide to set the engine name. This MUST be unique, so do not * provide unless it is guaranteed to be a unique engine/worker guid * when identifying the point of presence within the mesh. */ guid?: string; /** * Scoped Postgres credentials for database-level worker isolation. * When provided, the worker connects as a restricted Postgres role * that can only dequeue/ack/respond on its allowed stream names * via SECURITY DEFINER stored procedures. * * Provision credentials via `HotMesh.provisionWorkerRole()` (or * the convenience alias `Durable.provisionWorkerRole()`). */ workerCredentials?: { user: string; password: string; }; /** * Optional system-event sink. When set, the worker fires `events.publish` * on `system.worker.{taskQueue}.started` and `system.worker.{taskQueue}.stopped`. */ events?: import('./system_events').EventsConfig; }; type FindWhereQuery = { field: string; is: '=' | '==' | '>=' | '<=' | '[]'; value: string | boolean | number | [number, number]; type?: string; }; type FindOptions = { workflowName?: string; taskQueue?: string; namespace?: string; index?: string; search?: WorkflowSearchOptions; }; type FindWhereOptions = { options?: FindOptions; count?: boolean; query: FindWhereQuery[]; return?: string[]; limit?: { start: number; size: number; }; }; type FindJobsOptions = { /** The workflow name; include an asterisk for wilcard search */ match?: string; /** * application namespace * @default durable */ namespace?: string; /** The suggested response limit. Reduce batch size to reduce the likelihood of large overages. */ limit?: number; /** How many records to scan at a time */ batch?: number; /** The start cursor; defaults to 0 */ cursor?: string; }; type WorkerOptions = { /** Log level: debug, info, warn, error */ logLevel?: LogLevel; /** Maximum number of attempts, default 50 (HMSH_DURABLE_MAX_ATTEMPTS) */ maximumAttempts?: number; /** Backoff coefficient for retry logic, default 10 (HMSH_DURABLE_EXP_BACKOFF) */ backoffCoefficient?: number; /** Initial interval before the first retry, default '1s' (HMSH_DURABLE_INITIAL_INTERVAL) */ initialInterval?: string; /** Maximum interval between retries, default 120s (HMSH_DURABLE_MAX_INTERVAL) */ maximumInterval?: string; }; type ContextType = { workflowId: string; workflowTopic: string; }; type FunctionSignature = T extends (...args: infer A) => infer R ? (...args: A) => R : never; type ProxyType = { [K in keyof ACT]: FunctionSignature; }; /** * Configuration settings for activities within a workflow. */ type ActivityConfig = { /** place holder setting; unused at this time (re: activity workflow expire configuration) */ expire?: number; /** Maximum time an activity can run after starting execution. If exceeded, the activity fails with a timeout error. Accepts duration strings (e.g., '30s', '5m', '1h'). */ startToCloseTimeout?: string; /** Configuration for specific activities, type not yet specified */ activities?: any; /** * Optional explicit task queue for activities. * * **Default Behavior (no taskQueue specified):** * Activities use the workflow's task queue + "-activity" suffix. * Example: workflow "my-workflow" → activity queue "my-workflow-activity" * * **Explicit Task Queue (when specified):** * Activities use the specified task queue + "-activity" suffix. * Useful for: * - Shared activity pools across multiple workflows * - Interceptors (prevents per-workflow queue creation) * - Isolated activity worker pools * * @example * ```typescript * // Default: uses workflow's task queue (backward compatible) * const activities = Durable.workflow.proxyActivities({ * activities, * retry: { maximumAttempts: 3 } * }); * // If workflow taskQueue is "orders", uses "orders-activity" * * // Explicit: shared queue for interceptors (prevents explosion) * const { auditLog } = Durable.workflow.proxyActivities({ * activities: { auditLog }, * taskQueue: 'shared-activities', // Uses "shared-activities-activity" * retry: { maximumAttempts: 3 } * }); * ``` */ taskQueue?: string; /** Optional metadata to pass alongside activity arguments. This metadata * is transported as a dedicated schema field (not inside args) and made * available to the activity function via `Durable.activity.getContext()`. */ headers?: Record; /** Retry policy configuration for activities */ retry?: { /** Maximum number of retry attempts, default is 50 (HMSH_DURABLE_MAX_ATTEMPTS) */ maximumAttempts?: number; /** Factor by which the retry timeout increases, default is 10 (HMSH_DURABLE_EXP_BACKOFF) */ backoffCoefficient?: number; /** Initial interval before the first retry. Formula: initialInterval * backoffCoefficient^retryCount, clamped by maximumInterval. Default is '1s' */ initialInterval?: string; /** Maximum interval between retries, default is '120s' (HMSH_DURABLE_MAX_INTERVAL) */ maximumInterval?: string; /** Whether to throw an error on failure, default is true */ throwOnError?: boolean; }; }; /** * The proxy response object returned from the activity proxy flow */ type ProxyResponseType = { data?: T; $error?: StreamError; done?: boolean; jc: string; ju: string; }; /** * The child flow response object returned from the main flow during recursion */ type ChildResponseType = { data?: T; $error?: StreamError; done?: boolean; jc: string; ju: string; }; interface ClientWorkflow { start(options: WorkflowOptions): Promise; signal(signalId: string, data: StringAnyType, namespace?: string): Promise; hook(options: HookOptions): Promise; getHandle(taskQueue: string, workflowName: string, workflowId: string, namespace?: string): Promise; search(taskQueue: string, workflowName: string, namespace: string | null, index: string, ...query: string[]): Promise; } /** * Workflow interceptor that can wrap workflow execution in an onion-like pattern. * Each interceptor wraps the next one, with the actual workflow execution at the center. * * Interceptors are executed in the order they are registered. Each interceptor can: * - Perform actions before workflow execution * - Modify or enhance the workflow context * - Handle or transform workflow results * - Catch and handle errors * - Add cross-cutting concerns like logging, metrics, or tracing * * @example * ```typescript * // Simple logging interceptor * const loggingInterceptor: WorkflowInboundCallsInterceptor = { * async execute(ctx, next) { * console.log('Before workflow'); * try { * const result = await next(); * console.log('After workflow'); * return result; * } catch (err) { * console.error('Workflow error:', err); * throw err; * } * } * }; * * // Register the interceptor * Durable.registerInterceptor(loggingInterceptor); * ``` */ export interface WorkflowInboundCallsInterceptor { /** * Called before workflow execution to wrap the workflow in custom logic * * @param ctx - The workflow context map containing workflow metadata and state * @param next - Function to call the next interceptor or the workflow itself * @returns The result of the workflow execution * * @example * ```typescript * // Metrics interceptor implementation * { * async execute(ctx, next) { * const workflowName = ctx.get('workflowName'); * const metrics = getMetricsClient(); * * metrics.increment(`workflow.start.${workflowName}`); * const timer = metrics.startTimer(); * * try { * const result = await next(); * metrics.increment(`workflow.success.${workflowName}`); * return result; * } catch (err) { * metrics.increment(`workflow.error.${workflowName}`); * throw err; * } finally { * timer.end(); * } * } * } * ``` */ execute(ctx: Map, next: () => Promise): Promise; } /** * Registry for workflow and activity interceptors that are executed in order * for each workflow or activity execution. */ export interface InterceptorRegistry { /** * Array of registered inbound interceptors that will wrap workflow execution * in the order they were registered (first registered = outermost wrapper). */ inbound: WorkflowInboundCallsInterceptor[]; /** * Array of registered outbound interceptors that will wrap individual * proxied activity calls in the order they were registered * (first registered = outermost wrapper). */ outbound: WorkflowOutboundCallsInterceptor[]; /** * Array of registered activity inbound interceptors that wrap the actual * activity function execution on the activity worker side. */ activityInbound: ActivityInboundCallsInterceptor[]; } /** * Context provided to an activity interceptor, containing metadata * about the proxied activity being invoked. */ export interface WorkflowOutboundCallsInterceptorContext { /** The name of the activity function being called */ activityName: string; /** The arguments passed to the activity call */ args: any[]; /** The activity configuration (retry, taskQueue, etc.) */ options?: ActivityConfig; } /** * Interceptor for individual proxied activity calls within a workflow. * Runs inside the workflow's async local storage context, so all Durable * workflow methods (proxyActivities, sleepFor, waitFor, execChild, etc.) * are available. * * Activity interceptors wrap proxied activity calls in an onion pattern, * supporting both **before** and **after** phases: * * - **Before phase** (code before `await next()`): Runs before the activity * executes. The interceptor can inspect or modify `activityCtx.args` to * transform the activity input before it is sent. * * - **After phase** (code after `await next()`): Runs on replay once the * activity result is available. The interceptor receives the activity * output as the return value of `next()` and can inspect or transform it. * * On first execution, `next()` registers the activity with the interruption * system and throws (the activity has not completed yet). On replay, `next()` * returns the stored result and the after-phase code executes. This follows * the same deterministic replay pattern as workflow interceptors. * * @example * ```typescript * const auditInterceptor: WorkflowOutboundCallsInterceptor = { * async execute(activityCtx, workflowCtx, next) { * const { auditLog } = Durable.workflow.proxyActivities<{ * auditLog: (id: string, action: string) => Promise; * }>({ * taskQueue: 'shared-audit', * retry: { maximumAttempts: 3 }, * }); * * await auditLog(workflowCtx.get('workflowId'), `before:${activityCtx.activityName}`); * const result = await next(); * await auditLog(workflowCtx.get('workflowId'), `after:${activityCtx.activityName}`); * return result; * }, * }; * * Durable.registerWorkflowOutboundCallsInterceptor(auditInterceptor); * ``` */ export interface WorkflowOutboundCallsInterceptor { /** * Called around each proxied activity invocation. Code before `next()` * runs in the before phase; code after `next()` runs in the after phase * once the activity result is available on replay. * * @param activityCtx - Metadata about the activity being called (args may be modified) * @param workflowCtx - The workflow context map (same as WorkflowInboundCallsInterceptor receives) * @param next - Call to proceed to the next interceptor or the core activity function * @returns The activity result (from replay or after interruption/re-execution) */ execute(activityCtx: WorkflowOutboundCallsInterceptorContext, workflowCtx: Map, next: () => Promise): Promise; } /** * Interceptor for activity function execution on the activity worker side. * Runs inside the activity's `activityAsyncLocalStorage` context, wrapping * the actual activity function invocation — not the proxy call in the workflow. * * Unlike workflow-side interceptors, this runs where the activity actually executes. * Use it for cross-cutting concerns like logging, metrics, auth validation, * or error enrichment at the point where the activity actually executes. * * @example * ```typescript * Durable.registerActivityInboundInterceptor({ * async execute(activityName, args, next) { * console.log(`Activity ${activityName} starting with`, args); * const start = Date.now(); * try { * const result = await next(); * console.log(`Activity ${activityName} completed in ${Date.now() - start}ms`); * return result; * } catch (err) { * console.error(`Activity ${activityName} failed`, err); * throw err; * } * } * }); * ``` */ export interface ActivityInboundCallsInterceptor { /** * Called around the actual activity function execution on the worker. * * @param activityName - The name of the activity being executed * @param args - The arguments passed to the activity * @param next - Call to execute the next interceptor or the activity itself * @returns The activity function's return value */ execute(activityName: string, args: any[], next: () => Promise): Promise; } export { ActivityConfig, DurableActivityContext, ActivityWorkflowDataType, ChildResponseType, ClientConfig, ClientWorkflow, ContextType, Connection, FunctionSignature, ProxyResponseType, ProxyType, Registry, SignalOptions, FindJobsOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, SearchResults, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, WorkflowContext, };