@lostgradient/weft 0.10.0 → 0.11.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 (34) hide show
  1. package/README.md +10 -2
  2. package/dist/cli-main.js +62 -62
  3. package/dist/core/checkpoint/serialization.js +2 -2
  4. package/dist/core/engine/disposal.js +4 -0
  5. package/dist/core/engine/engine-leak-warnings.d.ts +2 -0
  6. package/dist/core/engine/engine-leak-warnings.js +7 -1
  7. package/dist/core/engine/index.d.ts +6 -3
  8. package/dist/core/engine/index.js +19 -3
  9. package/dist/core/engine/inline-parking.js +14 -1
  10. package/dist/core/engine/internals.d.ts +21 -0
  11. package/dist/core/engine/operations-time.js +8 -28
  12. package/dist/core/engine/sleep-timer-acknowledgements.d.ts +15 -0
  13. package/dist/core/engine/sleep-timer-acknowledgements.js +144 -0
  14. package/dist/core/engine/termination/cleanup.js +3 -0
  15. package/dist/mcp/cli.js +17 -17
  16. package/dist/server/handler.js +12 -12
  17. package/dist/server/index.js +17 -17
  18. package/dist/service-worker/index.js +12 -12
  19. package/dist/service-worker/scheduler.js +1 -0
  20. package/dist/storage/lazy-postgres-pool.d.ts +38 -0
  21. package/dist/storage/lazy-postgres-pool.js +38 -0
  22. package/dist/storage/neon-batch.d.ts +8 -8
  23. package/dist/storage/neon.d.ts +26 -79
  24. package/dist/storage/neon.js +3 -3
  25. package/dist/storage/postgres-key-value-queries.d.ts +1 -1
  26. package/dist/storage/postgres-key-value-queries.js +1 -1
  27. package/dist/storage/postgres-key-value-storage.d.ts +110 -0
  28. package/dist/storage/postgres-key-value-storage.js +204 -0
  29. package/dist/storage/postgres.d.ts +59 -0
  30. package/dist/storage/postgres.js +15 -0
  31. package/dist/testing/index.js +26 -26
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +16 -3
@@ -29,7 +29,7 @@ export function validateCheckpointShape(value) {
29
29
  validateAccumulatedResultReplayWatermark(record);
30
30
  validateWorkerReplaySignatures(record);
31
31
  validateWorkerReplayFailures(record);
32
- dropLegacyPendingSignals(record);
32
+ dropRetiredPendingSignals(record);
33
33
  assertRecordField(record, "searchAttributes");
34
34
  assertStringField(record, "version");
35
35
  assertNumberField(record, "createdAt");
@@ -148,7 +148,7 @@ function validateAccumulatedResultReplayWatermark(record) {
148
148
  if (typeof watermark !== "number" || !Number.isSafeInteger(watermark) || watermark < 0)
149
149
  throw Error('Invalid checkpoint: invalid "accumulatedResultReplayWatermark" (expected non-negative safe integer)');
150
150
  }
151
- function dropLegacyPendingSignals(record) {
151
+ function dropRetiredPendingSignals(record) {
152
152
  delete record.pendingSignals;
153
153
  }
154
154
  function assertCurrentSchemaVersion(record) {
@@ -1,6 +1,7 @@
1
1
  import { disposeEngineCleanupInterval } from "./engine-runtime-helpers.js";
2
2
  import { EngineDisposedError } from "./errors.js";
3
3
  import { disposeQueuedInlineWorkflowStarts } from "./inline-launch-queue.js";
4
+ import { rejectAllSleepTimerAcknowledgements } from "./sleep-timer-acknowledgements.js";
4
5
  export function disposeEngine(internals) {
5
6
  internals.disposed = !0;
6
7
  internals.alertManager?.[Symbol.dispose]();
@@ -30,6 +31,7 @@ export function disposeEngine(internals) {
30
31
  internals.handleCache.clear();
31
32
  for (const waiter of internals.resultResolvers.values())
32
33
  waiter.reject(new EngineDisposedError);
34
+ rejectAllSleepTimerAcknowledgements(internals, new EngineDisposedError);
33
35
  internals.resultResolvers.clear();
34
36
  internals.updateWaiters.clear();
35
37
  internals.updateWaitersByWorkflow.clear();
@@ -46,6 +48,8 @@ export function disposeEngine(internals) {
46
48
  internals.pendingWebhooks.clear();
47
49
  internals.sleepResolvers.clear();
48
50
  internals.sleepResolversByWorkflow.clear();
51
+ internals.sleepTimerAcknowledgementWaiters.clear();
52
+ internals.durableInlineOperations.clear();
49
53
  internals.sleepTimersFiredWithoutResolver.clear();
50
54
  internals.checkpoints.clear();
51
55
  internals.pendingExecutionStateOwnerId = void 0;
@@ -15,6 +15,8 @@ export type EngineCleanupIntervalDisposalTracker = {
15
15
  testToken: symbol | undefined;
16
16
  };
17
17
  export declare const engineCleanupIntervalFinalizer: FinalizationRegistry<EngineCleanupIntervalDisposalTracker>;
18
+ /** Test-only hook for the finalizer callback's synchronous warning gate. */
19
+ export declare function finalizeEngineCleanupIntervalTrackerForTesting(tracker: EngineCleanupIntervalDisposalTracker): void;
18
20
  export declare function shouldEmitEngineLeakWarning(): boolean;
19
21
  /** Test-only override for the engine leak-warning environment gate. */
20
22
  export declare function setEngineLeakWarningOverrideForTesting(value: boolean | undefined): void;
@@ -1,6 +1,6 @@
1
1
  let engineLeakWarningOverrideForTesting, engineLeakCollectionCountForTesting = 0, nextEngineLeakWarningTokenForTesting;
2
2
  const engineLeakWarningTokensForTesting = new Set;
3
- export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker) => {
3
+ function finalizeEngineCleanupIntervalTracker(tracker) {
4
4
  engineLeakCollectionCountForTesting++;
5
5
  if (tracker.cleanupInterval !== null) {
6
6
  clearInterval(tracker.cleanupInterval);
@@ -15,7 +15,13 @@ export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker)
15
15
  engineLeakWarningTokensForTesting.add(tracker.testToken);
16
16
  process.emitWarning("WeftEngineLeakWarning: A Weft Engine was garbage-collected without calling [Symbol.dispose](). Use `using`, `await using`, or call engine[Symbol.dispose]() to clear background timers and release runtime resources.");
17
17
  }
18
+ }
19
+ export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker) => {
20
+ finalizeEngineCleanupIntervalTracker(tracker);
18
21
  });
22
+ export function finalizeEngineCleanupIntervalTrackerForTesting(tracker) {
23
+ finalizeEngineCleanupIntervalTracker(tracker);
24
+ }
19
25
  export function shouldEmitEngineLeakWarning() {
20
26
  if (engineLeakWarningOverrideForTesting !== void 0)
21
27
  return engineLeakWarningOverrideForTesting;
@@ -220,9 +220,12 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
220
220
  * (signal-with-start). With an absent target, the workflow record and the
221
221
  * first signal commit in one batch and the freshly-launched run consumes the
222
222
  * signal on its first drive. A non-terminal target (running, pending, or
223
- * suspended) is signalled through the normal signal path; a terminal target
224
- * throws {@link StartOrSignalConflictError} unless
225
- * `options.onTerminalConflict: 'start-new'` is supplied with an explicit
223
+ * suspended) is signalled through a workflow-serialized signal path. If
224
+ * terminal completion wins that handoff, the target is treated as terminal:
225
+ * the default path throws {@link StartOrSignalConflictError}, while
226
+ * `options.onTerminalConflict: 'start-new'` transfers the signal to the
227
+ * replacement run. A terminal target throws {@link StartOrSignalConflictError}
228
+ * unless `options.onTerminalConflict: 'start-new'` is supplied with an explicit
226
229
  * workflow id and deterministic `signal.signalId`.
227
230
  *
228
231
  * Concurrent callers converge on one workflow and one delivered signal. Pass
@@ -74,7 +74,11 @@ import {
74
74
  shouldStartEngineScheduler,
75
75
  validateEngineCreateBackgroundTaskOptions
76
76
  } from "./engine-runtime-helpers.js";
77
- import { EngineCreateNameMismatchError, EngineDisposedError } from "./errors.js";
77
+ import {
78
+ EngineCreateNameMismatchError,
79
+ EngineDisposedError,
80
+ StartOrSignalConflictError
81
+ } from "./errors.js";
78
82
  import { assertLeaseHeldForEngineWork, commitFencedEngineWrite } from "./fenced-write.js";
79
83
  import { recordFinalizerState } from "./finalizer-state.js";
80
84
  import {
@@ -145,7 +149,11 @@ import {
145
149
  createSecondInstanceDetector
146
150
  } from "./second-instance-detector.js";
147
151
  import { signal as signalWorkflow } from "./signals.js";
148
- import { loadScheduleState, loadWorkflowState } from "./storage-io.js";
152
+ import {
153
+ loadScheduleState,
154
+ loadWorkflowState,
155
+ runSerializedWorkflowStateWrite
156
+ } from "./storage-io.js";
149
157
  import {
150
158
  feedOperationResult,
151
159
  getComposedWorkflowInterceptor,
@@ -163,6 +171,7 @@ import {
163
171
  submitCoordinatedUpdate as submitCoordinatedUpdateFromInternals,
164
172
  update as updateFromInternals
165
173
  } from "./updates.js";
174
+ import { isTerminalWorkflowStatus } from "./validation.js";
166
175
  import { coerceScheduleId } from "./validation/schedule.js";
167
176
  import {
168
177
  replayWorkflowFeed,
@@ -280,6 +289,8 @@ export class Engine extends EventTarget {
280
289
  getInternals(this).updateWaitersByWorkflow = new Map;
281
290
  getInternals(this).sleepResolvers = new Map;
282
291
  getInternals(this).sleepResolversByWorkflow = new Map;
292
+ getInternals(this).sleepTimerAcknowledgementWaiters = new Map;
293
+ getInternals(this).durableInlineOperations = new Map;
283
294
  getInternals(this).sleepTimersFiredWithoutResolver = new Map;
284
295
  getInternals(this).interceptors = resolveEngineInterceptors(options);
285
296
  getInternals(this).composedWorkflowInterceptor = void 0;
@@ -568,7 +579,12 @@ export class Engine extends EventTarget {
568
579
  #createStartOrSignalCallbacks() {
569
580
  return {
570
581
  ...this.#createLifecycleCallbacks(),
571
- signalExistingWorkflow: (workflowId, signalName, payload, signalId) => this.signal(workflowId, signalName, payload, { signalId })
582
+ signalExistingWorkflow: (workflowId, signalName, payload, signalId) => runSerializedWorkflowStateWrite(getInternals(this), workflowId, async () => {
583
+ const state = await loadWorkflowState(getInternals(this), workflowId);
584
+ if (state && isTerminalWorkflowStatus(state.status))
585
+ throw new StartOrSignalConflictError(workflowId, state.status);
586
+ await this.signal(workflowId, signalName, payload, { signalId });
587
+ })
572
588
  };
573
589
  }
574
590
  getHandle(workflowId) {
@@ -2,6 +2,10 @@ import { hasExposedAccessors, hasUpdateHandlers } from "../context/context-prese
2
2
  import {
3
3
  resumeWorkflowFromStorage as resumeWorkflowFromStorageFromLifecycle
4
4
  } from "./lifecycle.js";
5
+ import {
6
+ recordDurableInlineOperation,
7
+ rejectSleepTimerAcknowledgements
8
+ } from "./sleep-timer-acknowledgements.js";
5
9
  import {
6
10
  completeWorkflow as completeWorkflowFromTermination,
7
11
  failWorkflow as failWorkflowFromTermination
@@ -53,7 +57,7 @@ export async function getParkedWorkflowResumeDisposition(internals, workflowId,
53
57
  return "corrupt";
54
58
  return "resumable";
55
59
  }
56
- export async function handleStrategyMessage(internals, message, callbacks) {
60
+ async function dispatchStrategyMessage(internals, message, callbacks) {
57
61
  switch (message.type) {
58
62
  case "completed":
59
63
  await completeWorkflowFromTermination(internals, message.workflowId, message.result, callbacks.createTerminationCallbacks());
@@ -68,6 +72,7 @@ export async function handleStrategyMessage(internals, message, callbacks) {
68
72
  case "checkpoint": {
69
73
  const operation = callbacks.translateOperationRequest(message.operationRequest);
70
74
  await callbacks.persistCheckpoint(message.workflowId, operation, message.checkpoint);
75
+ recordDurableInlineOperation(internals, message.workflowId, operation);
71
76
  callbacks.validateDevelopmentCheckpoint(message.workflowId);
72
77
  if (await callbacks.evaluateConstraints(message.workflowId))
73
78
  break;
@@ -85,3 +90,11 @@ export async function handleStrategyMessage(internals, message, callbacks) {
85
90
  }
86
91
  }
87
92
  }
93
+ export async function handleStrategyMessage(internals, message, callbacks) {
94
+ try {
95
+ await dispatchStrategyMessage(internals, message, callbacks);
96
+ } catch (error) {
97
+ rejectSleepTimerAcknowledgements(internals, message.workflowId, error);
98
+ throw error;
99
+ }
100
+ }
@@ -22,6 +22,7 @@ import type { AlertManager } from '../../alerting/alert-manager.ts';
22
22
  import type { Storage as WeftStorage } from '../../storage/interface.ts';
23
23
  import type { ActivityWorkerDispatcher } from '../../workers/activity-worker-dispatcher.ts';
24
24
  import type { ActivityRegistry } from '../activity-registry.ts';
25
+ import type { ContextOperationRequest } from '../context.ts';
25
26
  import type { EventHeadRecord } from '../event-log.ts';
26
27
  import type { ExecutionStrategy } from '../execution-strategy.ts';
27
28
  import type { InlineExecutionStrategy } from '../inline-execution-strategy.ts';
@@ -39,6 +40,17 @@ import type { WorkflowFeedListener } from './index.ts';
39
40
  import type { LeaseManager } from './lease-manager.ts';
40
41
  import type { ScheduleHandleEngine } from './schedule-handle.ts';
41
42
  import type { SecondInstanceDetector } from './second-instance-detector.ts';
43
+ export type SleepTimerAcknowledgementWaiter = {
44
+ fireAt: number;
45
+ operationId: string;
46
+ reject: (error: Error) => void;
47
+ resolve: () => void;
48
+ };
49
+ export type DurableInlineOperation = {
50
+ operationId: string;
51
+ scheduledFireAt?: number;
52
+ type: ContextOperationRequest['type'];
53
+ };
42
54
  type EngineRuntime = WorkflowHandleEngine & ScheduleHandleEngine & {
43
55
  start(type: string, input: unknown, options?: StartWorkflowOptions): Promise<WorkflowHandle>;
44
56
  };
@@ -102,6 +114,15 @@ export interface EngineInternals {
102
114
  fireAt: number;
103
115
  }>;
104
116
  sleepResolversByWorkflow: Map<string, Set<string>>;
117
+ /**
118
+ * Fired sleep timers awaiting proof that the awakened inline workflow reached
119
+ * its next durable checkpoint or terminal state. External schedulers must not
120
+ * delete a timer before these waiters settle, or a Service Worker eviction can
121
+ * lose the only durable wake-up between resolver settlement and checkpointing.
122
+ */
123
+ sleepTimerAcknowledgementWaiters: Map<string, Set<SleepTimerAcknowledgementWaiter>>;
124
+ /** Most recently persisted inline operation, used to reject stale sleep-timer callbacks. */
125
+ durableInlineOperations: Map<string, DurableInlineOperation>;
105
126
  /**
106
127
  * Per-workflow maps of `operationId` → fired timer `fireAt` for sleep timers
107
128
  * the scheduler tick fired before the resolver was registered. Keyed by
@@ -4,6 +4,11 @@ import { encode } from "../codec.js";
4
4
  import { buildTimerBatchOperations, normalizeStorageTimestamp } from "../scheduler.js";
5
5
  import { commitFencedEngineWrite } from "./fenced-write.js";
6
6
  import { reprovideRecoveredServices } from "./lifecycle/recovered-services.js";
7
+ import {
8
+ acknowledgeSupersededSleepTimers,
9
+ handleSleepTimerWithAcknowledgement,
10
+ resolveSleepTimer
11
+ } from "./sleep-timer-acknowledgements.js";
7
12
  import { buildWorkflowVisibilityIndexTransition } from "./workflow-indexes.js";
8
13
  export function createDelayedStartTimerEntry(_internals, workflowId, scheduledStartAt, options, callbacks) {
9
14
  return {
@@ -26,6 +31,8 @@ function sleepTimerFiredEarly(internals, workflowId, operation) {
26
31
  workflowMarkers.delete(operation.operationId);
27
32
  if (workflowMarkers.size === 0)
28
33
  internals.sleepTimersFiredWithoutResolver.delete(workflowId);
34
+ if (markedFireAt < operation.scheduledFireAt)
35
+ acknowledgeSupersededSleepTimers(internals, workflowId, operation.scheduledFireAt);
29
36
  return markedFireAt >= operation.scheduledFireAt;
30
37
  }
31
38
  export async function processSleepOperation(internals, workflowId, operation, callbacks) {
@@ -166,7 +173,7 @@ export async function handleTimerFired(internals, entry, callbacks) {
166
173
  return;
167
174
  }
168
175
  if (entry.kind === "sleep")
169
- resolveSleepTimer(internals, entry);
176
+ await handleSleepTimerWithAcknowledgement(internals, entry, callbacks.loadWorkflowState);
170
177
  else if (entry.kind === "wait-condition")
171
178
  resolveConditionTimer(internals, entry);
172
179
  else if (entry.kind === "execution-deadline")
@@ -189,30 +196,3 @@ async function handleReviewTimer(internals, entry, callbacks) {
189
196
  return;
190
197
  await handler(entry);
191
198
  }
192
- function resolveSleepTimer(internals, entry) {
193
- const operationId = entry.id.replace("sleep:", ""), resolverKey = `${entry.workflowId}:${operationId}`, resolver = internals.sleepResolvers.get(resolverKey);
194
- if (!resolver) {
195
- let workflowMarkers = internals.sleepTimersFiredWithoutResolver.get(entry.workflowId);
196
- if (!workflowMarkers) {
197
- workflowMarkers = new Map;
198
- internals.sleepTimersFiredWithoutResolver.set(entry.workflowId, workflowMarkers);
199
- }
200
- const existing = workflowMarkers.get(operationId);
201
- if (existing === void 0 || entry.fireAt > existing)
202
- workflowMarkers.set(operationId, entry.fireAt);
203
- return;
204
- }
205
- if (entry.fireAt < resolver.fireAt)
206
- return;
207
- internals.sleepResolvers.delete(resolverKey);
208
- untrackSleepResolver(internals, entry.workflowId, operationId);
209
- resolver.resolve();
210
- }
211
- function untrackSleepResolver(internals, workflowId, operationId) {
212
- const workflowOperations = internals.sleepResolversByWorkflow.get(workflowId);
213
- if (!workflowOperations)
214
- return;
215
- workflowOperations.delete(operationId);
216
- if (workflowOperations.size === 0)
217
- internals.sleepResolversByWorkflow.delete(workflowId);
218
- }
@@ -0,0 +1,15 @@
1
+ import type { ContextOperationRequest } from '../context.ts';
2
+ import type { TimerEntry, WorkflowState } from '../types.ts';
3
+ import type { EngineInternals } from './internals.ts';
4
+ export type SleepTimerAcknowledgement = {
5
+ cancel: () => void;
6
+ promise: Promise<void>;
7
+ };
8
+ export declare function handleSleepTimerWithAcknowledgement(internals: EngineInternals, entry: TimerEntry, loadWorkflowState: (workflowId: string) => Promise<WorkflowState | null>): Promise<void>;
9
+ export declare function createSleepTimerAcknowledgement(internals: EngineInternals, workflowId: string, operationId: string, fireAt: number): SleepTimerAcknowledgement;
10
+ export declare function recordDurableInlineOperation(internals: EngineInternals, workflowId: string, operation: ContextOperationRequest): void;
11
+ export declare function settleSleepTimerAcknowledgements(internals: EngineInternals, workflowId: string, disposition: 'suspended' | 'terminal'): void;
12
+ export declare function acknowledgeSupersededSleepTimers(internals: EngineInternals, workflowId: string, currentDeadline: number): void;
13
+ export declare function rejectSleepTimerAcknowledgements(internals: EngineInternals, workflowId: string, error: unknown): void;
14
+ export declare function rejectAllSleepTimerAcknowledgements(internals: EngineInternals, error: Error): void;
15
+ export declare function resolveSleepTimer(internals: EngineInternals, entry: TimerEntry): boolean;
@@ -0,0 +1,144 @@
1
+ export async function handleSleepTimerWithAcknowledgement(internals, entry, loadWorkflowState) {
2
+ const operationId = entry.id.replace("sleep:", "");
3
+ if (await shouldIgnoreUnclaimedSleepTimer(internals, entry, operationId, loadWorkflowState))
4
+ return;
5
+ const acknowledgement = internals.inlineStrategy === null ? null : createSleepTimerAcknowledgement(internals, entry.workflowId, operationId, entry.fireAt);
6
+ if (!resolveSleepTimer(internals, entry)) {
7
+ acknowledgement?.cancel();
8
+ return;
9
+ }
10
+ await acknowledgement?.promise;
11
+ }
12
+ async function shouldIgnoreUnclaimedSleepTimer(internals, entry, operationId, loadWorkflowState) {
13
+ const resolverKey = `${entry.workflowId}:${operationId}`;
14
+ if (internals.sleepResolvers.has(resolverKey) || internals.inlineStrategy === null)
15
+ return !1;
16
+ const durableOperation = internals.durableInlineOperations.get(entry.workflowId);
17
+ if (durableOperationBelongsToAnotherOperation(durableOperation, operationId))
18
+ return !0;
19
+ if (workflowReplayIsAdvancing(internals, entry.workflowId))
20
+ return !1;
21
+ if ((await loadWorkflowState(entry.workflowId))?.status !== "running")
22
+ return !0;
23
+ throw Error(`Sleep timer "${entry.id}" fired before workflow "${entry.workflowId}" was ready to consume it.`);
24
+ }
25
+ function durableOperationBelongsToAnotherOperation(durableOperation, operationId) {
26
+ if (durableOperation === void 0)
27
+ return !1;
28
+ if (durableOperation.type !== "sleep")
29
+ return !0;
30
+ return durableOperation.operationId !== operationId;
31
+ }
32
+ function workflowReplayIsAdvancing(internals, workflowId) {
33
+ const inlineStrategy = internals.inlineStrategy;
34
+ if (inlineStrategy === null)
35
+ return !1;
36
+ return inlineStrategy.waitForWorkflowAdvance(workflowId) !== void 0 || inlineStrategy.waitForWorkflowTurn(workflowId) !== void 0;
37
+ }
38
+ function removeWaiter(internals, workflowId, waiter) {
39
+ const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
40
+ if (!workflowWaiters)
41
+ return;
42
+ workflowWaiters.delete(waiter);
43
+ if (workflowWaiters.size === 0)
44
+ internals.sleepTimerAcknowledgementWaiters.delete(workflowId);
45
+ }
46
+ export function createSleepTimerAcknowledgement(internals, workflowId, operationId, fireAt) {
47
+ const { promise, reject, resolve } = Promise.withResolvers(), waiter = { fireAt, operationId, reject, resolve };
48
+ let workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
49
+ if (!workflowWaiters) {
50
+ workflowWaiters = new Set;
51
+ internals.sleepTimerAcknowledgementWaiters.set(workflowId, workflowWaiters);
52
+ }
53
+ workflowWaiters.add(waiter);
54
+ return {
55
+ promise,
56
+ cancel: () => {
57
+ removeWaiter(internals, workflowId, waiter);
58
+ resolve();
59
+ }
60
+ };
61
+ }
62
+ export function recordDurableInlineOperation(internals, workflowId, operation) {
63
+ const durableOperation = {
64
+ operationId: operation.operationId,
65
+ type: operation.type,
66
+ ...operation.type === "sleep" && { scheduledFireAt: operation.scheduledFireAt }
67
+ };
68
+ internals.durableInlineOperations.set(workflowId, durableOperation);
69
+ const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
70
+ if (!workflowWaiters)
71
+ return;
72
+ for (const waiter of workflowWaiters) {
73
+ const stillWaitingOnThisSleep = durableOperation.type === "sleep" && durableOperation.operationId === waiter.operationId, supersededByLaterDeadline = stillWaitingOnThisSleep && durableOperation.scheduledFireAt !== void 0 && waiter.fireAt < durableOperation.scheduledFireAt;
74
+ if (stillWaitingOnThisSleep && !supersededByLaterDeadline)
75
+ continue;
76
+ removeWaiter(internals, workflowId, waiter);
77
+ waiter.resolve();
78
+ }
79
+ }
80
+ export function settleSleepTimerAcknowledgements(internals, workflowId, disposition) {
81
+ internals.durableInlineOperations.delete(workflowId);
82
+ if (disposition === "suspended") {
83
+ rejectSleepTimerAcknowledgements(internals, workflowId, Error(`Workflow "${workflowId}" was suspended before its sleep timer was acknowledged.`));
84
+ return;
85
+ }
86
+ const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
87
+ if (!workflowWaiters)
88
+ return;
89
+ internals.sleepTimerAcknowledgementWaiters.delete(workflowId);
90
+ for (const waiter of workflowWaiters)
91
+ waiter.resolve();
92
+ }
93
+ export function acknowledgeSupersededSleepTimers(internals, workflowId, currentDeadline) {
94
+ const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
95
+ if (!workflowWaiters)
96
+ return;
97
+ for (const waiter of workflowWaiters) {
98
+ if (waiter.fireAt >= currentDeadline)
99
+ continue;
100
+ removeWaiter(internals, workflowId, waiter);
101
+ waiter.resolve();
102
+ }
103
+ }
104
+ export function rejectSleepTimerAcknowledgements(internals, workflowId, error) {
105
+ const workflowWaiters = internals.sleepTimerAcknowledgementWaiters.get(workflowId);
106
+ if (!workflowWaiters)
107
+ return;
108
+ internals.sleepTimerAcknowledgementWaiters.delete(workflowId);
109
+ const rejection = error instanceof Error ? error : Error(String(error));
110
+ for (const waiter of workflowWaiters)
111
+ waiter.reject(rejection);
112
+ }
113
+ export function rejectAllSleepTimerAcknowledgements(internals, error) {
114
+ for (const workflowId of internals.sleepTimerAcknowledgementWaiters.keys())
115
+ rejectSleepTimerAcknowledgements(internals, workflowId, error);
116
+ }
117
+ export function resolveSleepTimer(internals, entry) {
118
+ const operationId = entry.id.replace("sleep:", ""), resolverKey = `${entry.workflowId}:${operationId}`, resolver = internals.sleepResolvers.get(resolverKey);
119
+ if (!resolver) {
120
+ let workflowMarkers = internals.sleepTimersFiredWithoutResolver.get(entry.workflowId);
121
+ if (!workflowMarkers) {
122
+ workflowMarkers = new Map;
123
+ internals.sleepTimersFiredWithoutResolver.set(entry.workflowId, workflowMarkers);
124
+ }
125
+ const existing = workflowMarkers.get(operationId);
126
+ if (existing === void 0 || entry.fireAt > existing)
127
+ workflowMarkers.set(operationId, entry.fireAt);
128
+ return !0;
129
+ }
130
+ if (entry.fireAt < resolver.fireAt)
131
+ return !1;
132
+ internals.sleepResolvers.delete(resolverKey);
133
+ untrackSleepResolver(internals, entry.workflowId, operationId);
134
+ resolver.resolve();
135
+ return !0;
136
+ }
137
+ function untrackSleepResolver(internals, workflowId, operationId) {
138
+ const workflowOperations = internals.sleepResolversByWorkflow.get(workflowId);
139
+ if (!workflowOperations)
140
+ return;
141
+ workflowOperations.delete(operationId);
142
+ if (workflowOperations.size === 0)
143
+ internals.sleepResolversByWorkflow.delete(workflowId);
144
+ }
@@ -2,6 +2,7 @@ import { KEYS, encodeStorageKeyComponent, storageHas } from "../../../storage/in
2
2
  import { CleanupWarningEvent } from "../../events.js";
3
3
  import { asyncActivityWorkflowPrefix } from "../async-activity-records.js";
4
4
  import { forgetCommittedCheckpointBytes } from "../checkpoint-commit-snapshots.js";
5
+ import { settleSleepTimerAcknowledgements } from "../sleep-timer-acknowledgements.js";
5
6
  import { parseTerminalCleanupTimerId, workflowFeedListenerKey } from "../state-utilities.js";
6
7
  import { releaseWorkflowConcurrencySlot } from "../workflow-concurrency.js";
7
8
  export const TERMINAL_WORKFLOW_STATUSES = new Set([
@@ -102,6 +103,7 @@ export function cleanupWaiters(internals, workflowId, callbacks) {
102
103
  internals.workflowTypeByWorkflowId.delete(workflowId);
103
104
  }
104
105
  export function evictSuspendedWorkflowWaiters(internals, workflowId, callbacks) {
106
+ settleSleepTimerAcknowledgements(internals, workflowId, "suspended");
105
107
  for (const kind of TRACKED_WAITER_KINDS)
106
108
  cleanupTrackedWaiter(internals, workflowId, kind);
107
109
  evictSleepResolversWithoutResolving(internals, workflowId);
@@ -148,6 +150,7 @@ export async function cleanupWorkflowStorage(internals, workflowId, includeOutpu
148
150
  await flush();
149
151
  }
150
152
  export function cleanupTerminalWorkflowMemory(internals, workflowId, callbacks) {
153
+ settleSleepTimerAcknowledgements(internals, workflowId, "terminal");
151
154
  internals.workflowsNeedingTerminalCleanup.delete(workflowId);
152
155
  forgetCommittedCheckpointBytes(internals, workflowId);
153
156
  internals.checkpoints.delete(workflowId);