@lostgradient/weft 0.16.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 (107) hide show
  1. package/README.md +1 -1
  2. package/dist/alerting/alert-manager.d.ts +5 -5
  3. package/dist/alerting/alert-manager.js +24 -6
  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/core/engine/disposal.js +7 -0
  8. package/dist/core/engine/engine-runtime-helpers.d.ts +3 -1
  9. package/dist/core/engine/engine-runtime-helpers.js +2 -2
  10. package/dist/core/engine/errors.d.ts +20 -0
  11. package/dist/core/engine/errors.js +8 -0
  12. package/dist/core/engine/index.d.ts +39 -17
  13. package/dist/core/engine/index.js +81 -9
  14. package/dist/core/engine/inline-launch-queue.d.ts +6 -2
  15. package/dist/core/engine/inline-launch-queue.js +41 -6
  16. package/dist/core/engine/internals.d.ts +2 -0
  17. package/dist/core/engine/lease-manager.d.ts +3 -2
  18. package/dist/core/engine/lease-manager.js +7 -3
  19. package/dist/core/engine/operations-time.js +6 -0
  20. package/dist/core/execution-strategy.d.ts +8 -0
  21. package/dist/core/inline-execution-strategy.d.ts +1 -0
  22. package/dist/core/inline-execution-strategy.js +33 -20
  23. package/dist/core/weft-error.d.ts +1 -1
  24. package/dist/core/weft-error.js +1 -0
  25. package/dist/core/worker-execution-strategy.d.ts +2 -0
  26. package/dist/core/worker-execution-strategy.js +7 -2
  27. package/dist/core/worker-turn-watchdog.d.ts +4 -1
  28. package/dist/core/worker-turn-watchdog.js +9 -4
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.js +1 -0
  31. package/dist/indexeddb.js +1 -1
  32. package/dist/json-schema.js +1 -1
  33. package/dist/mcp/tools.js +1 -3
  34. package/dist/server/dashboard-assets.d.ts +12 -1
  35. package/dist/server/dashboard-assets.js +145 -31
  36. package/dist/server/operations/aggregate-workflows.js +0 -7
  37. package/dist/server/operations/async-activity.js +5 -24
  38. package/dist/server/operations/bulk-cancel-workflows.js +3 -8
  39. package/dist/server/operations/bulk-delete-workflows.js +3 -8
  40. package/dist/server/operations/bulk-mutate-workflow-tags.js +3 -8
  41. package/dist/server/operations/bulk-operation-helpers.d.ts +2 -10
  42. package/dist/server/operations/bulk-operation-helpers.js +0 -6
  43. package/dist/server/operations/bulk-retry-failed-workflows.js +3 -8
  44. package/dist/server/operations/bulk-signal-workflows.js +3 -8
  45. package/dist/server/operations/cancel-schedule.js +1 -3
  46. package/dist/server/operations/cancel-workflow.js +1 -3
  47. package/dist/server/operations/create-schedule.js +2 -3
  48. package/dist/server/operations/fork-workflow.js +2 -3
  49. package/dist/server/operations/get-checkpoint-at.js +1 -6
  50. package/dist/server/operations/get-registry.js +1 -7
  51. package/dist/server/operations/get-retention-overview.js +2 -15
  52. package/dist/server/operations/get-review.js +2 -15
  53. package/dist/server/operations/get-schedule.js +2 -8
  54. package/dist/server/operations/get-stream-chunks.js +2 -6
  55. package/dist/server/operations/get-system-lease.js +1 -7
  56. package/dist/server/operations/get-system-metrics.js +0 -4
  57. package/dist/server/operations/get-task-diagnostics.js +2 -13
  58. package/dist/server/operations/get-update-result.js +1 -6
  59. package/dist/server/operations/get-workflow-attributes.js +2 -15
  60. package/dist/server/operations/get-workflow-events.js +2 -15
  61. package/dist/server/operations/get-workflow-observability.js +2 -5
  62. package/dist/server/operations/get-workflow-result.js +0 -7
  63. package/dist/server/operations/get-workflow-timeline.js +1 -6
  64. package/dist/server/operations/get-workflow.d.ts +3 -6
  65. package/dist/server/operations/get-workflow.js +2 -12
  66. package/dist/server/operations/list-alerts.d.ts +5 -2
  67. package/dist/server/operations/list-alerts.js +2 -9
  68. package/dist/server/operations/list-checkpoints.js +1 -6
  69. package/dist/server/operations/list-reviews.js +0 -7
  70. package/dist/server/operations/list-schedules.js +3 -8
  71. package/dist/server/operations/list-task-queues.js +0 -4
  72. package/dist/server/operations/list-workers.js +0 -4
  73. package/dist/server/operations/list-workflows.js +0 -7
  74. package/dist/server/operations/pause-schedule.js +1 -3
  75. package/dist/server/operations/purge-workflows.js +2 -5
  76. package/dist/server/operations/query-workflow.js +4 -15
  77. package/dist/server/operations/replay-workflow.js +2 -6
  78. package/dist/server/operations/resume-schedule.js +1 -3
  79. package/dist/server/operations/resume-workflow.js +1 -3
  80. package/dist/server/operations/set-workflow-attributes.js +3 -15
  81. package/dist/server/operations/signal-workflow.js +3 -12
  82. package/dist/server/operations/single-workflow-tag-mutation.js +0 -7
  83. package/dist/server/operations/start-or-signal-workflow.js +2 -3
  84. package/dist/server/operations/start-workflow.js +2 -3
  85. package/dist/server/operations/storage-capabilities.js +1 -3
  86. package/dist/server/operations/storage.js +7 -13
  87. package/dist/server/operations/submit-review-decision.js +3 -12
  88. package/dist/server/operations/suspend-workflow.js +1 -3
  89. package/dist/server/operations/timeout-workflow.js +1 -3
  90. package/dist/server/operations/update-schedule.js +1 -3
  91. package/dist/server/operations/update-workflow.js +2 -3
  92. package/dist/server/operations/worker-drain.js +0 -8
  93. package/dist/server/rest-binding.d.ts +3 -2
  94. package/dist/server/route-model.d.ts +2 -0
  95. package/dist/server/route-model.js +1 -1
  96. package/dist/server/runtime/task-reconciliation.d.ts +10 -1
  97. package/dist/server/runtime/task-reconciliation.js +26 -2
  98. package/dist/server/serve-internals.js +12 -6
  99. package/dist/storage/indexeddb.js +28 -48
  100. package/dist/storage/testing.d.ts +6 -3
  101. package/dist/storage/testing.js +1 -1
  102. package/dist/version.d.ts +1 -1
  103. package/dist/version.js +1 -1
  104. package/dist/worker/protocol-schemas.d.ts +2 -2
  105. package/dist/worker/protocol-schemas.js +2 -2
  106. package/dist/worker/protocol.js +1 -1
  107. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Weft
2
2
 
3
- A Bun-native durable execution engine. Current release: `0.16.0`.
3
+ A Bun-native durable execution engine. Current release: `0.17.0`.
4
4
 
5
5
  Install the library from npm as `@lostgradient/weft`:
6
6
 
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @module alerting/alert-manager
7
7
  */
8
- import type { AlertState, AlertingOptions } from './types';
8
+ import type { AlertStateSnapshot, AlertingOptions } from './types';
9
9
  /**
10
10
  * Event-driven alert manager that evaluates metric-based rules against sliding
11
11
  * time windows and fires `alert:fired` / `alert:resolved` lifecycle events.
@@ -38,9 +38,9 @@ export declare class AlertManager implements Disposable {
38
38
  constructor(target: EventTarget, options: AlertingOptions, getNow?: () => number, startBackgroundTick?: boolean);
39
39
  /** Re-evaluate every rule once, for hosts that drive maintenance explicitly. */
40
40
  tick(): void;
41
- /** Get current state of all alert rules (for debugging/testing). */
42
- get states(): readonly AlertState[];
43
- /** Get the alert rules that are currently firing. */
44
- get activeStates(): readonly AlertState[];
41
+ /** Get a detached snapshot of all alert rules (for debugging/testing). */
42
+ get states(): readonly AlertStateSnapshot[];
43
+ /** Get detached snapshots of the alert rules that are currently firing. */
44
+ get activeStates(): readonly AlertStateSnapshot[];
45
45
  [Symbol.dispose](): void;
46
46
  }
@@ -7,6 +7,15 @@ import {
7
7
  import { parseDuration } from "../core/scheduler.js";
8
8
  import { CounterWindow, HistogramWindow } from "./sliding-window.js";
9
9
  const TICK_INTERVAL_MS = 1e4;
10
+ function snapshotAlertState(state) {
11
+ return {
12
+ rule: { ...state.rule },
13
+ status: state.status,
14
+ currentValue: state.currentValue,
15
+ ...state.lastFiredAt === void 0 ? {} : { lastFiredAt: state.lastFiredAt },
16
+ ...state.lastResolvedAt === void 0 ? {} : { lastResolvedAt: state.lastResolvedAt }
17
+ };
18
+ }
10
19
 
11
20
  export class AlertManager {
12
21
  #target;
@@ -20,21 +29,30 @@ export class AlertManager {
20
29
  #tickInterval;
21
30
  constructor(target, options, getNow = Date.now, startBackgroundTick = !0) {
22
31
  this.#target = target;
23
- this.#options = options;
32
+ this.#options = {
33
+ ...options,
34
+ rules: options.rules.map((rule) => ({ ...rule })),
35
+ ...options.webhooks === void 0 ? {} : {
36
+ webhooks: options.webhooks.map((webhook) => ({
37
+ ...webhook,
38
+ events: [...webhook.events]
39
+ }))
40
+ }
41
+ };
24
42
  this.#getNow = getNow;
25
43
  this.#latestStorageSize = 0;
26
44
  this.#pendingWebhooks = new Set;
27
45
  this.#listeners = [];
28
46
  this.#states = [];
29
- for (const rule of options.rules)
47
+ for (const rule of this.#options.rules)
30
48
  this.#states.push({
31
49
  rule,
32
50
  status: "idle",
33
51
  currentValue: 0
34
52
  });
35
53
  this.#windows = new Map;
36
- for (let i = 0;i < options.rules.length; i++) {
37
- const rule = options.rules[i], windowMs = rule.window ? parseDuration(rule.window) : 60000;
54
+ for (let i = 0;i < this.#options.rules.length; i++) {
55
+ const rule = this.#options.rules[i], windowMs = rule.window ? parseDuration(rule.window) : 60000;
38
56
  if (rule.metric === "workflow.failure_rate")
39
57
  this.#windows.set(i, new CounterWindow(windowMs));
40
58
  else if (rule.metric === "activity.p99_duration")
@@ -153,10 +171,10 @@ export class AlertManager {
153
171
  }
154
172
  }
155
173
  get states() {
156
- return this.#states;
174
+ return this.#states.map(snapshotAlertState);
157
175
  }
158
176
  get activeStates() {
159
- return this.#states.filter((state) => state.status === "firing");
177
+ return this.#states.filter((state) => state.status === "firing").map(snapshotAlertState);
160
178
  }
161
179
  [Symbol.dispose]() {
162
180
  if (this.#tickInterval !== null) {
@@ -1,2 +1,2 @@
1
1
  export { AlertManager } from './alert-manager';
2
- export type { AlertAction, AlertMetric, AlertRule, AlertState, AlertStatus, AlertingOptions, WebhookTarget, } from './types';
2
+ export type { AlertAction, AlertMetric, AlertRule, AlertStateSnapshot, AlertStatus, AlertingOptions, WebhookTarget, } from './types';
@@ -6,6 +6,8 @@
6
6
  * {@link WebhookTarget} URLs.
7
7
  */
8
8
  export type AlertAction = 'log' | 'webhook';
9
+ /** Runtime vocabulary shared by alerting types and operation schemas. */
10
+ export declare const ALERT_METRICS: readonly ["workflow.failure_rate", "activity.p99_duration", "storage.size"];
9
11
  /**
10
12
  * Which built-in metric an {@link AlertRule} monitors.
11
13
  *
@@ -30,7 +32,7 @@ export type AlertAction = 'log' | 'webhook';
30
32
  * void rule;
31
33
  * ```
32
34
  */
33
- export type AlertMetric = 'workflow.failure_rate' | 'activity.p99_duration' | 'storage.size';
35
+ export type AlertMetric = (typeof ALERT_METRICS)[number];
34
36
  /**
35
37
  * Defines a single alerting threshold applied to a named metric.
36
38
  *
@@ -124,7 +126,8 @@ export type AlertingOptions = {
124
126
  *
125
127
  * Transitions from `'idle'` to `'firing'` when the metric exceeds the
126
128
  * threshold and back to `'idle'` once the metric drops below it again.
127
- * Read this off an {@link AlertState} object to see the current state of a rule.
129
+ * Read this off an {@link AlertStateSnapshot} object to see the current state
130
+ * of a rule.
128
131
  *
129
132
  * @example
130
133
  * ```ts
@@ -141,17 +144,18 @@ export type AlertingOptions = {
141
144
  */
142
145
  export type AlertStatus = 'idle' | 'firing';
143
146
  /**
144
- * Runtime tracking state for a single {@link AlertRule} managed by
145
- * {@link AlertManager}.
147
+ * Detached, deeply read-only observation of a single alert rule's runtime
148
+ * state. The manager returns a fresh snapshot for every getter call, so
149
+ * changing it cannot affect alert evaluation.
146
150
  *
147
- * Users observe this shape via `AlertManager.states` to see the current
148
- * metric value, firing status, and timestamps of the last transition. It is
149
- * created and owned by the manager — callers do not construct it directly.
151
+ * Users observe this shape via `AlertManager.states` or
152
+ * `Engine.getActiveAlerts()` to see the current metric value, firing status,
153
+ * and timestamps of the last transition.
150
154
  */
151
- export type AlertState = {
152
- rule: AlertRule;
153
- status: AlertStatus;
154
- currentValue: number;
155
- lastFiredAt?: number;
156
- lastResolvedAt?: number;
155
+ export type AlertStateSnapshot = {
156
+ readonly rule: Readonly<AlertRule>;
157
+ readonly status: AlertStatus;
158
+ readonly currentValue: number;
159
+ readonly lastFiredAt?: number;
160
+ readonly lastResolvedAt?: number;
157
161
  };
@@ -0,0 +1,5 @@
1
+ export const ALERT_METRICS = [
2
+ "workflow.failure_rate",
3
+ "activity.p99_duration",
4
+ "storage.size"
5
+ ];
@@ -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,4 +1,4 @@
1
- import type { AlertState } from '../../alerting/types.ts';
1
+ import type { AlertStateSnapshot } from '../../alerting/types.ts';
2
2
  import { type Storage as WeftStorage } from '../../storage/interface.ts';
3
3
  import { type ActivityMetadata, type ActivityRegistrationOptions, type RegisteredActivityFunction } from '../activity-registry.ts';
4
4
  import type { StoredStreamChunk } from '../context.ts';
@@ -25,7 +25,7 @@ export { ActivityReconciliationCapabilityError, ActivityReconciliationConflictEr
25
25
  export { AsyncActivityTokenNotFoundError } from './async-activity-completion.ts';
26
26
  export type { PendingAsyncActivity } from './async-activity-records.ts';
27
27
  export type { PendingTimelineEntry, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter, } from './engine-internal-types.ts';
28
- 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';
29
29
  export { HANDLE_RESULT_PROMISE, WorkflowHandle } from './handles.ts';
30
30
  export { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError, EngineLeaseNotHeldError, } from './lease-errors.ts';
31
31
  export type { EngineLeaseHealth, LeaseLostReason } from './lease-health.ts';
@@ -39,6 +39,30 @@ export { assertCompatiblePersistedDataVersion };
39
39
  export declare const ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING: unique symbol;
40
40
  export declare const ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING: unique symbol;
41
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;
42
66
  /**
43
67
  * The `name` of the `process` warning emitted when a lease-owning engine is
44
68
  * disposed through the synchronous `[Symbol.dispose]()` path. Sync disposal can
@@ -146,8 +170,8 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
146
170
  removeEventListener<K extends Extract<keyof WeftEventMap, string>>(type: K, listener: (event: WeftEventMap[K]) => void, options?: boolean | EventListenerOptions): void;
147
171
  removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
148
172
  get state(): EngineStateNamespace;
149
- /** Return the alert rules that are currently firing. */
150
- getActiveAlerts(): readonly AlertState[];
173
+ /** Return detached snapshots of the alert rules that are currently firing. */
174
+ getActiveAlerts(): readonly AlertStateSnapshot[];
151
175
  /**
152
176
  * Register a workflow by name or definition, or register an activity
153
177
  * definition. Definition overloads return this same engine with the
@@ -314,6 +338,11 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
314
338
  [ENGINE_PARKED_WORKFLOW_COUNT_FOR_TESTING](): number;
315
339
  [ENGINE_SIGNAL_WAITER_COUNT_FOR_TESTING](): number;
316
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;
317
346
  signal(workflowId: string, name: SignalDefinition): Promise<void>;
318
347
  signal<TInput>(workflowId: string, name: SignalDefinition<TInput>, payload: TInput, options?: SignalDeliveryOptions): Promise<void>;
319
348
  signal(workflowId: string, name: string, payload?: unknown, options?: SignalDeliveryOptions): Promise<void>;
@@ -480,11 +509,13 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
480
509
  *
481
510
  * Under `ownership: 'lease'`, this is the explicit prompt-handoff primitive:
482
511
  * it drains queued inline starts, tears down in-memory write paths, and awaits
483
- * lease release before resolving. Synchronous disposal remains immediate and
484
- * can make the next engine wait for `leaseTtl` if the process exits before its
485
- * 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.
486
517
  */
487
- shutdown(): Promise<void>;
518
+ shutdown(): Promise<boolean>;
488
519
  /**
489
520
  * Synchronous teardown (`using engine = ...`). Pending inline launches that
490
521
  * have not yet run are **discarded**, not executed. When you need queued
@@ -493,15 +524,6 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
493
524
  * via `await using`.
494
525
  */
495
526
  [Symbol.dispose](): void;
496
- /**
497
- * Async teardown (`await using engine = ...`). Drains pending inline launches
498
- * so each queued workflow completes its first turn before disposal, leaving no
499
- * deferred-launch macrotask to fire against torn-down state. The drain is
500
- * bounded (a pass cap and the abort signal); in the pathological case where it
501
- * cannot converge, anything still queued is discarded by the synchronous
502
- * teardown that always follows. Prefer this over the synchronous
503
- * {@link Engine[Symbol.dispose]} in async contexts and tests.
504
- */
505
527
  [Symbol.asyncDispose](): Promise<void>;
506
528
  get storage(): WeftStorage;
507
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);
@@ -699,6 +709,34 @@ export class Engine extends EventTarget {
699
709
  [ENGINE_SLEEP_RESOLVER_COUNT_FOR_TESTING]() {
700
710
  return getInternals(this).sleepResolvers.size;
701
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
+ }
702
740
  async signal(workflowId, nameOrDefinition, payload, options) {
703
741
  return signalWorkflow(getInternals(this), workflowId, messageName(nameOrDefinition), payload, {
704
742
  loadWorkflowState: (id) => loadWorkflowState(getInternals(this), id),
@@ -871,28 +909,62 @@ export class Engine extends EventTarget {
871
909
  return submitCoordinatedUpdateFromInternals(getInternals(this), workflowId, name, payload, options, this.#createUpdateCallbacks());
872
910
  }
873
911
  async shutdown() {
874
- 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;
875
927
  }
876
928
  [Symbol.dispose]() {
929
+ if (this.#asyncDisposeResult !== null) {
930
+ this.#asyncDisposeResult;
931
+ return;
932
+ }
877
933
  const leaseManager = getInternals(this).leaseManager;
878
934
  if (leaseManager !== null && leaseManager.currentEpochBytes() !== null)
879
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);
880
936
  disposeEngine(getInternals(this));
881
- leaseManager?.release();
937
+ if (this.#synchronousDisposeResult === null)
938
+ this.#synchronousDisposeResult = leaseManager?.release() ?? Promise.resolve(!0);
939
+ this.#synchronousDisposeResult;
882
940
  }
883
- async[Symbol.asyncDispose]() {
941
+ async#disposeAsyncWithLeaseResult() {
884
942
  if (!getInternals(this).disposed) {
885
943
  const leaseManager = getInternals(this).leaseManager;
944
+ let leaseReleased = !0, drainFailure = null;
886
945
  try {
887
- await drainQueuedInlineWorkflowStartsForEngine(this);
946
+ await drainQueuedInlineWorkflowStartsForEngine(this, {
947
+ abortStartedWorkflows: !0
948
+ });
949
+ } catch (error) {
950
+ drainFailure = { error };
888
951
  } finally {
889
952
  disposeEngine(getInternals(this));
890
953
  await getInternals(this).inFlightLeaseAcquire?.catch(() => {});
891
- await leaseManager?.release();
954
+ leaseReleased = await (this.#synchronousDisposeResult ?? leaseManager?.release() ?? Promise.resolve(!0));
892
955
  }
893
- return;
956
+ if (drainFailure !== null)
957
+ throw new EngineDisposalError(drainFailure.error, leaseReleased);
958
+ return leaseReleased;
894
959
  }
895
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;
896
968
  }
897
969
  get storage() {
898
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