@lostgradient/weft 0.9.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 (106) hide show
  1. package/README.md +12 -4
  2. package/dist/alerting/alert-manager.d.ts +3 -1
  3. package/dist/alerting/alert-manager.js +5 -2
  4. package/dist/cli-main.js +63 -63
  5. package/dist/core/checkpoint/serialization.js +2 -2
  6. package/dist/core/context/index.d.ts +2 -1
  7. package/dist/core/context/index.js +8 -0
  8. package/dist/core/context/operation-request.d.ts +2 -0
  9. package/dist/core/context/parallel-cache-entry.d.ts +1 -1
  10. package/dist/core/context/parallel-cache-entry.js +12 -8
  11. package/dist/core/context/parallel-operations.d.ts +1 -1
  12. package/dist/core/context/parallel-operations.js +50 -8
  13. package/dist/core/engine/async-activity-completion.d.ts +24 -54
  14. package/dist/core/engine/async-activity-completion.js +25 -90
  15. package/dist/core/engine/async-activity-records.d.ts +106 -0
  16. package/dist/core/engine/async-activity-records.js +124 -0
  17. package/dist/core/engine/bulk-operations-purge.js +2 -1
  18. package/dist/core/engine/callback-creators-core.js +1 -0
  19. package/dist/core/engine/construction.d.ts +1 -3
  20. package/dist/core/engine/construction.js +2 -5
  21. package/dist/core/engine/deferred-consume-envelope.d.ts +10 -1
  22. package/dist/core/engine/deferred-consume-envelope.js +9 -1
  23. package/dist/core/engine/disposal.js +4 -0
  24. package/dist/core/engine/engine-internal-types.d.ts +1 -0
  25. package/dist/core/engine/engine-leak-warnings.d.ts +2 -0
  26. package/dist/core/engine/engine-leak-warnings.js +7 -1
  27. package/dist/core/engine/engine-runtime-helpers.d.ts +8 -0
  28. package/dist/core/engine/engine-runtime-helpers.js +9 -0
  29. package/dist/core/engine/errors.js +1 -1
  30. package/dist/core/engine/index.d.ts +18 -5
  31. package/dist/core/engine/index.js +41 -10
  32. package/dist/core/engine/inline-parking.js +14 -1
  33. package/dist/core/engine/internals.d.ts +23 -2
  34. package/dist/core/engine/lifecycle/recovered-services.d.ts +4 -2
  35. package/dist/core/engine/lifecycle/recovered-services.js +37 -7
  36. package/dist/core/engine/lifecycle/resume.d.ts +2 -2
  37. package/dist/core/engine/lifecycle/resume.js +39 -7
  38. package/dist/core/engine/lifecycle/shared.d.ts +34 -0
  39. package/dist/core/engine/lifecycle/transition.d.ts +1 -1
  40. package/dist/core/engine/lifecycle/transition.js +3 -3
  41. package/dist/core/engine/lifecycle.d.ts +1 -1
  42. package/dist/core/engine/memo-durable-activity.js +7 -9
  43. package/dist/core/engine/operations-activity.js +1 -1
  44. package/dist/core/engine/operations-coordination.d.ts +15 -1
  45. package/dist/core/engine/operations-coordination.js +43 -10
  46. package/dist/core/engine/operations-time.js +8 -28
  47. package/dist/core/engine/ownership-options.d.ts +1 -0
  48. package/dist/core/engine/ownership-options.js +14 -0
  49. package/dist/core/engine/retention.js +4 -0
  50. package/dist/core/engine/schedule-run-metadata.d.ts +3 -0
  51. package/dist/core/engine/schedule-run-metadata.js +29 -0
  52. package/dist/core/engine/schedule-run.js +19 -4
  53. package/dist/core/engine/schedules.js +4 -3
  54. package/dist/core/engine/sleep-timer-acknowledgements.d.ts +15 -0
  55. package/dist/core/engine/sleep-timer-acknowledgements.js +144 -0
  56. package/dist/core/engine/sub-operation.js +6 -13
  57. package/dist/core/engine/termination/cleanup.js +6 -1
  58. package/dist/core/types/options.d.ts +12 -0
  59. package/dist/core/types/services-resolution.d.ts +8 -3
  60. package/dist/core/types/workflow-builder.d.ts +2 -2
  61. package/dist/core/types/workflow-context.d.ts +27 -0
  62. package/dist/index.d.ts +2 -2
  63. package/dist/mcp/cli.js +17 -17
  64. package/dist/server/authorization.d.ts +3 -3
  65. package/dist/server/fault-to-json-rpc.d.ts +2 -1
  66. package/dist/server/handler.js +22 -22
  67. package/dist/server/index.js +17 -17
  68. package/dist/server/json-rpc-dispatch.d.ts +2 -2
  69. package/dist/server/json-rpc-parse.d.ts +1 -1
  70. package/dist/server/json-rpc-protocol.d.ts +2 -2
  71. package/dist/server/json-rpc-websocket.d.ts +1 -1
  72. package/dist/server/operation-fault.d.ts +3 -1
  73. package/dist/server/operations/async-activity.js +2 -2
  74. package/dist/server/principal.d.ts +2 -2
  75. package/dist/server/stdio-session.d.ts +4 -4
  76. package/dist/service-worker/index.js +13 -13
  77. package/dist/service-worker/scheduler.js +1 -0
  78. package/dist/storage/compressed-storage.js +1 -1
  79. package/dist/storage/http.js +2 -2
  80. package/dist/storage/index.d.ts +1 -0
  81. package/dist/storage/indexeddb.js +1 -1
  82. package/dist/storage/interface.d.ts +1 -0
  83. package/dist/storage/interface.js +1 -1
  84. package/dist/storage/lazy-postgres-pool.d.ts +38 -0
  85. package/dist/storage/lazy-postgres-pool.js +38 -0
  86. package/dist/storage/lmdb.js +1 -1
  87. package/dist/storage/memory.js +1 -1
  88. package/dist/storage/neon-batch.d.ts +8 -8
  89. package/dist/storage/neon.d.ts +26 -79
  90. package/dist/storage/neon.js +3 -3
  91. package/dist/storage/postgres-key-value-queries.d.ts +1 -1
  92. package/dist/storage/postgres-key-value-queries.js +1 -1
  93. package/dist/storage/postgres-key-value-storage.d.ts +110 -0
  94. package/dist/storage/postgres-key-value-storage.js +204 -0
  95. package/dist/storage/postgres.d.ts +59 -0
  96. package/dist/storage/postgres.js +15 -0
  97. package/dist/storage/resolve.js +1 -1
  98. package/dist/storage/scoped-storage.js +1 -1
  99. package/dist/storage/testing.js +1 -1
  100. package/dist/storage/turso.js +1 -1
  101. package/dist/storage/typed-storage.js +1 -1
  102. package/dist/storage/web-extension.js +1 -1
  103. package/dist/testing/index.js +17 -17
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +17 -4
@@ -0,0 +1,124 @@
1
+ import { KEYS, encodeStorageKeyComponent } from "../../storage/interface.js";
2
+ import { decode, encode } from "../codec.js";
3
+ import { ActivityAsyncPendingEvent } from "../events.js";
4
+ import { commitFencedEngineWrite } from "./fenced-write.js";
5
+ const ASYNC_ACTIVITY_TOKEN_PREFIX = "async-act:v1";
6
+ export const ASYNC_ACTIVITY_KEY_PREFIX = "async-act:v1:";
7
+ export function asyncActivityWorkflowPrefix(workflowId) {
8
+ return `${ASYNC_ACTIVITY_KEY_PREFIX}${encodeStorageKeyComponent(workflowId)}:`;
9
+ }
10
+ function isPersistedAsyncActivity(value) {
11
+ if (typeof value !== "object" || value === null)
12
+ return !1;
13
+ const record = value;
14
+ return record.version === 1 && typeof record.token === "string" && typeof record.workflowId === "string" && typeof record.activityName === "string" && typeof record.operationId === "string" && typeof record.step === "number" && typeof record.attempt === "number" && typeof record.createdAt === "number";
15
+ }
16
+ function isPersistedOperationOutcome(value) {
17
+ if (typeof value !== "object" || value === null)
18
+ return !1;
19
+ const record = value;
20
+ if (record.status === "completed")
21
+ return "value" in record;
22
+ if (record.status === "failed")
23
+ return typeof record.error === "string";
24
+ return !1;
25
+ }
26
+ function isPersistedAsyncActivityResolution(value) {
27
+ if (typeof value !== "object" || value === null)
28
+ return !1;
29
+ const record = value;
30
+ return record.version === 1 && record.kind === "resolution" && typeof record.token === "string" && typeof record.workflowId === "string" && isPersistedOperationOutcome(record.outcome);
31
+ }
32
+ export function deriveAsyncActivityToken(workflowId, step, attempt) {
33
+ return `${ASYNC_ACTIVITY_TOKEN_PREFIX}:${workflowId}:${step}:${attempt}`;
34
+ }
35
+ function buildPersistPendingAsyncActivityOperation(pending) {
36
+ const record = {
37
+ version: 1,
38
+ token: pending.token,
39
+ workflowId: pending.workflowId,
40
+ activityName: pending.activityName,
41
+ operationId: pending.operationId,
42
+ step: pending.step,
43
+ attempt: pending.attempt,
44
+ createdAt: pending.createdAt
45
+ };
46
+ return {
47
+ type: "put",
48
+ key: KEYS.asyncActivity(pending.workflowId, pending.token),
49
+ value: encode(record)
50
+ };
51
+ }
52
+ export function buildAsyncActivityAcknowledgementOperations(pending, outcome) {
53
+ const record = {
54
+ version: 1,
55
+ kind: "resolution",
56
+ token: pending.token,
57
+ workflowId: pending.workflowId,
58
+ outcome
59
+ };
60
+ return [
61
+ { type: "delete", key: KEYS.asyncActivity(pending.workflowId, pending.token) },
62
+ {
63
+ type: "put",
64
+ key: KEYS.asyncActivityResolution(pending.workflowId, pending.token),
65
+ value: encode(record)
66
+ }
67
+ ];
68
+ }
69
+ export async function registerPendingAsyncActivity(internals, pending) {
70
+ const alreadyRegistered = internals.pendingAsyncActivities.has(pending.token);
71
+ internals.pendingAsyncActivities.set(pending.token, pending);
72
+ await commitFencedEngineWrite(internals, [buildPersistPendingAsyncActivityOperation(pending)], [], () => Error(`Async activity registration for token "${pending.token}" lost its precondition.`));
73
+ if (!alreadyRegistered)
74
+ internals.engine.dispatchEvent(new ActivityAsyncPendingEvent(pending.token, pending.operationId, pending.workflowId, pending.activityName, pending.attempt));
75
+ }
76
+ export async function recoverPendingAsyncActivities(internals) {
77
+ for await (const [, bytes] of internals.storage.scan(ASYNC_ACTIVITY_KEY_PREFIX)) {
78
+ const decoded = decode(bytes);
79
+ if (isPersistedAsyncActivity(decoded)) {
80
+ internals.pendingAsyncActivities.set(decoded.token, {
81
+ token: decoded.token,
82
+ workflowId: decoded.workflowId,
83
+ activityName: decoded.activityName,
84
+ operationId: decoded.operationId,
85
+ step: decoded.step,
86
+ attempt: decoded.attempt,
87
+ createdAt: decoded.createdAt
88
+ });
89
+ continue;
90
+ }
91
+ if (isPersistedAsyncActivityResolution(decoded))
92
+ queuePendingAsyncActivityResolution(internals, decoded.workflowId, {
93
+ token: decoded.token,
94
+ outcome: decoded.outcome,
95
+ timelineStatus: decoded.outcome.status,
96
+ timelineOutput: decoded.outcome.status === "completed" ? decoded.outcome.value : decoded.outcome.error
97
+ });
98
+ }
99
+ }
100
+ export function shouldBufferPendingAsyncActivityResolution(internals, workflowId) {
101
+ return internals.inlineStrategy !== null && !internals.inlineStrategy.hasGenerator(workflowId);
102
+ }
103
+ export function queuePendingAsyncActivityResolution(internals, workflowId, resolution) {
104
+ internals.pendingAsyncActivityResolutions ??= new Map;
105
+ const queued = internals.pendingAsyncActivityResolutions.get(workflowId) ?? [];
106
+ queued.push(resolution);
107
+ internals.pendingAsyncActivityResolutions.set(workflowId, queued);
108
+ }
109
+ export function takePendingAsyncActivityResolution(internals, workflowId, token) {
110
+ internals.pendingAsyncActivityResolutions ??= new Map;
111
+ const queued = internals.pendingAsyncActivityResolutions.get(workflowId);
112
+ if (queued === void 0)
113
+ return;
114
+ const index = queued.findIndex((resolution) => resolution.token === token);
115
+ if (index === -1)
116
+ return;
117
+ const resolution = queued[index];
118
+ if (resolution === void 0)
119
+ return;
120
+ queued.splice(index, 1);
121
+ if (queued.length === 0)
122
+ internals.pendingAsyncActivityResolutions.delete(workflowId);
123
+ return resolution;
124
+ }
@@ -8,7 +8,7 @@ import {
8
8
  import { decode } from "../codec.js";
9
9
  import { buildIndexOperations } from "../search-attributes.js";
10
10
  import { buildWorkflowTagIndexOperations, normalizeWorkflowTags } from "../workflow-tags.js";
11
- import { asyncActivityWorkflowPrefix } from "./async-activity-completion.js";
11
+ import { asyncActivityWorkflowPrefix } from "./async-activity-records.js";
12
12
  import { forgetCommittedCheckpointBytes } from "./checkpoint-commit-snapshots.js";
13
13
  import { commitFencedEngineWrite } from "./fenced-write.js";
14
14
  import { streamWorkflowStates } from "./listing.js";
@@ -192,6 +192,7 @@ function buildBaseWorkflowDeleteKeys(state) {
192
192
  KEYS.workflowHeaders(state.id),
193
193
  KEYS.terminalCleanupNeeded(state.id),
194
194
  KEYS.workflowConcurrencyHolder(state.id),
195
+ KEYS.scheduleRun(state.id),
195
196
  KEYS.workflowHasServices(state.id),
196
197
  KEYS.finalizerState(state.id),
197
198
  KEYS.teardownOwed(state.id),
@@ -76,6 +76,7 @@ export function createLifecycleCallbacks(engine) {
76
76
  swallowPromiseRejection: (promise) => swallowPromiseRejection(promise),
77
77
  enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON),
78
78
  failWorkflowForUnavailableServices: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
79
+ failWorkflowForRecoveryHook: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system"),
79
80
  failWorkflowForCheckpointDecodeError: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
80
81
  };
81
82
  }
@@ -1,8 +1,7 @@
1
- import { AlertManager } from '../../alerting/alert-manager.ts';
2
1
  import type { Storage as WeftStorage } from '../../storage/interface.ts';
3
2
  import { ActivityWorkerDispatcher } from '../../workers/activity-worker-dispatcher.ts';
4
3
  import type { ComposedWorkflowInterceptor, Interceptor } from '../interceptor.ts';
5
- import { type AnyActivityDefinition, type AnyWorkflowDefinition, type EngineOptions, type RegisteredWorkflowDefinition } from '../types.ts';
4
+ import { type AnyActivityDefinition, type AnyWorkflowDefinition, type RegisteredWorkflowDefinition } from '../types.ts';
6
5
  import type { WorkflowLogRecord } from '../types/workflow-log.ts';
7
6
  import type { EngineConstructorOptions, ExecutionStrategyBundle, RegistrationEntry, ResolvedOptions } from './engine-internal-types.ts';
8
7
  export type KnownWorkflowNames<TWorkflows extends object> = Extract<keyof TWorkflows, string>;
@@ -60,5 +59,4 @@ export declare function createExecutionStrategyBundle(parameters: {
60
59
  getLogSink?: () => ((record: WorkflowLogRecord) => void) | undefined;
61
60
  }): ExecutionStrategyBundle;
62
61
  export declare function createActivityWorkerDispatcher(activityExecution: EngineConstructorOptions['activityExecution']): ActivityWorkerDispatcher | null;
63
- export declare function createAlertManagerForEngine(engine: EventTarget, alerts: EngineOptions['alerts'] | undefined, getNow: () => number): AlertManager | null;
64
62
  export {};
@@ -1,4 +1,3 @@
1
- import { AlertManager } from "../../alerting/alert-manager.js";
2
1
  import { CompressedStorage } from "../../storage/compressed-storage.js";
3
2
  import { MemoryStorage } from "../../storage/memory.js";
4
3
  import { ActivityWorkerDispatcher } from "../../workers/activity-worker-dispatcher.js";
@@ -16,7 +15,7 @@ import {
16
15
  DEFAULT_WORKER_TURN_TIMEOUT_MS,
17
16
  MIN_WORKER_PROTOCOL_MESSAGE_BYTES
18
17
  } from "../worker-protocol.js";
19
- import { resolveOwnershipFields } from "./ownership-options.js";
18
+ import { resolveBackgroundTaskMode, resolveOwnershipFields } from "./ownership-options.js";
20
19
  import {
21
20
  normalizeHistoryPolicy,
22
21
  normalizePayloadSizePolicy,
@@ -122,6 +121,7 @@ export function resolveEngineOptions(storage, options, getNow) {
122
121
  getNow,
123
122
  resolveWorkflowServices: options?.resolveWorkflowServices ?? null,
124
123
  onLog: options?.onLog ?? null,
124
+ backgroundTaskMode: resolveBackgroundTaskMode(options),
125
125
  ...resolveBooleanDefaults(options),
126
126
  ...resolveNumericDefaults(options),
127
127
  ...resolveRetentionFields(options),
@@ -235,6 +235,3 @@ export function createActivityWorkerDispatcher(activityExecution) {
235
235
  smol: activityExecution.smol ?? !1
236
236
  }));
237
237
  }
238
- export function createAlertManagerForEngine(engine, alerts, getNow) {
239
- return alerts ? new AlertManager(engine, alerts, getNow) : null;
240
- }
@@ -25,6 +25,7 @@
25
25
  * nested coordinator surfaces up to its parent.
26
26
  */
27
27
  declare const DEFERRED_CONSUME_BRAND: unique symbol;
28
+ declare const KEYED_RACE_RESULT_BRAND: unique symbol;
28
29
  /**
29
30
  * A branch result whose real value is produced by a single deferred consume that
30
31
  * only the winning coordinator performs.
@@ -34,6 +35,12 @@ export type DeferredConsumeEnvelope = {
34
35
  /** Perform the single destructive consume and return the consumed payload. */
35
36
  readonly finalize: () => Promise<unknown>;
36
37
  };
38
+ type KeyedRaceResultEnvelope = {
39
+ readonly [KEYED_RACE_RESULT_BRAND]: true;
40
+ readonly key: string;
41
+ readonly value: unknown;
42
+ };
43
+ export declare function createKeyedRaceResultEnvelope(key: string, value: unknown): KeyedRaceResultEnvelope;
37
44
  /** Wrap a deferred consume into a branded envelope. */
38
45
  export declare function createDeferredConsumeEnvelope(finalize: () => Promise<unknown>): DeferredConsumeEnvelope;
39
46
  /** Detect an envelope by its brand symbol only (never structurally). */
@@ -43,7 +50,9 @@ export declare function isDeferredConsumeEnvelope(value: unknown): value is Defe
43
50
  * cache. A winning `wait-signal` branch surfaces a {@link DeferredConsumeEnvelope}
44
51
  * whose `finalize()` performs the single consume; a nested `ctx.all` branch
45
52
  * surfaces an ARRAY that may hold envelopes at arbitrary positions, so arrays are
46
- * walked and each element finalized. Any other value passes through untouched.
53
+ * walked and each element finalized. A nested `ctx.raceKeyed()` winner is walked
54
+ * through its value while retaining its key. Any other value passes through
55
+ * untouched.
47
56
  *
48
57
  * This runs only on the WINNING path (race winner, or every branch of a settled
49
58
  * `ctx.all`), so finalizing here is exactly the linearization point of "this
@@ -1,4 +1,10 @@
1
- const DEFERRED_CONSUME_BRAND = Symbol("weft.deferredConsume");
1
+ const DEFERRED_CONSUME_BRAND = Symbol("weft.deferredConsume"), KEYED_RACE_RESULT_BRAND = Symbol("weft.keyedRaceResult");
2
+ export function createKeyedRaceResultEnvelope(key, value) {
3
+ return { [KEYED_RACE_RESULT_BRAND]: !0, key, value };
4
+ }
5
+ function isKeyedRaceResultEnvelope(value) {
6
+ return typeof value === "object" && value !== null && value[KEYED_RACE_RESULT_BRAND] === !0;
7
+ }
2
8
  export function createDeferredConsumeEnvelope(finalize) {
3
9
  return { [DEFERRED_CONSUME_BRAND]: !0, finalize };
4
10
  }
@@ -10,5 +16,7 @@ export async function finalizeAndUnwrap(value) {
10
16
  return value.finalize();
11
17
  if (Array.isArray(value))
12
18
  return Promise.all(value.map((element) => finalizeAndUnwrap(element)));
19
+ if (isKeyedRaceResultEnvelope(value))
20
+ return { key: value.key, value: await finalizeAndUnwrap(value.value) };
13
21
  return value;
14
22
  }
@@ -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;
@@ -24,6 +24,7 @@ export interface RegistrationEntry {
24
24
  export interface ResolvedOptions {
25
25
  storage: WeftStorage;
26
26
  development: boolean;
27
+ backgroundTaskMode: 'automatic' | 'manual';
27
28
  checkpointHistory: number;
28
29
  checkpointSizeWarningThreshold: number;
29
30
  maxNestingDepth: number;
@@ -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;
@@ -4,6 +4,13 @@ import type { Engine } from './index.ts';
4
4
  import { type EngineInternals } from './internals.ts';
5
5
  import type { SecondInstanceDetector } from './second-instance-detector.ts';
6
6
  export declare function isActivityDefinition(value: unknown): value is AnyActivityDefinition;
7
+ type EngineCreateBackgroundTaskOptions = {
8
+ backgroundTasks?: 'automatic' | 'manual' | undefined;
9
+ recover?: boolean | undefined;
10
+ startScheduler?: boolean | undefined;
11
+ };
12
+ export declare function validateEngineCreateBackgroundTaskOptions(options: EngineCreateBackgroundTaskOptions): void;
13
+ export declare function shouldStartEngineScheduler(options: EngineCreateBackgroundTaskOptions, backgroundTaskMode: 'automatic' | 'manual'): boolean;
7
14
  export declare function createQueuedInlineWorkflowStartHandler<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>, channel: MessageChannel): () => void;
8
15
  /**
9
16
  * Drain pending inline launches for `engine` before teardown. Built with the
@@ -23,3 +30,4 @@ export declare function createCleanupIntervalTick<TWorkflows extends object, TAc
23
30
  */
24
31
  export declare function createSecondInstanceDetectorResolver<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>): () => SecondInstanceDetector | null;
25
32
  export declare function disposeEngineCleanupInterval(internals: EngineInternals): void;
33
+ export {};
@@ -12,6 +12,15 @@ import { swallowPromiseRejection } from "./strategy-helpers.js";
12
12
  export function isActivityDefinition(value) {
13
13
  return typeof value === "function" && typeof value.name === "string" && "execute" in value && typeof value.execute === "function";
14
14
  }
15
+ export function validateEngineCreateBackgroundTaskOptions(options) {
16
+ if (options.backgroundTasks === "manual" && options.startScheduler === !0)
17
+ throw Error('startScheduler cannot be true when backgroundTasks is "manual"');
18
+ }
19
+ export function shouldStartEngineScheduler(options, backgroundTaskMode) {
20
+ if (backgroundTaskMode === "manual")
21
+ return !1;
22
+ return options.startScheduler ?? options.recover !== !1;
23
+ }
15
24
  function inlineLaunchQueueCallbacksForEngine(engine) {
16
25
  const lifecycleCallbacks = createLifecycleCallbacks(engine);
17
26
  return {
@@ -43,7 +43,7 @@ export class WorkflowTypeNotRegisteredForRecoveryError extends WeftError {
43
43
  const missingWorkflowCount = parameters.missingWorkflows.length, missingTypes = [
44
44
  ...new Set(parameters.missingWorkflows.map((workflow) => workflow.type))
45
45
  ].toSorted(), registeredTypes = [...parameters.registeredTypes].toSorted(), summarizedTypes = summarizeMissingWorkflowTypes(missingTypes);
46
- super("WorkflowTypeNotRegisteredForRecoveryError", `Cannot recover ${missingWorkflowCount} running workflow(s): workflow type(s) not registered: ${summarizedTypes}. Register the missing workflow types before calling \`recoverAll()\`, or pass ` + "`{ acknowledgeUnknownWorkflowTypes: true }` (dangerous \u2014 see migration docs).");
46
+ super("WorkflowTypeNotRegisteredForRecoveryError", `Cannot recover ${missingWorkflowCount} running workflow(s): workflow type(s) not registered: ${summarizedTypes}. Register the missing workflow types before calling \`recoverAll()\`, or pass ` + "`{ acknowledgeUnknownWorkflowTypes: true }` (dangerous \u2014 see " + "https://github.com/stevekinney/weft/blob/main/documentation/guides/recovery-and-deploys.md#acknowledging-drift-acknowledgeunknownworkflowtypes).");
47
47
  this.registeredTypes = registeredTypes;
48
48
  this.missingTypes = missingTypes;
49
49
  this.missingWorkflowSamples = parameters.missingWorkflows.slice(0, MISSING_WORKFLOW_SAMPLE_LIMIT).map((workflow) => ({ ...workflow }));
@@ -21,12 +21,12 @@ import { ScheduleHandle } from './schedule-handle.ts';
21
21
  import { type WorkflowFeedListener, type WorkflowFeedRecord, type WorkflowFeedSelector } from './workflow-feed.ts';
22
22
  export { ActivityReconciliationCapabilityError, ActivityReconciliationConflictError, ActivityReconciliationIndeterminateError, } from './activity-reconciliation.ts';
23
23
  export { AsyncActivityTokenNotFoundError } from './async-activity-completion.ts';
24
- export type { PendingAsyncActivity } from './async-activity-completion.ts';
24
+ export type { PendingAsyncActivity } from './async-activity-records.ts';
25
25
  export type { PendingTimelineEntry, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter, } from './engine-internal-types.ts';
26
26
  export { ActivityResolutionError, BulkDeleteRequiresTerminalWorkflowsError, BulkOperationConfirmationError, EngineCreateNameMismatchError, EngineDisposedError, IdempotencyKeyPurgedError, PersistedDataIncompatibleError, StartOrSignalConflictError, WorkflowAlreadyExistsError, WorkflowConcurrencyLimitExceededError, WorkflowNotFoundError, WorkflowNotRegisteredError, WorkflowSuspendNotSupportedError, WorkflowTeardownPendingError, WorkflowTypeNotRegisteredForRecoveryError, } from './errors.ts';
27
27
  export { HANDLE_RESULT_PROMISE, WorkflowHandle } from './handles.ts';
28
28
  export { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError, EngineLeaseNotHeldError, } from './lease-errors.ts';
29
- export type { RecoverAllOptions } from './lifecycle.ts';
29
+ export type { RecoverAllOptions, RecoveredWorkflowInfo } from './lifecycle.ts';
30
30
  export { ScheduleHandle } from './schedule-handle.ts';
31
31
  export type { WorkflowFeedListener, WorkflowFeedRecord, WorkflowFeedSelector, } from './workflow-feed.ts';
32
32
  export type { EngineCreateOptions } from './engine-create-types.ts';
@@ -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
@@ -239,6 +242,16 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
239
242
  list<const TAttributeKeys extends readonly AttributeFilterKey[] = readonly AttributeFilterKey[]>(filter?: TypedListFilter<TAttributeKeys>, options?: ListOptions): Promise<PaginatedResult<WorkflowSummary>>;
240
243
  aggregate(filter: ListFilter | undefined, options: AggregateOptions): Promise<AggregateResult>;
241
244
  getRetentionOverview(): RetentionOverview;
245
+ /**
246
+ * Run one host-driven maintenance cycle. This fires due durable timers,
247
+ * deletes expired update responses, applies configured retention, and
248
+ * re-evaluates alert rules without relying on process-local intervals.
249
+ *
250
+ * Use with `backgroundTasks: 'manual'` from a serverless alarm, Cron trigger,
251
+ * or another externally scheduled wake-up. Concurrent calls are safe, but a
252
+ * host should await each cycle before scheduling another.
253
+ */
254
+ runMaintenance(now?: number): Promise<void>;
242
255
  purge(filter?: ListFilter): Promise<PurgeResult>;
243
256
  cancelAll(filter: ListFilter, options: BulkOperationDryRunOptions): Promise<BulkOperationDryRunResult>;
244
257
  cancelAll(filter: ListFilter, options?: BulkOperationCommitOptions): Promise<BulkCancelResult>;
@@ -1,3 +1,4 @@
1
+ import { AlertManager } from "../../alerting/alert-manager.js";
1
2
  import {
2
3
  KEYS,
3
4
  requireStorageCapability
@@ -19,9 +20,9 @@ import {
19
20
  } from "./aggregate.js";
20
21
  import {
21
22
  completeAsyncActivity as completeAsyncActivityFromInternals,
22
- failAsyncActivity as failAsyncActivityFromInternals,
23
- recoverPendingAsyncActivities
23
+ failAsyncActivity as failAsyncActivityFromInternals
24
24
  } from "./async-activity-completion.js";
25
+ import { recoverPendingAsyncActivities } from "./async-activity-records.js";
25
26
  import { broadcast as broadcastFromInternals } from "./broadcast.js";
26
27
  import {
27
28
  cancelAll as cancelAllWorkflows,
@@ -52,7 +53,6 @@ import {
52
53
  import {
53
54
  copyWorkflowDefinition,
54
55
  createActivityWorkerDispatcher,
55
- createAlertManagerForEngine,
56
56
  createExecutionStrategyBundle,
57
57
  definitionEntries,
58
58
  resolveEngineInterceptors,
@@ -70,9 +70,15 @@ import {
70
70
  createQueuedInlineWorkflowStartHandler,
71
71
  createSecondInstanceDetectorResolver,
72
72
  drainQueuedInlineWorkflowStartsForEngine,
73
- isActivityDefinition
73
+ isActivityDefinition,
74
+ shouldStartEngineScheduler,
75
+ validateEngineCreateBackgroundTaskOptions
74
76
  } from "./engine-runtime-helpers.js";
75
- import { EngineCreateNameMismatchError, EngineDisposedError } from "./errors.js";
77
+ import {
78
+ EngineCreateNameMismatchError,
79
+ EngineDisposedError,
80
+ StartOrSignalConflictError
81
+ } from "./errors.js";
76
82
  import { assertLeaseHeldForEngineWork, commitFencedEngineWrite } from "./fenced-write.js";
77
83
  import { recordFinalizerState } from "./finalizer-state.js";
78
84
  import {
@@ -143,7 +149,11 @@ import {
143
149
  createSecondInstanceDetector
144
150
  } from "./second-instance-detector.js";
145
151
  import { signal as signalWorkflow } from "./signals.js";
146
- import { loadScheduleState, loadWorkflowState } from "./storage-io.js";
152
+ import {
153
+ loadScheduleState,
154
+ loadWorkflowState,
155
+ runSerializedWorkflowStateWrite
156
+ } from "./storage-io.js";
147
157
  import {
148
158
  feedOperationResult,
149
159
  getComposedWorkflowInterceptor,
@@ -161,6 +171,7 @@ import {
161
171
  submitCoordinatedUpdate as submitCoordinatedUpdateFromInternals,
162
172
  update as updateFromInternals
163
173
  } from "./updates.js";
174
+ import { isTerminalWorkflowStatus } from "./validation.js";
164
175
  import { coerceScheduleId } from "./validation/schedule.js";
165
176
  import {
166
177
  replayWorkflowFeed,
@@ -222,6 +233,7 @@ function scheduleDefinitionFromInternals(internals, definition) {
222
233
 
223
234
  export class Engine extends EventTarget {
224
235
  static async create(options) {
236
+ validateEngineCreateBackgroundTaskOptions(options);
225
237
  const engine = new Engine(options);
226
238
  try {
227
239
  await assertCompatiblePersistedDataVersion(getInternals(engine).storage);
@@ -238,7 +250,7 @@ export class Engine extends EventTarget {
238
250
  await engine.#acquireLeaseIfConfigured();
239
251
  if (options.recover !== !1)
240
252
  await engine.recoverAll(options.acknowledgeUnknownWorkflowTypes !== void 0 ? { acknowledgeUnknownWorkflowTypes: options.acknowledgeUnknownWorkflowTypes } : {});
241
- if (options.startScheduler ?? options.recover !== !1)
253
+ if (shouldStartEngineScheduler(options, getInternals(engine).options.backgroundTaskMode))
242
254
  getInternals(engine).scheduler.start();
243
255
  } catch (error) {
244
256
  await engine[Symbol.asyncDispose]();
@@ -277,6 +289,8 @@ export class Engine extends EventTarget {
277
289
  getInternals(this).updateWaitersByWorkflow = new Map;
278
290
  getInternals(this).sleepResolvers = new Map;
279
291
  getInternals(this).sleepResolversByWorkflow = new Map;
292
+ getInternals(this).sleepTimerAcknowledgementWaiters = new Map;
293
+ getInternals(this).durableInlineOperations = new Map;
280
294
  getInternals(this).sleepTimersFiredWithoutResolver = new Map;
281
295
  getInternals(this).interceptors = resolveEngineInterceptors(options);
282
296
  getInternals(this).composedWorkflowInterceptor = void 0;
@@ -341,7 +355,7 @@ export class Engine extends EventTarget {
341
355
  cleanupInterval: null,
342
356
  secondInstanceDetectionInterval: null,
343
357
  testToken: consumeNextEngineLeakWarningTokenForTesting()
344
- }, cleanupInterval = setInterval(createCleanupIntervalTick(weakEngine, cleanupIntervalDisposalTracker), 60000);
358
+ }, cleanupInterval = resolvedOptions.backgroundTaskMode === "automatic" ? setInterval(createCleanupIntervalTick(weakEngine, cleanupIntervalDisposalTracker), 60000) : null;
345
359
  cleanupIntervalDisposalTracker.cleanupInterval = cleanupInterval;
346
360
  getInternals(this).cleanupInterval = cleanupInterval;
347
361
  getInternals(this).cleanupIntervalDisposalTracker = cleanupIntervalDisposalTracker;
@@ -364,7 +378,7 @@ export class Engine extends EventTarget {
364
378
  getInternals(this).workflowVisibilityWatermarkExpiresAt = void 0;
365
379
  getInternals(this).activityWorkerDispatcher = createActivityWorkerDispatcher(options?.activityExecution);
366
380
  getInternals(this).strategy.onMessage(this.#handleStrategyMessage.bind(this));
367
- getInternals(this).alertManager = createAlertManagerForEngine(this, options?.alerts, getNow);
381
+ getInternals(this).alertManager = options?.alerts ? new AlertManager(this, options.alerts, getNow, resolvedOptions.backgroundTaskMode === "automatic") : null;
368
382
  this.#ensureRetentionSweepInterval();
369
383
  this.#startSecondInstanceDetection();
370
384
  }
@@ -565,7 +579,12 @@ export class Engine extends EventTarget {
565
579
  #createStartOrSignalCallbacks() {
566
580
  return {
567
581
  ...this.#createLifecycleCallbacks(),
568
- 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
+ })
569
588
  };
570
589
  }
571
590
  getHandle(workflowId) {
@@ -586,6 +605,18 @@ export class Engine extends EventTarget {
586
605
  getRetentionOverview() {
587
606
  return getRetentionOverviewSnapshot(getInternals(this), (type) => resolveWorkflowTypeRetention(getInternals(this), type));
588
607
  }
608
+ async runMaintenance(now = getInternals(this).options.getNow()) {
609
+ const internals = getInternals(this);
610
+ await internals.scheduler.tick(now);
611
+ try {
612
+ await internals.updateCoordinator.cleanupExpiredResponses();
613
+ } catch (error) {
614
+ this.#createTerminationCallbacks().handleCleanupError("cleanupExpiredResponses", error);
615
+ }
616
+ if (this.#hasConfiguredRetention())
617
+ await this.#runRetentionSweep();
618
+ internals.alertManager?.tick();
619
+ }
589
620
  async purge(filter) {
590
621
  return purgeWorkflows(getInternals(this), filter, (workflowId) => cleanupWaitersFromTermination(getInternals(this), workflowId, this.#createTerminationCallbacks()));
591
622
  }
@@ -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
@@ -186,13 +207,13 @@ export interface EngineInternals {
186
207
  * by their durable task token. Mirrored to storage (`KEYS.asyncActivity`) and
187
208
  * reloaded by `recoverAll()`. See `async-activity-completion.ts`.
188
209
  */
189
- pendingAsyncActivities: Map<string, import('./async-activity-completion.ts').PendingAsyncActivity>;
210
+ pendingAsyncActivities: Map<string, import('./async-activity-records.ts').PendingAsyncActivity>;
190
211
  /**
191
212
  * Completed or failed async-activity tokens that were consumed before inline
192
213
  * recovery adopted the workflow generator. Replay drains these by workflow id
193
214
  * when it reaches the same deterministic async-activity token again.
194
215
  */
195
- pendingAsyncActivityResolutions: Map<string, import('./async-activity-completion.ts').PendingAsyncActivityResolution[]>;
216
+ pendingAsyncActivityResolutions: Map<string, import('./async-activity-records.ts').PendingAsyncActivityResolution[]>;
196
217
  pendingStarts: Set<string>;
197
218
  pendingScheduleCreations: Set<string>;
198
219
  workflowsNeedingTerminalCleanup: Set<string>;