@lostgradient/weft 0.24.1 → 0.24.2

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.
Files changed (97) hide show
  1. package/README.md +1 -1
  2. package/dist/core/engine/bulk-operations-purge.d.ts +1 -1
  3. package/dist/core/engine/bulk-operations-purge.js +3 -2
  4. package/dist/core/engine/bulk-operations-retry.d.ts +16 -1
  5. package/dist/core/engine/bulk-operations-retry.js +19 -6
  6. package/dist/core/engine/bulk-operations.d.ts +3 -2
  7. package/dist/core/engine/bulk-operations.js +2 -2
  8. package/dist/core/engine/callback-creators-bundles.js +1 -1
  9. package/dist/core/engine/callback-creators-schedule.js +2 -2
  10. package/dist/core/engine/child-workflow.d.ts +13 -1
  11. package/dist/core/engine/child-workflow.js +22 -4
  12. package/dist/core/engine/generation-codec.d.ts +22 -0
  13. package/dist/core/engine/generation-codec.js +2 -0
  14. package/dist/core/engine/index.js +1 -1
  15. package/dist/core/engine/lifecycle/recovery-isolation.d.ts +20 -0
  16. package/dist/core/engine/lifecycle/recovery-isolation.js +26 -0
  17. package/dist/core/engine/lifecycle/resume-body.d.ts +44 -0
  18. package/dist/core/engine/lifecycle/resume-body.js +211 -0
  19. package/dist/core/engine/lifecycle/resume.d.ts +46 -3
  20. package/dist/core/engine/lifecycle/resume.js +21 -210
  21. package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +68 -2
  22. package/dist/core/engine/lifecycle/standalone-claim-acquire.js +14 -4
  23. package/dist/core/engine/lifecycle/start-commit.d.ts +2 -0
  24. package/dist/core/engine/lifecycle/start-commit.js +16 -8
  25. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +8 -2
  26. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +11 -3
  27. package/dist/core/engine/lifecycle/start-precondition-attribution.d.ts +10 -4
  28. package/dist/core/engine/lifecycle/start-schedule-timing.d.ts +11 -0
  29. package/dist/core/engine/lifecycle/start-schedule-timing.js +21 -0
  30. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.d.ts +90 -11
  31. package/dist/core/engine/lifecycle/start-terminal-conflict-purge.js +24 -5
  32. package/dist/core/engine/lifecycle/start.d.ts +40 -3
  33. package/dist/core/engine/lifecycle/start.js +19 -26
  34. package/dist/core/engine/lifecycle/transition.d.ts +9 -0
  35. package/dist/core/engine/lifecycle/transition.js +20 -25
  36. package/dist/core/engine/lifecycle.d.ts +2 -1
  37. package/dist/core/engine/lifecycle.js +2 -1
  38. package/dist/core/engine/schedule-overlap.js +2 -1
  39. package/dist/core/engine/schedule-run.d.ts +10 -0
  40. package/dist/core/engine/schedule-run.js +1 -1
  41. package/dist/core/engine/schedules.d.ts +11 -2
  42. package/dist/core/engine/termination/complete.js +1 -1
  43. package/dist/core/engine/termination/suspend.js +3 -0
  44. package/dist/core/engine/validation/schedule.d.ts +25 -1
  45. package/dist/core/engine/validation/schedule.js +4 -10
  46. package/dist/core/engine/validation.js +10 -17
  47. package/dist/core/engine/workflow-claim-reclaim-target.js +6 -6
  48. package/dist/core/engine/workflow-claim-registry.d.ts +79 -75
  49. package/dist/core/engine/workflow-claim-registry.js +15 -2
  50. package/dist/core/engine/workflow-generation-fence.d.ts +74 -0
  51. package/dist/core/engine/workflow-generation-fence.js +19 -0
  52. package/dist/core/start-workflow-validation.d.ts +15 -0
  53. package/dist/core/start-workflow-validation.js +11 -1
  54. package/dist/core/workflow-identifiers.d.ts +37 -0
  55. package/dist/core/workflow-identifiers.js +19 -1
  56. package/dist/http.js +2 -2
  57. package/dist/indexeddb.js +1 -1
  58. package/dist/server/fleet-event-feed.js +6 -6
  59. package/dist/server/rest-binding.d.ts +11 -0
  60. package/dist/server/runtime/task-dispatch-envelope.d.ts +32 -5
  61. package/dist/server/runtime/task-dispatch-envelope.js +23 -1
  62. package/dist/server/runtime/task-dispatch.d.ts +3 -1
  63. package/dist/server/runtime/task-dispatch.js +9 -8
  64. package/dist/server/task-ledger-codec.d.ts +19 -0
  65. package/dist/server/task-ledger-codec.js +3 -0
  66. package/dist/server/task-ledger.d.ts +1 -1
  67. package/dist/server/task-ledger.js +1 -0
  68. package/dist/storage/bun-sql.js +132 -99
  69. package/dist/storage/compressed-storage.js +1 -1
  70. package/dist/storage/generation-keys.d.ts +52 -0
  71. package/dist/storage/generation-keys.js +4 -0
  72. package/dist/storage/index.d.ts +76 -65
  73. package/dist/storage/interface.d.ts +24 -232
  74. package/dist/storage/interface.js +1 -1
  75. package/dist/storage/key-prefixes.d.ts +1 -1
  76. package/dist/storage/key-prefixes.js +1 -0
  77. package/dist/storage/lease-keys.d.ts +48 -0
  78. package/dist/storage/lease-keys.js +8 -0
  79. package/dist/storage/lmdb.js +1 -1
  80. package/dist/storage/memory.js +1 -1
  81. package/dist/storage/neon.js +3 -3
  82. package/dist/storage/node-sqlite.js +132 -99
  83. package/dist/storage/postgres.js +3 -3
  84. package/dist/storage/resolve.js +1 -1
  85. package/dist/storage/scoped-storage.js +1 -1
  86. package/dist/storage/signal-keys.d.ts +23 -0
  87. package/dist/storage/signal-keys.js +9 -0
  88. package/dist/storage/testing.js +1 -1
  89. package/dist/storage/turso.js +2 -2
  90. package/dist/storage/workflow-lifecycle-keys.d.ts +128 -0
  91. package/dist/storage/workflow-lifecycle-keys.js +46 -0
  92. package/dist/storage/workflow-record-keys.d.ts +98 -0
  93. package/dist/storage/workflow-record-keys.js +47 -0
  94. package/dist/version.d.ts +1 -1
  95. package/dist/version.js +1 -1
  96. package/dist/web-extension.js +1 -1
  97. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Weft
2
2
 
3
- A Bun-native durable execution engine. Current release: `0.24.1`.
3
+ A Bun-native durable execution engine. Current release: `0.24.2`.
4
4
 
5
5
  Install the library from npm as `@lostgradient/weft`:
6
6
 
@@ -21,7 +21,7 @@ export declare function purgeWorkflow(internals: EngineInternals, state: Workflo
21
21
  * `onTerminalConflict: 'start-new'` restart path prepends them to the create batch
22
22
  * so purge-and-recreate land as one atomic unit (no window where the prior run is
23
23
  * gone but the new one has not committed). Keep this the single source of truth
24
- * for "what a purge deletes" — do not fork the delete-set.
24
+ * for "what a purge deletes" — do not fork the delete-set. (`wf-gen:<id>` is deliberately excluded; WFT-153, see {@link buildBaseWorkflowDeleteKeys}.)
25
25
  */
26
26
  export declare function collectWorkflowPurgeDeleteOperations(internals: EngineInternals, state: WorkflowState): Promise<BatchOperation[]>;
27
27
  /**
@@ -20,6 +20,7 @@ import { decodeScheduleRunMetadata } from "./schedule-run-metadata.js";
20
20
  import { createTerminalCleanupTimerId } from "./state-utilities.js";
21
21
  import { buildExternalTerminalRotationFragment } from "./storage-io.js";
22
22
  import { decodeWorkflowState, isTerminalWorkflowStatus } from "./validation.js";
23
+ import { foldWorkflowGenerationBumpForPurge } from "./workflow-generation-fence.js";
23
24
  import { buildWorkflowVisibilityIndexTransition } from "./workflow-indexes.js";
24
25
  import { getWorkflowRetentionDeadline } from "./workflow-retention-deadline.js";
25
26
  export const TERMINAL_CLEANUP_DELAY_MS = 60000;
@@ -127,8 +128,8 @@ async function shouldPurgeWorkflowState(internals, state, expiredOnly, now) {
127
128
  return deadline !== null && deadline <= now;
128
129
  }
129
130
  export async function purgeWorkflow(internals, state, cleanupWaiters) {
130
- const deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state), rotation = await buildExternalTerminalRotationFragment(internals, state.id), operations = [...deleteOperations, ...rotation.operations];
131
- await commitFencedEngineWrite(internals, null, operations, rotation.conditions, () => {
131
+ const deleteOperations = await collectWorkflowPurgeDeleteOperations(internals, state), rotation = await buildExternalTerminalRotationFragment(internals, state.id), fenced = await foldWorkflowGenerationBumpForPurge(internals, state.id, rotation), operations = [...deleteOperations, ...fenced.operations];
132
+ await commitFencedEngineWrite(internals, null, operations, fenced.conditions, () => {
132
133
  return Error(`Purge commit for workflow "${state.id}" lost its precondition.`);
133
134
  });
134
135
  clearPurgedWorkflowInMemoryState(internals, state.id, cleanupWaiters);
@@ -1,9 +1,24 @@
1
1
  import type { BulkOperationDryRunResult, BulkOperationOptions, BulkRetryFailedResult, ListFilter } from '../types.ts';
2
2
  import type { EngineInternals } from './internals.ts';
3
+ import { type LifecycleCallbacks } from './lifecycle.ts';
3
4
  /**
4
5
  * `bulkOperations.ts`'s `retryFailedAll()` implementation, split into its
5
6
  * own module (with the rest of the retry-failed cluster below) to keep
6
7
  * `bulk-operations.ts` under the 800-line implementation-file ceiling —
7
8
  * the same reason `bulk-operations-purge.ts` was split out earlier.
8
9
  */
9
- export declare function runBulkFailedWorkflowRetry(internals: EngineInternals, filter: ListFilter, options?: BulkOperationOptions): Promise<BulkRetryFailedResult | BulkOperationDryRunResult>;
10
+ export declare function runBulkFailedWorkflowRetry(internals: EngineInternals, filter: ListFilter, options?: BulkOperationOptions,
11
+ /**
12
+ * Threaded down from `Engine#retryFailedAll()`'s own `#createLifecycleCallbacks()`
13
+ * (WFT-95 issue 2 follow-up). Needed only by `retryFailedWorkflow()`'s
14
+ * checkpoint-absent fallback, which must call the internal `startWorkflow()`
15
+ * directly (see its doc comment) rather than the public `engine.start()`.
16
+ * Sourced this way — as a parameter, rather than this module importing
17
+ * `createLifecycleCallbacks` from `callback-creators-core.ts` itself — to avoid
18
+ * introducing a `bulk-operations-retry.ts` -> `callback-creators-core.ts` ->
19
+ * `termination.ts` -> `termination/complete.ts` -> `bulk-operations.ts` ->
20
+ * `bulk-operations-retry.ts` import cycle. Optional so direct-internals test
21
+ * harnesses that never reach the checkpoint-absent fallback (e.g.
22
+ * `bulk-operations-retry-direct.test.ts`) do not need to construct one.
23
+ */
24
+ callbacks?: LifecycleCallbacks): Promise<BulkRetryFailedResult | BulkOperationDryRunResult>;
@@ -19,8 +19,13 @@ import {
19
19
  validateBulkConfirmation,
20
20
  withBulkAuditEvent
21
21
  } from "./bulk-operations-shared.js";
22
+ import { ensureWorkflowCatalogReady, isWorkflowCatalogReady } from "./catalog-readiness.js";
22
23
  import { resolveExecutableRegistrationForRetry } from "./dynamic-source-execution.js";
23
- import { commitFencedEngineWriteAllowingPreconditionFailure } from "./fenced-write.js";
24
+ import {
25
+ assertLeaseHeldForEngineWork,
26
+ commitFencedEngineWriteAllowingPreconditionFailure
27
+ } from "./fenced-write.js";
28
+ import { startWorkflow } from "./lifecycle.js";
24
29
  import { buildCatalogEntryRevisionCondition } from "./lifecycle/start-commit.js";
25
30
  import { createTerminalCleanupTimerId } from "./state-utilities.js";
26
31
  import { loadWorkflowState, runSerializedWorkflowStateWrite } from "./storage-io.js";
@@ -33,7 +38,7 @@ import {
33
38
  import { buildWorkflowConcurrencyStartOperations } from "./workflow-concurrency.js";
34
39
  import { buildWorkflowVisibilityIndexTransition } from "./workflow-indexes.js";
35
40
  const FAILED_WORKFLOW_STATUSES = ["failed"], CHECKPOINT_RETRY_CONCURRENCY_ADMISSION_MAX_ATTEMPTS = 5;
36
- export async function runBulkFailedWorkflowRetry(internals, filter, options = {}) {
41
+ export async function runBulkFailedWorkflowRetry(internals, filter, options = {}, callbacks) {
37
42
  options = normalizeBulkOperationOptions(options);
38
43
  assertScopedBulkWorkflowFilter(filter);
39
44
  const actionableFilter = buildActionableBulkWorkflowFilter(internals, filter, FAILED_WORKFLOW_STATUSES), preparation = await prepareBulkOperation(internals, "retry-failed", actionableFilter, filter, {}, options);
@@ -43,7 +48,7 @@ export async function runBulkFailedWorkflowRetry(internals, filter, options = {}
43
48
  const bulkConcurrency = resolveBulkOperationConcurrency(options);
44
49
  let retried = 0;
45
50
  const errors = [], retryResults = await runBulkWorkflowPool(preparation.workflowIds, bulkConcurrency, async (workflowId) => {
46
- await retryFailedWorkflow(internals, workflowId);
51
+ await retryFailedWorkflow(internals, workflowId, callbacks);
47
52
  return { status: "retried" };
48
53
  });
49
54
  for (const retryResult of retryResults) {
@@ -58,7 +63,7 @@ export async function runBulkFailedWorkflowRetry(internals, filter, options = {}
58
63
  return result;
59
64
  return withBulkAuditEvent(internals, preparation, options, result, retried);
60
65
  }
61
- async function retryFailedWorkflow(internals, workflowId) {
66
+ async function retryFailedWorkflow(internals, workflowId, callbacks) {
62
67
  const state = await loadWorkflowState(internals, workflowId);
63
68
  if (state === null)
64
69
  throw Error("Workflow no longer exists");
@@ -69,11 +74,19 @@ async function retryFailedWorkflow(internals, workflowId) {
69
74
  await internals.engine.resume(workflowId);
70
75
  return;
71
76
  }
72
- await internals.engine.start(state.type, state.input, {
77
+ const restartOptions = {
73
78
  id: workflowId,
74
79
  onTerminalConflict: "start-new",
75
80
  ...state.tags !== void 0 ? { tags: state.tags } : {}
76
- });
81
+ };
82
+ if (callbacks === void 0) {
83
+ await internals.engine.start(state.type, state.input, restartOptions);
84
+ return;
85
+ }
86
+ assertLeaseHeldForEngineWork(internals);
87
+ if (!isWorkflowCatalogReady(internals.engine))
88
+ await ensureWorkflowCatalogReady(internals.engine);
89
+ await startWorkflow(internals, state.type, state.input, restartOptions, void 0, callbacks, void 0, void 0, "bulk-retry-only");
77
90
  }
78
91
  async function reactivateFailedWorkflowFromCheckpoint(internals, state) {
79
92
  const reactivated = await runSerializedWorkflowStateWrite(internals, state.id, async () => reactivateFailedWorkflowFromCheckpointSerialized(internals, state.id));
@@ -1,14 +1,15 @@
1
1
  import type { BulkCancelResult, BulkDeleteResult, BulkOperationCommitOptions, BulkOperationDryRunOptions, BulkOperationDryRunResult, BulkOperationOptions, BulkRetryFailedResult, BulkSignalAllCommitOptions, BulkSignalAllDryRunOptions, BulkSignalResult, BulkTagResult, ListFilter, PurgeResult } from '../types.ts';
2
2
  import { type CleanupWaiters } from './bulk-operations-purge.ts';
3
3
  import type { EngineInternals } from './internals.ts';
4
+ import type { LifecycleCallbacks } from './lifecycle.ts';
4
5
  export { purgeInternal, TERMINAL_CLEANUP_DELAY_MS } from './bulk-operations-purge.ts';
5
6
  export declare function purge(internals: EngineInternals, filter: ListFilter | undefined, cleanupWaiters: CleanupWaiters): Promise<PurgeResult>;
6
7
  export declare function cancelAll(internals: EngineInternals, filter: ListFilter, options: BulkOperationDryRunOptions): Promise<BulkOperationDryRunResult>;
7
8
  export declare function cancelAll(internals: EngineInternals, filter: ListFilter, options?: BulkOperationCommitOptions): Promise<BulkCancelResult>;
8
9
  export declare function cancelAll(internals: EngineInternals, filter: ListFilter, options?: BulkOperationDryRunOptions | BulkOperationCommitOptions): Promise<BulkCancelResult | BulkOperationDryRunResult>;
9
10
  export declare function retryFailedAll(internals: EngineInternals, filter: ListFilter, options: BulkOperationDryRunOptions): Promise<BulkOperationDryRunResult>;
10
- export declare function retryFailedAll(internals: EngineInternals, filter: ListFilter, options?: BulkOperationCommitOptions): Promise<BulkRetryFailedResult>;
11
- export declare function retryFailedAll(internals: EngineInternals, filter: ListFilter, options?: BulkOperationDryRunOptions | BulkOperationCommitOptions): Promise<BulkRetryFailedResult | BulkOperationDryRunResult>;
11
+ export declare function retryFailedAll(internals: EngineInternals, filter: ListFilter, options?: BulkOperationCommitOptions, callbacks?: LifecycleCallbacks): Promise<BulkRetryFailedResult>;
12
+ export declare function retryFailedAll(internals: EngineInternals, filter: ListFilter, options?: BulkOperationDryRunOptions | BulkOperationCommitOptions, callbacks?: LifecycleCallbacks): Promise<BulkRetryFailedResult | BulkOperationDryRunResult>;
12
13
  export declare function signalAll(internals: EngineInternals, filter: ListFilter, name: string, payload: unknown, options: BulkSignalAllDryRunOptions): Promise<BulkOperationDryRunResult>;
13
14
  export declare function signalAll(internals: EngineInternals, filter: ListFilter, name: string, payload: unknown, options: BulkSignalAllCommitOptions): Promise<BulkSignalResult>;
14
15
  export declare function signalAll(internals: EngineInternals, filter: ListFilter, name: string, payload?: unknown, options?: BulkOperationCommitOptions): Promise<BulkSignalResult>;
@@ -64,9 +64,9 @@ async function runBulkCancellation(internals, filter, options = {}) {
64
64
  export async function cancelAll(internals, filter, options = {}) {
65
65
  return runBulkCancellation(internals, filter, options);
66
66
  }
67
- export async function retryFailedAll(internals, filter, options = {}) {
67
+ export async function retryFailedAll(internals, filter, options = {}, callbacks) {
68
68
  assertLeaseHeldForEngineWork(internals);
69
- return runBulkFailedWorkflowRetry(internals, filter, options);
69
+ return runBulkFailedWorkflowRetry(internals, filter, options, callbacks);
70
70
  }
71
71
  export async function signalAll(internals, filter, name, payload, maybeOptions) {
72
72
  const options = normalizeBulkOperationOptions(maybeOptions ?? {});
@@ -58,7 +58,7 @@ import {
58
58
  export function createChildWorkflowOperationCallbacks(engine) {
59
59
  return {
60
60
  runOperationWithResult: (workflowId, operation, execute) => runOperationWithResultForEngine(engine, workflowId, operation, execute),
61
- start: (type, input, options) => startWorkflow(getInternals(engine), type, input, options, void 0, createLifecycleCallbacks(engine)),
61
+ start: (type, input, options, skipAdmissionIdCheck) => startWorkflow(getInternals(engine), type, input, options, void 0, createLifecycleCallbacks(engine), void 0, void 0, skipAdmissionIdCheck),
62
62
  loadWorkflowState: (workflowId) => loadWorkflowState(getInternals(engine), workflowId),
63
63
  getHandle: (workflowId) => engine.getHandle(workflowId),
64
64
  getComposedWorkflowInterceptor: () => getComposedWorkflowInterceptor(getInternals(engine))
@@ -20,8 +20,8 @@ import { feedOperationResult } from "./strategy-helpers.js";
20
20
  import { ensureTerminalCleanupTracked, failWorkflow, handleCleanupError } from "./termination.js";
21
21
  export function createScheduleCallbacks(engine) {
22
22
  return {
23
- startWorkflow: async (type, input, options, additionalStartOperations, revisionOverride) => {
24
- await startWorkflow(getInternals(engine), type, input, options, additionalStartOperations, createLifecycleCallbacks(engine), void 0, revisionOverride);
23
+ startWorkflow: async (type, input, options, additionalStartOperations, revisionOverride, skipAdmissionIdCheck) => {
24
+ await startWorkflow(getInternals(engine), type, input, options, additionalStartOperations, createLifecycleCallbacks(engine), void 0, revisionOverride, skipAdmissionIdCheck);
25
25
  },
26
26
  loadWorkflowState: (workflowId) => loadWorkflowState(getInternals(engine), workflowId),
27
27
  cancelWorkflow: (workflowId) => engine.cancel(workflowId),
@@ -8,7 +8,19 @@ type ChildWorkflowOperation = Extract<ContextOperationRequest, {
8
8
  }>;
9
9
  export type ChildWorkflowOperationCallbacks = {
10
10
  runOperationWithResult: (workflowId: string, operation: ChildWorkflowOperation, execute: () => Promise<unknown>) => Promise<void>;
11
- start: (type: string, input: unknown, options?: StartOptions) => Promise<WorkflowHandle>;
11
+ /**
12
+ * `skipAdmissionIdCheck` (WFT-95, internal only) is threaded straight through
13
+ * to `startWorkflow`'s parameter of the same name — see its doc comment. It
14
+ * is NEVER part of the public `StartOptions` a caller passes; only
15
+ * `dispatchChildWorkflowStart`'s crash-reattach retry sets it, and only
16
+ * after confirming a matching persisted child record already exists for
17
+ * `childWorkflowId`. This retry always passes the literal `'reattach-only'`,
18
+ * not `true`: the confirming read here is separate from
19
+ * `startWorkflow`'s own atomic re-check, so `'reattach-only'` fences the
20
+ * bypass to that re-check actually finding a match to reattach to (see
21
+ * `startWorkflow`'s doc comment for the full TOCTOU race this closes).
22
+ */
23
+ start: (type: string, input: unknown, options?: StartOptions, skipAdmissionIdCheck?: boolean | 'reattach-only') => Promise<WorkflowHandle>;
12
24
  loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>;
13
25
  getHandle: (workflowId: string) => WorkflowHandle;
14
26
  getComposedWorkflowInterceptor: () => ComposedWorkflowInterceptor | null;
@@ -1,4 +1,8 @@
1
- import { assertOnTerminalConflictUnsupported } from "../start-workflow-validation.js";
1
+ import {
2
+ assertOnTerminalConflictUnsupported,
3
+ StartWorkflowValidationError
4
+ } from "../start-workflow-validation.js";
5
+ import { isReservedWorkflowIdLiteral } from "../workflow-identifiers.js";
2
6
  import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
3
7
  import {
4
8
  buildChildCancellationOperations,
@@ -50,6 +54,18 @@ async function resolveCollisionChildHandle(childWorkflowId, operation, execution
50
54
  throw Error(`Child workflow id collision for "${childWorkflowId}" does not match the requested child workflow`, { cause: collisionError });
51
55
  return callbacks.getHandle(childWorkflowId);
52
56
  }
57
+ async function reattachLegacyReservedChildOrRethrow(childWorkflowId, operation, context, callbacks, originalError) {
58
+ const existingState = await callbacks.loadWorkflowState(childWorkflowId);
59
+ if (!existingState || !existingChildMatchesRequest(existingState, operation, context.pendingExecutionStateOwnerId ?? void 0, context.pendingParentWorkflowId, context.pendingParentWorkflowExecutionToken))
60
+ throw originalError;
61
+ try {
62
+ return await callbacks.start(operation.workflowType, operation.input, { id: childWorkflowId }, "reattach-only");
63
+ } catch (retryError) {
64
+ if (!(retryError instanceof WorkflowAlreadyExistsError))
65
+ throw retryError;
66
+ return resolveCollisionChildHandle(childWorkflowId, operation, context.pendingExecutionStateOwnerId ?? void 0, context.pendingParentWorkflowId, context.pendingParentWorkflowExecutionToken, retryError, callbacks);
67
+ }
68
+ }
53
69
  async function dispatchChildWorkflowStart(internals, childWorkflowId, operation, context, callbacks) {
54
70
  assertOnTerminalConflictUnsupported(operation.options, "ctx.startChild");
55
71
  applyPendingChildExecutionContext(internals, context);
@@ -58,9 +74,11 @@ async function dispatchChildWorkflowStart(internals, childWorkflowId, operation,
58
74
  id: childWorkflowId
59
75
  });
60
76
  } catch (error) {
61
- if (!(error instanceof WorkflowAlreadyExistsError))
62
- throw error;
63
- return resolveCollisionChildHandle(childWorkflowId, operation, context.pendingExecutionStateOwnerId ?? void 0, context.pendingParentWorkflowId, context.pendingParentWorkflowExecutionToken, error, callbacks);
77
+ if (error instanceof WorkflowAlreadyExistsError)
78
+ return resolveCollisionChildHandle(childWorkflowId, operation, context.pendingExecutionStateOwnerId ?? void 0, context.pendingParentWorkflowId, context.pendingParentWorkflowExecutionToken, error, callbacks);
79
+ if (error instanceof StartWorkflowValidationError && isReservedWorkflowIdLiteral(childWorkflowId))
80
+ return reattachLegacyReservedChildOrRethrow(childWorkflowId, operation, context, callbacks, error);
81
+ throw error;
64
82
  } finally {
65
83
  clearPendingChildExecutionContext(internals, context);
66
84
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pure codec for the durable per-workflow-id generation counter
3
+ * (`wf-gen:<id>`, WFT-153).
4
+ *
5
+ * Reuses `lease-codec.ts`'s `encodeEpoch`/`decodeEpoch` under
6
+ * generation-specific names rather than reimplementing them: both are
7
+ * permanently-retained monotonic uint64 counters minted the same way
8
+ * (`(observed ?? 0) + 1`, never a literal), so sharing one encoder/decoder
9
+ * keeps their on-disk representation byte-identical and their validity range
10
+ * ([1, MAX_SAFE_INTEGER)) — fail-closed on corruption — in lockstep by
11
+ * construction rather than by convention.
12
+ *
13
+ * @module core/engine/generation-codec
14
+ */
15
+ import { decodeEpoch, encodeEpoch } from './lease-codec.ts';
16
+ /** Encode an 8-byte big-endian uint64 generation. See `lease-codec.ts#encodeEpoch`. */
17
+ export declare const encodeGeneration: typeof encodeEpoch;
18
+ /**
19
+ * Decode an 8-byte big-endian uint64 generation, or `null` when the stored
20
+ * value is not a usable generation. See `lease-codec.ts#decodeEpoch`.
21
+ */
22
+ export declare const decodeGeneration: typeof decodeEpoch;
@@ -0,0 +1,2 @@
1
+ import { decodeEpoch, encodeEpoch } from "./lease-codec.js";
2
+ export const encodeGeneration = encodeEpoch, decodeGeneration = decodeEpoch;
@@ -847,7 +847,7 @@ export class Engine extends EventTarget {
847
847
  return cancelAllWorkflows(getInternals(this), filter, options);
848
848
  }
849
849
  async retryFailedAll(filter, options) {
850
- return retryFailedAllWorkflows(getInternals(this), filter, options);
850
+ return retryFailedAllWorkflows(getInternals(this), filter, options, this.#createLifecycleCallbacks());
851
851
  }
852
852
  async signalAll(filter, name, payloadOrOptions, options) {
853
853
  if (options === void 0)
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `recoverAll()`'s per-entry error isolation — split out of `transition.ts`,
3
+ * which has no headroom under this repository's 500-line implementation-file
4
+ * ceiling for the WFT-134 claim-release wrapping `recoverEntryOrIsolateFailure`
5
+ * needed (mirrors `resume-body.ts`'s same split). This file's own job is just
6
+ * the branch-matching: given the error `resume()` rejected with, either
7
+ * isolate it to just this workflow (returning `null`) or rethrow it.
8
+ *
9
+ * @module core/engine/lifecycle/recovery-isolation
10
+ */
11
+ import type { WorkflowHandle } from '../handles.ts';
12
+ import type { LifecycleCallbacks, RecoverAllOptions } from './shared.ts';
13
+ /**
14
+ * Isolate one `recoverAll()` entry's failure to just this workflow when it
15
+ * matches a known-recoverable error, committing the matching `failWorkflowForXxx`
16
+ * fenced write; rethrows anything else, including an opted-in
17
+ * `VersionMismatchError` throw. See `transition.ts`'s `recoverEntryOrIsolateFailure`
18
+ * for the caller-side claim-release wrapping around this.
19
+ */
20
+ export declare function isolateRecoveryFailure(workflowId: string, callbacks: LifecycleCallbacks, options: RecoverAllOptions | undefined, error: unknown): Promise<WorkflowHandle | null>;
@@ -0,0 +1,26 @@
1
+ import { RegExpExtensionDecodeError } from "../../codec/extension-codec.js";
2
+ import { VersionMismatchError } from "../../versioning.js";
3
+ import { DynamicWorkflowSourceUnavailableError } from "../dynamic-source-errors.js";
4
+ import { WorkflowClaimUnavailableError } from "../lease-errors.js";
5
+ import { WorkflowRevisionUnavailableError } from "../revision-errors.js";
6
+ export async function isolateRecoveryFailure(workflowId, callbacks, options, error) {
7
+ if (error instanceof RegExpExtensionDecodeError) {
8
+ await callbacks.failWorkflowForCheckpointDecodeError(workflowId, error);
9
+ return null;
10
+ }
11
+ if (error instanceof VersionMismatchError && options?.versionMismatchPolicy !== "throw") {
12
+ await callbacks.failWorkflowForVersionMismatch(workflowId, error);
13
+ return null;
14
+ }
15
+ if (error instanceof WorkflowClaimUnavailableError)
16
+ return null;
17
+ if (error instanceof WorkflowRevisionUnavailableError) {
18
+ await callbacks.failWorkflowForRevisionUnavailable(workflowId, error);
19
+ return null;
20
+ }
21
+ if (error instanceof DynamicWorkflowSourceUnavailableError) {
22
+ await callbacks.failWorkflowForUnavailableDynamicSource(workflowId, error);
23
+ return null;
24
+ }
25
+ throw error;
26
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `resumeWorkflowFromStorage()`'s guts — split out of `resume.ts`, which has
3
+ * no headroom under the repository's 500-line implementation-file ceiling
4
+ * for the WFT-134 claim-release wrapping added around this logic (mirrors
5
+ * `resume-generation-guard.ts`'s same split). `resume.ts` itself is now just
6
+ * the public entry point: load/validate state, acquire a claim if needed,
7
+ * and call {@link performResumeAfterClaimAcquired} below inside the
8
+ * claim-release `try`/`catch`.
9
+ *
10
+ * @module core/engine/lifecycle/resume-body
11
+ */
12
+ import { type EventHeadRecord } from '../../event-log.ts';
13
+ import type { Checkpoint, WorkflowState } from '../../types.ts';
14
+ import { type WorkflowVersionTuple } from '../../workflow-version-tuple.ts';
15
+ import { type WorkflowHandle } from '../handles.ts';
16
+ import type { EngineInternals } from '../internals.ts';
17
+ import { deriveResumeGeneration } from './resume-generation-guard.ts';
18
+ import { type LifecycleCallbacks, type RecoverAllOptions, type RegistrationEntry } from './shared.ts';
19
+ export type SerializedResumeArgs = {
20
+ workflowId: string;
21
+ resumeCheckpoint: Checkpoint;
22
+ serializedCheckpoint: Uint8Array;
23
+ registeredVersionTuple: WorkflowVersionTuple;
24
+ restoredHead: EventHeadRecord;
25
+ workflowStartHeaders: Map<string, string> | undefined;
26
+ registration: RegistrationEntry;
27
+ /**
28
+ * The EXACT revision `registration` resolved against, NOT `state.revision`
29
+ * re-read independently (WFT-19 round 5): a legacy record with one registered
30
+ * candidate has `state.revision === undefined` even though the resolver
31
+ * resolved it — caching `state.revision` here would disable exact-revision lookup.
32
+ */
33
+ resolvedRevision: string | undefined;
34
+ expectedGeneration: ReturnType<typeof deriveResumeGeneration>;
35
+ callbacks: LifecycleCallbacks;
36
+ };
37
+ /**
38
+ * Everything `resumeWorkflowFromStorage` does once a claim (if any) is
39
+ * acquired: load tracking/checkpoint/registration state, prepare the resume,
40
+ * and either relaunch inline or in a worker. `resumeWorkflowFromStorage`
41
+ * itself just wraps a call to this in the WFT-134 claim-release
42
+ * `try`/`catch`.
43
+ */
44
+ export declare function performResumeAfterClaimAcquired(internals: EngineInternals, workflowId: string, state: WorkflowState, dispatchResumedEvent: boolean, callbacks: LifecycleCallbacks, onRecoveredWorkflow?: RecoverAllOptions['onRecoveredWorkflow']): Promise<WorkflowHandle>;
@@ -0,0 +1,211 @@
1
+ import { KEYS } from "../../../storage/interface.js";
2
+ import { deserializeCheckpoint, serializeCheckpoint } from "../../checkpoint.js";
3
+ import { encode } from "../../codec.js";
4
+ import { Context, setContextWorkflowInterceptor } from "../../context.js";
5
+ import { EventLog } from "../../event-log.js";
6
+ import { WorkflowResumedEvent } from "../../events.js";
7
+ import { buildTimerBatchOperations } from "../../scheduler.js";
8
+ import { createCancelHandlerRegistration, resetCancelHandlers } from "../cancel-handlers.js";
9
+ import { rememberCommittedCheckpointBytes } from "../checkpoint-commit-snapshots.js";
10
+ import { rehydrateChildCancellationHandlers } from "../child-workflow-cancellation.js";
11
+ import { resolveExecutableRegistrationOrRenamedNotFound } from "../dynamic-source-execution.js";
12
+ import { commitFencedEngineWrite } from "../fenced-write.js";
13
+ import { getWorkflowExecutionStartedAt } from "../handles.js";
14
+ import { loadWorkflowState } from "../storage-io.js";
15
+ import { getComposedWorkflowInterceptor } from "../strategy-helpers.js";
16
+ import { buildWorkflowVisibilityIndexTransition } from "../workflow-indexes.js";
17
+ import { prepareResumeState } from "./persist.js";
18
+ import {
19
+ reprovideRecoveredServices,
20
+ workflowServicesResolverInfoFromState
21
+ } from "./recovered-services.js";
22
+ import { assertSameGeneration, deriveResumeGeneration } from "./resume-generation-guard.js";
23
+ import {
24
+ enforceHistoryPolicyBeforeReplay,
25
+ loadTerminalCleanupTrackedState,
26
+ loadWorkflowStartHeaders,
27
+ setWorkflowStartHeaders
28
+ } from "./shared.js";
29
+ async function prepareRecoveredServicesOrFail(internals, state, callbacks, resolverInfo) {
30
+ return reprovideRecoveredServices(internals, state, callbacks.failWorkflowForUnavailableServices, callbacks.handleCleanupError, callbacks.dispatchEvent, resolverInfo);
31
+ }
32
+ async function runRecoveredWorkflowHookOrFail(internals, state, handle, resolverInfo, onRecoveredWorkflow, callbacks) {
33
+ const info = {
34
+ ...resolverInfo,
35
+ handle,
36
+ launchOptions: resolverInfo.launchOptions ?? { id: state.id },
37
+ services: internals.workflowServices.get(state.id)
38
+ };
39
+ try {
40
+ await onRecoveredWorkflow(info);
41
+ return !1;
42
+ } catch (error) {
43
+ const reason = error instanceof Error ? error.message : String(error), hookError = Error(`Recovery hook failed for workflow "${state.id}": ${reason}`);
44
+ try {
45
+ await callbacks.failWorkflowForRecoveryHook(state.id, hookError);
46
+ } catch (commitError) {
47
+ callbacks.handleCleanupError("onRecoveredWorkflow", commitError, state.id);
48
+ }
49
+ return !0;
50
+ }
51
+ }
52
+ async function prepareRecoveredWorkflowOrFail(internals, state, callbacks, onRecoveredWorkflow) {
53
+ const resolverInfo = onRecoveredWorkflow === void 0 ? void 0 : await workflowServicesResolverInfoFromState(internals, state), handle = callbacks.getHandle(state.id);
54
+ if (await prepareRecoveredServicesOrFail(internals, state, callbacks, resolverInfo))
55
+ return { handle, shouldStop: !0 };
56
+ if (onRecoveredWorkflow === void 0 || resolverInfo === void 0)
57
+ return { handle, shouldStop: !1 };
58
+ const shouldStop = await runRecoveredWorkflowHookOrFail(internals, state, handle, resolverInfo, onRecoveredWorkflow, callbacks);
59
+ return { handle, shouldStop };
60
+ }
61
+ function assertResumeNotTerminating(internals, workflowId) {
62
+ if (internals.terminalizingWorkflows.has(workflowId))
63
+ throw Error(`Cannot resume workflow "${workflowId}": termination is in progress`);
64
+ }
65
+ function commitSerializedResumeState(internals, args) {
66
+ const {
67
+ workflowId,
68
+ resumeCheckpoint,
69
+ serializedCheckpoint,
70
+ registeredVersionTuple,
71
+ restoredHead,
72
+ callbacks
73
+ } = args;
74
+ internals.checkpoints.set(workflowId, resumeCheckpoint);
75
+ rememberCommittedCheckpointBytes(internals, workflowId, serializedCheckpoint);
76
+ internals.workflowVersionTuples.set(workflowId, registeredVersionTuple);
77
+ internals.eventLogHeads.set(workflowId, restoredHead);
78
+ setWorkflowStartHeaders(internals, workflowId, args.workflowStartHeaders, callbacks);
79
+ internals.parkedInlineWorkflows.delete(workflowId);
80
+ }
81
+ async function relaunchInlineWorkflowAfterResume(internals, latestState, args) {
82
+ const { workflowId, resumeCheckpoint, registration, resolvedRevision, callbacks } = args, accumulatedResults = new Map(resumeCheckpoint.accumulatedResults), workflowAbort = new AbortController;
83
+ internals.workflowTypeByWorkflowId.set(workflowId, {
84
+ type: latestState.type,
85
+ revision: resolvedRevision
86
+ });
87
+ resetCancelHandlers(internals, workflowId);
88
+ await rehydrateChildCancellationHandlers(internals, workflowId, callbacks);
89
+ const context = new Context({
90
+ workflowId,
91
+ ...latestState.workflowExecutionToken !== void 0 && {
92
+ workflowExecutionToken: latestState.workflowExecutionToken
93
+ },
94
+ workflowType: latestState.type,
95
+ startedAt: getWorkflowExecutionStartedAt(latestState),
96
+ abortController: workflowAbort,
97
+ getNow: internals.options.getNow,
98
+ resolveWorkflowType: callbacks.resolveWorkflowTypeTarget,
99
+ executionStateOwnerId: latestState.executionStateOwnerId ?? workflowId,
100
+ accumulatedResults,
101
+ locals: resumeCheckpoint.locals,
102
+ searchAttributes: resumeCheckpoint.searchAttributes,
103
+ registerCancelHandler: createCancelHandlerRegistration(internals, workflowId),
104
+ ...registration.searchAttributes && {
105
+ searchAttributeSchema: registration.searchAttributes
106
+ },
107
+ sleepReferenceTime: resumeCheckpoint.createdAt,
108
+ ...latestState.executionDeadline !== void 0 && {
109
+ deadline: latestState.executionDeadline
110
+ },
111
+ services: internals.workflowServices.get(workflowId),
112
+ ...internals.options.onLog != null && { logSink: internals.options.onLog }
113
+ });
114
+ setContextWorkflowInterceptor(context, getComposedWorkflowInterceptor(internals));
115
+ if (internals.options.development)
116
+ context.explain(!0);
117
+ const generator = registration.handler(context, latestState.input), inlineStrategy = internals.inlineStrategy;
118
+ inlineStrategy.adoptWorkflow(workflowId, generator, context, workflowAbort);
119
+ inlineStrategy.continueWorkflow(workflowId, void 0);
120
+ }
121
+ async function relaunchWorkerWorkflowAfterResume(internals, latestState, args) {
122
+ const { workflowId, resumeCheckpoint, workflowStartHeaders, resolvedRevision, callbacks } = args;
123
+ internals.workflowTypeByWorkflowId.set(workflowId, {
124
+ type: latestState.type,
125
+ revision: resolvedRevision
126
+ });
127
+ resetCancelHandlers(internals, workflowId);
128
+ await rehydrateChildCancellationHandlers(internals, workflowId, callbacks);
129
+ const serialized = serializeCheckpoint(resumeCheckpoint);
130
+ internals.strategy.startWorkflow({
131
+ workflowId,
132
+ ...latestState.workflowExecutionToken !== void 0 && {
133
+ workflowExecutionToken: latestState.workflowExecutionToken
134
+ },
135
+ workflowType: latestState.type,
136
+ input: latestState.input,
137
+ checkpoint: serialized,
138
+ nestingDepth: internals.workflowNestingDepths.get(workflowId) ?? 0,
139
+ executionStateOwnerId: latestState.executionStateOwnerId ?? workflowId,
140
+ ...latestState.executionDeadline !== void 0 && {
141
+ deadline: latestState.executionDeadline
142
+ },
143
+ ...workflowStartHeaders !== void 0 && workflowStartHeaders.size > 0 && {
144
+ headers: [...workflowStartHeaders]
145
+ }
146
+ });
147
+ }
148
+ async function reactivateSuspendedWorkflowState(internals, state) {
149
+ if (state.status !== "suspended")
150
+ return;
151
+ const previousState = { ...state };
152
+ state.status = "running";
153
+ state.updatedAt = internals.options.getNow();
154
+ await commitFencedEngineWrite(internals, state.id, [
155
+ { type: "put", key: KEYS.workflow(state.id), value: encode(state) },
156
+ ...buildWorkflowVisibilityIndexTransition(state.id, previousState, state).batchOps,
157
+ ...state.executionDeadline !== void 0 ? buildTimerBatchOperations({
158
+ id: `deadline:${state.id}`,
159
+ workflowId: state.id,
160
+ fireAt: state.executionDeadline,
161
+ kind: "execution-deadline"
162
+ }) : []
163
+ ], [], () => Error(`Resume of workflow "${state.id}" lost its CAS race.`));
164
+ }
165
+ async function performSerializedResume(internals, args) {
166
+ const { workflowId } = args;
167
+ assertResumeNotTerminating(internals, workflowId);
168
+ const latestState = await loadWorkflowState(internals, workflowId);
169
+ assertResumeNotTerminating(internals, workflowId);
170
+ if (!latestState)
171
+ throw Error(`Workflow "${workflowId}" not found in storage`);
172
+ if (latestState.status !== "running" && latestState.status !== "suspended")
173
+ throw Error(`Cannot resume workflow "${workflowId}": status is "${latestState.status}", expected "running" or "suspended"`);
174
+ assertSameGeneration(workflowId, latestState, args.expectedGeneration);
175
+ await reactivateSuspendedWorkflowState(internals, latestState);
176
+ commitSerializedResumeState(internals, args);
177
+ if (internals.inlineStrategy) {
178
+ await relaunchInlineWorkflowAfterResume(internals, latestState, args);
179
+ return;
180
+ }
181
+ await relaunchWorkerWorkflowAfterResume(internals, latestState, args);
182
+ }
183
+ export async function performResumeAfterClaimAcquired(internals, workflowId, state, dispatchResumedEvent, callbacks, onRecoveredWorkflow) {
184
+ await loadTerminalCleanupTrackedState(internals, workflowId, callbacks);
185
+ const checkpointBytes = await internals.storage.get(KEYS.checkpoint(workflowId));
186
+ if (!checkpointBytes)
187
+ throw Error(`Checkpoint not found for workflow "${workflowId}"`);
188
+ const checkpoint = deserializeCheckpoint(checkpointBytes), { entry: registration, revision: resolvedRevision } = await resolveExecutableRegistrationOrRenamedNotFound((type) => callbacks.resolveExecutableRegistrationForRevision(type, state.revision), state.type, () => Error(`No workflow registered with name "${state.type}" (needed to resume "${workflowId}")`)), preparedResumeState = await prepareResumeState(internals, workflowId, state, checkpoint, checkpointBytes, registration, callbacks), resumeCheckpoint = preparedResumeState.checkpoint, registeredVersionTuple = preparedResumeState.versionTuple, restoredHead = await new EventLog(internals.storage, workflowId).loadHead();
189
+ if (await enforceHistoryPolicyBeforeReplay(internals, workflowId, restoredHead, callbacks))
190
+ return callbacks.getHandle(workflowId);
191
+ const workflowStartHeaders = await loadWorkflowStartHeaders(internals, workflowId, callbacks), { handle, shouldStop } = await prepareRecoveredWorkflowOrFail(internals, state, callbacks, onRecoveredWorkflow);
192
+ if (shouldStop)
193
+ return handle;
194
+ await callbacks.runSerializedWorkflowStateWrite(workflowId, () => performSerializedResume(internals, {
195
+ workflowId,
196
+ resumeCheckpoint,
197
+ serializedCheckpoint: preparedResumeState.serializedCheckpoint,
198
+ registeredVersionTuple,
199
+ restoredHead,
200
+ workflowStartHeaders,
201
+ registration,
202
+ resolvedRevision,
203
+ expectedGeneration: deriveResumeGeneration(state),
204
+ callbacks
205
+ }));
206
+ if (dispatchResumedEvent)
207
+ callbacks.dispatchEvent(new WorkflowResumedEvent(workflowId, resumeCheckpoint.step));
208
+ if (internals.inlineStrategy)
209
+ callbacks.swallowPromiseRejection(callbacks.processPendingUpdatesAfterInlineAdvance(workflowId));
210
+ return handle;
211
+ }