@lostgradient/weft 0.7.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 (62) hide show
  1. package/README.md +4 -2
  2. package/dist/cli-main.js +63 -63
  3. package/dist/core/context/activity-retry-state.d.ts +6 -5
  4. package/dist/core/context/activity-retry-state.js +31 -21
  5. package/dist/core/context/durable-activity.d.ts +117 -0
  6. package/dist/core/context/durable-activity.js +79 -0
  7. package/dist/core/context/operation-request.d.ts +15 -0
  8. package/dist/core/context/parallel-operations.js +1 -0
  9. package/dist/core/context/run-operation-cached-request.d.ts +8 -0
  10. package/dist/core/context/run-operation-cached-request.js +59 -0
  11. package/dist/core/context/run-operation.d.ts +23 -5
  12. package/dist/core/context/run-operation.js +62 -47
  13. package/dist/core/engine/activity-heartbeat-tracking.d.ts +8 -17
  14. package/dist/core/engine/activity-heartbeat-tracking.js +7 -1
  15. package/dist/core/engine/activity-reconciliation.d.ts +1 -0
  16. package/dist/core/engine/activity-reconciliation.js +7 -2
  17. package/dist/core/engine/anonymous-signal-sequence.js +6 -4
  18. package/dist/core/engine/async-activity-completion.d.ts +5 -7
  19. package/dist/core/engine/async-activity-completion.js +13 -9
  20. package/dist/core/engine/bulk-operations-purge.js +2 -1
  21. package/dist/core/engine/bulk-operations.js +8 -7
  22. package/dist/core/engine/callback-checkpoint-persistence.d.ts +3 -0
  23. package/dist/core/engine/callback-checkpoint-persistence.js +25 -0
  24. package/dist/core/engine/callback-creators-bundles.js +4 -1
  25. package/dist/core/engine/checkpoint-io.d.ts +4 -1
  26. package/dist/core/engine/checkpoint-io.js +19 -10
  27. package/dist/core/engine/completed-review-storage.d.ts +2 -1
  28. package/dist/core/engine/completed-review-storage.js +4 -3
  29. package/dist/core/engine/index.d.ts +31 -0
  30. package/dist/core/engine/index.js +6 -1
  31. package/dist/core/engine/internals.d.ts +2 -1
  32. package/dist/core/engine/lease-manager.js +2 -2
  33. package/dist/core/engine/memo-durable-activity.d.ts +11 -0
  34. package/dist/core/engine/memo-durable-activity.js +282 -0
  35. package/dist/core/engine/operations-activity.d.ts +5 -1
  36. package/dist/core/engine/operations-activity.js +19 -7
  37. package/dist/core/engine/operations-data.d.ts +4 -1
  38. package/dist/core/engine/operations-data.js +3 -3
  39. package/dist/core/engine/reviews.js +7 -4
  40. package/dist/core/engine/schedule-timer.js +2 -0
  41. package/dist/core/engine/storage-io.js +1 -1
  42. package/dist/core/json.js +1 -1
  43. package/dist/core/weft-error.d.ts +1 -1
  44. package/dist/core/weft-error.js +2 -0
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.js +6 -0
  47. package/dist/json-schema.js +1 -1
  48. package/dist/mcp/cli.js +26 -26
  49. package/dist/server/handler.js +21 -21
  50. package/dist/server/index.js +17 -17
  51. package/dist/server/runtime/websocket-worker.js +7 -2
  52. package/dist/server/serve-internals.d.ts +28 -0
  53. package/dist/server/serve-internals.js +4 -2
  54. package/dist/service-worker/index.js +22 -22
  55. package/dist/service-worker/setup.d.ts +18 -1
  56. package/dist/service-worker/setup.js +7 -4
  57. package/dist/storage/typed-storage.d.ts +1 -1
  58. package/dist/storage/typed-storage.js +1 -1
  59. package/dist/testing/index.js +27 -27
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import type { Storage } from '../../storage/interface.ts';
2
2
  import type { HumanReviewResult, ReviewRequest } from '../review/index.ts';
3
3
  import type { CompletedReviewEntry, ReviewListFilter } from '../types.ts';
4
+ import type { EngineInternals } from './internals.ts';
4
5
  type ReviewListFilterableEntry = {
5
6
  workflowId?: string;
6
7
  reviewType?: string;
@@ -9,6 +10,6 @@ export declare function matchesReviewListFilter(review: ReviewListFilterableEntr
9
10
  export declare function completedReviewStoragePrefix(workflowId?: string): string;
10
11
  export declare function completedReviewStorageKey(workflowId: string, reviewId: string): string;
11
12
  export declare function listCompletedReviewsFromStorage(storage: Storage, filter: ReviewListFilter): Promise<CompletedReviewEntry[]>;
12
- export declare function persistCompletedReviewRecord(storage: Storage, reviewKey: string, reviewData: ReviewRequest, decisionResult: HumanReviewResult): Promise<void>;
13
+ export declare function persistCompletedReviewRecord(internals: EngineInternals, reviewKey: string, reviewData: ReviewRequest, decisionResult: HumanReviewResult): Promise<void>;
13
14
  export declare function deleteCompletedReviewsForWorkflow(storage: Storage, workflowId: string): Promise<void>;
14
15
  export {};
@@ -1,5 +1,6 @@
1
1
  import { encodeStorageKeyComponent } from "../../storage/interface.js";
2
2
  import { encode } from "../codec.js";
3
+ import { commitFencedEngineWrite } from "./fenced-write.js";
3
4
  import { parseCompletedReviewEntry, toCompletedReviewEntry } from "./review-list-entries.js";
4
5
  export function matchesReviewListFilter(review, filter) {
5
6
  if (filter.workflowId !== void 0 && review.workflowId !== filter.workflowId)
@@ -32,16 +33,16 @@ export async function listCompletedReviewsFromStorage(storage, filter) {
32
33
  await appendCompletedReviews(reviews, storage.scan("review-decision:"), filter, (key) => scopedPrefix === null || !key.startsWith(scopedPrefix));
33
34
  return reviews;
34
35
  }
35
- export async function persistCompletedReviewRecord(storage, reviewKey, reviewData, decisionResult) {
36
+ export async function persistCompletedReviewRecord(internals, reviewKey, reviewData, decisionResult) {
36
37
  const completedReview = toCompletedReviewEntry(reviewData, decisionResult);
37
- await storage.batch([
38
+ await commitFencedEngineWrite(internals, [
38
39
  {
39
40
  type: "put",
40
41
  key: completedReviewStorageKey(reviewData.workflowId, reviewData.reviewId),
41
42
  value: encode(completedReview)
42
43
  },
43
44
  { type: "delete", key: reviewKey }
44
- ]);
45
+ ], [], () => Error(`Completed review commit for review "${reviewData.reviewId}" lost its precondition.`));
45
46
  }
46
47
  export async function deleteCompletedReviewsForWorkflow(storage, workflowId) {
47
48
  const completedPrefix = completedReviewStoragePrefix(workflowId), deleteOperations = [];
@@ -36,6 +36,25 @@ export { assertCompatiblePersistedDataVersion };
36
36
  export declare const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING: unique symbol;
37
37
  export declare const ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING: unique symbol;
38
38
  export declare const ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING: unique symbol;
39
+ /**
40
+ * The `name` of the `process` warning emitted when a lease-owning engine is
41
+ * disposed through the synchronous `[Symbol.dispose]()` path. Sync disposal can
42
+ * only fire the lease release in the background; use `await engine.shutdown()`,
43
+ * `await using`, or `await engine[Symbol.asyncDispose]()` when prompt rolling
44
+ * deploy handoff matters.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import { ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME } from '@lostgradient/weft';
49
+ *
50
+ * process.on('warning', (warning) => {
51
+ * if (warning.name === ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME) {
52
+ * // Alert on shutdown paths that can make lease handoff wait for leaseTtl.
53
+ * }
54
+ * });
55
+ * ```
56
+ */
57
+ export declare const ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME = "WeftEngineLeaseSynchronousDisposeWarning";
39
58
  export { ENGINE_LEASE_LOST_WARNING_NAME };
40
59
  /**
41
60
  * Durable execution engine.
@@ -408,6 +427,18 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
408
427
  timeout?: number;
409
428
  idempotencyKey?: string;
410
429
  }): Promise<CoordinatedUpdateResult>;
430
+ /**
431
+ * Awaited engine shutdown. Equivalent to
432
+ * `await engine[Symbol.asyncDispose]()` and useful in explicit signal handlers
433
+ * where `await using` syntax cannot own the process lifetime directly.
434
+ *
435
+ * Under `ownership: 'lease'`, this is the explicit prompt-handoff primitive:
436
+ * it drains queued inline starts, tears down in-memory write paths, and awaits
437
+ * lease release before resolving. Synchronous disposal remains immediate and
438
+ * can make the next engine wait for `leaseTtl` if the process exits before its
439
+ * background release completes.
440
+ */
441
+ shutdown(): Promise<void>;
411
442
  /**
412
443
  * Synchronous teardown (`using engine = ...`). Pending inline launches that
413
444
  * have not yet run are **discarded**, not executed. When you need queued
@@ -206,7 +206,7 @@ export {
206
206
  shouldEmitEngineLeakWarningForTesting
207
207
  } from "./engine-leak-warnings.js";
208
208
  export { assertCompatiblePersistedDataVersion };
209
- export const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING = Symbol("engineParkedWorkflowCountForTesting"), ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING = Symbol("engineSignalWaiterCountForTesting"), ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING = Symbol("engineSleepResolverCountForTesting");
209
+ export const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING = Symbol("engineParkedWorkflowCountForTesting"), ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING = Symbol("engineSignalWaiterCountForTesting"), ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING = Symbol("engineSleepResolverCountForTesting"), ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME = "WeftEngineLeaseSynchronousDisposeWarning";
210
210
 
211
211
  export { ENGINE_LEASE_LOST_WARNING_NAME };
212
212
 
@@ -769,8 +769,13 @@ export class Engine extends EventTarget {
769
769
  async submitCoordinatedUpdate(workflowId, name, payload, options) {
770
770
  return submitCoordinatedUpdateFromInternals(getInternals(this), workflowId, name, payload, options, this.#createUpdateCallbacks());
771
771
  }
772
+ async shutdown() {
773
+ return this[Symbol.asyncDispose]();
774
+ }
772
775
  [Symbol.dispose]() {
773
776
  const leaseManager = getInternals(this).leaseManager;
777
+ if (leaseManager !== null && leaseManager.currentEpochBytes() !== null)
778
+ process.emitWarning("engine ownership lease disposed synchronously; lease release is fire-and-forget, so exiting before the release completes can make the next instance wait for leaseTtl. Use await engine.shutdown(), await using, or await engine[Symbol.asyncDispose]() for prompt rolling-deploy handoff.", ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME);
774
779
  disposeEngine(getInternals(this));
775
780
  leaseManager?.release();
776
781
  }
@@ -31,6 +31,7 @@ import type { Scheduler } from '../scheduler.ts';
31
31
  import type { Checkpoint, StartWorkflowOptions } from '../types.ts';
32
32
  import type { UpdateCoordinator } from '../updates.ts';
33
33
  import type { WorkflowVersionTuple } from '../workflow-version-tuple.ts';
34
+ import type { ActivityHeartbeatKey } from './activity-heartbeat-tracking.ts';
34
35
  import type { PendingTimelineEntry, QueuedInlineWorkflowExecutionStart, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter } from './engine-internal-types.ts';
35
36
  import type { EngineCleanupIntervalDisposalTracker } from './engine-leak-warnings.ts';
36
37
  import type { WorkflowHandle, WorkflowHandleEngine } from './handles.ts';
@@ -148,7 +149,7 @@ export interface EngineInternals {
148
149
  * {@link heartbeatDetails}. Inline-execution only; worker-executed activities run
149
150
  * their function out of process and never observe this.
150
151
  */
151
- lastHeartbeatDetailsByStep: Map<string, Map<number, unknown>>;
152
+ lastHeartbeatDetailsByStep: Map<string, Map<ActivityHeartbeatKey, unknown>>;
152
153
  /**
153
154
  * Per-run, non-serialized `services` value exposed to the workflow body as
154
155
  * `ctx.services`. Set at `engine.start({ services })` and re-provided on
@@ -8,7 +8,7 @@ import {
8
8
  import { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError } from "./lease-errors.js";
9
9
  const DEFAULT_ACQUIRE_POLL_INTERVAL_MS = 1000;
10
10
  export function createLeaseManager(options) {
11
- const { storage, holderId, getNow, ttlMs, renewIntervalMs, waitTimeoutMs } = options, acquirePollIntervalMs = options.acquirePollIntervalMs ?? DEFAULT_ACQUIRE_POLL_INTERVAL_MS, unconfirmableMarginMs = renewIntervalMs;
11
+ const { storage, holderId, getNow, ttlMs, renewIntervalMs, waitTimeoutMs } = options, acquirePollIntervalMs = options.acquirePollIntervalMs ?? DEFAULT_ACQUIRE_POLL_INTERVAL_MS;
12
12
  let stopped = !1, heldEpoch = null, heldEpochBytes = null, lastHolderBytes = null, renewalInterval = null, leaseLost = !1, inFlightRenewal = null;
13
13
  const delay = options.delay ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
14
14
  function reportLeaseLost(reason) {
@@ -81,7 +81,7 @@ export function createLeaseManager(options) {
81
81
  committed = await storageConditionalBatch(storage, [{ key: KEYS.leaseHolder(), expectedValue: lastHolderBytes }], [{ type: "put", key: KEYS.leaseHolder(), value: holderBytes }]);
82
82
  } catch {
83
83
  const priorExpiry = decodeHolder(lastHolderBytes)?.expiresAt ?? 0;
84
- if (getNow() >= priorExpiry - unconfirmableMarginMs)
84
+ if (getNow() >= priorExpiry)
85
85
  reportLeaseLost("renewal-unconfirmable");
86
86
  return;
87
87
  }
@@ -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;
@@ -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;
@@ -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));
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) {
@@ -25,7 +25,7 @@
25
25
  * }
26
26
  * ```
27
27
  */
28
- export type WeftErrorCode = 'WorkflowAlreadyExistsError' | 'BulkDeleteRequiresTerminalWorkflowsError' | 'BulkOperationConfirmationError' | 'WorkflowTypeNotRegisteredForRecoveryError' | 'EngineCreateNameMismatchError' | 'EngineDisposedError' | 'WorkflowNotFoundError' | 'WorkflowNotRegisteredError' | 'WorkflowConcurrencyLimitExceededError' | 'WorkflowSuspendNotSupportedError' | 'ActivityResolutionError' | 'BranchTopologyChangedError' | 'PersistedDataIncompatibleError' | 'WorkflowTimeoutError' | 'HttpClientError' | 'WorkerProtocolIncompatibleError' | 'UpdateTimeoutError' | 'UpdateValidationError' | 'WorkflowTerminalError' | 'WorkflowBuilderError' | 'VersionMismatchError' | 'EffectReplayConflictError' | 'ReviewTimeoutError' | 'AtomicStateConflictError' | 'StandardSchemaValidationError' | 'ActivityReconciliationCapabilityError' | 'ActivityReconciliationConflictError' | 'ActivityReconciliationIndeterminateError' | 'AsyncActivityTokenNotFoundError' | 'ActivityScheduleToCloseTimeoutError' | 'ActivityPerAttemptTimeoutError' | 'PayloadSizeExceededError' | 'StartOrSignalConflictError' | 'WorkflowTeardownPendingError' | 'IdempotencyKeyPurgedError';
28
+ export type WeftErrorCode = 'WorkflowAlreadyExistsError' | 'BulkDeleteRequiresTerminalWorkflowsError' | 'BulkOperationConfirmationError' | 'WorkflowTypeNotRegisteredForRecoveryError' | 'EngineCreateNameMismatchError' | 'EngineDisposedError' | 'WorkflowNotFoundError' | 'WorkflowNotRegisteredError' | 'WorkflowConcurrencyLimitExceededError' | 'WorkflowSuspendNotSupportedError' | 'ActivityResolutionError' | 'BranchTopologyChangedError' | 'PersistedDataIncompatibleError' | 'WorkflowTimeoutError' | 'HttpClientError' | 'WorkerProtocolIncompatibleError' | 'UpdateTimeoutError' | 'UpdateValidationError' | 'WorkflowTerminalError' | 'WorkflowBuilderError' | 'VersionMismatchError' | 'EffectReplayConflictError' | 'ReviewTimeoutError' | 'AtomicStateConflictError' | 'StandardSchemaValidationError' | 'ActivityReconciliationCapabilityError' | 'ActivityReconciliationConflictError' | 'ActivityReconciliationIndeterminateError' | 'DurableActivityScopeError' | 'DurableActivityUnsupportedError' | 'AsyncActivityTokenNotFoundError' | 'ActivityScheduleToCloseTimeoutError' | 'ActivityPerAttemptTimeoutError' | 'PayloadSizeExceededError' | 'StartOrSignalConflictError' | 'WorkflowTeardownPendingError' | 'IdempotencyKeyPurgedError';
29
29
  /**
30
30
  * Generic abstract base for all Weft library errors. The `TCode` parameter
31
31
  * makes each subclass's `code` its own literal type; the exported base surface
@@ -35,6 +35,8 @@ const publicWeftErrorCodeMap = {
35
35
  ActivityReconciliationCapabilityError: !0,
36
36
  ActivityReconciliationConflictError: !0,
37
37
  ActivityReconciliationIndeterminateError: !0,
38
+ DurableActivityScopeError: !0,
39
+ DurableActivityUnsupportedError: !0,
38
40
  AsyncActivityTokenNotFoundError: !0,
39
41
  ActivityScheduleToCloseTimeoutError: !0,
40
42
  ActivityPerAttemptTimeoutError: !0,