@lostgradient/weft 0.15.0 → 0.17.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 (147) hide show
  1. package/README.md +1 -1
  2. package/dist/alerting/alert-manager.d.ts +5 -3
  3. package/dist/alerting/alert-manager.js +26 -5
  4. package/dist/alerting/index.d.ts +1 -1
  5. package/dist/alerting/types.d.ts +17 -13
  6. package/dist/alerting/types.js +5 -0
  7. package/dist/cli/api.js +5 -19
  8. package/dist/cli/console.d.ts +12 -0
  9. package/dist/cli/console.js +62 -0
  10. package/dist/cli/generated/operation-catalog.snapshot.json +71 -0
  11. package/dist/cli/generated/operation-client.generated.d.ts +15 -2
  12. package/dist/cli/generated/operation-client.generated.js +2 -0
  13. package/dist/cli/help-text.d.ts +1 -1
  14. package/dist/cli/help-text.js +1 -0
  15. package/dist/cli/index.d.ts +1 -0
  16. package/dist/cli/index.js +1 -0
  17. package/dist/cli/json-input.d.ts +20 -0
  18. package/dist/cli/json-input.js +33 -0
  19. package/dist/cli/parse-arguments.js +2 -0
  20. package/dist/cli/schedule.js +7 -9
  21. package/dist/cli/types.d.ts +1 -0
  22. package/dist/cli/workflow-commands.js +5 -25
  23. package/dist/cli-main.js +4 -2
  24. package/dist/core/engine/disposal.js +7 -0
  25. package/dist/core/engine/engine-runtime-helpers.d.ts +3 -1
  26. package/dist/core/engine/engine-runtime-helpers.js +2 -2
  27. package/dist/core/engine/errors.d.ts +20 -0
  28. package/dist/core/engine/errors.js +8 -0
  29. package/dist/core/engine/index.d.ts +39 -14
  30. package/dist/core/engine/index.js +84 -9
  31. package/dist/core/engine/inline-launch-queue.d.ts +6 -2
  32. package/dist/core/engine/inline-launch-queue.js +41 -6
  33. package/dist/core/engine/internals.d.ts +2 -0
  34. package/dist/core/engine/lease-manager.d.ts +3 -2
  35. package/dist/core/engine/lease-manager.js +7 -3
  36. package/dist/core/engine/operations-time.js +6 -0
  37. package/dist/core/execution-strategy.d.ts +8 -0
  38. package/dist/core/inline-execution-strategy.d.ts +1 -0
  39. package/dist/core/inline-execution-strategy.js +33 -20
  40. package/dist/core/weft-error.d.ts +1 -1
  41. package/dist/core/weft-error.js +1 -0
  42. package/dist/core/worker-execution-strategy.d.ts +2 -0
  43. package/dist/core/worker-execution-strategy.js +7 -2
  44. package/dist/core/worker-turn-watchdog.d.ts +4 -1
  45. package/dist/core/worker-turn-watchdog.js +9 -4
  46. package/dist/diagnostics/recommendations.js +1 -9
  47. package/dist/http.js +2 -2
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.js +1 -0
  50. package/dist/indexeddb.js +1 -1
  51. package/dist/json-schema.js +1 -1
  52. package/dist/mcp/tools.js +1 -3
  53. package/dist/server/authorization-scope.d.ts +1 -1
  54. package/dist/server/authorization-scope.js +0 -2
  55. package/dist/server/dashboard-assets.d.ts +37 -0
  56. package/dist/server/dashboard-assets.js +209 -0
  57. package/dist/server/index.d.ts +10 -1
  58. package/dist/server/index.js +11 -2
  59. package/dist/server/interactive-operations.js +1 -0
  60. package/dist/server/operations/aggregate-workflows.js +0 -7
  61. package/dist/server/operations/async-activity.js +5 -24
  62. package/dist/server/operations/bulk-cancel-workflows.js +3 -8
  63. package/dist/server/operations/bulk-delete-workflows.js +3 -8
  64. package/dist/server/operations/bulk-mutate-workflow-tags.js +3 -8
  65. package/dist/server/operations/bulk-operation-helpers.d.ts +2 -10
  66. package/dist/server/operations/bulk-operation-helpers.js +0 -6
  67. package/dist/server/operations/bulk-retry-failed-workflows.js +3 -8
  68. package/dist/server/operations/bulk-signal-workflows.js +3 -8
  69. package/dist/server/operations/cancel-schedule.js +1 -3
  70. package/dist/server/operations/cancel-workflow.js +1 -3
  71. package/dist/server/operations/create-schedule.js +21 -49
  72. package/dist/server/operations/fork-workflow.js +2 -3
  73. package/dist/server/operations/get-checkpoint-at.js +1 -6
  74. package/dist/server/operations/get-registry.js +1 -7
  75. package/dist/server/operations/get-retention-overview.js +2 -15
  76. package/dist/server/operations/get-review.js +2 -15
  77. package/dist/server/operations/get-schedule.js +2 -8
  78. package/dist/server/operations/get-stream-chunks.js +2 -6
  79. package/dist/server/operations/get-system-lease.js +1 -7
  80. package/dist/server/operations/get-system-metrics.js +0 -4
  81. package/dist/server/operations/get-task-diagnostics.js +2 -13
  82. package/dist/server/operations/get-update-result.js +1 -6
  83. package/dist/server/operations/get-workflow-attributes.js +2 -15
  84. package/dist/server/operations/get-workflow-events.js +2 -15
  85. package/dist/server/operations/get-workflow-observability.js +2 -5
  86. package/dist/server/operations/get-workflow-result.js +0 -7
  87. package/dist/server/operations/get-workflow-timeline.js +1 -6
  88. package/dist/server/operations/get-workflow.d.ts +3 -6
  89. package/dist/server/operations/get-workflow.js +2 -12
  90. package/dist/server/operations/list-alerts.d.ts +29 -0
  91. package/dist/server/operations/list-alerts.js +47 -0
  92. package/dist/server/operations/list-checkpoints.js +1 -6
  93. package/dist/server/operations/list-reviews.js +0 -7
  94. package/dist/server/operations/list-schedules.js +3 -8
  95. package/dist/server/operations/list-task-queues.js +0 -4
  96. package/dist/server/operations/list-workers.js +0 -4
  97. package/dist/server/operations/list-workflows.js +0 -7
  98. package/dist/server/operations/pause-schedule.js +1 -3
  99. package/dist/server/operations/purge-workflows.js +2 -5
  100. package/dist/server/operations/query-workflow.js +4 -15
  101. package/dist/server/operations/replay-workflow.js +2 -6
  102. package/dist/server/operations/resume-schedule.js +1 -3
  103. package/dist/server/operations/resume-workflow.js +1 -3
  104. package/dist/server/operations/schedule-faults.d.ts +9 -1
  105. package/dist/server/operations/schedule-faults.js +51 -0
  106. package/dist/server/operations/set-workflow-attributes.js +3 -15
  107. package/dist/server/operations/signal-workflow.js +3 -12
  108. package/dist/server/operations/single-workflow-tag-mutation.js +0 -7
  109. package/dist/server/operations/start-or-signal-workflow.js +2 -3
  110. package/dist/server/operations/start-workflow.js +2 -3
  111. package/dist/server/operations/storage-capabilities.js +1 -3
  112. package/dist/server/operations/storage.js +7 -13
  113. package/dist/server/operations/submit-review-decision.js +3 -12
  114. package/dist/server/operations/suspend-workflow.js +1 -3
  115. package/dist/server/operations/timeout-workflow.js +1 -3
  116. package/dist/server/operations/update-schedule.js +7 -56
  117. package/dist/server/operations/update-workflow.js +2 -3
  118. package/dist/server/operations/worker-drain.js +0 -8
  119. package/dist/server/rest-binding.d.ts +3 -2
  120. package/dist/server/rest-bindings.js +3 -0
  121. package/dist/server/route-model.d.ts +2 -0
  122. package/dist/server/route-model.js +1 -1
  123. package/dist/server/runtime/task-reconciliation.d.ts +10 -1
  124. package/dist/server/runtime/task-reconciliation.js +26 -2
  125. package/dist/server/serve-internals.js +12 -6
  126. package/dist/service-worker/index.d.ts +4 -1
  127. package/dist/service-worker/index.js +2 -2
  128. package/dist/service-worker/setup.d.ts +20 -0
  129. package/dist/service-worker/setup.js +5 -5
  130. package/dist/storage/bun-sql.js +13 -31
  131. package/dist/storage/indexeddb.js +28 -48
  132. package/dist/storage/lmdb.js +1 -1
  133. package/dist/storage/memory.js +1 -1
  134. package/dist/storage/neon.js +2 -2
  135. package/dist/storage/postgres.js +2 -2
  136. package/dist/storage/resolve.js +1 -1
  137. package/dist/storage/scoped-storage.js +1 -1
  138. package/dist/storage/testing.d.ts +6 -3
  139. package/dist/storage/testing.js +1 -1
  140. package/dist/storage/turso.js +2 -2
  141. package/dist/version.d.ts +1 -1
  142. package/dist/version.js +1 -1
  143. package/dist/web-extension.js +1 -1
  144. package/dist/worker/protocol-schemas.d.ts +2 -2
  145. package/dist/worker/protocol-schemas.js +2 -2
  146. package/dist/worker/protocol.js +1 -1
  147. package/package.json +5 -1
package/dist/cli-main.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  executeWorkflow,
22
22
  findCliSubcommandName,
23
23
  HELP_TEXT,
24
+ loadConsoleMount,
24
25
  parseCliArguments,
25
26
  removeRunLockfile,
26
27
  SCHEDULE_HELP_TEXT,
@@ -53,7 +54,7 @@ if (parsedArguments.command === "version") {
53
54
  console.log(HELP_TEXT);
54
55
  process.exit(0);
55
56
  }
56
- const storage = await createStorage(parsedArguments.storage, parsedArguments.database), engine = new Engine({ storage });
57
+ const consoleMount = parsedArguments.console ? await loadConsoleMount() : void 0, storage = await createStorage(parsedArguments.storage, parsedArguments.database), engine = new Engine({ storage });
57
58
  if (parsedArguments.workflows) {
58
59
  const { loadRegistrationsFromModule } = await import("./diagnostics/validate.js"), { registerModuleExports } = await import("./cli/serve-registrations.js"), loaded = await loadRegistrationsFromModule(parsedArguments.workflows);
59
60
  registerModuleExports(engine, loaded.registrations, loaded.activities);
@@ -61,7 +62,8 @@ if (parsedArguments.command === "version") {
61
62
  console.log("No --workflows module provided; starting in inspect-only mode.");
62
63
  const server = serve({
63
64
  engine,
64
- port: Number(parsedArguments.port)
65
+ port: Number(parsedArguments.port),
66
+ ...consoleMount
65
67
  });
66
68
  await writeRunLockfile(server.url);
67
69
  console.log(`Weft API running at ${new URL("/api/v1", server.url).href}`);
@@ -2,6 +2,12 @@ import { disposeEngineCleanupInterval } from "./engine-runtime-helpers.js";
2
2
  import { EngineDisposedError } from "./errors.js";
3
3
  import { disposeQueuedInlineWorkflowStarts } from "./inline-launch-queue.js";
4
4
  import { rejectAllSleepTimerAcknowledgements } from "./sleep-timer-acknowledgements.js";
5
+ function settleSleepResolverReadyWaitersForTesting(internals) {
6
+ for (const waiters of internals.sleepResolverReadyWaitersForTesting?.values() ?? [])
7
+ for (const notifyReady of waiters)
8
+ notifyReady();
9
+ internals.sleepResolverReadyWaitersForTesting?.clear();
10
+ }
5
11
  export function disposeEngine(internals) {
6
12
  internals.disposed = !0;
7
13
  internals.alertManager?.[Symbol.dispose]();
@@ -48,6 +54,7 @@ export function disposeEngine(internals) {
48
54
  internals.pendingWebhooks.clear();
49
55
  internals.sleepResolvers.clear();
50
56
  internals.sleepResolversByWorkflow.clear();
57
+ settleSleepResolverReadyWaitersForTesting(internals);
51
58
  internals.sleepTimerAcknowledgementWaiters.clear();
52
59
  internals.durableInlineOperations.clear();
53
60
  internals.sleepTimersFiredWithoutResolver.clear();
@@ -19,7 +19,9 @@ export declare function createQueuedInlineWorkflowStartHandler<TWorkflows extend
19
19
  * `[Symbol.asyncDispose]` ahead of synchronous disposal (which aborts the signal
20
20
  * and would otherwise discard the queue).
21
21
  */
22
- export declare function drainQueuedInlineWorkflowStartsForEngine<TWorkflows extends object, TActivities extends object>(engine: Engine<TWorkflows, TActivities>): Promise<void>;
22
+ export declare function drainQueuedInlineWorkflowStartsForEngine<TWorkflows extends object, TActivities extends object>(engine: Engine<TWorkflows, TActivities>, options?: {
23
+ abortStartedWorkflows?: boolean;
24
+ }): Promise<void>;
23
25
  export declare function createCleanupIntervalTick<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>, tracker: EngineCleanupIntervalDisposalTracker): () => void;
24
26
  /**
25
27
  * Build the resolver the second-instance detection interval uses to find its live
@@ -40,8 +40,8 @@ export function createQueuedInlineWorkflowStartHandler(weakEngine, channel) {
40
40
  swallowPromiseRejection(flushQueuedInlineWorkflowStarts(getInternals(engine), inlineLaunchQueueCallbacksForEngine(engine)));
41
41
  };
42
42
  }
43
- export async function drainQueuedInlineWorkflowStartsForEngine(engine) {
44
- await drainQueuedInlineWorkflowStarts(getInternals(engine), inlineLaunchQueueCallbacksForEngine(engine));
43
+ export async function drainQueuedInlineWorkflowStartsForEngine(engine, options) {
44
+ await drainQueuedInlineWorkflowStarts(getInternals(engine), inlineLaunchQueueCallbacksForEngine(engine), options);
45
45
  }
46
46
  export function createCleanupIntervalTick(weakEngine, tracker) {
47
47
  return function cleanupExpiredResponsesForLiveEngine() {
@@ -163,6 +163,26 @@ export declare class EngineCreateNameMismatchError extends WeftError<'EngineCrea
163
163
  export declare class EngineDisposedError extends WeftError<'EngineDisposedError'> {
164
164
  constructor();
165
165
  }
166
+ /**
167
+ * Thrown when queued inline workflow draining fails during asynchronous engine
168
+ * disposal. The engine is still fully torn down, and `leaseReleased` reports
169
+ * whether the final fenced lease delete committed. The original drain failure
170
+ * is available as `cause`.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * import { EngineDisposalError } from '@lostgradient/weft';
175
+ *
176
+ * function releaseSucceeded(error: unknown): boolean {
177
+ * return error instanceof EngineDisposalError && error.leaseReleased;
178
+ * }
179
+ * void releaseSucceeded;
180
+ * ```
181
+ */
182
+ export declare class EngineDisposalError extends WeftError<'EngineDisposalError'> {
183
+ readonly leaseReleased: boolean;
184
+ constructor(cause: unknown, leaseReleased: boolean);
185
+ }
166
186
  /**
167
187
  * Thrown by engine APIs that need a workflow to be present in storage but
168
188
  * cannot find one with the given ID. Inspect `workflowId` to identify the
@@ -70,6 +70,14 @@ export class EngineDisposedError extends WeftError {
70
70
  }
71
71
  }
72
72
 
73
+ export class EngineDisposalError extends WeftError {
74
+ leaseReleased;
75
+ constructor(cause, leaseReleased) {
76
+ super("EngineDisposalError", "Engine disposal failed while draining queued work", { cause });
77
+ this.leaseReleased = leaseReleased;
78
+ }
79
+ }
80
+
73
81
  export class WorkflowNotFoundError extends WeftError {
74
82
  workflowId;
75
83
  constructor(workflowId) {
@@ -1,3 +1,4 @@
1
+ import type { AlertStateSnapshot } from '../../alerting/types.ts';
1
2
  import { type Storage as WeftStorage } from '../../storage/interface.ts';
2
3
  import { type ActivityMetadata, type ActivityRegistrationOptions, type RegisteredActivityFunction } from '../activity-registry.ts';
3
4
  import type { StoredStreamChunk } from '../context.ts';
@@ -24,7 +25,7 @@ export { ActivityReconciliationCapabilityError, ActivityReconciliationConflictEr
24
25
  export { AsyncActivityTokenNotFoundError } from './async-activity-completion.ts';
25
26
  export type { PendingAsyncActivity } from './async-activity-records.ts';
26
27
  export type { PendingTimelineEntry, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter, } from './engine-internal-types.ts';
27
- export { ActivityResolutionError, BulkDeleteRequiresTerminalWorkflowsError, BulkOperationConfirmationError, EngineCreateNameMismatchError, EngineDisposedError, IdempotencyKeyPurgedError, PersistedDataIncompatibleError, StartOrSignalConflictError, WorkflowAlreadyExistsError, WorkflowConcurrencyLimitExceededError, WorkflowNotFoundError, WorkflowNotRegisteredError, WorkflowSuspendNotSupportedError, WorkflowTeardownPendingError, WorkflowTypeNotRegisteredForRecoveryError, } from './errors.ts';
28
+ export { ActivityResolutionError, BulkDeleteRequiresTerminalWorkflowsError, BulkOperationConfirmationError, EngineCreateNameMismatchError, EngineDisposalError, EngineDisposedError, IdempotencyKeyPurgedError, PersistedDataIncompatibleError, StartOrSignalConflictError, WorkflowAlreadyExistsError, WorkflowConcurrencyLimitExceededError, WorkflowNotFoundError, WorkflowNotRegisteredError, WorkflowSuspendNotSupportedError, WorkflowTeardownPendingError, WorkflowTypeNotRegisteredForRecoveryError, } from './errors.ts';
28
29
  export { HANDLE_RESULT_PROMISE, WorkflowHandle } from './handles.ts';
29
30
  export { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError, EngineLeaseNotHeldError, } from './lease-errors.ts';
30
31
  export type { EngineLeaseHealth, LeaseLostReason } from './lease-health.ts';
@@ -38,6 +39,30 @@ export { assertCompatiblePersistedDataVersion };
38
39
  export declare const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING: unique symbol;
39
40
  export declare const ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING: unique symbol;
40
41
  export declare const ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING: unique symbol;
42
+ export declare const ENGINE_WAIT_FOR_SLEEP_RESOLVER_FOR_TESTING: unique symbol;
43
+ export declare const ENGINE_SET_WORKER_TURN_TIMEOUT_RESOLVER_FOR_TESTING: unique symbol;
44
+ /**
45
+ * Bound for `ENGINE_WAIT_FOR_SLEEP_RESOLVER_FOR_TESTING`.
46
+ *
47
+ * The only production-adjacent consumer awaits that hook inside the
48
+ * `weft:test:periodic-sync` Service Worker message handler in
49
+ * service-worker-browser.test.ts, and that file bounds each message round trip
50
+ * at 5s (`sendWorkerMessage`), each phase at 15s or whatever remains of the
51
+ * test budget, and each test at 30s. This bound has to be strictly tighter than
52
+ * the innermost of those — at 5s it would tie with the message bound and the
53
+ * generic "Service Worker message timed out" could win the race, hiding the
54
+ * diagnostic this bound exists to produce. 3s expires first, and the handler
55
+ * posts the rejection back over the port so the workflow-naming error is what
56
+ * reaches CI.
57
+ *
58
+ * The ordering holds whenever the phase bound is the 15s ceiling. It cannot
59
+ * hold once a test has burned all but a few seconds of its 30s budget, because
60
+ * no inner window survives an outer bound shorter than itself. That case is
61
+ * reported rather than papered over: the phase error says the bound was
62
+ * shortened by the remaining budget and that a nested timeout may have been
63
+ * preempted, which points at the earlier phases that actually consumed it.
64
+ */
65
+ export declare const SLEEP_RESOLVER_READY_WAIT_TIMEOUT_MS_FOR_TESTING = 3000;
41
66
  /**
42
67
  * The `name` of the `process` warning emitted when a lease-owning engine is
43
68
  * disposed through the synchronous `[Symbol.dispose]()` path. Sync disposal can
@@ -145,6 +170,8 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
145
170
  removeEventListener<K extends Extract<keyof WeftEventMap, string>>(type: K, listener: (event: WeftEventMap[K]) => void, options?: boolean | EventListenerOptions): void;
146
171
  removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
147
172
  get state(): EngineStateNamespace;
173
+ /** Return detached snapshots of the alert rules that are currently firing. */
174
+ getActiveAlerts(): readonly AlertStateSnapshot[];
148
175
  /**
149
176
  * Register a workflow by name or definition, or register an activity
150
177
  * definition. Definition overloads return this same engine with the
@@ -311,6 +338,11 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
311
338
  [ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING](): number;
312
339
  [ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING](): number;
313
340
  [ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING](): number;
341
+ [ENGINE_WAIT_FOR_SLEEP_RESOLVER_FOR_TESTING](workflowId: string): Promise<void>;
342
+ [ENGINE_SET_WORKER_TURN_TIMEOUT_RESOLVER_FOR_TESTING](resolver: (turn: {
343
+ workflowId: string;
344
+ kind: 'run' | 'resume';
345
+ }) => number): void;
314
346
  signal(workflowId: string, name: SignalDefinition): Promise<void>;
315
347
  signal<TInput>(workflowId: string, name: SignalDefinition<TInput>, payload: TInput, options?: SignalDeliveryOptions): Promise<void>;
316
348
  signal(workflowId: string, name: string, payload?: unknown, options?: SignalDeliveryOptions): Promise<void>;
@@ -477,11 +509,13 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
477
509
  *
478
510
  * Under `ownership: 'lease'`, this is the explicit prompt-handoff primitive:
479
511
  * it drains queued inline starts, tears down in-memory write paths, and awaits
480
- * lease release before resolving. Synchronous disposal remains immediate and
481
- * can make the next engine wait for `leaseTtl` if the process exits before its
482
- * background release completes.
512
+ * lease release before resolving. The returned boolean is `true` when no lease
513
+ * needed release or the holder delete committed, and `false` when the delete
514
+ * did not commit. Synchronous disposal remains immediate and can make the next
515
+ * engine wait for `leaseTtl` if the process exits before its background release
516
+ * completes.
483
517
  */
484
- shutdown(): Promise<void>;
518
+ shutdown(): Promise<boolean>;
485
519
  /**
486
520
  * Synchronous teardown (`using engine = ...`). Pending inline launches that
487
521
  * have not yet run are **discarded**, not executed. When you need queued
@@ -490,15 +524,6 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
490
524
  * via `await using`.
491
525
  */
492
526
  [Symbol.dispose](): void;
493
- /**
494
- * Async teardown (`await using engine = ...`). Drains pending inline launches
495
- * so each queued workflow completes its first turn before disposal, leaving no
496
- * deferred-launch macrotask to fire against torn-down state. The drain is
497
- * bounded (a pass cap and the abort signal); in the pathological case where it
498
- * cannot converge, anything still queued is discarded by the synchronous
499
- * teardown that always follows. Prefer this over the synchronous
500
- * {@link Engine[Symbol.dispose]} in async contexts and tests.
501
- */
502
527
  [Symbol.asyncDispose](): Promise<void>;
503
528
  get storage(): WeftStorage;
504
529
  get scheduler(): Scheduler;
@@ -79,6 +79,7 @@ import {
79
79
  } from "./engine-runtime-helpers.js";
80
80
  import {
81
81
  EngineCreateNameMismatchError,
82
+ EngineDisposalError,
82
83
  EngineDisposedError,
83
84
  StartOrSignalConflictError
84
85
  } from "./errors.js";
@@ -194,6 +195,7 @@ export {
194
195
  BulkDeleteRequiresTerminalWorkflowsError,
195
196
  BulkOperationConfirmationError,
196
197
  EngineCreateNameMismatchError,
198
+ EngineDisposalError,
197
199
  EngineDisposedError,
198
200
  IdempotencyKeyPurgedError,
199
201
  PersistedDataIncompatibleError,
@@ -222,7 +224,7 @@ export {
222
224
  shouldEmitEngineLeakWarningForTesting
223
225
  } from "./engine-leak-warnings.js";
224
226
  export { assertCompatiblePersistedDataVersion };
225
- 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";
227
+ 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_WAIT_FOR_SLEEP_RESOLVER_FOR_TESTING = Symbol("engineWaitForSleepResolverForTesting"), ENGINE_SET_WORKER_TURN_TIMEOUT_RESOLVER_FOR_TESTING = Symbol("engineSetWorkerTurnTimeoutResolverForTesting"), SLEEP_RESOLVER_READY_WAIT_TIMEOUT_MS_FOR_TESTING = 3000, ENGINE_LEASE_SYNCHRONOUS_DISPOSE_WARNING_NAME = "WeftEngineLeaseSynchronousDisposeWarning";
226
228
 
227
229
  export { ENGINE_LEASE_LOST_WARNING_NAME };
228
230
  function scheduleDefinitionFromInternals(internals, definition) {
@@ -237,6 +239,9 @@ function scheduleDefinitionFromInternals(internals, definition) {
237
239
  }
238
240
 
239
241
  export class Engine extends EventTarget {
242
+ #asyncDisposeResult = null;
243
+ #shutdownResult = null;
244
+ #synchronousDisposeResult = null;
240
245
  static async create(options) {
241
246
  validateEngineCreateBackgroundTaskOptions(options);
242
247
  const engine = new Engine(options);
@@ -294,6 +299,7 @@ export class Engine extends EventTarget {
294
299
  getInternals(this).updateWaitersByWorkflow = new Map;
295
300
  getInternals(this).sleepResolvers = new Map;
296
301
  getInternals(this).sleepResolversByWorkflow = new Map;
302
+ getInternals(this).sleepResolverReadyWaitersForTesting = new Map;
297
303
  getInternals(this).sleepTimerAcknowledgementWaiters = new Map;
298
304
  getInternals(this).durableInlineOperations = new Map;
299
305
  getInternals(this).sleepTimersFiredWithoutResolver = new Map;
@@ -434,7 +440,9 @@ export class Engine extends EventTarget {
434
440
  throw error;
435
441
  }
436
442
  if (internals.disposed) {
437
- await manager.release();
443
+ const releaseResult = manager.release();
444
+ this.#synchronousDisposeResult = releaseResult;
445
+ await releaseResult;
438
446
  if (internals.leaseManager === manager)
439
447
  internals.leaseManager = null;
440
448
  throw new EngineDisposedError;
@@ -455,7 +463,9 @@ export class Engine extends EventTarget {
455
463
  return;
456
464
  const leaseManager = internals.leaseManager;
457
465
  disposeEngine(internals);
458
- leaseManager?.release().catch(() => {});
466
+ if (this.#synchronousDisposeResult === null)
467
+ this.#synchronousDisposeResult = (leaseManager?.release() ?? Promise.resolve(!0)).catch(() => !1);
468
+ this.#synchronousDisposeResult;
459
469
  }
460
470
  #startSecondInstanceDetection() {
461
471
  const internals = getInternals(this);
@@ -522,6 +532,9 @@ export class Engine extends EventTarget {
522
532
  workflow: (workflowType, key, options) => new AtomicState(storage, KEYS.stateWorkflow(workflowType, key), options)
523
533
  };
524
534
  }
535
+ getActiveAlerts() {
536
+ return getInternals(this).alertManager?.activeStates ?? [];
537
+ }
525
538
  async#handleStrategyMessage(message) {
526
539
  return handleStrategyMessageFromInternals(getInternals(this), message, this.#createInlineParkingCallbacks());
527
540
  }
@@ -696,6 +709,34 @@ export class Engine extends EventTarget {
696
709
  [ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING]() {
697
710
  return getInternals(this).sleepResolvers.size;
698
711
  }
712
+ async[ENGINE_WAIT_FOR_SLEEP_RESOLVER_FOR_TESTING](workflowId) {
713
+ const internals = getInternals(this);
714
+ if (internals.sleepResolversByWorkflow.has(workflowId))
715
+ return;
716
+ const { promise, resolve, reject } = Promise.withResolvers();
717
+ let waiters = internals.sleepResolverReadyWaitersForTesting?.get(workflowId);
718
+ if (waiters === void 0) {
719
+ waiters = new Set;
720
+ internals.sleepResolverReadyWaitersForTesting?.set(workflowId, waiters);
721
+ }
722
+ waiters.add(resolve);
723
+ const expiry = setTimeout(() => {
724
+ const pendingWaiters = internals.sleepResolverReadyWaitersForTesting?.get(workflowId);
725
+ if (pendingWaiters !== void 0) {
726
+ pendingWaiters.delete(resolve);
727
+ if (pendingWaiters.size === 0)
728
+ internals.sleepResolverReadyWaitersForTesting?.delete(workflowId);
729
+ }
730
+ reject(Error(`Timed out after ${SLEEP_RESOLVER_READY_WAIT_TIMEOUT_MS_FOR_TESTING}ms waiting for workflow "${workflowId}" to register a sleep resolver`));
731
+ }, SLEEP_RESOLVER_READY_WAIT_TIMEOUT_MS_FOR_TESTING);
732
+ return await promise.finally(() => clearTimeout(expiry));
733
+ }
734
+ [ENGINE_SET_WORKER_TURN_TIMEOUT_RESOLVER_FOR_TESTING](resolver) {
735
+ const strategy = getInternals(this).strategy;
736
+ if (strategy.setWorkflowTurnTimeoutResolverForTesting === void 0)
737
+ throw Error("Worker turn timeout resolver is only available in Worker execution mode");
738
+ strategy.setWorkflowTurnTimeoutResolverForTesting(resolver);
739
+ }
699
740
  async signal(workflowId, nameOrDefinition, payload, options) {
700
741
  return signalWorkflow(getInternals(this), workflowId, messageName(nameOrDefinition), payload, {
701
742
  loadWorkflowState: (id) => loadWorkflowState(getInternals(this), id),
@@ -868,28 +909,62 @@ export class Engine extends EventTarget {
868
909
  return submitCoordinatedUpdateFromInternals(getInternals(this), workflowId, name, payload, options, this.#createUpdateCallbacks());
869
910
  }
870
911
  async shutdown() {
871
- return this[Symbol.asyncDispose]();
912
+ if (this.#shutdownResult === null)
913
+ this.#shutdownResult = (async () => {
914
+ await this[Symbol.asyncDispose]();
915
+ const baseResult = this.#asyncDisposeResult;
916
+ if (baseResult === null) {
917
+ await getInternals(this).inFlightLeaseAcquire?.catch(() => {});
918
+ return await this.#synchronousDisposeResult ?? !0;
919
+ }
920
+ try {
921
+ return await baseResult;
922
+ } catch (error) {
923
+ return error instanceof EngineDisposalError ? error.leaseReleased : !0;
924
+ }
925
+ })();
926
+ return this.#shutdownResult;
872
927
  }
873
928
  [Symbol.dispose]() {
929
+ if (this.#asyncDisposeResult !== null) {
930
+ this.#asyncDisposeResult;
931
+ return;
932
+ }
874
933
  const leaseManager = getInternals(this).leaseManager;
875
934
  if (leaseManager !== null && leaseManager.currentEpochBytes() !== null)
876
935
  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);
877
936
  disposeEngine(getInternals(this));
878
- leaseManager?.release();
937
+ if (this.#synchronousDisposeResult === null)
938
+ this.#synchronousDisposeResult = leaseManager?.release() ?? Promise.resolve(!0);
939
+ this.#synchronousDisposeResult;
879
940
  }
880
- async[Symbol.asyncDispose]() {
941
+ async#disposeAsyncWithLeaseResult() {
881
942
  if (!getInternals(this).disposed) {
882
943
  const leaseManager = getInternals(this).leaseManager;
944
+ let leaseReleased = !0, drainFailure = null;
883
945
  try {
884
- await drainQueuedInlineWorkflowStartsForEngine(this);
946
+ await drainQueuedInlineWorkflowStartsForEngine(this, {
947
+ abortStartedWorkflows: !0
948
+ });
949
+ } catch (error) {
950
+ drainFailure = { error };
885
951
  } finally {
886
952
  disposeEngine(getInternals(this));
887
953
  await getInternals(this).inFlightLeaseAcquire?.catch(() => {});
888
- await leaseManager?.release();
954
+ leaseReleased = await (this.#synchronousDisposeResult ?? leaseManager?.release() ?? Promise.resolve(!0));
889
955
  }
890
- return;
956
+ if (drainFailure !== null)
957
+ throw new EngineDisposalError(drainFailure.error, leaseReleased);
958
+ return leaseReleased;
891
959
  }
892
960
  this[Symbol.dispose]();
961
+ await getInternals(this).inFlightLeaseAcquire?.catch(() => {});
962
+ return await this.#synchronousDisposeResult ?? !0;
963
+ }
964
+ async[Symbol.asyncDispose]() {
965
+ if (this.#asyncDisposeResult === null)
966
+ this.#asyncDisposeResult = this.#disposeAsyncWithLeaseResult();
967
+ await this.#asyncDisposeResult;
893
968
  }
894
969
  get storage() {
895
970
  return getInternals(this).storage;
@@ -6,7 +6,9 @@ export type InlineLaunchQueueCallbacks = {
6
6
  };
7
7
  /** Queue a new inline workflow start and schedule a flush if one is not already scheduled. */
8
8
  export declare function queueInlineWorkflowExecutionStart(internals: EngineInternals, start: QueuedInlineWorkflowExecutionStart, callbacks: InlineLaunchQueueCallbacks): void;
9
- export declare function flushQueuedInlineWorkflowStarts(internals: EngineInternals, callbacks: InlineLaunchQueueCallbacks): Promise<void>;
9
+ export declare function flushQueuedInlineWorkflowStarts(internals: EngineInternals, callbacks: InlineLaunchQueueCallbacks, options?: {
10
+ abortStartedWorkflows?: boolean;
11
+ }): Promise<void>;
10
12
  /** Used by scheduler-driven direct backfill flushes. Clears the scheduled flag first. */
11
13
  export declare function flushQueuedInlineWorkflowStartsDirectly(internals: EngineInternals, callbacks: InlineLaunchQueueCallbacks): Promise<void>;
12
14
  /**
@@ -22,7 +24,9 @@ export declare function flushQueuedInlineWorkflowStartsDirectly(internals: Engin
22
24
  * is left for the synchronous `disposeQueuedInlineWorkflowStarts` path, which
23
25
  * discards it and settles its `defer: false` awaiter.
24
26
  */
25
- export declare function drainQueuedInlineWorkflowStarts(internals: EngineInternals, callbacks: InlineLaunchQueueCallbacks): Promise<void>;
27
+ export declare function drainQueuedInlineWorkflowStarts(internals: EngineInternals, callbacks: InlineLaunchQueueCallbacks, options?: {
28
+ abortStartedWorkflows?: boolean;
29
+ }): Promise<void>;
26
30
  export declare function dropQueuedInlineWorkflowStart(internals: EngineInternals, workflowId: string): boolean;
27
31
  export declare function disposeQueuedInlineWorkflowStarts(internals: EngineInternals): void;
28
32
  export declare function hasQueuedInlineWorkflowStart(internals: EngineInternals, workflowId: string): boolean;
@@ -1,6 +1,21 @@
1
1
  import { WorkflowStartedEvent } from "../events.js";
2
2
  import { startWorkflowExecution } from "./lifecycle.js";
3
3
  import { loadWorkflowState } from "./storage-io.js";
4
+ async function yieldQueuedShutdownAdvanceOpportunity() {
5
+ await new Promise((resolve) => {
6
+ setTimeout(resolve, 0);
7
+ });
8
+ }
9
+ async function settleQueuedShutdownWork(pendingWork) {
10
+ if (pendingWork.length === 0)
11
+ return [];
12
+ const opportunityElapsed = yieldQueuedShutdownAdvanceOpportunity().then(() => !1);
13
+ return (await Promise.all(pendingWork.map(async ({ workflowId, promise }) => {
14
+ if (promise === void 0)
15
+ return workflowId;
16
+ return await Promise.race([promise.then(() => !0), opportunityElapsed]) ? workflowId : null;
17
+ }))).filter((workflowId) => workflowId !== null);
18
+ }
4
19
  export function queueInlineWorkflowExecutionStart(internals, start, callbacks) {
5
20
  internals.queuedInlineWorkflowStartIds.add(start.workflowId);
6
21
  internals.queuedOrLaunchingInlineWorkflowStartIds.add(start.workflowId);
@@ -24,7 +39,7 @@ function settleDiscardedInlineStarts(internals, discarded) {
24
39
  start.onStarted?.();
25
40
  }
26
41
  }
27
- export async function flushQueuedInlineWorkflowStarts(internals, callbacks) {
42
+ export async function flushQueuedInlineWorkflowStarts(internals, callbacks, options) {
28
43
  if (internals.abortController.signal.aborted) {
29
44
  const discarded = internals.queuedInlineWorkflowStarts;
30
45
  internals.queuedInlineWorkflowStarts = [];
@@ -36,22 +51,39 @@ export async function flushQueuedInlineWorkflowStarts(internals, callbacks) {
36
51
  const pendingStarts = internals.queuedInlineWorkflowStarts;
37
52
  internals.queuedInlineWorkflowStarts = [];
38
53
  for (const start of pendingStarts)
39
- await callbacks.swallowPromiseRejection(startQueuedInlineWorkflowExecution(internals, start, callbacks));
54
+ await callbacks.swallowPromiseRejection(startQueuedInlineWorkflowExecution(internals, start, callbacks, options));
40
55
  }
41
56
  export async function flushQueuedInlineWorkflowStartsDirectly(internals, callbacks) {
42
57
  internals.queuedInlineWorkflowStartFlushScheduled = !1;
43
58
  await flushQueuedInlineWorkflowStarts(internals, callbacks);
44
59
  }
45
- export async function drainQueuedInlineWorkflowStarts(internals, callbacks) {
60
+ export async function drainQueuedInlineWorkflowStarts(internals, callbacks, options) {
46
61
  internals.queuedInlineWorkflowStartFlushScheduled = !1;
47
62
  let passes = 0;
48
63
  const maxPasses = 1000;
49
64
  while (internals.queuedInlineWorkflowStarts.length > 0 && !internals.abortController.signal.aborted && passes < maxPasses) {
50
65
  passes += 1;
51
- await callbacks.swallowPromiseRejection(flushQueuedInlineWorkflowStarts(internals, callbacks));
66
+ const workflowIds = internals.queuedInlineWorkflowStarts.map((start) => start.workflowId);
67
+ await callbacks.swallowPromiseRejection(flushQueuedInlineWorkflowStarts(internals, callbacks, options));
68
+ const pendingAdvances = workflowIds.map((workflowId) => {
69
+ const pendingAdvance = internals.inlineStrategy?.waitForWorkflowAdvance(workflowId);
70
+ return {
71
+ workflowId,
72
+ promise: pendingAdvance === void 0 ? void 0 : callbacks.swallowPromiseRejection(pendingAdvance)
73
+ };
74
+ }), settledWorkflowIds = await settleQueuedShutdownWork(pendingAdvances);
75
+ if (settledWorkflowIds.length > 0) {
76
+ const terminalTurns = settledWorkflowIds.flatMap((workflowId) => {
77
+ if (internals.inlineStrategy?.hasGenerator(workflowId))
78
+ return [];
79
+ const pendingTurn = internals.inlineStrategy?.waitForWorkflowTurn(workflowId);
80
+ return pendingTurn === void 0 ? [] : [callbacks.swallowPromiseRejection(pendingTurn)];
81
+ });
82
+ await Promise.all(terminalTurns);
83
+ }
52
84
  }
53
85
  }
54
- async function startQueuedInlineWorkflowExecution(internals, start, callbacks) {
86
+ async function startQueuedInlineWorkflowExecution(internals, start, callbacks, options) {
55
87
  try {
56
88
  const state = await loadWorkflowState(internals, start.workflowId);
57
89
  if (!state || state.status !== "running")
@@ -59,7 +91,10 @@ async function startQueuedInlineWorkflowExecution(internals, start, callbacks) {
59
91
  internals.queuedInlineWorkflowStartIds.delete(start.workflowId);
60
92
  internals.engine.dispatchEvent(new WorkflowStartedEvent(start.workflowId, start.workflowType, start.input));
61
93
  startWorkflowExecution(internals, start.workflowId, state.workflowExecutionToken, start.workflowType, start.input, start.checkpoint, start.nestingDepth, start.executionDeadline, start.executionStateOwnerId);
62
- await callbacks.processPendingUpdatesAfterInlineAdvance(start.workflowId);
94
+ if (options?.abortStartedWorkflows === !0)
95
+ internals.inlineStrategy?.abortWorkflowAdvanceForShutdown(start.workflowId);
96
+ else
97
+ await callbacks.processPendingUpdatesAfterInlineAdvance(start.workflowId);
63
98
  } finally {
64
99
  internals.queuedInlineWorkflowStartIds.delete(start.workflowId);
65
100
  internals.queuedOrLaunchingInlineWorkflowStartIds.delete(start.workflowId);
@@ -114,6 +114,8 @@ export interface EngineInternals {
114
114
  fireAt: number;
115
115
  }>;
116
116
  sleepResolversByWorkflow: Map<string, Set<string>>;
117
+ /** Test-only event waiters notified when a workflow registers a sleep resolver. */
118
+ sleepResolverReadyWaitersForTesting?: Map<string, Set<() => void>>;
117
119
  /**
118
120
  * Fired sleep timers awaiting proof that the awakened inline workflow reached
119
121
  * its next durable checkpoint or terminal state. External schedulers must not
@@ -95,7 +95,8 @@ export type LeaseManagerOptions = {
95
95
  * - `currentEpochBytes()` returns the held epoch as bytes for Step-2 fencing
96
96
  * conditions; `null` before a successful acquire. Synchronous and stable across
97
97
  * renewals.
98
- * - `release()` best-effort relinquishes the lease (holder key only) on dispose.
98
+ * - `release()` best-effort relinquishes the lease (holder key only) on dispose
99
+ * and resolves to whether the holder delete committed.
99
100
  */
100
101
  export type LeaseManager = {
101
102
  acquire(): Promise<void>;
@@ -104,7 +105,7 @@ export type LeaseManager = {
104
105
  currentEpochBytes(): Uint8Array | null;
105
106
  /** Return a defensive, synchronous view of this process's last-known lease state. */
106
107
  health(): LeaseManagerHealth;
107
- release(): Promise<void>;
108
+ release(): Promise<boolean>;
108
109
  stop(): void;
109
110
  };
110
111
  /**
@@ -149,11 +149,15 @@ export function createLeaseManager(options) {
149
149
  if (inFlightRenewal !== null)
150
150
  await inFlightRenewal;
151
151
  if (lastHolderBytes === null)
152
- return;
152
+ return !0;
153
153
  try {
154
- if (await storageConditionalBatch(storage, [{ key: KEYS.leaseHolder(), expectedValue: lastHolderBytes }], [{ type: "delete", key: KEYS.leaseHolder() }]))
154
+ const deleted = await storageConditionalBatch(storage, [{ key: KEYS.leaseHolder(), expectedValue: lastHolderBytes }], [{ type: "delete", key: KEYS.leaseHolder() }]);
155
+ if (deleted)
155
156
  lastHolderBytes = null;
156
- } catch {}
157
+ return deleted;
158
+ } catch {
159
+ return !1;
160
+ }
157
161
  }
158
162
  function stop() {
159
163
  stopped = !0;
@@ -71,6 +71,12 @@ export function registerSleepResolver(internals, workflowId, operationId, resolv
71
71
  internals.sleepResolversByWorkflow.set(workflowId, workflowOperations);
72
72
  }
73
73
  workflowOperations.add(operationId);
74
+ const readinessWaiters = internals.sleepResolverReadyWaitersForTesting?.get(workflowId);
75
+ if (readinessWaiters !== void 0) {
76
+ internals.sleepResolverReadyWaitersForTesting?.delete(workflowId);
77
+ for (const notifyReady of readinessWaiters)
78
+ notifyReady();
79
+ }
74
80
  }
75
81
  export async function startDelayedWorkflow(internals, entry, callbacks) {
76
82
  const state = await callbacks.loadWorkflowState(entry.workflowId);
@@ -71,6 +71,14 @@
71
71
  */
72
72
  import type { OperationOutcome, WorkerOutboundMessage } from './types.ts';
73
73
  export interface ExecutionStrategy extends Disposable, AsyncDisposable {
74
+ /**
75
+ * Internal test seam for selecting a load-tolerant timeout around one
76
+ * intentionally wedged Worker turn. Production construction never sets it.
77
+ */
78
+ setWorkflowTurnTimeoutResolverForTesting?: (resolver: (turn: {
79
+ workflowId: string;
80
+ kind: 'run' | 'resume';
81
+ }) => number) => void;
74
82
  /**
75
83
  * Start a new workflow execution from the beginning.
76
84
  */
@@ -53,6 +53,7 @@ export declare class InlineExecutionStrategy implements ExecutionStrategy {
53
53
  clearParkedContext(workflowId: string): void;
54
54
  getAbortController(workflowId: string): AbortController | undefined;
55
55
  waitForWorkflowTurn(workflowId: string): Promise<void> | undefined;
56
+ abortWorkflowAdvanceForShutdown(workflowId: string): void;
56
57
  waitForWorkflowAdvance(workflowId: string): Promise<void> | undefined;
57
58
  hasGenerator(workflowId: string): boolean;
58
59
  /**