@lostgradient/weft 0.6.0 → 0.8.0

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 (135) hide show
  1. package/README.md +7 -5
  2. package/dist/cli/generated/operation-client.generated.d.ts +1 -0
  3. package/dist/cli-main.js +63 -63
  4. package/dist/client/event-stream-options.d.ts +58 -0
  5. package/dist/client/event-stream-options.js +0 -0
  6. package/dist/client/event-stream.d.ts +27 -44
  7. package/dist/client/event-stream.js +21 -56
  8. package/dist/client/event-tail-lifecycle.d.ts +21 -0
  9. package/dist/client/event-tail-lifecycle.js +71 -0
  10. package/dist/client/event-tail.d.ts +3 -1
  11. package/dist/client/http-client.d.ts +4 -5
  12. package/dist/client/http-client.js +4 -1
  13. package/dist/client/http-handle.d.ts +4 -5
  14. package/dist/client/http-request.d.ts +7 -0
  15. package/dist/client/index.d.ts +2 -1
  16. package/dist/client/interface.d.ts +23 -7
  17. package/dist/client/local-handles.d.ts +2 -0
  18. package/dist/client/local-handles.js +10 -1
  19. package/dist/client/local.d.ts +3 -3
  20. package/dist/client/open-event-subscription.d.ts +3 -2
  21. package/dist/client/open-event-subscription.js +28 -1
  22. package/dist/client/sse-event-stream.d.ts +27 -0
  23. package/dist/client/sse-event-stream.js +276 -0
  24. package/dist/client/start-body.d.ts +2 -2
  25. package/dist/client/start-body.js +1 -0
  26. package/dist/core/context/activity-retry-state.d.ts +6 -5
  27. package/dist/core/context/activity-retry-state.js +31 -21
  28. package/dist/core/context/durable-activity.d.ts +117 -0
  29. package/dist/core/context/durable-activity.js +79 -0
  30. package/dist/core/context/operation-request.d.ts +15 -0
  31. package/dist/core/context/parallel-operations.js +1 -0
  32. package/dist/core/context/run-operation-cached-request.d.ts +8 -0
  33. package/dist/core/context/run-operation-cached-request.js +59 -0
  34. package/dist/core/context/run-operation.d.ts +23 -5
  35. package/dist/core/context/run-operation.js +62 -47
  36. package/dist/core/engine/activity-heartbeat-tracking.d.ts +8 -17
  37. package/dist/core/engine/activity-heartbeat-tracking.js +7 -1
  38. package/dist/core/engine/activity-reconciliation.d.ts +1 -0
  39. package/dist/core/engine/activity-reconciliation.js +7 -2
  40. package/dist/core/engine/anonymous-signal-sequence.js +6 -4
  41. package/dist/core/engine/async-activity-completion.d.ts +5 -7
  42. package/dist/core/engine/async-activity-completion.js +13 -9
  43. package/dist/core/engine/bulk-operations-purge.js +2 -1
  44. package/dist/core/engine/bulk-operations.js +8 -7
  45. package/dist/core/engine/callback-checkpoint-persistence.d.ts +3 -0
  46. package/dist/core/engine/callback-checkpoint-persistence.js +25 -0
  47. package/dist/core/engine/callback-creators-bundles.js +4 -1
  48. package/dist/core/engine/checkpoint-io.d.ts +4 -1
  49. package/dist/core/engine/checkpoint-io.js +19 -10
  50. package/dist/core/engine/completed-review-storage.d.ts +2 -1
  51. package/dist/core/engine/completed-review-storage.js +4 -3
  52. package/dist/core/engine/construction.js +3 -1
  53. package/dist/core/engine/engine-internal-types.d.ts +2 -0
  54. package/dist/core/engine/errors.d.ts +6 -6
  55. package/dist/core/engine/errors.js +1 -1
  56. package/dist/core/engine/handles.js +1 -1
  57. package/dist/core/engine/index.d.ts +37 -5
  58. package/dist/core/engine/index.js +7 -1
  59. package/dist/core/engine/internals.d.ts +2 -1
  60. package/dist/core/engine/lease-manager.js +2 -2
  61. package/dist/core/engine/lifecycle/recovered-services.js +8 -1
  62. package/dist/core/engine/lifecycle/start-or-signal-create.d.ts +2 -2
  63. package/dist/core/engine/lifecycle/start-or-signal-create.js +17 -15
  64. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +1 -1
  65. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +23 -5
  66. package/dist/core/engine/lifecycle/start-or-signal.d.ts +6 -4
  67. package/dist/core/engine/lifecycle/start-or-signal.js +21 -4
  68. package/dist/core/engine/memo-durable-activity.d.ts +11 -0
  69. package/dist/core/engine/memo-durable-activity.js +282 -0
  70. package/dist/core/engine/operations-activity.d.ts +5 -1
  71. package/dist/core/engine/operations-activity.js +19 -7
  72. package/dist/core/engine/operations-data.d.ts +4 -1
  73. package/dist/core/engine/operations-data.js +3 -3
  74. package/dist/core/engine/reviews.js +7 -4
  75. package/dist/core/engine/schedule-run.js +12 -3
  76. package/dist/core/engine/schedule-timer.js +2 -0
  77. package/dist/core/engine/storage-io.js +2 -2
  78. package/dist/core/engine/termination/complete.js +3 -3
  79. package/dist/core/json.js +1 -1
  80. package/dist/core/scheduler/scheduler-class.js +2 -1
  81. package/dist/core/start-workflow-validation.d.ts +3 -4
  82. package/dist/core/start-workflow-validation.js +1 -1
  83. package/dist/core/timeouts.d.ts +13 -1
  84. package/dist/core/timeouts.js +4 -1
  85. package/dist/core/types/constants.d.ts +16 -0
  86. package/dist/core/types/options.d.ts +60 -12
  87. package/dist/core/types/services-resolution.d.ts +49 -3
  88. package/dist/core/weft-error.d.ts +1 -1
  89. package/dist/core/weft-error.js +3 -0
  90. package/dist/index.d.ts +5 -4
  91. package/dist/index.js +7 -0
  92. package/dist/json-schema.js +1 -1
  93. package/dist/mcp/cli.js +26 -26
  94. package/dist/server/asyncapi-channels.js +82 -16
  95. package/dist/server/handler/auth-context-principal.d.ts +19 -0
  96. package/dist/server/handler/auth-context-principal.js +25 -0
  97. package/dist/server/handler/index.d.ts +2 -1
  98. package/dist/server/handler/index.js +35 -18
  99. package/dist/server/handler/route-dispatch.d.ts +12 -22
  100. package/dist/server/handler/route-dispatch.js +30 -36
  101. package/dist/server/handler/sse-route-dispatch.d.ts +25 -0
  102. package/dist/server/handler/sse-route-dispatch.js +45 -0
  103. package/dist/server/handler.js +28 -28
  104. package/dist/server/index.d.ts +8 -7
  105. package/dist/server/index.js +17 -17
  106. package/dist/server/operations/fleet-events-sse.d.ts +11 -0
  107. package/dist/server/operations/fleet-events-sse.js +164 -0
  108. package/dist/server/operations/sse-stream.d.ts +43 -0
  109. package/dist/server/operations/sse-stream.js +152 -7
  110. package/dist/server/operations/start-or-signal-workflow.d.ts +2 -0
  111. package/dist/server/operations/start-or-signal-workflow.js +25 -14
  112. package/dist/server/operations/start-workflow-options.d.ts +9 -6
  113. package/dist/server/operations/start-workflow-options.js +13 -0
  114. package/dist/server/operations/workflow-events-sse.d.ts +17 -0
  115. package/dist/server/operations/workflow-events-sse.js +200 -0
  116. package/dist/server/rest-bindings.js +12 -0
  117. package/dist/server/runtime/authentication-bridge.js +4 -0
  118. package/dist/server/runtime/cors.d.ts +1 -1
  119. package/dist/server/runtime/cors.js +6 -1
  120. package/dist/server/runtime/websocket-stream.d.ts +4 -0
  121. package/dist/server/runtime/websocket-stream.js +20 -3
  122. package/dist/server/runtime/websocket-worker.js +7 -2
  123. package/dist/server/serve-internals.d.ts +28 -0
  124. package/dist/server/serve-internals.js +4 -2
  125. package/dist/server/workflow-event-feed.d.ts +1 -0
  126. package/dist/server/workflow-event-feed.js +17 -6
  127. package/dist/service-worker/index.js +28 -28
  128. package/dist/service-worker/setup.d.ts +18 -1
  129. package/dist/service-worker/setup.js +7 -4
  130. package/dist/storage/typed-storage.d.ts +1 -1
  131. package/dist/storage/typed-storage.js +1 -1
  132. package/dist/testing/index.js +27 -27
  133. package/dist/version.d.ts +1 -1
  134. package/dist/version.js +1 -1
  135. package/package.json +1 -1
@@ -23,12 +23,30 @@ async function awaitReservationCleared(internals, workflowId) {
23
23
  await sleep(RESERVATION_CLEAR_RETRY_DELAY_MS);
24
24
  }
25
25
  }
26
- export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSpec, signalId, callbacks) {
27
- const resolved = await signalOrConflictExistingWorkflow(internals, winnerId, signalSpec, signalId, callbacks);
28
- if (resolved !== void 0)
29
- return resolved;
26
+ export async function resolveCallerIdWinnerOrRetry(internals, winnerId, signalSpec, signalId, callbacks, allowTerminalRestart = !1) {
27
+ const state = await loadWorkflowState(internals, winnerId);
28
+ if (state !== null) {
29
+ if (!isTerminalWorkflowStatus(state.status)) {
30
+ await callbacks.signalExistingWorkflow(winnerId, signalSpec.name, signalSpec.payload, signalId);
31
+ return callbacks.getHandle(winnerId);
32
+ }
33
+ if (!allowTerminalRestart)
34
+ throw new StartOrSignalConflictError(winnerId, state.status);
35
+ }
30
36
  await awaitReservationCleared(internals, winnerId);
31
- return signalOrConflictExistingWorkflow(internals, winnerId, signalSpec, signalId, callbacks);
37
+ const stateAfterReservation = await loadWorkflowState(internals, winnerId);
38
+ if (stateAfterReservation === null)
39
+ return;
40
+ if (isTerminalWorkflowStatus(stateAfterReservation.status)) {
41
+ if (state !== null && isSameTerminalRun(state, stateAfterReservation))
42
+ return;
43
+ throw new StartOrSignalConflictError(winnerId, stateAfterReservation.status);
44
+ }
45
+ await callbacks.signalExistingWorkflow(winnerId, signalSpec.name, signalSpec.payload, signalId);
46
+ return callbacks.getHandle(winnerId);
47
+ }
48
+ function isSameTerminalRun(before, after) {
49
+ return before.id === after.id && before.type === after.type && before.status === after.status && before.createdAt === after.createdAt && before.updatedAt === after.updatedAt && before.terminalCleanupToken === after.terminalCleanupToken;
32
50
  }
33
51
  export async function signalOrConflictExistingWorkflow(internals, workflowId, signalSpec, signalId, callbacks) {
34
52
  const state = await loadWorkflowState(internals, workflowId);
@@ -1,4 +1,4 @@
1
- import type { StartOptions, StartOrSignalSignal } from '../../types.ts';
1
+ import type { StartOptions, StartOrSignalOptions, StartOrSignalSignal } from '../../types.ts';
2
2
  import { type WorkflowHandle } from '../handles.ts';
3
3
  import type { EngineInternals } from '../internals.ts';
4
4
  import { type LifecycleCallbacks } from './shared.ts';
@@ -28,8 +28,10 @@ export declare function startWithIdempotency(internals: EngineInternals, type: s
28
28
  * - **Non-terminal** (running, pending, suspended) → deliver the signal through
29
29
  * the standard engine signal path with the same `signalId`, so it dedups
30
30
  * against a create-batch signal a concurrent winner may have written.
31
- * - **Terminal** → throw {@link StartOrSignalConflictError}: a finished run
32
- * cannot be signalled and is not silently replaced.
31
+ * - **Terminal** → throw {@link StartOrSignalConflictError} by default. With
32
+ * `options.onTerminalConflict: 'start-new'`, purge the prior terminal run
33
+ * through the shared terminal-replacement path, start a fresh run, and deliver
34
+ * the initial signal in the create batch.
33
35
  *
34
36
  * Convergence requires a SHARED workflow identity. Concurrent callers converge on
35
37
  * one workflow and one signal only when they share an `options.idempotencyKey`
@@ -43,4 +45,4 @@ export declare function startWithIdempotency(internals: EngineInternals, type: s
43
45
  * convergence primitive. Use `idempotencyKey` (id-free convergence) or
44
46
  * `id` + `signalId` when concurrent callers must converge.
45
47
  */
46
- export declare function startOrSignal(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, options: StartOptions | undefined, callbacks: StartOrSignalCallbacks): Promise<StartOrSignalResult>;
48
+ export declare function startOrSignal(internals: EngineInternals, type: string, input: unknown, signalSpec: StartOrSignalSignal, options: StartOrSignalOptions | undefined, callbacks: StartOrSignalCallbacks): Promise<StartOrSignalResult>;
@@ -1,12 +1,11 @@
1
1
  import { KEYS, requireStorageCapability } from "../../../storage/interface.js";
2
2
  import {
3
3
  assertIdAndIdempotencyKeyExclusive,
4
- assertOnTerminalConflictUnsupported,
5
4
  assertValidIdempotencyKey,
6
5
  assertValidOnTerminalConflict,
7
6
  StartWorkflowValidationError
8
7
  } from "../../start-workflow-validation.js";
9
- import { IdempotencyKeyPurgedError } from "../errors.js";
8
+ import { IdempotencyKeyPurgedError, StartOrSignalConflictError } from "../errors.js";
10
9
  import { StartIdempotencyRaceLostError } from "./start-commit.js";
11
10
  import {
12
11
  createWithSignalOrFallback,
@@ -54,14 +53,20 @@ function validateStartOrSignalConvergence(signalSpec, options) {
54
53
  if (signalSpec.signalId !== void 0)
55
54
  throw new StartWorkflowValidationError("startOrSignal does not accept both signal.signalId and options.idempotencyKey: the signal id derives from the idempotency key for convergence. Provide exactly one.");
56
55
  }
56
+ function validateStartOrSignalRestartPolicy(signalSpec, options) {
57
+ if (options?.onTerminalConflict !== "start-new" || signalSpec.signalId !== void 0)
58
+ return;
59
+ throw new StartWorkflowValidationError("startOrSignal options.onTerminalConflict: 'start-new' requires signal.signalId so concurrent restart-capable callers converge on one deterministic initial signal.");
60
+ }
57
61
  export async function startOrSignal(internals, type, input, signalSpec, options, callbacks) {
58
62
  requireStorageCapability(internals.storage, "conditionalBatch", "startOrSignal");
59
- assertOnTerminalConflictUnsupported(options, "startOrSignal");
63
+ assertValidOnTerminalConflict(options);
64
+ validateStartOrSignalRestartPolicy(signalSpec, options);
60
65
  const idempotencyKey = options?.idempotencyKey;
61
66
  validateStartOrSignalConvergence(signalSpec, options);
62
67
  const signalId = resolveSignalId(signalSpec, idempotencyKey), mappedId = idempotencyKey !== void 0 ? await resolveIdempotencyKeyWorkflowId(internals, idempotencyKey) : void 0, existingId = mappedId ?? options?.id;
63
68
  if (existingId !== void 0) {
64
- const resolved = await signalOrConflictExistingWorkflow(internals, existingId, signalSpec, signalId, callbacks);
69
+ const resolved = await resolveExistingStartOrSignalTarget(internals, existingId, mappedId, signalSpec, signalId, options, callbacks);
65
70
  if (resolved !== void 0)
66
71
  return { handle: resolved, outcome: "signalled" };
67
72
  if (mappedId !== void 0)
@@ -69,3 +74,15 @@ export async function startOrSignal(internals, type, input, signalSpec, options,
69
74
  }
70
75
  return createWithSignalOrFallback(internals, type, input, signalSpec, signalId, options, callbacks);
71
76
  }
77
+ async function resolveExistingStartOrSignalTarget(internals, existingId, mappedId, signalSpec, signalId, options, callbacks) {
78
+ try {
79
+ return await signalOrConflictExistingWorkflow(internals, existingId, signalSpec, signalId, callbacks);
80
+ } catch (error) {
81
+ if (canRestartTerminalCallerIdTarget(error, options, mappedId))
82
+ return;
83
+ throw error;
84
+ }
85
+ }
86
+ function canRestartTerminalCallerIdTarget(error, options, mappedId) {
87
+ return error instanceof StartOrSignalConflictError && options?.onTerminalConflict === "start-new" && mappedId === void 0;
88
+ }
@@ -0,0 +1,11 @@
1
+ import type { ContextOperationRequest } from '../context.ts';
2
+ import type { EngineInternals } from './internals.ts';
3
+ import { type ActivityOperationCallbacks } from './operations-activity.ts';
4
+ type MemoOperation = Extract<ContextOperationRequest, {
5
+ type: 'memo';
6
+ }>;
7
+ export declare function callMemoFunctionWithDurableActivityScope(internals: EngineInternals, workflowId: string, operation: MemoOperation, callbacks: {
8
+ getActivityOperationCallbacks?: () => ActivityOperationCallbacks;
9
+ persistCheckpoint: (workflowId: string, operation: ContextOperationRequest) => Promise<void>;
10
+ }): Promise<unknown>;
11
+ export {};
@@ -0,0 +1,282 @@
1
+ import { hashString } from "../../runtime/portable.js";
2
+ import {
3
+ DurableActivityScopeError,
4
+ DurableActivityUnsupportedError,
5
+ runWithDurableActivityScope
6
+ } from "../context/durable-activity.js";
7
+ import {
8
+ readOrInitActivityRetrySleepFireAt,
9
+ runActivityWithRetryAtStep
10
+ } from "../context/run-operation.js";
11
+ import { AsyncActivityDeferral } from "./async-activity-completion.js";
12
+ import {
13
+ executeActivityOperationResult
14
+ } from "./operations-activity.js";
15
+ import { registerSleepResolver } from "./operations-time.js";
16
+ import { callMemoFunction } from "./state-utilities.js";
17
+ export async function callMemoFunctionWithDurableActivityScope(internals, workflowId, operation, callbacks) {
18
+ const context = internals.inlineStrategy?.getContext(workflowId);
19
+ if (context === void 0 || typeof operation.step !== "number" || !Number.isSafeInteger(operation.step) || operation.step < 0 || callbacks.getActivityOperationCallbacks === void 0)
20
+ return callMemoFunction(operation.fn);
21
+ const memoOperation = { ...operation, step: operation.step };
22
+ return new MemoDurableActivityScope(internals, workflowId, memoOperation, context, callbacks.getActivityOperationCallbacks(), callbacks.persistCheckpoint).run(() => callMemoFunction(operation.fn));
23
+ }
24
+
25
+ class MemoDurableActivityScope {
26
+ #internals;
27
+ #workflowId;
28
+ #operation;
29
+ #context;
30
+ #activityCallbacks;
31
+ #persistCheckpoint;
32
+ #scopeAbortController = new AbortController;
33
+ #pendingPromises = new Set;
34
+ #identityPrefix;
35
+ #activePromise;
36
+ #activeNonCancellableWriteCount = 0;
37
+ #closed = !1;
38
+ #closeReason;
39
+ #nextOrdinal = 0;
40
+ constructor(internals, workflowId, operation, context, activityCallbacks, persistCheckpoint) {
41
+ this.#internals = internals;
42
+ this.#workflowId = workflowId;
43
+ this.#operation = operation;
44
+ this.#context = context;
45
+ this.#activityCallbacks = activityCallbacks;
46
+ this.#persistCheckpoint = persistCheckpoint;
47
+ this.#identityPrefix = `memo:${operation.step}:${hashString(operation.key)}`;
48
+ this.#internals.workflowTypeByWorkflowId.set(workflowId, context.workflowType);
49
+ }
50
+ async run(execute) {
51
+ const cleanupAbortForwarding = this.#forwardAbortSignals();
52
+ try {
53
+ const result = await runWithDurableActivityScope(this, execute);
54
+ if (this.#pendingPromises.size > 0) {
55
+ const error = new DurableActivityScopeError("durableActivity() calls started inside ctx.memo() must be awaited before the memo callback returns.");
56
+ await this.#closeAndDrain(error);
57
+ throw error;
58
+ }
59
+ this.#close();
60
+ return result;
61
+ } catch (error) {
62
+ await this.#closeAndDrain(error);
63
+ throw error;
64
+ } finally {
65
+ cleanupAbortForwarding();
66
+ }
67
+ }
68
+ dispatch(invocation) {
69
+ try {
70
+ this.#throwIfClosed();
71
+ } catch (error) {
72
+ return Promise.reject(error);
73
+ }
74
+ if (this.#activePromise !== void 0)
75
+ return Promise.reject(new DurableActivityScopeError("durableActivity() calls inside one ctx.memo() scope must be awaited sequentially. Start the next durableActivity() call after the previous promise settles."));
76
+ const ordinal = this.#nextOrdinal;
77
+ this.#nextOrdinal += 1;
78
+ const execution = this.#executeInvocation(invocation, ordinal);
79
+ this.#activePromise = execution;
80
+ this.#pendingPromises.add(execution);
81
+ execution.finally(() => {
82
+ if (this.#activePromise === execution)
83
+ this.#activePromise = void 0;
84
+ this.#pendingPromises.delete(execution);
85
+ }).catch(() => {});
86
+ execution.catch(() => {});
87
+ return execution;
88
+ }
89
+ async#executeInvocation(invocation, ordinal) {
90
+ const activityStateKey = this.#activityStateKey(ordinal), generator = runActivityWithRetryAtStep(this.#context, invocation.activity, invocation.arguments, this.#operation.step, {
91
+ activityStateKey,
92
+ cacheResultStep: !1,
93
+ retryStateKey: activityStateKey,
94
+ retrySleep: (duration, nextAttempt) => this.#retrySleepOperation(duration, ordinal, nextAttempt)
95
+ });
96
+ let next = generator.next();
97
+ while (!next.done)
98
+ try {
99
+ const result = await this.#executeYieldedOperation(next.value, invocation, ordinal);
100
+ next = generator.next(result);
101
+ } catch (error) {
102
+ if (error instanceof AsyncActivityDeferral)
103
+ throw new DurableActivityUnsupportedError("ActivityContext.completeAsync() is not supported from durableActivity(). Use yield* ctx.run() for async-completion activities.");
104
+ next = generator.throw(error);
105
+ }
106
+ return next.value;
107
+ }
108
+ async#executeYieldedOperation(operation, invocation, ordinal) {
109
+ this.#throwIfClosed();
110
+ if (operation.type === "activity")
111
+ return this.#executeActivityOperation(operation, invocation.callerStack, ordinal);
112
+ if (operation.type === "sleep") {
113
+ await this.#raceWithScopeAbort(this.#executeDurableRetrySleepOperation(operation));
114
+ this.#throwIfClosed();
115
+ return;
116
+ }
117
+ throw new DurableActivityScopeError(`durableActivity() retry driver yielded unsupported operation "${operation.type}".`);
118
+ }
119
+ #executeActivityOperation(request, callerStack, ordinal) {
120
+ const attempt = typeof request.attempt === "number" ? request.attempt : 1, operation = {
121
+ ...request,
122
+ operationId: this.#activityOperationId(ordinal, attempt),
123
+ callerStack
124
+ };
125
+ return this.#raceWithScopeAbort(executeActivityOperationResult(this.#internals, this.#workflowId, operation, this.#activityCallbacks, this.#scopeAbortController.signal, void 0, {
126
+ reconciliationCompletion: "immediate-fenced",
127
+ beforeImmediateReconciliationCommit: () => this.#beginNonCancellableWrite()
128
+ }));
129
+ }
130
+ async#executeDurableRetrySleepOperation(operation) {
131
+ const finishCheckpointWrite = this.#beginNonCancellableWrite();
132
+ try {
133
+ await this.#persistCheckpoint(this.#workflowId, operation);
134
+ } finally {
135
+ finishCheckpointWrite();
136
+ }
137
+ this.#throwIfClosed();
138
+ if (operation.scheduledFireAt <= this.#internals.options.getNow())
139
+ return;
140
+ const { promise, resolve } = Promise.withResolvers(), finishTimerWrite = this.#beginNonCancellableWrite();
141
+ try {
142
+ await this.#internals.scheduler.schedule({
143
+ id: `sleep:${operation.operationId}`,
144
+ workflowId: this.#workflowId,
145
+ fireAt: operation.scheduledFireAt,
146
+ kind: "sleep"
147
+ });
148
+ } finally {
149
+ finishTimerWrite();
150
+ }
151
+ this.#throwIfClosed();
152
+ registerSleepResolver(this.#internals, this.#workflowId, operation.operationId, resolve);
153
+ await promise;
154
+ }
155
+ #activityOperationId(ordinal, attempt) {
156
+ return `${this.#activityStateKey(ordinal)}:activity:${attempt}`;
157
+ }
158
+ #retrySleepOperationId(ordinal, nextAttempt) {
159
+ return `${this.#activityStateKey(ordinal)}:retry-sleep:${nextAttempt}`;
160
+ }
161
+ *#retrySleepOperation(duration, ordinal, nextAttempt) {
162
+ const operationId = this.#retrySleepOperationId(ordinal, nextAttempt);
163
+ yield {
164
+ type: "sleep",
165
+ operationId,
166
+ duration,
167
+ scheduledFireAt: this.#readOrInitRetrySleepFireAt(operationId, duration)
168
+ };
169
+ }
170
+ #readOrInitRetrySleepFireAt(operationId, duration) {
171
+ return readOrInitActivityRetrySleepFireAt(this.#context, operationId, duration);
172
+ }
173
+ #activityStateKey(ordinal) {
174
+ if (!Number.isSafeInteger(ordinal) || ordinal < 0)
175
+ throw new DurableActivityScopeError(`Invalid durableActivity() call ordinal ${String(ordinal)} for ctx.memo() step ${String(this.#operation.step)}.`);
176
+ return `${this.#identityPrefix}:call:${String(ordinal)}`;
177
+ }
178
+ #raceWithScopeAbort(operation) {
179
+ operation.catch(() => {});
180
+ this.#throwIfClosed();
181
+ return new Promise((resolve, reject) => {
182
+ let settled = !1;
183
+ const signal = this.#scopeAbortController.signal, cleanup = () => {
184
+ signal.removeEventListener("abort", onAbort);
185
+ }, settle = (callback) => {
186
+ if (settled)
187
+ return;
188
+ settled = !0;
189
+ cleanup();
190
+ callback();
191
+ }, onAbort = () => {
192
+ if (this.#activeNonCancellableWriteCount > 0)
193
+ return;
194
+ settle(() => reject(this.#abortError()));
195
+ };
196
+ signal.addEventListener("abort", onAbort, { once: !0 });
197
+ if (signal.aborted) {
198
+ onAbort();
199
+ return;
200
+ }
201
+ operation.then((value) => {
202
+ settle(() => {
203
+ if (this.#closed || signal.aborted) {
204
+ reject(this.#abortError());
205
+ return;
206
+ }
207
+ resolve(value);
208
+ });
209
+ }).catch((error) => {
210
+ settle(() => reject(error));
211
+ });
212
+ });
213
+ }
214
+ #beginNonCancellableWrite() {
215
+ this.#throwIfClosed();
216
+ this.#activeNonCancellableWriteCount += 1;
217
+ let finished = !1;
218
+ return () => {
219
+ if (finished)
220
+ return;
221
+ finished = !0;
222
+ this.#activeNonCancellableWriteCount -= 1;
223
+ if (this.#closed && this.#activeNonCancellableWriteCount === 0 && !this.#scopeAbortController.signal.aborted)
224
+ this.#scopeAbortController.abort(this.#abortError());
225
+ };
226
+ }
227
+ #forwardAbortSignals() {
228
+ const signals = [this.#context.signal, this.#internals.abortController.signal], listeningSignals = [], onAbort = () => {
229
+ this.#close(new DurableActivityScopeError("durableActivity() scope closed before completion."));
230
+ }, cleanup = () => {
231
+ for (const signal of listeningSignals)
232
+ signal.removeEventListener("abort", onAbort);
233
+ };
234
+ for (const signal of signals) {
235
+ if (signal.aborted) {
236
+ onAbort();
237
+ return cleanup;
238
+ }
239
+ signal.addEventListener("abort", onAbort, { once: !0 });
240
+ listeningSignals.push(signal);
241
+ }
242
+ return cleanup;
243
+ }
244
+ #close(reason) {
245
+ if (this.#closed)
246
+ return;
247
+ this.#closed = !0;
248
+ this.#closeReason = toScopeError(reason);
249
+ if (this.#activeNonCancellableWriteCount === 0 && !this.#scopeAbortController.signal.aborted)
250
+ this.#scopeAbortController.abort(this.#closeReason);
251
+ }
252
+ async#closeAndDrain(reason) {
253
+ this.#close(reason);
254
+ if (this.#pendingPromises.size === 0)
255
+ return;
256
+ await Promise.allSettled(this.#pendingPromises);
257
+ }
258
+ #throwIfClosed() {
259
+ if (this.#closed || this.#scopeAbortController.signal.aborted)
260
+ throw this.#abortError();
261
+ }
262
+ #abortError() {
263
+ if (this.#closeReason !== void 0)
264
+ return this.#closeReason;
265
+ const reason = this.#scopeAbortController.signal.reason;
266
+ return reason instanceof Error ? reason : new DurableActivityScopeError("durableActivity() scope closed before completion.");
267
+ }
268
+ }
269
+ function toScopeError(reason) {
270
+ if (reason instanceof Error)
271
+ return reason;
272
+ if (reason === void 0)
273
+ return new DurableActivityScopeError("durableActivity() scope closed before completion.");
274
+ return new DurableActivityScopeError(formatCloseReason(reason));
275
+ }
276
+ function formatCloseReason(reason) {
277
+ if (typeof reason === "string")
278
+ return reason;
279
+ if (typeof reason === "number" || typeof reason === "boolean" || typeof reason === "bigint")
280
+ return reason.toString();
281
+ return "durableActivity() scope closed before completion.";
282
+ }
@@ -12,6 +12,10 @@ export type ActivityFunctionWithMetadata = ((...arguments_: unknown[]) => unknow
12
12
  type ActivityOperation = Extract<ContextOperationRequest, {
13
13
  type: 'activity';
14
14
  }>;
15
+ export interface ActivityExecutionOptions {
16
+ reconciliationCompletion?: 'stage-with-workflow-commit' | 'immediate-fenced';
17
+ beforeImmediateReconciliationCommit?: () => void | (() => void);
18
+ }
15
19
  export type ActivityOperationCallbacks = {
16
20
  runOperationWithResult: (workflowId: string, operation: ActivityOperation, execute: () => Promise<unknown>) => Promise<void>;
17
21
  finalizePendingTimelineEntry: (workflowId: string, status: 'completed' | 'failed', value: unknown) => void;
@@ -28,6 +32,6 @@ export declare function invokeInlineActivity(internals: EngineInternals, workflo
28
32
  * `activityExecution` is configured, or running inline on the main thread.
29
33
  */
30
34
  export declare function executeActivity(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks, attempt?: number, coordinatorSignal?: AbortSignal): Promise<unknown>;
31
- export declare function executeActivityOperationResult(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks, coordinatorSignal?: AbortSignal, speculativeState?: SpeculativeExecutionState): Promise<unknown>;
35
+ export declare function executeActivityOperationResult(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks, coordinatorSignal?: AbortSignal, speculativeState?: SpeculativeExecutionState, executionOptions?: ActivityExecutionOptions): Promise<unknown>;
32
36
  export declare function processActivityOperation(internals: EngineInternals, workflowId: string, operation: ActivityOperation, callbacks: ActivityOperationCallbacks): Promise<void>;
33
37
  export {};
@@ -8,6 +8,7 @@ import { resolvePerAttemptTimeout, withPerAttemptTimeout } from "./activity-per-
8
8
  import {
9
9
  buildActivityReconciliationReference,
10
10
  buildActivityVerificationContext,
11
+ commitActivityReconciliationTransitionWithFencedWrite,
11
12
  createCompletedActivityReconciliationRecord,
12
13
  resolveActivityIdempotencyKey,
13
14
  resolveStartedActivityReconciliationRecord,
@@ -69,10 +70,13 @@ function getActivityAttempt(operation) {
69
70
  const attempt = operation.attempt;
70
71
  return typeof attempt === "number" && Number.isInteger(attempt) && attempt > 0 ? attempt : 1;
71
72
  }
73
+ function getActivityStateKey(operation) {
74
+ return operation.activityStateKey ?? operation.step ?? 0;
75
+ }
72
76
  export async function executeActivity(internals, workflowId, operation, callbacks, attempt = getActivityAttempt(operation), coordinatorSignal) {
73
- const activityInput = operation.input, step = operation.step ?? 0;
74
- warnIfRetryMissingHeartbeat(internals, workflowId, step, attempt);
75
- const asyncToken = deriveAsyncActivityToken(workflowId, step, attempt), { perAttemptTimeoutMs, attemptAbortController, activitySignal } = resolvePerAttemptTimeout(internals, workflowId, operation, coordinatorSignal), activityContext = buildActivityContext(internals, workflowId, step, activitySignal, () => {
77
+ const activityInput = operation.input, activityStateKey = getActivityStateKey(operation);
78
+ warnIfRetryMissingHeartbeat(internals, workflowId, activityStateKey, attempt);
79
+ const asyncToken = deriveAsyncActivityToken(workflowId, activityStateKey, attempt), { perAttemptTimeoutMs, attemptAbortController, activitySignal } = resolvePerAttemptTimeout(internals, workflowId, operation, coordinatorSignal), activityContext = buildActivityContext(internals, workflowId, activityStateKey, activitySignal, () => {
76
80
  throw new AsyncActivityDeferral(asyncToken);
77
81
  }), invokeActivity = internals.activityWorkerDispatcher ? (activityName, input) => invokeWorkerActivity(internals, operation.operationId, activityName, input, attempt) : (activityName, input) => withPerAttemptTimeout(invokeInlineActivity(internals, workflowId, operation, activityContext, activityName, input), perAttemptTimeoutMs, activityName, attempt, attemptAbortController), composedActivity = callbacks.getComposedActivityInterceptor(), executeWithActivityInterceptors = async (activityName, input, headers) => {
78
82
  if (!composedActivity)
@@ -107,7 +111,7 @@ export async function executeActivity(internals, workflowId, operation, callback
107
111
  copyActivityHeadersToOperation(operation, headers);
108
112
  return result;
109
113
  }
110
- export async function executeActivityOperationResult(internals, workflowId, operation, callbacks, coordinatorSignal, speculativeState) {
114
+ export async function executeActivityOperationResult(internals, workflowId, operation, callbacks, coordinatorSignal, speculativeState, executionOptions = {}) {
111
115
  const activity = getActivityFunctionWithMetadata(internals, workflowId, operation), idempotencyKey = resolveActivityIdempotencyKey(activity, operation), operationAttempt = getActivityAttempt(operation);
112
116
  if (idempotencyKey !== void 0) {
113
117
  const reference = await buildActivityReconciliationReference(workflowId, operation.activityName, idempotencyKey), started = await resolveStartedActivityReconciliationRecord(internals, workflowId, operation, reference, activity, idempotencyKey, operationAttempt);
@@ -119,16 +123,24 @@ export async function executeActivityOperationResult(internals, workflowId, oper
119
123
  validateActivityResultForReconciliation(result, internals.options.payloadSizePolicy.maxBytes);
120
124
  await finalizeActivityResult(internals, workflowId, operation, result, activity, idempotencyKey, started.attempt, speculativeState, !0);
121
125
  const completedRecord = createCompletedActivityReconciliationRecord(started, result, internals.options.getNow());
122
- stageActivityReconciliationTransitionWithAtomicWorkflowCommit(internals, workflowId, reference, started, completedRecord);
126
+ if (executionOptions.reconciliationCompletion === "immediate-fenced") {
127
+ const finishImmediateReconciliationCommit = executionOptions.beforeImmediateReconciliationCommit?.();
128
+ try {
129
+ await commitActivityReconciliationTransitionWithFencedWrite(internals, reference, started, completedRecord);
130
+ } finally {
131
+ finishImmediateReconciliationCommit?.();
132
+ }
133
+ } else
134
+ stageActivityReconciliationTransitionWithAtomicWorkflowCommit(internals, workflowId, reference, started, completedRecord);
123
135
  if (!speculativeState)
124
- clearLastHeartbeatForStep(internals, workflowId, operation.step ?? 0);
136
+ clearLastHeartbeatForStep(internals, workflowId, getActivityStateKey(operation));
125
137
  return result;
126
138
  }
127
139
  const result = await executeActivity(internals, workflowId, operation, callbacks, operationAttempt, coordinatorSignal);
128
140
  assertPayloadWithinLimit(result, internals.options.payloadSizePolicy.maxBytes, "activity result");
129
141
  await finalizeActivityResult(internals, workflowId, operation, result, activity, idempotencyKey, operationAttempt, speculativeState);
130
142
  if (!speculativeState)
131
- clearLastHeartbeatForStep(internals, workflowId, operation.step ?? 0);
143
+ clearLastHeartbeatForStep(internals, workflowId, getActivityStateKey(operation));
132
144
  return result;
133
145
  }
134
146
  async function finalizeActivityResult(internals, workflowId, operation, result, activity, idempotencyKey, attempt, speculativeState, awaitSpeculativeVerification = !1) {
@@ -1,5 +1,6 @@
1
1
  import type { ContextOperationRequest } from '../context.ts';
2
2
  import type { EngineInternals } from './internals.ts';
3
+ import type { ActivityOperationCallbacks } from './operations-activity.ts';
3
4
  import type { OperationWithCallerStack } from './operations-router.ts';
4
5
  type MemoOperation = Extract<ContextOperationRequest, {
5
6
  type: 'memo';
@@ -15,8 +16,10 @@ type ArchiveOperation = Extract<ContextOperationRequest, {
15
16
  }>;
16
17
  export type DataOperationCallbacks = {
17
18
  runOperationWithResult: (workflowId: string, operation: OperationWithCallerStack, execute: () => Promise<unknown>) => Promise<void>;
19
+ persistCheckpoint: (workflowId: string, operation: ContextOperationRequest) => Promise<void>;
20
+ getActivityOperationCallbacks?: () => ActivityOperationCallbacks;
18
21
  };
19
- export declare function processMemoOperation(_internals: EngineInternals, workflowId: string, operation: MemoOperation, callbacks: DataOperationCallbacks): Promise<void>;
22
+ export declare function processMemoOperation(internals: EngineInternals, workflowId: string, operation: MemoOperation, callbacks: DataOperationCallbacks): Promise<void>;
20
23
  export declare function processOffloadOperation(internals: EngineInternals, workflowId: string, operation: OffloadOperation, callbacks: DataOperationCallbacks): Promise<void>;
21
24
  export declare function processLoadOperation(internals: EngineInternals, workflowId: string, operation: LoadOperation, callbacks: DataOperationCallbacks): Promise<void>;
22
25
  /**
@@ -1,8 +1,8 @@
1
1
  import { KEYS } from "../../storage/interface.js";
2
2
  import { decode, encode } from "../codec.js";
3
- import { callMemoFunction } from "./state-utilities.js";
4
- export async function processMemoOperation(_internals, workflowId, operation, callbacks) {
5
- return callbacks.runOperationWithResult(workflowId, operation, async () => callMemoFunction(operation.fn));
3
+ import { callMemoFunctionWithDurableActivityScope } from "./memo-durable-activity.js";
4
+ export async function processMemoOperation(internals, workflowId, operation, callbacks) {
5
+ return callbacks.runOperationWithResult(workflowId, operation, async () => callMemoFunctionWithDurableActivityScope(internals, workflowId, operation, callbacks));
6
6
  }
7
7
  export async function processOffloadOperation(internals, workflowId, operation, callbacks) {
8
8
  return callbacks.runOperationWithResult(workflowId, operation, async () => {
@@ -3,6 +3,7 @@ import { ReviewCompletedEvent, ReviewRequestedEvent } from "../review/events.js"
3
3
  import {
4
4
  ReviewTimeoutError
5
5
  } from "../review/index.js";
6
+ import { stageAtomicWorkflowCommitSideEffects } from "./checkpoint-side-effects.js";
6
7
  import {
7
8
  deleteCompletedReviewsForWorkflow,
8
9
  listCompletedReviewsFromStorage,
@@ -26,7 +27,7 @@ async function listPendingReviews(internals, filter) {
26
27
  return reviews;
27
28
  }
28
29
  async function dispatchCompletedReview(internals, reviewKey, reviewData, decisionResult, dispatchEvent) {
29
- await persistCompletedReviewRecord(internals.storage, reviewKey, reviewData, decisionResult);
30
+ await persistCompletedReviewRecord(internals, reviewKey, reviewData, decisionResult);
30
31
  dispatchEvent(new ReviewCompletedEvent(reviewData.workflowId, reviewData.reviewId, decisionResult.decision, decisionResult.reviewer, decisionResult.timestamp - reviewData.createdAt));
31
32
  }
32
33
  export async function listReviews(internals, filter = {}) {
@@ -96,9 +97,11 @@ export async function handleReviewEscalationTimer(internals, workflowId, reviewI
96
97
  if (entry.id === `review-timeout:${reviewId}`) {
97
98
  internals.reviewWaiters.delete(waiterKey);
98
99
  untrackWaiterKey(internals.reviewWaitersByWorkflow, workflowId, waiterKey);
99
- const elapsed = internals.options.getNow() - reviewRequest.createdAt;
100
- await internals.storage.delete(KEYS.review(workflowId, reviewId));
101
- const timeoutError = new ReviewTimeoutError(reviewId, elapsed);
100
+ const elapsed = internals.options.getNow() - reviewRequest.createdAt, timeoutError = new ReviewTimeoutError(reviewId, elapsed);
101
+ stageAtomicWorkflowCommitSideEffects(internals, workflowId, {
102
+ operations: [{ type: "delete", key: KEYS.review(workflowId, reviewId) }],
103
+ conditions: []
104
+ });
102
105
  await callbacks.failWorkflow(workflowId, timeoutError);
103
106
  resolve({ ok: !1, error: timeoutError });
104
107
  return !0;
@@ -4,7 +4,7 @@ import { ScheduleFiredEvent } from "../events.js";
4
4
  import { unavailableServicesError } from "./lifecycle/recovered-services.js";
5
5
  import { EMPTY_STORAGE_VALUE } from "./lifecycle/shared.js";
6
6
  export async function startScheduledRun(internals, state, callbacks, occurrence) {
7
- const workflowId = crypto.randomUUID(), scheduleRunOperations = state.overlap === "allow" ? [] : [{ type: "put", key: KEYS.scheduleRun(workflowId), value: encode(state.id) }], resolution = await resolveScheduledRunServices(internals, workflowId, state);
7
+ const workflowId = crypto.randomUUID(), scheduleRunOperations = state.overlap === "allow" ? [] : [{ type: "put", key: KEYS.scheduleRun(workflowId), value: encode(state.id) }], resolution = await resolveScheduledRunServices(internals, workflowId, state, occurrence);
8
8
  if (resolution !== null) {
9
9
  scheduleRunOperations.push({ type: "put", key: KEYS.workflowHasServices(workflowId), value: EMPTY_STORAGE_VALUE }, { type: "put", key: KEYS.terminalCleanupNeeded(workflowId), value: EMPTY_STORAGE_VALUE });
10
10
  internals.workflowsNeedingTerminalCleanup.add(workflowId);
@@ -23,12 +23,21 @@ export async function startScheduledRun(internals, state, callbacks, occurrence)
23
23
  }
24
24
  return workflowId;
25
25
  }
26
- async function resolveScheduledRunServices(internals, workflowId, state) {
26
+ async function resolveScheduledRunServices(internals, workflowId, state, occurrence) {
27
27
  const resolver = internals.options.resolveWorkflowServices;
28
28
  if (internals.inlineStrategy === null || !resolver)
29
29
  return null;
30
30
  try {
31
- return await resolver({ workflowId, workflowType: state.workflowType, input: state.input });
31
+ return await resolver({
32
+ workflowId,
33
+ workflowType: state.workflowType,
34
+ input: state.input,
35
+ launchOptions: { id: workflowId },
36
+ schedule: {
37
+ id: state.id,
38
+ ...occurrence !== void 0 ? { occurrence } : {}
39
+ }
40
+ });
32
41
  } catch (error) {
33
42
  return {
34
43
  status: "unavailable",
@@ -12,6 +12,8 @@ class ScheduleStatePersistenceError extends Error {
12
12
  }
13
13
  }
14
14
  export async function handleScheduleTimer(internals, entry, callbacks) {
15
+ if (internals.deposed)
16
+ return;
15
17
  const state = await loadScheduleState(internals, entry.workflowId);
16
18
  if (!isCurrentActiveScheduleTimer(state, entry))
17
19
  return;
@@ -52,7 +52,7 @@ export async function loadWorkflowResult(internals, workflowId) {
52
52
  throw Error("Workflow cancelled");
53
53
  if (state.status === "timed-out") {
54
54
  const elapsed = state.executionDeadline ? state.executionDeadline - getWorkflowExecutionStartedAt(state) : 0;
55
- throw new WorkflowTimeoutError(workflowId, "execution", elapsed);
55
+ throw new WorkflowTimeoutError(workflowId, "execution", elapsed, state.terminationReason);
56
56
  }
57
57
  throw Error(`Workflow "${workflowId}" is still ${state.status}`);
58
58
  }
@@ -93,7 +93,7 @@ export async function writeScheduleState(internals, state, options) {
93
93
  kind: "schedule"
94
94
  }));
95
95
  }
96
- await internals.storage.batch(operations);
96
+ await commitFencedEngineWrite(internals, operations, [], () => Error(`Schedule state commit for schedule "${state.id}" lost its precondition.`));
97
97
  }
98
98
  export async function loadWorkflowStartHeaders(internals, workflowId) {
99
99
  const bytes = await internals.storage.get(KEYS.workflowHeaders(workflowId));
@@ -86,7 +86,7 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
86
86
  callbacks.swallowPromiseRejection(internals.scheduler.cancel(`deadline:${workflowId}`, workflowId));
87
87
  if (previousState.status === "pending")
88
88
  callbacks.swallowPromiseRejection(internals.scheduler.cancel(`delayed-start:${workflowId}`, workflowId));
89
- const resolver = internals.resultResolvers.get(workflowId), terminalError = buildTerminalError(workflowId, status, elapsed);
89
+ const resolver = internals.resultResolvers.get(workflowId), terminalError = buildTerminalError(workflowId, status, elapsed, reason);
90
90
  try {
91
91
  await cleanupTerminalWorkflowSynchronously(internals, workflowId, !0, callbacks);
92
92
  const event = buildTerminalEvent(workflowId, status, elapsed, reason);
@@ -106,8 +106,8 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
106
106
  internals.terminalizingWorkflows.delete(workflowId);
107
107
  }
108
108
  }
109
- function buildTerminalError(workflowId, status, elapsed) {
110
- return status === "timed-out" ? new WorkflowTimeoutError(workflowId, "execution", elapsed) : Error("Workflow cancelled");
109
+ function buildTerminalError(workflowId, status, elapsed, reason) {
110
+ return status === "timed-out" ? new WorkflowTimeoutError(workflowId, "execution", elapsed, reason) : Error("Workflow cancelled");
111
111
  }
112
112
  function buildTerminalEvent(workflowId, status, elapsed, reason) {
113
113
  return status === "timed-out" ? new WorkflowTimedOutEvent(workflowId, "execution", elapsed, reason) : new WorkflowCancelledEvent(workflowId);
package/dist/core/json.js CHANGED
@@ -33,7 +33,7 @@ function isJSONPrimitive(value) {
33
33
  if (value === null)
34
34
  return !0;
35
35
  if (typeof value === "number")
36
- return Number.isFinite(value);
36
+ return Number.isFinite(value) && !Object.is(value, -0);
37
37
  return typeof value === "string" || typeof value === "boolean";
38
38
  }
39
39
  function isUnsupportedJSONType(value) {