@dudousxd/nestjs-durable-core 0.49.2 → 0.49.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +161 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -2
- package/dist/index.d.ts +104 -2
- package/dist/index.js +157 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -330,6 +330,27 @@ interface WorkflowRun {
|
|
|
330
330
|
createdAt: Date;
|
|
331
331
|
updatedAt: Date;
|
|
332
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* WHAT an event-parked suspended run is waiting on, resolved for display — the engine keeps one
|
|
335
|
+
* generic `suspended` status (it's what drives recovery/timers/queries), but to a human "waiting on
|
|
336
|
+
* signal `approve`" reads very differently from "waiting on webhook `stripe-cb`". Computed by the
|
|
337
|
+
* store-backed gateway from the run's signal waiters (`listSignalWaiters`, ONE bulk scan), so the
|
|
338
|
+
* dashboard can NAME the wait in a list row without fetching each run's timeline. `on` is derived from
|
|
339
|
+
* the waiter token's prefix (see `classifyWaiterToken`): `wh:<runId>:<seq>` ⇒ `webhook`, `child:<id>`
|
|
340
|
+
* ⇒ `child`, `event:<name>:…` ⇒ `signal` (name decoded), anything else ⇒ `signal` (the token IS the
|
|
341
|
+
* signal name).
|
|
342
|
+
*
|
|
343
|
+
* Deliberately EVENT-only (no timer/step): `wakeAt` alone can't tell a real `ctx.sleep` from the
|
|
344
|
+
* reconcile-fallback `wakeAt` an event/step suspend now carries, and the store exposes no cheap bulk
|
|
345
|
+
* checkpoint scan to disambiguate — so the client shows a non-event suspend with a live worker as
|
|
346
|
+
* `running` (it flips off "no worker" the moment a worker returns) rather than guessing "sleeping".
|
|
347
|
+
* The detail view (which has the timeline) still distinguishes sleep vs in-flight step precisely.
|
|
348
|
+
*/
|
|
349
|
+
interface RunWaiting {
|
|
350
|
+
on: 'signal' | 'webhook' | 'child';
|
|
351
|
+
/** The token/name the run is parked on — the signal name, webhook token, or awaited child id. */
|
|
352
|
+
name: string;
|
|
353
|
+
}
|
|
333
354
|
type StepKind = 'local' | 'remote' | 'sleep' | 'signal';
|
|
334
355
|
/**
|
|
335
356
|
* The recorded result of a single step at a deterministic logical position (`seq`).
|
|
@@ -709,6 +730,9 @@ type RunRequestKind = {
|
|
|
709
730
|
kind: 'retryWithInput';
|
|
710
731
|
runId: string;
|
|
711
732
|
input: unknown;
|
|
733
|
+
} | {
|
|
734
|
+
kind: 'redispatch';
|
|
735
|
+
runId: string;
|
|
712
736
|
};
|
|
713
737
|
/** The control plane's answer to a {@link RunRequest}, correlated by `requestId`. */
|
|
714
738
|
interface RunReply {
|
|
@@ -1797,6 +1821,27 @@ interface WorkflowEngineDeps {
|
|
|
1797
1821
|
* for a genuinely-orphaned run. Default 5min; set `0` to disable (the prior wake-forever behavior).
|
|
1798
1822
|
*/
|
|
1799
1823
|
reconcileMs?: number | undefined;
|
|
1824
|
+
/**
|
|
1825
|
+
* Opt-in self-heal window (ms) for a remote step with NO `timeoutMs` whose dispatched job was LOST —
|
|
1826
|
+
* the worker crashed mid-step leaving no result, or the transport dropped the job (a Redis
|
|
1827
|
+
* flush/eviction, or BullMQ moving a stalled job to `failed` and removing it). The `pending`
|
|
1828
|
+
* checkpoint then has no result to resume on, and — BY DESIGN (see {@link reconcileMs}) — the
|
|
1829
|
+
* reconcile re-drive re-suspends a still-pending step rather than re-dispatching, so a slow-but-live
|
|
1830
|
+
* worker is never double-run. That safety means a genuinely-lost dispatch would otherwise hang
|
|
1831
|
+
* forever. When set, a reconcile re-drive that finds a remote step still `pending` PAST this window
|
|
1832
|
+
* re-dispatches it (bumping `attempts`, bounded by {@link remoteRedispatchMax}). Off by default:
|
|
1833
|
+
* re-dispatch can double-run a step whose original job is merely slow, so the window MUST exceed the
|
|
1834
|
+
* longest such step and the step MUST be idempotent. Prefer a per-step `timeoutMs` (tighter,
|
|
1835
|
+
* heartbeat-aware) where you can; this is the store-driven net for no-timeout steps that must
|
|
1836
|
+
* survive a lost dispatch. {@link WorkflowEngine.redispatchPending} is the manual counterpart.
|
|
1837
|
+
*/
|
|
1838
|
+
remoteRedispatchMs?: number | undefined;
|
|
1839
|
+
/**
|
|
1840
|
+
* Max times {@link remoteRedispatchMs} re-dispatches one lost remote step before giving up and
|
|
1841
|
+
* failing it as a {@link RemoteStepError} (`code: 'remote_step_lost'`), so the run fails / dead-letters
|
|
1842
|
+
* instead of re-dispatching forever. Default 10. Ignored when `remoteRedispatchMs` is unset.
|
|
1843
|
+
*/
|
|
1844
|
+
remoteRedispatchMax?: number | undefined;
|
|
1800
1845
|
/**
|
|
1801
1846
|
* Build the public callback URL for a `ctx.webhook()` token (e.g.
|
|
1802
1847
|
* ``(t) => `https://api.example.com/durable/webhooks/${t}` ``). Populates
|
|
@@ -1873,6 +1918,8 @@ declare class WorkflowEngine {
|
|
|
1873
1918
|
private readonly maxRecoveryAttempts?;
|
|
1874
1919
|
private readonly remoteAdvanceSilenceMs?;
|
|
1875
1920
|
private readonly reconcileMs;
|
|
1921
|
+
private readonly remoteRedispatchMs;
|
|
1922
|
+
private readonly remoteRedispatchMax;
|
|
1876
1923
|
private readonly webhookUrl?;
|
|
1877
1924
|
private readonly traceparent?;
|
|
1878
1925
|
private readonly context?;
|
|
@@ -2165,6 +2212,19 @@ declare class WorkflowEngine {
|
|
|
2165
2212
|
* the HTTP request on workflow execution.
|
|
2166
2213
|
*/
|
|
2167
2214
|
requeue(runId: string): Promise<RunResult | null>;
|
|
2215
|
+
/**
|
|
2216
|
+
* Explicitly re-dispatch every remote step of `runId` stuck `pending` — the OPERATOR escape hatch for
|
|
2217
|
+
* a run whose dispatched step job was LOST (worker crashed with no result, or a transport that dropped
|
|
2218
|
+
* the job). No automatic recovery re-drives these: a reconcile re-drive re-suspends a still-pending
|
|
2219
|
+
* step by design (unless `remoteRedispatchMs` is set), `recoverIncomplete` only reclaims LEASED runs,
|
|
2220
|
+
* and `requeue` just replays back to the pending guard. This re-enqueues the same `stepId` so the
|
|
2221
|
+
* (idempotent) step re-runs and its result resumes the run. Safe to call on a healthy run — it re-runs
|
|
2222
|
+
* only in-flight `pending` remote steps, which are idempotent by the durable contract. Returns the
|
|
2223
|
+
* run's current status (with the count re-dispatched), or null if the run is unknown.
|
|
2224
|
+
*/
|
|
2225
|
+
redispatchPending(runId: string): Promise<(RunResult & {
|
|
2226
|
+
redispatched: number;
|
|
2227
|
+
}) | null>;
|
|
2168
2228
|
/**
|
|
2169
2229
|
* **Fix-and-replay**: re-run a run (typically a `dead`/`failed` one) with a corrected `input`, as a
|
|
2170
2230
|
* fresh run with clean history. It's a NEW run — `newRunId` defaults to `<runId>~retry~<uuid>` — so
|
|
@@ -2467,6 +2527,10 @@ declare class WorkflowEngine {
|
|
|
2467
2527
|
*/
|
|
2468
2528
|
update(runId: string, name: string, arg: unknown): Promise<UpdateResult>;
|
|
2469
2529
|
private callRemote;
|
|
2530
|
+
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
2531
|
+
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
2532
|
+
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
2533
|
+
private dispatchRemoteTask;
|
|
2470
2534
|
/**
|
|
2471
2535
|
* Complete a durable remote step from its worker result and resume the run — runs on whichever
|
|
2472
2536
|
* instance receives the result (the dispatching one may be gone), so the run is crash/scale-safe.
|
|
@@ -2753,6 +2817,13 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
|
|
|
2753
2817
|
dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
|
|
2754
2818
|
}
|
|
2755
2819
|
|
|
2820
|
+
/** A run as returned by the gateway's list — the durable run plus an optional {@link RunWaiting}
|
|
2821
|
+
* descriptor the control plane resolves (what a suspended run is parked on), so the dashboard can
|
|
2822
|
+
* name the wait in a list row without fetching each run's timeline. Additive: `waiting` is absent
|
|
2823
|
+
* on a non-parked run and on gateways/versions that don't compute it. */
|
|
2824
|
+
type RunListItem = WorkflowRun & {
|
|
2825
|
+
waiting?: RunWaiting | undefined;
|
|
2826
|
+
};
|
|
2756
2827
|
/** Which durable topology a gateway speaks for — surfaced in the dashboard so an operator can tell a
|
|
2757
2828
|
* control plane from a tenant at a glance (the store-backed gateway is the operator; the proxy is a
|
|
2758
2829
|
* tenant). Cheap, synchronous, local knowledge — no round-trip. */
|
|
@@ -2780,7 +2851,10 @@ interface RunGateway {
|
|
|
2780
2851
|
/** This deployment's durable role (control plane vs tenant) — synchronous local metadata. */
|
|
2781
2852
|
topology(): DurableTopology;
|
|
2782
2853
|
getRunDetail(runId: string): Promise<RunDetail | null>;
|
|
2783
|
-
|
|
2854
|
+
/** List runs, each optionally carrying a {@link RunWaiting} descriptor (what a suspended run is
|
|
2855
|
+
* parked on). The store-backed gateway resolves `waiting`; a proxy relays whatever the control
|
|
2856
|
+
* plane sent. */
|
|
2857
|
+
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
2784
2858
|
/** Per-group worker health (queue backlog + live worker heartbeats). On the control plane this is
|
|
2785
2859
|
* every group; over a tenant proxy the operator scopes it to the tenant's own groups. */
|
|
2786
2860
|
workerHealth(): Promise<GroupHealth[]>;
|
|
@@ -2789,6 +2863,12 @@ interface RunGateway {
|
|
|
2789
2863
|
}): Promise<RunResult | null>;
|
|
2790
2864
|
retry(runId: string): Promise<RunResult | null>;
|
|
2791
2865
|
continue(runId: string): Promise<RunResult | null>;
|
|
2866
|
+
/** Re-dispatch every remote step of a run stuck `pending` — the operator recovery for a LOST step
|
|
2867
|
+
* dispatch (crashed worker / dropped job) that no automatic path re-drives. Returns the run's status
|
|
2868
|
+
* plus how many steps were re-dispatched, or null if the run is unknown. */
|
|
2869
|
+
redispatchPending(runId: string): Promise<(RunResult & {
|
|
2870
|
+
redispatched: number;
|
|
2871
|
+
}) | null>;
|
|
2792
2872
|
retryWithInput(runId: string, input: unknown): Promise<{
|
|
2793
2873
|
runId: string;
|
|
2794
2874
|
} | null>;
|
|
@@ -2796,6 +2876,25 @@ interface RunGateway {
|
|
|
2796
2876
|
subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
|
|
2797
2877
|
}
|
|
2798
2878
|
|
|
2879
|
+
/**
|
|
2880
|
+
* Resolve WHAT a suspended run is parked on from the token of the {@link SignalWaiter} it registered.
|
|
2881
|
+
* The token prefix carries the kind — no extra store lookup and no new persisted field. Used by the
|
|
2882
|
+
* store-backed run gateway to stamp {@link RunWaiting} onto each listed run so the dashboard can name
|
|
2883
|
+
* the wait in a row without fetching that run's timeline.
|
|
2884
|
+
*/
|
|
2885
|
+
declare function classifyWaiterToken(token: string): RunWaiting;
|
|
2886
|
+
/**
|
|
2887
|
+
* Resolve a suspended run's {@link RunWaiting} from the pending signal waiters (indexed by `runId`).
|
|
2888
|
+
* Event-only by design (see {@link RunWaiting}): a `waitForSignal`/`waitForEvent`/webhook/awaited-child
|
|
2889
|
+
* suspend registers a waiter and gets named here; every other suspend (a bare `ctx.sleep`, an in-flight
|
|
2890
|
+
* remote step, a reconcile-parked orphan) returns `undefined`, so the client layers its own display
|
|
2891
|
+
* state (no-worker, singleton, running) on top — none of which can be told apart from `wakeAt` alone.
|
|
2892
|
+
*/
|
|
2893
|
+
declare function resolveRunWaiting(run: WorkflowRun, waiterByRun: ReadonlyMap<string, SignalWaiter>): RunWaiting | undefined;
|
|
2894
|
+
/** Index signal waiters by `runId`, keeping the first seen per run (a run parks on one waiter at a
|
|
2895
|
+
* time in the sequential case; the earliest is the one it's blocked on). */
|
|
2896
|
+
declare function indexWaitersByRun(waiters: readonly SignalWaiter[]): Map<string, SignalWaiter>;
|
|
2897
|
+
|
|
2799
2898
|
/**
|
|
2800
2899
|
* Transforms a payload value as it crosses the store boundary — e.g. encrypt-at-rest, compress, or
|
|
2801
2900
|
* redact PII. `decode` must be the exact inverse of `encode`. Both are synchronous (use a local
|
|
@@ -2859,6 +2958,9 @@ declare class CodecStateStore implements StateStore {
|
|
|
2859
2958
|
declare function eventPrefix(name: string): string;
|
|
2860
2959
|
/** Build the unique waiter token for one `waitForEvent` call (name + match + run position). */
|
|
2861
2960
|
declare function eventToken(name: string, match: Record<string, unknown> | undefined, runId: string, seq: number): string;
|
|
2961
|
+
/** The event name embedded in an event waiter token (the 2nd `:`-segment is the base64 name), so a
|
|
2962
|
+
* dashboard can show "waiting on event `<name>`" instead of the opaque token. */
|
|
2963
|
+
declare function eventNameOf(token: string): string;
|
|
2862
2964
|
/** The match criteria embedded in an event waiter token (the 3rd `:`-segment is the base64 match). */
|
|
2863
2965
|
declare function eventMatchOf(token: string): Record<string, unknown>;
|
|
2864
2966
|
/** Whether `payload` satisfies a waiter's `match`: every match key deep-equals the payload's value. */
|
|
@@ -3173,4 +3275,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
|
|
|
3173
3275
|
onControl(handler: (msg: ControlMessage) => void): void;
|
|
3174
3276
|
}
|
|
3175
3277
|
|
|
3176
|
-
export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableTopology, type DurableWebhook, DurableWorkflow, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, collectMetrics, createStepLogger, currentWorkflowCtx, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
|
|
3278
|
+
export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableTopology, type DurableWebhook, DurableWorkflow, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, classifyWaiterToken, collectMetrics, createStepLogger, currentWorkflowCtx, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, indexWaitersByRun, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, resolveRunWaiting, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
|
package/dist/index.d.ts
CHANGED
|
@@ -330,6 +330,27 @@ interface WorkflowRun {
|
|
|
330
330
|
createdAt: Date;
|
|
331
331
|
updatedAt: Date;
|
|
332
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* WHAT an event-parked suspended run is waiting on, resolved for display — the engine keeps one
|
|
335
|
+
* generic `suspended` status (it's what drives recovery/timers/queries), but to a human "waiting on
|
|
336
|
+
* signal `approve`" reads very differently from "waiting on webhook `stripe-cb`". Computed by the
|
|
337
|
+
* store-backed gateway from the run's signal waiters (`listSignalWaiters`, ONE bulk scan), so the
|
|
338
|
+
* dashboard can NAME the wait in a list row without fetching each run's timeline. `on` is derived from
|
|
339
|
+
* the waiter token's prefix (see `classifyWaiterToken`): `wh:<runId>:<seq>` ⇒ `webhook`, `child:<id>`
|
|
340
|
+
* ⇒ `child`, `event:<name>:…` ⇒ `signal` (name decoded), anything else ⇒ `signal` (the token IS the
|
|
341
|
+
* signal name).
|
|
342
|
+
*
|
|
343
|
+
* Deliberately EVENT-only (no timer/step): `wakeAt` alone can't tell a real `ctx.sleep` from the
|
|
344
|
+
* reconcile-fallback `wakeAt` an event/step suspend now carries, and the store exposes no cheap bulk
|
|
345
|
+
* checkpoint scan to disambiguate — so the client shows a non-event suspend with a live worker as
|
|
346
|
+
* `running` (it flips off "no worker" the moment a worker returns) rather than guessing "sleeping".
|
|
347
|
+
* The detail view (which has the timeline) still distinguishes sleep vs in-flight step precisely.
|
|
348
|
+
*/
|
|
349
|
+
interface RunWaiting {
|
|
350
|
+
on: 'signal' | 'webhook' | 'child';
|
|
351
|
+
/** The token/name the run is parked on — the signal name, webhook token, or awaited child id. */
|
|
352
|
+
name: string;
|
|
353
|
+
}
|
|
333
354
|
type StepKind = 'local' | 'remote' | 'sleep' | 'signal';
|
|
334
355
|
/**
|
|
335
356
|
* The recorded result of a single step at a deterministic logical position (`seq`).
|
|
@@ -709,6 +730,9 @@ type RunRequestKind = {
|
|
|
709
730
|
kind: 'retryWithInput';
|
|
710
731
|
runId: string;
|
|
711
732
|
input: unknown;
|
|
733
|
+
} | {
|
|
734
|
+
kind: 'redispatch';
|
|
735
|
+
runId: string;
|
|
712
736
|
};
|
|
713
737
|
/** The control plane's answer to a {@link RunRequest}, correlated by `requestId`. */
|
|
714
738
|
interface RunReply {
|
|
@@ -1797,6 +1821,27 @@ interface WorkflowEngineDeps {
|
|
|
1797
1821
|
* for a genuinely-orphaned run. Default 5min; set `0` to disable (the prior wake-forever behavior).
|
|
1798
1822
|
*/
|
|
1799
1823
|
reconcileMs?: number | undefined;
|
|
1824
|
+
/**
|
|
1825
|
+
* Opt-in self-heal window (ms) for a remote step with NO `timeoutMs` whose dispatched job was LOST —
|
|
1826
|
+
* the worker crashed mid-step leaving no result, or the transport dropped the job (a Redis
|
|
1827
|
+
* flush/eviction, or BullMQ moving a stalled job to `failed` and removing it). The `pending`
|
|
1828
|
+
* checkpoint then has no result to resume on, and — BY DESIGN (see {@link reconcileMs}) — the
|
|
1829
|
+
* reconcile re-drive re-suspends a still-pending step rather than re-dispatching, so a slow-but-live
|
|
1830
|
+
* worker is never double-run. That safety means a genuinely-lost dispatch would otherwise hang
|
|
1831
|
+
* forever. When set, a reconcile re-drive that finds a remote step still `pending` PAST this window
|
|
1832
|
+
* re-dispatches it (bumping `attempts`, bounded by {@link remoteRedispatchMax}). Off by default:
|
|
1833
|
+
* re-dispatch can double-run a step whose original job is merely slow, so the window MUST exceed the
|
|
1834
|
+
* longest such step and the step MUST be idempotent. Prefer a per-step `timeoutMs` (tighter,
|
|
1835
|
+
* heartbeat-aware) where you can; this is the store-driven net for no-timeout steps that must
|
|
1836
|
+
* survive a lost dispatch. {@link WorkflowEngine.redispatchPending} is the manual counterpart.
|
|
1837
|
+
*/
|
|
1838
|
+
remoteRedispatchMs?: number | undefined;
|
|
1839
|
+
/**
|
|
1840
|
+
* Max times {@link remoteRedispatchMs} re-dispatches one lost remote step before giving up and
|
|
1841
|
+
* failing it as a {@link RemoteStepError} (`code: 'remote_step_lost'`), so the run fails / dead-letters
|
|
1842
|
+
* instead of re-dispatching forever. Default 10. Ignored when `remoteRedispatchMs` is unset.
|
|
1843
|
+
*/
|
|
1844
|
+
remoteRedispatchMax?: number | undefined;
|
|
1800
1845
|
/**
|
|
1801
1846
|
* Build the public callback URL for a `ctx.webhook()` token (e.g.
|
|
1802
1847
|
* ``(t) => `https://api.example.com/durable/webhooks/${t}` ``). Populates
|
|
@@ -1873,6 +1918,8 @@ declare class WorkflowEngine {
|
|
|
1873
1918
|
private readonly maxRecoveryAttempts?;
|
|
1874
1919
|
private readonly remoteAdvanceSilenceMs?;
|
|
1875
1920
|
private readonly reconcileMs;
|
|
1921
|
+
private readonly remoteRedispatchMs;
|
|
1922
|
+
private readonly remoteRedispatchMax;
|
|
1876
1923
|
private readonly webhookUrl?;
|
|
1877
1924
|
private readonly traceparent?;
|
|
1878
1925
|
private readonly context?;
|
|
@@ -2165,6 +2212,19 @@ declare class WorkflowEngine {
|
|
|
2165
2212
|
* the HTTP request on workflow execution.
|
|
2166
2213
|
*/
|
|
2167
2214
|
requeue(runId: string): Promise<RunResult | null>;
|
|
2215
|
+
/**
|
|
2216
|
+
* Explicitly re-dispatch every remote step of `runId` stuck `pending` — the OPERATOR escape hatch for
|
|
2217
|
+
* a run whose dispatched step job was LOST (worker crashed with no result, or a transport that dropped
|
|
2218
|
+
* the job). No automatic recovery re-drives these: a reconcile re-drive re-suspends a still-pending
|
|
2219
|
+
* step by design (unless `remoteRedispatchMs` is set), `recoverIncomplete` only reclaims LEASED runs,
|
|
2220
|
+
* and `requeue` just replays back to the pending guard. This re-enqueues the same `stepId` so the
|
|
2221
|
+
* (idempotent) step re-runs and its result resumes the run. Safe to call on a healthy run — it re-runs
|
|
2222
|
+
* only in-flight `pending` remote steps, which are idempotent by the durable contract. Returns the
|
|
2223
|
+
* run's current status (with the count re-dispatched), or null if the run is unknown.
|
|
2224
|
+
*/
|
|
2225
|
+
redispatchPending(runId: string): Promise<(RunResult & {
|
|
2226
|
+
redispatched: number;
|
|
2227
|
+
}) | null>;
|
|
2168
2228
|
/**
|
|
2169
2229
|
* **Fix-and-replay**: re-run a run (typically a `dead`/`failed` one) with a corrected `input`, as a
|
|
2170
2230
|
* fresh run with clean history. It's a NEW run — `newRunId` defaults to `<runId>~retry~<uuid>` — so
|
|
@@ -2467,6 +2527,10 @@ declare class WorkflowEngine {
|
|
|
2467
2527
|
*/
|
|
2468
2528
|
update(runId: string, name: string, arg: unknown): Promise<UpdateResult>;
|
|
2469
2529
|
private callRemote;
|
|
2530
|
+
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
2531
|
+
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
2532
|
+
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
2533
|
+
private dispatchRemoteTask;
|
|
2470
2534
|
/**
|
|
2471
2535
|
* Complete a durable remote step from its worker result and resume the run — runs on whichever
|
|
2472
2536
|
* instance receives the result (the dispatching one may be gone), so the run is crash/scale-safe.
|
|
@@ -2753,6 +2817,13 @@ declare class RemoteWorkflowExecutor implements WorkflowExecutor {
|
|
|
2753
2817
|
dispatch(run: WorkflowRun, history: HistoryEvent[], taskId: string, pendingSignals?: WorkflowTask['pendingSignals']): Promise<void>;
|
|
2754
2818
|
}
|
|
2755
2819
|
|
|
2820
|
+
/** A run as returned by the gateway's list — the durable run plus an optional {@link RunWaiting}
|
|
2821
|
+
* descriptor the control plane resolves (what a suspended run is parked on), so the dashboard can
|
|
2822
|
+
* name the wait in a list row without fetching each run's timeline. Additive: `waiting` is absent
|
|
2823
|
+
* on a non-parked run and on gateways/versions that don't compute it. */
|
|
2824
|
+
type RunListItem = WorkflowRun & {
|
|
2825
|
+
waiting?: RunWaiting | undefined;
|
|
2826
|
+
};
|
|
2756
2827
|
/** Which durable topology a gateway speaks for — surfaced in the dashboard so an operator can tell a
|
|
2757
2828
|
* control plane from a tenant at a glance (the store-backed gateway is the operator; the proxy is a
|
|
2758
2829
|
* tenant). Cheap, synchronous, local knowledge — no round-trip. */
|
|
@@ -2780,7 +2851,10 @@ interface RunGateway {
|
|
|
2780
2851
|
/** This deployment's durable role (control plane vs tenant) — synchronous local metadata. */
|
|
2781
2852
|
topology(): DurableTopology;
|
|
2782
2853
|
getRunDetail(runId: string): Promise<RunDetail | null>;
|
|
2783
|
-
|
|
2854
|
+
/** List runs, each optionally carrying a {@link RunWaiting} descriptor (what a suspended run is
|
|
2855
|
+
* parked on). The store-backed gateway resolves `waiting`; a proxy relays whatever the control
|
|
2856
|
+
* plane sent. */
|
|
2857
|
+
listRuns(query: RunQuery): Promise<RunListItem[]>;
|
|
2784
2858
|
/** Per-group worker health (queue backlog + live worker heartbeats). On the control plane this is
|
|
2785
2859
|
* every group; over a tenant proxy the operator scopes it to the tenant's own groups. */
|
|
2786
2860
|
workerHealth(): Promise<GroupHealth[]>;
|
|
@@ -2789,6 +2863,12 @@ interface RunGateway {
|
|
|
2789
2863
|
}): Promise<RunResult | null>;
|
|
2790
2864
|
retry(runId: string): Promise<RunResult | null>;
|
|
2791
2865
|
continue(runId: string): Promise<RunResult | null>;
|
|
2866
|
+
/** Re-dispatch every remote step of a run stuck `pending` — the operator recovery for a LOST step
|
|
2867
|
+
* dispatch (crashed worker / dropped job) that no automatic path re-drives. Returns the run's status
|
|
2868
|
+
* plus how many steps were re-dispatched, or null if the run is unknown. */
|
|
2869
|
+
redispatchPending(runId: string): Promise<(RunResult & {
|
|
2870
|
+
redispatched: number;
|
|
2871
|
+
}) | null>;
|
|
2792
2872
|
retryWithInput(runId: string, input: unknown): Promise<{
|
|
2793
2873
|
runId: string;
|
|
2794
2874
|
} | null>;
|
|
@@ -2796,6 +2876,25 @@ interface RunGateway {
|
|
|
2796
2876
|
subscribe(runId: string, onEvent: (event: EngineEvent) => void): () => void;
|
|
2797
2877
|
}
|
|
2798
2878
|
|
|
2879
|
+
/**
|
|
2880
|
+
* Resolve WHAT a suspended run is parked on from the token of the {@link SignalWaiter} it registered.
|
|
2881
|
+
* The token prefix carries the kind — no extra store lookup and no new persisted field. Used by the
|
|
2882
|
+
* store-backed run gateway to stamp {@link RunWaiting} onto each listed run so the dashboard can name
|
|
2883
|
+
* the wait in a row without fetching that run's timeline.
|
|
2884
|
+
*/
|
|
2885
|
+
declare function classifyWaiterToken(token: string): RunWaiting;
|
|
2886
|
+
/**
|
|
2887
|
+
* Resolve a suspended run's {@link RunWaiting} from the pending signal waiters (indexed by `runId`).
|
|
2888
|
+
* Event-only by design (see {@link RunWaiting}): a `waitForSignal`/`waitForEvent`/webhook/awaited-child
|
|
2889
|
+
* suspend registers a waiter and gets named here; every other suspend (a bare `ctx.sleep`, an in-flight
|
|
2890
|
+
* remote step, a reconcile-parked orphan) returns `undefined`, so the client layers its own display
|
|
2891
|
+
* state (no-worker, singleton, running) on top — none of which can be told apart from `wakeAt` alone.
|
|
2892
|
+
*/
|
|
2893
|
+
declare function resolveRunWaiting(run: WorkflowRun, waiterByRun: ReadonlyMap<string, SignalWaiter>): RunWaiting | undefined;
|
|
2894
|
+
/** Index signal waiters by `runId`, keeping the first seen per run (a run parks on one waiter at a
|
|
2895
|
+
* time in the sequential case; the earliest is the one it's blocked on). */
|
|
2896
|
+
declare function indexWaitersByRun(waiters: readonly SignalWaiter[]): Map<string, SignalWaiter>;
|
|
2897
|
+
|
|
2799
2898
|
/**
|
|
2800
2899
|
* Transforms a payload value as it crosses the store boundary — e.g. encrypt-at-rest, compress, or
|
|
2801
2900
|
* redact PII. `decode` must be the exact inverse of `encode`. Both are synchronous (use a local
|
|
@@ -2859,6 +2958,9 @@ declare class CodecStateStore implements StateStore {
|
|
|
2859
2958
|
declare function eventPrefix(name: string): string;
|
|
2860
2959
|
/** Build the unique waiter token for one `waitForEvent` call (name + match + run position). */
|
|
2861
2960
|
declare function eventToken(name: string, match: Record<string, unknown> | undefined, runId: string, seq: number): string;
|
|
2961
|
+
/** The event name embedded in an event waiter token (the 2nd `:`-segment is the base64 name), so a
|
|
2962
|
+
* dashboard can show "waiting on event `<name>`" instead of the opaque token. */
|
|
2963
|
+
declare function eventNameOf(token: string): string;
|
|
2862
2964
|
/** The match criteria embedded in an event waiter token (the 3rd `:`-segment is the base64 match). */
|
|
2863
2965
|
declare function eventMatchOf(token: string): Record<string, unknown>;
|
|
2864
2966
|
/** Whether `payload` satisfies a waiter's `match`: every match key deep-equals the payload's value. */
|
|
@@ -3173,4 +3275,4 @@ declare class InMemoryTransport implements Transport, ControlPlane {
|
|
|
3173
3275
|
onControl(handler: (msg: ControlMessage) => void): void;
|
|
3174
3276
|
}
|
|
3175
3277
|
|
|
3176
|
-
export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableTopology, type DurableWebhook, DurableWorkflow, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, collectMetrics, createStepLogger, currentWorkflowCtx, eventMatchOf, eventMatches, eventPrefix, eventToken, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
|
|
3278
|
+
export { type AccumulatorHost, type Admission, type AdmissionBackend, type AdmissionItem, type AttributeFilter, type AttributeOp, type BackoffStrategy, type ChildCallOptions, CodecStateStore, ContinueAsNew, type ControlMessage, type ControlPlane, DURABLE_OPTIONS, DURABLE_OPTIONS_CANONICAL, DURABLE_STEP_CONFIG, DURABLE_STEP_NAME, type DurableTopology, type DurableWebhook, DurableWorkflow, type EngineEvent, type EngineEventType, type EngineListener, Entities, type EntityConfig, type EntityHandler, type EntityHost, EventAccumulators, type EventBatchConfig, FatalError, GatherError, type GroupHealth, type Heartbeat, type HistoryEvent, InMemoryAdmissionBackend, InMemoryStateStore, InMemoryTransport, type MetricsCollector, type MetricsSnapshot, type NamedTransport, NonDeterminismError, type PayloadCodec, type QueueConfig, QueueController, RemoteStepError, RemoteStepTimeout, type RemoteTask, RemoteWorkflowExecutor, RemoteWorkflowTimeout, type RetentionPolicy, type RunAttributeRow, type RunDetail, type RunDispatcher, type RunGateway, type RunListItem, type RunQuery, type RunReply, type RunReplyResult, type RunRequest, type RunRequestKind, type RunResult, type RunSchedulesOptions, type RunStatus, type RunWaiting, STATE_STORE, STATE_STORE_CANONICAL, type ScheduledWorkflow, type SearchAttributes, SignalTimeoutError, type SignalWaiter, type SingletonConfig, SingletonQueueFullError, type StartOptions, type StartRunMessage, type StateStore, type StepCheckpoint, type StepConfig, type StepDef, type StepDispatchOpts, type StepError, type StepEvent, type StepHandler, type StepInterceptor, type StepInvocation, type StepKind, type StepLogger, type StepOptions, type StepRef, type StepResult, type StepUndo, type StoreTransaction, type SubProcessHandle, TERMINAL_RUN_STATUSES, TRANSPORT, TRANSPORT_CANONICAL, type TenantEvent, type Transport, type UndoOf, type UpdateResult, type UpdateValidator, WORKFLOW_NAME_KEY, type WorkerAdjust, type WorkerConcurrencyStatus, type WorkerHeartbeat, type WorkerStatus, type WorkflowClass, type WorkflowCommand, type WorkflowCtx, type WorkflowDecision, WorkflowEngine, type WorkflowEngineDeps, type WorkflowExecutor, type WorkflowInputOf, type WorkflowOutputOf, type WorkflowRef, type WorkflowRun, type WorkflowStartClient, type WorkflowStepEvent, WorkflowSuspended, type WorkflowTask, applyAttributeQuery, attributeColumnFor, attributeOperand, bindWorkflowClass, breakpointToken, classifyWaiterToken, collectMetrics, createStepLogger, currentWorkflowCtx, eventMatchOf, eventMatches, eventNameOf, eventPrefix, eventToken, indexWaitersByRun, matchesAttributes, normalizeAttributeRows, parseDuration, prevCronFireMs, resolveRunWaiting, runInWorkflowCtx, runSchedules, runStepHandler, sanitizeQueueToken, scheduledRunId, sqlComparator, stepConfigOf, stepId, stepNameOf, tenantGroup, workflowName };
|
package/dist/index.js
CHANGED
|
@@ -780,6 +780,14 @@ function eventToken(name, match, runId, seq) {
|
|
|
780
780
|
return `${eventPrefix(name)}${enc(JSON.stringify(match ?? {}))}:${runId}#${seq}`;
|
|
781
781
|
}
|
|
782
782
|
__name(eventToken, "eventToken");
|
|
783
|
+
function eventNameOf(token) {
|
|
784
|
+
try {
|
|
785
|
+
return dec(token.split(":")[1] ?? "");
|
|
786
|
+
} catch {
|
|
787
|
+
return token;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
__name(eventNameOf, "eventNameOf");
|
|
783
791
|
function eventMatchOf(token) {
|
|
784
792
|
try {
|
|
785
793
|
return JSON.parse(dec(token.split(":")[2] ?? ""));
|
|
@@ -1721,6 +1729,8 @@ var WorkflowEngine = class {
|
|
|
1721
1729
|
maxRecoveryAttempts;
|
|
1722
1730
|
remoteAdvanceSilenceMs;
|
|
1723
1731
|
reconcileMs;
|
|
1732
|
+
remoteRedispatchMs;
|
|
1733
|
+
remoteRedispatchMax;
|
|
1724
1734
|
webhookUrl;
|
|
1725
1735
|
traceparent;
|
|
1726
1736
|
context;
|
|
@@ -1792,6 +1802,8 @@ var WorkflowEngine = class {
|
|
|
1792
1802
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
1793
1803
|
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
1794
1804
|
this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
|
|
1805
|
+
this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
|
|
1806
|
+
this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
|
|
1795
1807
|
this.webhookUrl = deps.webhookUrl;
|
|
1796
1808
|
this.traceparent = deps.traceparent;
|
|
1797
1809
|
this.context = deps.context;
|
|
@@ -2503,6 +2515,49 @@ var WorkflowEngine = class {
|
|
|
2503
2515
|
};
|
|
2504
2516
|
}
|
|
2505
2517
|
/**
|
|
2518
|
+
* Explicitly re-dispatch every remote step of `runId` stuck `pending` — the OPERATOR escape hatch for
|
|
2519
|
+
* a run whose dispatched step job was LOST (worker crashed with no result, or a transport that dropped
|
|
2520
|
+
* the job). No automatic recovery re-drives these: a reconcile re-drive re-suspends a still-pending
|
|
2521
|
+
* step by design (unless `remoteRedispatchMs` is set), `recoverIncomplete` only reclaims LEASED runs,
|
|
2522
|
+
* and `requeue` just replays back to the pending guard. This re-enqueues the same `stepId` so the
|
|
2523
|
+
* (idempotent) step re-runs and its result resumes the run. Safe to call on a healthy run — it re-runs
|
|
2524
|
+
* only in-flight `pending` remote steps, which are idempotent by the durable contract. Returns the
|
|
2525
|
+
* run's current status (with the count re-dispatched), or null if the run is unknown.
|
|
2526
|
+
*/
|
|
2527
|
+
async redispatchPending(runId) {
|
|
2528
|
+
const run = await this.store.getRun(runId);
|
|
2529
|
+
if (!run) return null;
|
|
2530
|
+
const pending = (await this.store.listCheckpoints(runId)).filter((cp) => cp.kind === "remote" && cp.status === "pending");
|
|
2531
|
+
let redispatched = 0;
|
|
2532
|
+
for (const cp of pending) {
|
|
2533
|
+
if (!cp.stepId) continue;
|
|
2534
|
+
const reAttempt = cp.attempts + 1;
|
|
2535
|
+
const reEnqueuedAt = /* @__PURE__ */ new Date();
|
|
2536
|
+
await this.store.saveCheckpoint({
|
|
2537
|
+
...cp,
|
|
2538
|
+
attempts: reAttempt,
|
|
2539
|
+
enqueuedAt: reEnqueuedAt,
|
|
2540
|
+
startedAt: reEnqueuedAt,
|
|
2541
|
+
finishedAt: reEnqueuedAt
|
|
2542
|
+
});
|
|
2543
|
+
await this.dispatchRemoteTask({
|
|
2544
|
+
runId,
|
|
2545
|
+
seq: cp.seq,
|
|
2546
|
+
name: cp.name,
|
|
2547
|
+
stepId: cp.stepId,
|
|
2548
|
+
group: cp.workerGroup ?? sanitizeQueueToken(cp.name),
|
|
2549
|
+
input: cp.input,
|
|
2550
|
+
attempt: reAttempt
|
|
2551
|
+
});
|
|
2552
|
+
redispatched += 1;
|
|
2553
|
+
}
|
|
2554
|
+
return {
|
|
2555
|
+
runId,
|
|
2556
|
+
status: run.status,
|
|
2557
|
+
redispatched
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
/**
|
|
2506
2561
|
* **Fix-and-replay**: re-run a run (typically a `dead`/`failed` one) with a corrected `input`, as a
|
|
2507
2562
|
* fresh run with clean history. It's a NEW run — `newRunId` defaults to `<runId>~retry~<uuid>` — so
|
|
2508
2563
|
* the original stays inspectable. Returns the new run's id, or null if `runId` is unknown.
|
|
@@ -3990,7 +4045,47 @@ var WorkflowEngine = class {
|
|
|
3990
4045
|
if (existing?.status === "completed") return existing.output;
|
|
3991
4046
|
if (this.pool.size === 0) throw new Error("remote steps require a Transport");
|
|
3992
4047
|
if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport);
|
|
3993
|
-
if (existing?.status === "pending")
|
|
4048
|
+
if (existing?.status === "pending") {
|
|
4049
|
+
if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
|
|
4050
|
+
if (existing.wakeAt == null) {
|
|
4051
|
+
const wakeAt = this.clock() + this.remoteRedispatchMs;
|
|
4052
|
+
await this.store.saveCheckpoint({
|
|
4053
|
+
...existing,
|
|
4054
|
+
wakeAt
|
|
4055
|
+
});
|
|
4056
|
+
throw new WorkflowSuspended(wakeAt);
|
|
4057
|
+
}
|
|
4058
|
+
if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
|
|
4059
|
+
if (existing.attempts >= this.remoteRedispatchMax) {
|
|
4060
|
+
throw new RemoteStepError({
|
|
4061
|
+
message: `remote step "${step.name}" lost \u2014 no result after ${existing.attempts} re-dispatch(es)`,
|
|
4062
|
+
code: "remote_step_lost"
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
const reAttempt = existing.attempts + 1;
|
|
4066
|
+
const nextDeadline = this.clock() + this.remoteRedispatchMs;
|
|
4067
|
+
const reEnqueuedAt = /* @__PURE__ */ new Date();
|
|
4068
|
+
await this.store.saveCheckpoint({
|
|
4069
|
+
...existing,
|
|
4070
|
+
attempts: reAttempt,
|
|
4071
|
+
wakeAt: nextDeadline,
|
|
4072
|
+
enqueuedAt: reEnqueuedAt,
|
|
4073
|
+
startedAt: reEnqueuedAt,
|
|
4074
|
+
finishedAt: reEnqueuedAt
|
|
4075
|
+
});
|
|
4076
|
+
await this.dispatchRemoteTask({
|
|
4077
|
+
runId,
|
|
4078
|
+
seq,
|
|
4079
|
+
name: step.name,
|
|
4080
|
+
stepId: existing.stepId ?? stepId(runId, seq),
|
|
4081
|
+
group: existing.workerGroup ?? tenantGroup(sanitizeQueueToken(step.name), step.partition),
|
|
4082
|
+
input: existing.input,
|
|
4083
|
+
priority: admission?.priority,
|
|
4084
|
+
attempt: reAttempt,
|
|
4085
|
+
transport
|
|
4086
|
+
});
|
|
4087
|
+
throw new WorkflowSuspended(nextDeadline);
|
|
4088
|
+
}
|
|
3994
4089
|
let attempt = 1;
|
|
3995
4090
|
if (existing?.status === "failed") {
|
|
3996
4091
|
const maxAttempts = Math.max(1, step.retries ?? 1);
|
|
@@ -4038,18 +4133,17 @@ var WorkflowEngine = class {
|
|
|
4038
4133
|
startedAt: enqueuedAt,
|
|
4039
4134
|
finishedAt: enqueuedAt
|
|
4040
4135
|
});
|
|
4041
|
-
await this.
|
|
4136
|
+
await this.dispatchRemoteTask({
|
|
4042
4137
|
runId,
|
|
4043
4138
|
seq,
|
|
4044
4139
|
name: step.name,
|
|
4045
4140
|
stepId: id,
|
|
4046
4141
|
group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
|
|
4047
4142
|
input,
|
|
4048
|
-
traceparent: this.traceparent?.(),
|
|
4049
|
-
context: this.context?.(),
|
|
4050
4143
|
priority: admission?.priority,
|
|
4051
|
-
attempt
|
|
4052
|
-
|
|
4144
|
+
attempt,
|
|
4145
|
+
transport
|
|
4146
|
+
});
|
|
4053
4147
|
this.emit({
|
|
4054
4148
|
type: "step.started",
|
|
4055
4149
|
runId,
|
|
@@ -4059,6 +4153,23 @@ var WorkflowEngine = class {
|
|
|
4059
4153
|
});
|
|
4060
4154
|
throw new WorkflowSuspended();
|
|
4061
4155
|
}
|
|
4156
|
+
/** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
|
|
4157
|
+
* dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
|
|
4158
|
+
* {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
|
|
4159
|
+
dispatchRemoteTask(task) {
|
|
4160
|
+
return this.pool.dispatch({
|
|
4161
|
+
runId: task.runId,
|
|
4162
|
+
seq: task.seq,
|
|
4163
|
+
name: task.name,
|
|
4164
|
+
stepId: task.stepId,
|
|
4165
|
+
group: task.group,
|
|
4166
|
+
input: task.input,
|
|
4167
|
+
traceparent: this.traceparent?.(),
|
|
4168
|
+
context: this.context?.(),
|
|
4169
|
+
priority: task.priority,
|
|
4170
|
+
attempt: task.attempt
|
|
4171
|
+
}, task.transport);
|
|
4172
|
+
}
|
|
4062
4173
|
/**
|
|
4063
4174
|
* Complete a durable remote step from its worker result and resume the run — runs on whichever
|
|
4064
4175
|
* instance receives the result (the dispatching one may be gone), so the run is crash/scale-safe.
|
|
@@ -4295,6 +4406,42 @@ var TERMINAL_RUN_STATUSES = [
|
|
|
4295
4406
|
"dead"
|
|
4296
4407
|
];
|
|
4297
4408
|
|
|
4409
|
+
// src/run-waiting.ts
|
|
4410
|
+
var WEBHOOK_PREFIX = "wh:";
|
|
4411
|
+
var CHILD_PREFIX = "child:";
|
|
4412
|
+
var EVENT_PREFIX = "event:";
|
|
4413
|
+
function classifyWaiterToken(token) {
|
|
4414
|
+
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
4415
|
+
on: "webhook",
|
|
4416
|
+
name: token
|
|
4417
|
+
};
|
|
4418
|
+
if (token.startsWith(CHILD_PREFIX)) return {
|
|
4419
|
+
on: "child",
|
|
4420
|
+
name: token.slice(CHILD_PREFIX.length)
|
|
4421
|
+
};
|
|
4422
|
+
if (token.startsWith(EVENT_PREFIX)) return {
|
|
4423
|
+
on: "signal",
|
|
4424
|
+
name: eventNameOf(token)
|
|
4425
|
+
};
|
|
4426
|
+
return {
|
|
4427
|
+
on: "signal",
|
|
4428
|
+
name: token
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
__name(classifyWaiterToken, "classifyWaiterToken");
|
|
4432
|
+
function resolveRunWaiting(run, waiterByRun) {
|
|
4433
|
+
if (run.status !== "suspended") return void 0;
|
|
4434
|
+
const waiter = waiterByRun.get(run.id);
|
|
4435
|
+
return waiter ? classifyWaiterToken(waiter.token) : void 0;
|
|
4436
|
+
}
|
|
4437
|
+
__name(resolveRunWaiting, "resolveRunWaiting");
|
|
4438
|
+
function indexWaitersByRun(waiters) {
|
|
4439
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
4440
|
+
for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
|
|
4441
|
+
return byRun;
|
|
4442
|
+
}
|
|
4443
|
+
__name(indexWaitersByRun, "indexWaitersByRun");
|
|
4444
|
+
|
|
4298
4445
|
// src/codec-state-store.ts
|
|
4299
4446
|
var CodecStateStore = class {
|
|
4300
4447
|
static {
|
|
@@ -5049,17 +5196,21 @@ export {
|
|
|
5049
5196
|
attributeOperand,
|
|
5050
5197
|
bindWorkflowClass,
|
|
5051
5198
|
breakpointToken,
|
|
5199
|
+
classifyWaiterToken,
|
|
5052
5200
|
collectMetrics,
|
|
5053
5201
|
createStepLogger,
|
|
5054
5202
|
currentWorkflowCtx,
|
|
5055
5203
|
eventMatchOf,
|
|
5056
5204
|
eventMatches,
|
|
5205
|
+
eventNameOf,
|
|
5057
5206
|
eventPrefix,
|
|
5058
5207
|
eventToken,
|
|
5208
|
+
indexWaitersByRun,
|
|
5059
5209
|
matchesAttributes,
|
|
5060
5210
|
normalizeAttributeRows,
|
|
5061
5211
|
parseDuration,
|
|
5062
5212
|
prevCronFireMs,
|
|
5213
|
+
resolveRunWaiting,
|
|
5063
5214
|
runInWorkflowCtx,
|
|
5064
5215
|
runSchedules,
|
|
5065
5216
|
runStepHandler,
|