@lostgradient/weft 0.9.0 → 0.10.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 (87) hide show
  1. package/README.md +3 -3
  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/context/index.d.ts +2 -1
  6. package/dist/core/context/index.js +8 -0
  7. package/dist/core/context/operation-request.d.ts +2 -0
  8. package/dist/core/context/parallel-cache-entry.d.ts +1 -1
  9. package/dist/core/context/parallel-cache-entry.js +12 -8
  10. package/dist/core/context/parallel-operations.d.ts +1 -1
  11. package/dist/core/context/parallel-operations.js +50 -8
  12. package/dist/core/engine/async-activity-completion.d.ts +24 -54
  13. package/dist/core/engine/async-activity-completion.js +25 -90
  14. package/dist/core/engine/async-activity-records.d.ts +106 -0
  15. package/dist/core/engine/async-activity-records.js +124 -0
  16. package/dist/core/engine/bulk-operations-purge.js +2 -1
  17. package/dist/core/engine/callback-creators-core.js +1 -0
  18. package/dist/core/engine/construction.d.ts +1 -3
  19. package/dist/core/engine/construction.js +2 -5
  20. package/dist/core/engine/deferred-consume-envelope.d.ts +10 -1
  21. package/dist/core/engine/deferred-consume-envelope.js +9 -1
  22. package/dist/core/engine/engine-internal-types.d.ts +1 -0
  23. package/dist/core/engine/engine-runtime-helpers.d.ts +8 -0
  24. package/dist/core/engine/engine-runtime-helpers.js +9 -0
  25. package/dist/core/engine/errors.js +1 -1
  26. package/dist/core/engine/index.d.ts +12 -2
  27. package/dist/core/engine/index.js +22 -7
  28. package/dist/core/engine/internals.d.ts +2 -2
  29. package/dist/core/engine/lifecycle/recovered-services.d.ts +4 -2
  30. package/dist/core/engine/lifecycle/recovered-services.js +37 -7
  31. package/dist/core/engine/lifecycle/resume.d.ts +2 -2
  32. package/dist/core/engine/lifecycle/resume.js +39 -7
  33. package/dist/core/engine/lifecycle/shared.d.ts +34 -0
  34. package/dist/core/engine/lifecycle/transition.d.ts +1 -1
  35. package/dist/core/engine/lifecycle/transition.js +3 -3
  36. package/dist/core/engine/lifecycle.d.ts +1 -1
  37. package/dist/core/engine/memo-durable-activity.js +7 -9
  38. package/dist/core/engine/operations-activity.js +1 -1
  39. package/dist/core/engine/operations-coordination.d.ts +15 -1
  40. package/dist/core/engine/operations-coordination.js +43 -10
  41. package/dist/core/engine/ownership-options.d.ts +1 -0
  42. package/dist/core/engine/ownership-options.js +14 -0
  43. package/dist/core/engine/retention.js +4 -0
  44. package/dist/core/engine/schedule-run-metadata.d.ts +3 -0
  45. package/dist/core/engine/schedule-run-metadata.js +29 -0
  46. package/dist/core/engine/schedule-run.js +19 -4
  47. package/dist/core/engine/schedules.js +4 -3
  48. package/dist/core/engine/sub-operation.js +6 -13
  49. package/dist/core/engine/termination/cleanup.js +3 -1
  50. package/dist/core/types/options.d.ts +12 -0
  51. package/dist/core/types/services-resolution.d.ts +8 -3
  52. package/dist/core/types/workflow-builder.d.ts +2 -2
  53. package/dist/core/types/workflow-context.d.ts +27 -0
  54. package/dist/index.d.ts +2 -2
  55. package/dist/mcp/cli.js +17 -17
  56. package/dist/server/authorization.d.ts +3 -3
  57. package/dist/server/fault-to-json-rpc.d.ts +2 -1
  58. package/dist/server/handler.js +22 -22
  59. package/dist/server/index.js +15 -15
  60. package/dist/server/json-rpc-dispatch.d.ts +2 -2
  61. package/dist/server/json-rpc-parse.d.ts +1 -1
  62. package/dist/server/json-rpc-protocol.d.ts +2 -2
  63. package/dist/server/json-rpc-websocket.d.ts +1 -1
  64. package/dist/server/operation-fault.d.ts +3 -1
  65. package/dist/server/operations/async-activity.js +2 -2
  66. package/dist/server/principal.d.ts +2 -2
  67. package/dist/server/stdio-session.d.ts +4 -4
  68. package/dist/service-worker/index.js +13 -13
  69. package/dist/storage/compressed-storage.js +1 -1
  70. package/dist/storage/http.js +2 -2
  71. package/dist/storage/index.d.ts +1 -0
  72. package/dist/storage/indexeddb.js +1 -1
  73. package/dist/storage/interface.d.ts +1 -0
  74. package/dist/storage/interface.js +1 -1
  75. package/dist/storage/lmdb.js +1 -1
  76. package/dist/storage/memory.js +1 -1
  77. package/dist/storage/neon.js +1 -1
  78. package/dist/storage/resolve.js +1 -1
  79. package/dist/storage/scoped-storage.js +1 -1
  80. package/dist/storage/testing.js +1 -1
  81. package/dist/storage/turso.js +1 -1
  82. package/dist/storage/typed-storage.js +1 -1
  83. package/dist/storage/web-extension.js +1 -1
  84. package/dist/testing/index.js +26 -26
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.js +1 -1
  87. package/package.json +2 -2
@@ -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
  }
@@ -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;
@@ -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';
@@ -239,6 +239,16 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
239
239
  list<const TAttributeKeys extends readonly AttributeFilterKey[] = readonly AttributeFilterKey[]>(filter?: TypedListFilter<TAttributeKeys>, options?: ListOptions): Promise<PaginatedResult<WorkflowSummary>>;
240
240
  aggregate(filter: ListFilter | undefined, options: AggregateOptions): Promise<AggregateResult>;
241
241
  getRetentionOverview(): RetentionOverview;
242
+ /**
243
+ * Run one host-driven maintenance cycle. This fires due durable timers,
244
+ * deletes expired update responses, applies configured retention, and
245
+ * re-evaluates alert rules without relying on process-local intervals.
246
+ *
247
+ * Use with `backgroundTasks: 'manual'` from a serverless alarm, Cron trigger,
248
+ * or another externally scheduled wake-up. Concurrent calls are safe, but a
249
+ * host should await each cycle before scheduling another.
250
+ */
251
+ runMaintenance(now?: number): Promise<void>;
242
252
  purge(filter?: ListFilter): Promise<PurgeResult>;
243
253
  cancelAll(filter: ListFilter, options: BulkOperationDryRunOptions): Promise<BulkOperationDryRunResult>;
244
254
  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,7 +70,9 @@ 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
77
  import { EngineCreateNameMismatchError, EngineDisposedError } from "./errors.js";
76
78
  import { assertLeaseHeldForEngineWork, commitFencedEngineWrite } from "./fenced-write.js";
@@ -222,6 +224,7 @@ function scheduleDefinitionFromInternals(internals, definition) {
222
224
 
223
225
  export class Engine extends EventTarget {
224
226
  static async create(options) {
227
+ validateEngineCreateBackgroundTaskOptions(options);
225
228
  const engine = new Engine(options);
226
229
  try {
227
230
  await assertCompatiblePersistedDataVersion(getInternals(engine).storage);
@@ -238,7 +241,7 @@ export class Engine extends EventTarget {
238
241
  await engine.#acquireLeaseIfConfigured();
239
242
  if (options.recover !== !1)
240
243
  await engine.recoverAll(options.acknowledgeUnknownWorkflowTypes !== void 0 ? { acknowledgeUnknownWorkflowTypes: options.acknowledgeUnknownWorkflowTypes } : {});
241
- if (options.startScheduler ?? options.recover !== !1)
244
+ if (shouldStartEngineScheduler(options, getInternals(engine).options.backgroundTaskMode))
242
245
  getInternals(engine).scheduler.start();
243
246
  } catch (error) {
244
247
  await engine[Symbol.asyncDispose]();
@@ -341,7 +344,7 @@ export class Engine extends EventTarget {
341
344
  cleanupInterval: null,
342
345
  secondInstanceDetectionInterval: null,
343
346
  testToken: consumeNextEngineLeakWarningTokenForTesting()
344
- }, cleanupInterval = setInterval(createCleanupIntervalTick(weakEngine, cleanupIntervalDisposalTracker), 60000);
347
+ }, cleanupInterval = resolvedOptions.backgroundTaskMode === "automatic" ? setInterval(createCleanupIntervalTick(weakEngine, cleanupIntervalDisposalTracker), 60000) : null;
345
348
  cleanupIntervalDisposalTracker.cleanupInterval = cleanupInterval;
346
349
  getInternals(this).cleanupInterval = cleanupInterval;
347
350
  getInternals(this).cleanupIntervalDisposalTracker = cleanupIntervalDisposalTracker;
@@ -364,7 +367,7 @@ export class Engine extends EventTarget {
364
367
  getInternals(this).workflowVisibilityWatermarkExpiresAt = void 0;
365
368
  getInternals(this).activityWorkerDispatcher = createActivityWorkerDispatcher(options?.activityExecution);
366
369
  getInternals(this).strategy.onMessage(this.#handleStrategyMessage.bind(this));
367
- getInternals(this).alertManager = createAlertManagerForEngine(this, options?.alerts, getNow);
370
+ getInternals(this).alertManager = options?.alerts ? new AlertManager(this, options.alerts, getNow, resolvedOptions.backgroundTaskMode === "automatic") : null;
368
371
  this.#ensureRetentionSweepInterval();
369
372
  this.#startSecondInstanceDetection();
370
373
  }
@@ -586,6 +589,18 @@ export class Engine extends EventTarget {
586
589
  getRetentionOverview() {
587
590
  return getRetentionOverviewSnapshot(getInternals(this), (type) => resolveWorkflowTypeRetention(getInternals(this), type));
588
591
  }
592
+ async runMaintenance(now = getInternals(this).options.getNow()) {
593
+ const internals = getInternals(this);
594
+ await internals.scheduler.tick(now);
595
+ try {
596
+ await internals.updateCoordinator.cleanupExpiredResponses();
597
+ } catch (error) {
598
+ this.#createTerminationCallbacks().handleCleanupError("cleanupExpiredResponses", error);
599
+ }
600
+ if (this.#hasConfiguredRetention())
601
+ await this.#runRetentionSweep();
602
+ internals.alertManager?.tick();
603
+ }
589
604
  async purge(filter) {
590
605
  return purgeWorkflows(getInternals(this), filter, (workflowId) => cleanupWaitersFromTermination(getInternals(this), workflowId, this.#createTerminationCallbacks()));
591
606
  }
@@ -186,13 +186,13 @@ export interface EngineInternals {
186
186
  * by their durable task token. Mirrored to storage (`KEYS.asyncActivity`) and
187
187
  * reloaded by `recoverAll()`. See `async-activity-completion.ts`.
188
188
  */
189
- pendingAsyncActivities: Map<string, import('./async-activity-completion.ts').PendingAsyncActivity>;
189
+ pendingAsyncActivities: Map<string, import('./async-activity-records.ts').PendingAsyncActivity>;
190
190
  /**
191
191
  * Completed or failed async-activity tokens that were consumed before inline
192
192
  * recovery adopted the workflow generator. Replay drains these by workflow id
193
193
  * when it reaches the same deterministic async-activity token again.
194
194
  */
195
- pendingAsyncActivityResolutions: Map<string, import('./async-activity-completion.ts').PendingAsyncActivityResolution[]>;
195
+ pendingAsyncActivityResolutions: Map<string, import('./async-activity-records.ts').PendingAsyncActivityResolution[]>;
196
196
  pendingStarts: Set<string>;
197
197
  pendingScheduleCreations: Set<string>;
198
198
  workflowsNeedingTerminalCleanup: Set<string>;
@@ -1,4 +1,4 @@
1
- import type { WorkflowState } from '../../types.ts';
1
+ import type { WorkflowServicesResolverInfo, WorkflowState } from '../../types.ts';
2
2
  import type { EngineInternals } from '../internals.ts';
3
3
  /**
4
4
  * Re-provide a recovered inline workflow's non-serialized `services` before its
@@ -36,7 +36,9 @@ import type { EngineInternals } from '../internals.ts';
36
36
  * @param onCommitError - Records a fail-warn when `failRun` itself throws, so the
37
37
  * swallowed terminal-commit fault is still observable.
38
38
  */
39
- export declare function reprovideRecoveredServices(internals: EngineInternals, state: WorkflowState, failRun: (workflowId: string, error: Error) => Promise<void>, onCommitError: (source: string, error: unknown, workflowId: string) => void, dispatchDiagnostic?: (event: Event) => void): Promise<boolean>;
39
+ export declare function reprovideRecoveredServices(internals: EngineInternals, state: WorkflowState, failRun: (workflowId: string, error: Error) => Promise<void>, onCommitError: (source: string, error: unknown, workflowId: string) => void, dispatchDiagnostic?: (event: Event) => void, resolverInfo?: WorkflowServicesResolverInfo): Promise<boolean>;
40
+ /** Build the durable recovery context shared by the services resolver and recovery hook. */
41
+ export declare function workflowServicesResolverInfoFromState(internals: EngineInternals, state: WorkflowState): Promise<WorkflowServicesResolverInfo>;
40
42
  /**
41
43
  * The canonical terminal error for any run whose services could not be provided —
42
44
  * both recovery re-provision paths and a scheduled-occurrence launch. Shared so
@@ -1,7 +1,9 @@
1
1
  import { KEYS } from "../../../storage/interface.js";
2
2
  import { DevelopmentWarningEvent } from "../../events.js";
3
+ import { decodeScheduleRunMetadata } from "../schedule-run-metadata.js";
4
+ import { loadScheduleState } from "../storage-io.js";
3
5
  const RESOLVE_WORKFLOW_SERVICES_OPTION_PATH = "EngineOptions.resolveWorkflowServices";
4
- export async function reprovideRecoveredServices(internals, state, failRun, onCommitError, dispatchDiagnostic = () => {}) {
6
+ export async function reprovideRecoveredServices(internals, state, failRun, onCommitError, dispatchDiagnostic = () => {}, resolverInfo) {
5
7
  const resolver = internals.options.resolveWorkflowServices;
6
8
  if (internals.inlineStrategy === null)
7
9
  return !1;
@@ -20,13 +22,9 @@ export async function reprovideRecoveredServices(internals, state, failRun, onCo
20
22
  return !0;
21
23
  }
22
24
  let reason;
25
+ const info = resolverInfo ?? await workflowServicesResolverInfoFromState(internals, state);
23
26
  try {
24
- const resolution = await resolver({
25
- workflowId: state.id,
26
- workflowType: state.type,
27
- input: state.input,
28
- launchOptions: launchOptionsFromWorkflowState(state)
29
- });
27
+ const resolution = await resolver(info);
30
28
  if (resolution.status === "available") {
31
29
  internals.workflowServices.set(state.id, resolution.services);
32
30
  return !1;
@@ -42,6 +40,16 @@ export async function reprovideRecoveredServices(internals, state, failRun, onCo
42
40
  }
43
41
  return !0;
44
42
  }
43
+ export async function workflowServicesResolverInfoFromState(internals, state) {
44
+ const schedule = await scheduleFromWorkflowState(internals, state);
45
+ return {
46
+ workflowId: state.id,
47
+ workflowType: state.type,
48
+ input: state.input,
49
+ launchOptions: launchOptionsFromWorkflowState(state),
50
+ ...schedule !== null ? { schedule } : {}
51
+ };
52
+ }
45
53
  export function unavailableServicesError(workflowId, reason) {
46
54
  return Error(`Workflow "${workflowId}" services unavailable: ${reason}`);
47
55
  }
@@ -54,3 +62,25 @@ function launchOptionsFromWorkflowState(state) {
54
62
  ...state.tags !== void 0 && state.tags.length > 0 ? { tags: [...state.tags] } : {}
55
63
  };
56
64
  }
65
+ async function scheduleFromWorkflowState(internals, state) {
66
+ const bytes = await internals.storage.get(KEYS.scheduleRun(state.id));
67
+ if (bytes === null)
68
+ return null;
69
+ const metadata = decodeScheduleRunMetadata(bytes);
70
+ if (metadata === null)
71
+ return null;
72
+ const scheduleState = await loadScheduleState(internals, metadata.id);
73
+ if (scheduleState === null)
74
+ return null;
75
+ if (scheduleState.workflowType !== state.type)
76
+ return null;
77
+ if (scheduleState.overlap === "allow")
78
+ return metadata.occurrence !== void 0 ? metadata : null;
79
+ if (scheduleState.currentWorkflowId === state.id)
80
+ return metadata;
81
+ if (metadata.occurrence !== void 0 && scheduleState.nextFireAt === metadata.occurrence)
82
+ return metadata;
83
+ if (metadata.occurrence === void 0 && scheduleState.overlap === "queue" && scheduleState.queuedRuns > 0)
84
+ return metadata;
85
+ return null;
86
+ }
@@ -1,4 +1,4 @@
1
1
  import { type WorkflowHandle } from '../handles.ts';
2
2
  import type { EngineInternals } from '../internals.ts';
3
- import { type LifecycleCallbacks } from './shared.ts';
4
- export declare function resumeWorkflowFromStorage(internals: EngineInternals, workflowId: string, dispatchResumedEvent: boolean, callbacks: LifecycleCallbacks): Promise<WorkflowHandle>;
3
+ import { type LifecycleCallbacks, type RecoverAllOptions } from './shared.ts';
4
+ export declare function resumeWorkflowFromStorage(internals: EngineInternals, workflowId: string, dispatchResumedEvent: boolean, callbacks: LifecycleCallbacks, onRecoveredWorkflow?: RecoverAllOptions['onRecoveredWorkflow']): Promise<WorkflowHandle>;
@@ -15,15 +15,47 @@ import { getComposedWorkflowInterceptor } from "../strategy-helpers.js";
15
15
  import { decodeWorkflowState } from "../validation.js";
16
16
  import { buildWorkflowVisibilityIndexTransition } from "../workflow-indexes.js";
17
17
  import { prepareResumeState } from "./persist.js";
18
- import { reprovideRecoveredServices } from "./recovered-services.js";
18
+ import {
19
+ reprovideRecoveredServices,
20
+ workflowServicesResolverInfoFromState
21
+ } from "./recovered-services.js";
19
22
  import {
20
23
  enforceHistoryPolicyBeforeReplay,
21
24
  loadTerminalCleanupTrackedState,
22
25
  loadWorkflowStartHeaders,
23
26
  setWorkflowStartHeaders
24
27
  } from "./shared.js";
25
- async function prepareRecoveredServicesOrFail(internals, state, callbacks) {
26
- return reprovideRecoveredServices(internals, state, callbacks.failWorkflowForUnavailableServices, callbacks.handleCleanupError, callbacks.dispatchEvent);
28
+ async function prepareRecoveredServicesOrFail(internals, state, callbacks, resolverInfo) {
29
+ return reprovideRecoveredServices(internals, state, callbacks.failWorkflowForUnavailableServices, callbacks.handleCleanupError, callbacks.dispatchEvent, resolverInfo);
30
+ }
31
+ async function runRecoveredWorkflowHookOrFail(internals, state, handle, resolverInfo, onRecoveredWorkflow, callbacks) {
32
+ const info = {
33
+ ...resolverInfo,
34
+ handle,
35
+ launchOptions: resolverInfo.launchOptions ?? { id: state.id },
36
+ services: internals.workflowServices.get(state.id)
37
+ };
38
+ try {
39
+ await onRecoveredWorkflow(info);
40
+ return !1;
41
+ } catch (error) {
42
+ const reason = error instanceof Error ? error.message : String(error), hookError = Error(`Recovery hook failed for workflow "${state.id}": ${reason}`);
43
+ try {
44
+ await callbacks.failWorkflowForRecoveryHook(state.id, hookError);
45
+ } catch (commitError) {
46
+ callbacks.handleCleanupError("onRecoveredWorkflow", commitError, state.id);
47
+ }
48
+ return !0;
49
+ }
50
+ }
51
+ async function prepareRecoveredWorkflowOrFail(internals, state, callbacks, onRecoveredWorkflow) {
52
+ const resolverInfo = onRecoveredWorkflow === void 0 ? void 0 : await workflowServicesResolverInfoFromState(internals, state), handle = callbacks.getHandle(state.id);
53
+ if (await prepareRecoveredServicesOrFail(internals, state, callbacks, resolverInfo))
54
+ return { handle, shouldStop: !0 };
55
+ if (onRecoveredWorkflow === void 0 || resolverInfo === void 0)
56
+ return { handle, shouldStop: !1 };
57
+ const shouldStop = await runRecoveredWorkflowHookOrFail(internals, state, handle, resolverInfo, onRecoveredWorkflow, callbacks);
58
+ return { handle, shouldStop };
27
59
  }
28
60
  function assertResumeNotTerminating(internals, workflowId) {
29
61
  if (internals.terminalizingWorkflows.has(workflowId))
@@ -138,7 +170,7 @@ async function reactivateSuspendedWorkflowState(internals, state) {
138
170
  }) : []
139
171
  ], [], () => Error(`Resume of workflow "${state.id}" lost its CAS race.`));
140
172
  }
141
- export async function resumeWorkflowFromStorage(internals, workflowId, dispatchResumedEvent, callbacks) {
173
+ export async function resumeWorkflowFromStorage(internals, workflowId, dispatchResumedEvent, callbacks, onRecoveredWorkflow) {
142
174
  const stateBytes = await internals.storage.get(KEYS.workflow(workflowId));
143
175
  if (!stateBytes)
144
176
  throw Error(`Workflow "${workflowId}" not found in storage`);
@@ -156,9 +188,9 @@ export async function resumeWorkflowFromStorage(internals, workflowId, dispatchR
156
188
  return callbacks.getHandle(workflowId);
157
189
  const workflowStartHeaders = await loadWorkflowStartHeaders(internals, workflowId, callbacks);
158
190
  await loadTerminalCleanupTrackedState(internals, workflowId, callbacks);
159
- if (await prepareRecoveredServicesOrFail(internals, state, callbacks))
160
- return callbacks.getHandle(workflowId);
161
- const handle = callbacks.getHandle(workflowId);
191
+ const { handle, shouldStop } = await prepareRecoveredWorkflowOrFail(internals, state, callbacks, onRecoveredWorkflow);
192
+ if (shouldStop)
193
+ return handle;
162
194
  await callbacks.runSerializedWorkflowStateWrite(workflowId, () => performSerializedResume(internals, {
163
195
  workflowId,
164
196
  resumeCheckpoint,