@lostgradient/weft 0.2.0 → 0.3.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 (207) hide show
  1. package/README.md +47 -22
  2. package/dist/cli/generated/operation-client.generated.d.ts +28 -1
  3. package/dist/cli/generated/operation-client.generated.js +2 -0
  4. package/dist/cli-main.js +79 -79
  5. package/dist/client/handle-delegation.d.ts +4 -0
  6. package/dist/client/handle-delegation.js +6 -0
  7. package/dist/client/http-client-requests.d.ts +2 -0
  8. package/dist/client/http-client-requests.js +3 -0
  9. package/dist/client/http-client.d.ts +4 -1
  10. package/dist/client/http-client.js +9 -1
  11. package/dist/client/interface.d.ts +57 -2
  12. package/dist/client/local.d.ts +4 -1
  13. package/dist/client/local.js +7 -0
  14. package/dist/client/start-body.d.ts +7 -1
  15. package/dist/client/start-body.js +13 -4
  16. package/dist/core/codec/extension-codec.js +4 -2
  17. package/dist/core/codec/index.d.ts +1 -0
  18. package/dist/core/codec/index.js +1 -0
  19. package/dist/core/codec/serializer-registry.d.ts +122 -0
  20. package/dist/core/codec/serializer-registry.js +51 -0
  21. package/dist/core/context/index.d.ts +10 -0
  22. package/dist/core/context/index.js +3 -0
  23. package/dist/core/context/internals.d.ts +10 -0
  24. package/dist/core/context/internals.js +5 -1
  25. package/dist/core/context/run-operation.d.ts +16 -3
  26. package/dist/core/context/run-operation.js +16 -7
  27. package/dist/core/context/speculative-child.js +2 -0
  28. package/dist/core/context/types.d.ts +6 -0
  29. package/dist/core/engine/bulk-operations-purge.js +1 -0
  30. package/dist/core/engine/bulk-operations.js +1 -1
  31. package/dist/core/engine/callback-creators-bundles.js +2 -1
  32. package/dist/core/engine/callback-creators-core.js +2 -1
  33. package/dist/core/engine/construction.d.ts +1 -1
  34. package/dist/core/engine/construction.js +15 -3
  35. package/dist/core/engine/disposal.js +12 -0
  36. package/dist/core/engine/engine-create-types.d.ts +0 -14
  37. package/dist/core/engine/engine-internal-types.d.ts +17 -0
  38. package/dist/core/engine/engine-leak-warnings.d.ts +6 -0
  39. package/dist/core/engine/engine-leak-warnings.js +4 -0
  40. package/dist/core/engine/engine-runtime-helpers.d.ts +17 -0
  41. package/dist/core/engine/engine-runtime-helpers.js +26 -5
  42. package/dist/core/engine/errors.d.ts +74 -0
  43. package/dist/core/engine/errors.js +25 -1
  44. package/dist/core/engine/handle-result.js +1 -1
  45. package/dist/core/engine/handles.d.ts +89 -40
  46. package/dist/core/engine/handles.js +25 -27
  47. package/dist/core/engine/index.d.ts +122 -4
  48. package/dist/core/engine/index.js +82 -5
  49. package/dist/core/engine/inline-launch-queue.d.ts +14 -0
  50. package/dist/core/engine/inline-launch-queue.js +32 -7
  51. package/dist/core/engine/internals.d.ts +26 -10
  52. package/dist/core/engine/lifecycle/fork-helpers.js +1 -7
  53. package/dist/core/engine/lifecycle/persist.js +5 -20
  54. package/dist/core/engine/lifecycle/recovered-services.d.ts +45 -0
  55. package/dist/core/engine/lifecycle/recovered-services.js +34 -0
  56. package/dist/core/engine/lifecycle/resume.js +33 -5
  57. package/dist/core/engine/lifecycle/shared.d.ts +8 -0
  58. package/dist/core/engine/lifecycle/start-batch.js +23 -12
  59. package/dist/core/engine/lifecycle/start-commit.d.ts +47 -0
  60. package/dist/core/engine/lifecycle/start-commit.js +27 -0
  61. package/dist/core/engine/lifecycle/start-exec.d.ts +30 -2
  62. package/dist/core/engine/lifecycle/start-exec.js +38 -0
  63. package/dist/core/engine/lifecycle/start-or-signal-resolution.d.ts +79 -0
  64. package/dist/core/engine/lifecycle/start-or-signal-resolution.js +60 -0
  65. package/dist/core/engine/lifecycle/start-or-signal.d.ts +45 -0
  66. package/dist/core/engine/lifecycle/start-or-signal.js +141 -0
  67. package/dist/core/engine/lifecycle/start.d.ts +3 -3
  68. package/dist/core/engine/lifecycle/start.js +42 -37
  69. package/dist/core/engine/lifecycle.d.ts +3 -2
  70. package/dist/core/engine/lifecycle.js +9 -2
  71. package/dist/core/engine/listing.js +1 -1
  72. package/dist/core/engine/operations-data.d.ts +16 -0
  73. package/dist/core/engine/operations-data.js +6 -0
  74. package/dist/core/engine/operations-time.d.ts +3 -2
  75. package/dist/core/engine/operations-time.js +6 -1
  76. package/dist/core/engine/persisted-data-version.d.ts +5 -9
  77. package/dist/core/engine/persisted-data-version.js +4 -5
  78. package/dist/core/engine/schedule-handle.d.ts +45 -0
  79. package/dist/core/engine/schedule-handle.js +26 -0
  80. package/dist/core/engine/schedules.d.ts +1 -1
  81. package/dist/core/engine/schedules.js +7 -3
  82. package/dist/core/engine/second-instance-detector.d.ts +96 -0
  83. package/dist/core/engine/second-instance-detector.js +108 -0
  84. package/dist/core/engine/signals.d.ts +22 -0
  85. package/dist/core/engine/signals.js +15 -0
  86. package/dist/core/engine/termination/cleanup.d.ts +25 -0
  87. package/dist/core/engine/termination/cleanup.js +21 -1
  88. package/dist/core/engine/termination/complete.js +4 -3
  89. package/dist/core/engine/termination/suspend.d.ts +68 -0
  90. package/dist/core/engine/termination/suspend.js +41 -0
  91. package/dist/core/engine/termination.d.ts +4 -2
  92. package/dist/core/engine/termination.js +2 -0
  93. package/dist/core/engine/validation.js +25 -1
  94. package/dist/core/engine/workflow-feed.d.ts +5 -3
  95. package/dist/core/events/event-map.d.ts +2 -1
  96. package/dist/core/events/workflow-events.d.ts +23 -0
  97. package/dist/core/events/workflow-events.js +9 -0
  98. package/dist/core/inline-execution-strategy.d.ts +5 -0
  99. package/dist/core/inline-execution-strategy.js +2 -1
  100. package/dist/core/list-filter-validation.js +2 -1
  101. package/dist/core/start-workflow-validation.d.ts +22 -0
  102. package/dist/core/start-workflow-validation.js +11 -1
  103. package/dist/core/step-context.d.ts +10 -6
  104. package/dist/core/step-context.js +7 -15
  105. package/dist/core/types/activity.d.ts +6 -3
  106. package/dist/core/types/identity.d.ts +8 -1
  107. package/dist/core/types/launch-metadata.d.ts +33 -0
  108. package/dist/core/types/launch-metadata.js +0 -0
  109. package/dist/core/types/message-handles.d.ts +25 -0
  110. package/dist/core/types/options.d.ts +90 -7
  111. package/dist/core/types/reviews.d.ts +2 -1
  112. package/dist/core/types/services-resolution.d.ts +47 -0
  113. package/dist/core/types/services-resolution.js +0 -0
  114. package/dist/core/types/state.d.ts +11 -11
  115. package/dist/core/types/workflow-builder.d.ts +5 -4
  116. package/dist/core/types/workflow-context.d.ts +25 -0
  117. package/dist/core/types/workflow-function.d.ts +17 -0
  118. package/dist/core/types/workflow-snapshot.d.ts +29 -0
  119. package/dist/core/types/workflow-snapshot.js +0 -0
  120. package/dist/core/types.d.ts +3 -0
  121. package/dist/core/types.js +3 -0
  122. package/dist/core/weft-error.d.ts +46 -14
  123. package/dist/core/weft-error.js +12 -1
  124. package/dist/diagnostics/doctor.js +6 -3
  125. package/dist/diagnostics/format.js +2 -2
  126. package/dist/diagnostics/types.d.ts +1 -0
  127. package/dist/diagnostics/version-check.js +6 -4
  128. package/dist/index.d.ts +10 -5
  129. package/dist/index.js +11 -2
  130. package/dist/json-schema.js +3 -3
  131. package/dist/mcp/cli.js +35 -35
  132. package/dist/mcp/list-filter.js +2 -1
  133. package/dist/mcp/session.js +1 -0
  134. package/dist/observability/index.js +2 -2
  135. package/dist/server/handler.js +30 -30
  136. package/dist/server/index.js +33 -33
  137. package/dist/server/interactive-operations.js +1 -0
  138. package/dist/server/operations/resume-workflow.js +2 -2
  139. package/dist/server/operations/start-or-signal-workflow.d.ts +39 -0
  140. package/dist/server/operations/start-or-signal-workflow.js +140 -0
  141. package/dist/server/operations/start-workflow-options.d.ts +32 -0
  142. package/dist/server/operations/start-workflow-options.js +63 -0
  143. package/dist/server/operations/start-workflow.js +7 -69
  144. package/dist/server/operations/suspend-workflow.d.ts +13 -0
  145. package/dist/server/operations/suspend-workflow.js +36 -0
  146. package/dist/server/rest-binding.d.ts +18 -7
  147. package/dist/server/rest-bindings.js +12 -0
  148. package/dist/server/runtime/task-dispatch.js +5 -3
  149. package/dist/server/runtime/task-polling.d.ts +16 -2
  150. package/dist/server/runtime/task-polling.js +20 -5
  151. package/dist/server/runtime/websocket-worker.js +8 -0
  152. package/dist/server/serve-internals.d.ts +8 -0
  153. package/dist/server/serve-internals.js +4 -2
  154. package/dist/server/task-state.d.ts +8 -0
  155. package/dist/service-worker/index.js +28 -28
  156. package/dist/storage/capabilities.d.ts +10 -2
  157. package/dist/storage/capabilities.js +2 -2
  158. package/dist/storage/http.js +2 -2
  159. package/dist/storage/index.d.ts +7 -1
  160. package/dist/storage/indexeddb.js +1 -1
  161. package/dist/storage/interface.d.ts +40 -0
  162. package/dist/storage/interface.js +1 -1
  163. package/dist/storage/key-prefixes.d.ts +1 -1
  164. package/dist/storage/key-prefixes.js +3 -0
  165. package/dist/storage/lmdb.js +1 -1
  166. package/dist/storage/memory.js +1 -1
  167. package/dist/storage/neon-value-mapping.d.ts +47 -0
  168. package/dist/storage/neon-value-mapping.js +11 -0
  169. package/dist/storage/neon.d.ts +108 -0
  170. package/dist/storage/neon.js +10 -0
  171. package/dist/storage/node-sqlite-loader.d.ts +71 -0
  172. package/dist/storage/node-sqlite-loader.js +41 -0
  173. package/dist/storage/node-sqlite.d.ts +1 -19
  174. package/dist/storage/node-sqlite.js +38 -32
  175. package/dist/storage/postgres-key-value-queries.d.ts +79 -0
  176. package/dist/storage/postgres-key-value-queries.js +63 -0
  177. package/dist/storage/resolve.d.ts +2 -165
  178. package/dist/storage/resolve.js +1 -1
  179. package/dist/storage/scoped-storage.js +1 -1
  180. package/dist/storage/storage-configuration.d.ts +209 -0
  181. package/dist/storage/storage-configuration.js +0 -0
  182. package/dist/storage/text-value-store.d.ts +13 -10
  183. package/dist/storage/turso.js +2 -2
  184. package/dist/storage/typed-storage.js +1 -1
  185. package/dist/storage/web-extension.js +1 -1
  186. package/dist/testing/event-loop.d.ts +36 -2
  187. package/dist/testing/index.d.ts +31 -1
  188. package/dist/testing/index.js +33 -33
  189. package/dist/version.d.ts +1 -1
  190. package/dist/version.js +1 -1
  191. package/dist/worker/index.js +9 -5
  192. package/dist/worker/long-poll.js +4 -0
  193. package/dist/worker/protocol-messages.d.ts +20 -0
  194. package/dist/worker/protocol-schemas.d.ts +32 -0
  195. package/dist/worker/protocol-schemas.js +8 -4
  196. package/dist/worker/protocol-task-result.d.ts +28 -0
  197. package/dist/worker/protocol-task-result.js +76 -0
  198. package/dist/worker/protocol.d.ts +4 -15
  199. package/dist/worker/protocol.js +1 -1
  200. package/dist/worker/registry/fair-share.d.ts +29 -0
  201. package/dist/worker/registry/fair-share.js +30 -0
  202. package/dist/worker/registry/routing.d.ts +18 -0
  203. package/dist/worker/registry/routing.js +14 -0
  204. package/dist/worker/registry/types.d.ts +7 -0
  205. package/dist/worker/registry.d.ts +16 -1
  206. package/dist/worker/registry.js +24 -36
  207. package/package.json +17 -4
@@ -1,8 +1,15 @@
1
1
  import { calculateBackoff } from "../scheduler.js";
2
- import { DEFAULT_RETRY_POLICY } from "../types.js";
3
- import { getInternals } from "./internals.js";
2
+ import {
3
+ DEFAULT_RETRY_POLICY
4
+ } from "../types.js";
5
+ import { getInternals, hasContextInternals } from "./internals.js";
4
6
  import { isActivityCallOptions } from "./session-state.js";
5
7
  import { captureCallerStack } from "./validation.js";
8
+ export function asConcreteContext(context) {
9
+ if (!hasContextInternals(context))
10
+ throw Error("Step-based workflows (compileStepWorkflow / ctx.step) require workflowExecutionMode: 'inline'. The worker execution strategy runs workflows with a different context that has no durable step machinery. Use the generator workflow API for worker execution mode.");
11
+ return context;
12
+ }
6
13
  const ACTIVITY_RETRY_STATE_LOCAL_KEY = "__weftActivityRetryState", ACTIVITY_RETRY_STATE_VERSION = 1, MAX_CHECKPOINTED_RETRY_ATTEMPT = 1e4;
7
14
  function acceptsNoActivityInput(fn) {
8
15
  if (typeof fn !== "function")
@@ -104,7 +111,9 @@ function completeActivityRetryAttempt(internals, step, completedRetrySleepCount)
104
111
  }
105
112
  };
106
113
  }
107
- function getActivityName(activity) {
114
+ function getActivityName(activity, explicitName) {
115
+ if (explicitName !== void 0)
116
+ return explicitName;
108
117
  return typeof activity === "string" ? activity : activity.name || "anonymous";
109
118
  }
110
119
  function getActivityFunction(activity) {
@@ -190,8 +199,8 @@ function prepareActivityRetryRequest(request, attempt) {
190
199
  attempt
191
200
  };
192
201
  }
193
- export function createRunActivityRequest(context, activity, rest) {
194
- const { input, options } = parseRunArguments(activity, rest), activityName = getActivityName(activity), activityFunction = getActivityFunction(activity), internals = getInternals(context), step = internals.stepIndex++, cachedRequest = getCachedRunActivityRequest(internals, step, activityName, input);
202
+ export function createRunActivityRequest(context, activity, rest, explicitName) {
203
+ const { input, options } = parseRunArguments(activity, rest), activityName = getActivityName(activity, explicitName), activityFunction = getActivityFunction(activity), internals = getInternals(context), step = internals.stepIndex++, cachedRequest = getCachedRunActivityRequest(internals, step, activityName, input);
195
204
  if (cachedRequest.hasCachedResult)
196
205
  return cachedRequest;
197
206
  const retryPolicy = resolveActivityRetryPolicy(activity, options);
@@ -203,8 +212,8 @@ export function createRunActivityRequest(context, activity, rest) {
203
212
  ...retryPolicy === void 0 ? {} : { retryPolicy }
204
213
  };
205
214
  }
206
- export function* runActivityWithRetry(context, activity, rest) {
207
- const { request, step, hasCachedResult, cachedResult, retryAttempt, retryPolicy } = createRunActivityRequest(context, activity, rest);
215
+ export function* runActivityWithRetry(context, activity, rest, explicitName) {
216
+ const { request, step, hasCachedResult, cachedResult, retryAttempt, retryPolicy } = createRunActivityRequest(context, activity, rest, explicitName);
208
217
  if (hasCachedResult)
209
218
  return cachedResult;
210
219
  const internals = getInternals(context);
@@ -16,6 +16,8 @@ function assignOptionalContextOptions(options, internals) {
16
16
  options.sleepReferenceTime = internals.sleepReferenceTime;
17
17
  if (internals.resolveWorkflowType !== void 0)
18
18
  options.resolveWorkflowType = internals.resolveWorkflowType;
19
+ if (internals.services !== void 0)
20
+ options.services = internals.services;
19
21
  }
20
22
  function createSpeculativeChildOptions(parent, internals) {
21
23
  const options = {
@@ -178,4 +178,10 @@ export interface ContextOptions {
178
178
  * it is not persisted or restored after engine restart.
179
179
  */
180
180
  registerCancelHandler?: (handler: () => Promise<void> | void) => () => void;
181
+ /**
182
+ * Host-supplied, per-run capabilities exposed as `ctx.services`. Never
183
+ * checkpointed; held only for this run and re-provided on recovery via the
184
+ * engine's `resolveWorkflowServices` resolver.
185
+ */
186
+ services?: unknown;
181
187
  }
@@ -178,6 +178,7 @@ function buildBaseWorkflowDeleteKeys(state) {
178
178
  KEYS.checkpoint(state.id),
179
179
  KEYS.workflowHeaders(state.id),
180
180
  KEYS.terminalCleanupNeeded(state.id),
181
+ KEYS.workflowHasServices(state.id),
181
182
  KEYS.attribute(state.id),
182
183
  KEYS.terminalWorkflow(state.updatedAt, state.id)
183
184
  ]);
@@ -17,7 +17,7 @@ import { BULK_OPERATION_BATCH_SIZE } from "./listing.js";
17
17
  import { loadWorkflowState } from "./storage-io.js";
18
18
  import { isTerminalWorkflowStatus } from "./validation.js";
19
19
  export { purgeInternal, TERMINAL_CLEANUP_DELAY_MS } from "./bulk-operations-purge.js";
20
- const ACTIVE_WORKFLOW_STATUSES = ["pending", "running"];
20
+ const ACTIVE_WORKFLOW_STATUSES = ["pending", "running", "suspended"];
21
21
  export async function purge(internals, filter, cleanupWaiters) {
22
22
  return purgeInternal(internals, filter, { expiredOnly: !1, now: internals.options.getNow() }, cleanupWaiters);
23
23
  }
@@ -102,7 +102,8 @@ export function createTimeOperationCallbacks(engine) {
102
102
  parseStartOptionDuration: (value, fieldName) => parseStartOptionDuration(getInternals(engine), value, fieldName, createLifecycleCallbacks(engine)),
103
103
  runDeferredTerminalCleanup: (workflowId, timerId) => runDeferredTerminalCleanup(getInternals(engine), workflowId, timerId, createTerminationCallbacks(engine)),
104
104
  handleScheduleTimer: (entry) => handleScheduleTimerForEngine(engine, entry),
105
- timeout: (workflowId) => engine.timeout(workflowId)
105
+ timeout: (workflowId) => engine.timeout(workflowId),
106
+ handleCleanupError: (source, error, workflowId) => createTerminationCallbacks(engine).handleCleanupError(source, error, workflowId)
106
107
  };
107
108
  }
108
109
  export function createUpdateCallbacks(engine) {
@@ -74,7 +74,8 @@ export function createLifecycleCallbacks(engine) {
74
74
  hasLocalCheckpointOwnership: (workflowId, workflowStatus) => hasLocalCheckpointOwnership(getInternals(engine), workflowId, workflowStatus),
75
75
  handleCleanupError: (source, error, workflowId) => createTerminationCallbacks(engine).handleCleanupError(source, error, workflowId),
76
76
  swallowPromiseRejection: (promise) => swallowPromiseRejection(promise),
77
- enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON)
77
+ enforceHistoryCircuitBreaker: (workflowId) => terminateWorkflow(getInternals(engine), workflowId, "timed-out", createTerminationCallbacks(engine), HISTORY_CIRCUIT_BREAKER_REASON),
78
+ failWorkflowForUnavailableServices: (workflowId, error) => failWorkflow(getInternals(engine), workflowId, error, createTerminationCallbacks(engine), "system")
78
79
  };
79
80
  }
80
81
  export function createTerminationCallbacksWith(engine, handleScheduledWorkflowTerminal) {
@@ -18,7 +18,6 @@ export type EngineCreateRuntimeOptions = EngineConstructorOptions & {
18
18
  workflows?: Record<string, AnyWorkflowDefinition> | undefined;
19
19
  recover?: boolean | undefined;
20
20
  acknowledgeUnknownWorkflowTypes?: boolean | undefined;
21
- allowLegacyData?: boolean | undefined;
22
21
  };
23
22
  export type NormalizedWorkerExecutionConfiguration = {
24
23
  mode: 'inline';
@@ -54,6 +53,7 @@ export declare function createExecutionStrategyBundle(parameters: {
54
53
  getComposedWorkflowInterceptor?: () => ComposedWorkflowInterceptor | null;
55
54
  resolveWorkflowType: (target: string | Function) => string;
56
55
  registerCancelHandler?: (workflowId: string, handler: () => Promise<void> | void) => () => void;
56
+ getWorkflowServices?: (workflowId: string) => unknown;
57
57
  }): ExecutionStrategyBundle;
58
58
  export declare function createActivityWorkerDispatcher(activityExecution: EngineConstructorOptions['activityExecution']): ActivityWorkerDispatcher | null;
59
59
  export declare function createAlertManagerForEngine(engine: EventTarget, alerts: EngineOptions['alerts'] | undefined, getNow: () => number): AlertManager | null;
@@ -103,14 +103,24 @@ function resolveHistoryFields(options) {
103
103
  payloadSizePolicy: normalizePayloadSizePolicy(options?.payloadSize, "options.payloadSize")
104
104
  };
105
105
  }
106
+ const DEFAULT_SECOND_INSTANCE_HEARTBEAT_INTERVAL_MS = 15000;
107
+ function resolveSecondInstanceFields(options) {
108
+ const enabled = defaultTo(options?.detectSecondInstance, !1), intervalMs = enabled ? normalizeRetentionDuration(options?.secondInstanceHeartbeatInterval, "options.secondInstanceHeartbeatInterval") ?? DEFAULT_SECOND_INSTANCE_HEARTBEAT_INTERVAL_MS : DEFAULT_SECOND_INSTANCE_HEARTBEAT_INTERVAL_MS;
109
+ return {
110
+ secondInstanceDetectionEnabled: enabled,
111
+ secondInstanceHeartbeatIntervalMs: intervalMs
112
+ };
113
+ }
106
114
  export function resolveEngineOptions(storage, options, getNow) {
107
115
  return {
108
116
  storage,
109
117
  getNow,
118
+ resolveWorkflowServices: options?.resolveWorkflowServices ?? null,
110
119
  ...resolveBooleanDefaults(options),
111
120
  ...resolveNumericDefaults(options),
112
121
  ...resolveRetentionFields(options),
113
- ...resolveHistoryFields(options)
122
+ ...resolveHistoryFields(options),
123
+ ...resolveSecondInstanceFields(options)
114
124
  };
115
125
  }
116
126
  export function normalizeWorkerExecutionConfiguration(options) {
@@ -169,7 +179,8 @@ export function createExecutionStrategyBundle(parameters) {
169
179
  getRegistration,
170
180
  getComposedWorkflowInterceptor,
171
181
  resolveWorkflowType,
172
- registerCancelHandler
182
+ registerCancelHandler,
183
+ getWorkflowServices
173
184
  } = parameters, workerExecutionConfiguration = normalizeWorkerExecutionConfiguration(options);
174
185
  if (workerExecutionConfiguration.mode === "worker") {
175
186
  const pool = new WorkerPool({
@@ -195,7 +206,8 @@ export function createExecutionStrategyBundle(parameters) {
195
206
  maxNestingDepth,
196
207
  development,
197
208
  resolveWorkflowType,
198
- ...registerCancelHandler !== void 0 && { registerCancelHandler }
209
+ ...registerCancelHandler !== void 0 && { registerCancelHandler },
210
+ ...getWorkflowServices !== void 0 && { getWorkflowServices }
199
211
  });
200
212
  return { strategy: inlineStrategy, inlineStrategy };
201
213
  }
@@ -22,6 +22,7 @@ export function disposeEngine(internals) {
22
22
  internals.retentionSweepInterval = null;
23
23
  }
24
24
  internals.nextRetentionSweepAt = null;
25
+ disposeSecondInstanceDetection(internals);
25
26
  internals.handleCache.clear();
26
27
  for (const waiter of internals.resultResolvers.values())
27
28
  waiter.reject(new EngineDisposedError);
@@ -43,6 +44,7 @@ export function disposeEngine(internals) {
43
44
  internals.checkpoints.clear();
44
45
  internals.pendingExecutionStateOwnerId = void 0;
45
46
  internals.workflowNestingDepths.clear();
47
+ internals.workflowServices.clear();
46
48
  internals.workflowHeaders.clear();
47
49
  internals.pendingStarts.clear();
48
50
  internals.pendingScheduleCreations.clear();
@@ -56,3 +58,13 @@ export function disposeEngine(internals) {
56
58
  internals.broadcastChannel?.close();
57
59
  internals.broadcastChannel = null;
58
60
  }
61
+ function disposeSecondInstanceDetection(internals) {
62
+ if (internals.secondInstanceDetectionInterval !== null) {
63
+ clearInterval(internals.secondInstanceDetectionInterval ?? void 0);
64
+ internals.secondInstanceDetectionInterval = null;
65
+ }
66
+ if (internals.secondInstanceDetector !== null) {
67
+ internals.secondInstanceDetector.stop();
68
+ internals.secondInstanceDetector = null;
69
+ }
70
+ }
@@ -29,20 +29,6 @@ export type EngineCreateOptions<TWorkflowDefinitions extends Record<string, AnyW
29
29
  workflows?: TWorkflowDefinitions;
30
30
  /** Activity definitions to register before workflows. */
31
31
  activities?: TActivityDefinitions;
32
- /**
33
- * Opt out of the persisted-data schema-version gate when opening a storage
34
- * that contains workflow data written by `new Engine({ storage })` before
35
- * the schema-version sentinel existed.
36
- *
37
- * The gate normally rejects any storage that already holds workflow records
38
- * but lacks the sentinel — a safety check against silently classifying
39
- * pre-sentinel data as schema-current. Set `allowLegacyData: true` only when
40
- * you knowingly opened the same storage with the legacy constructor path and
41
- * are intentionally recovering it via `Engine.create`. On success, the
42
- * current schema-version sentinel is written. Remove the opt-in once the
43
- * data has been migrated.
44
- */
45
- allowLegacyData?: boolean;
46
32
  } & ({
47
33
  /**
48
34
  * Recover stored running workflows after registration. Defaults to
@@ -29,7 +29,16 @@ export interface ResolvedOptions {
29
29
  /** Operator-supplied sink for compacted event-log ranges; `null` when none. */
30
30
  archiveAdapter: ArchiveAdapter | null;
31
31
  payloadSizePolicy: NormalizedPayloadSizePolicy;
32
+ /** Whether the best-effort second-instance liveness detector is enabled. */
33
+ secondInstanceDetectionEnabled: boolean;
34
+ /** Heartbeat interval (ms) for the second-instance detector when enabled. */
35
+ secondInstanceHeartbeatIntervalMs: number;
32
36
  getNow: () => number;
37
+ /**
38
+ * Re-provides the non-serialized per-run `services` value on recovery; `null`
39
+ * when the engine was created without `resolveWorkflowServices`.
40
+ */
41
+ resolveWorkflowServices: EngineOptions['resolveWorkflowServices'] | null;
33
42
  }
34
43
  export interface WorkflowResultWaiter {
35
44
  promise: Promise<unknown>;
@@ -56,4 +65,12 @@ export type QueuedInlineWorkflowExecutionStart = {
56
65
  nestingDepth: number;
57
66
  executionDeadline: number | undefined;
58
67
  executionStateOwnerId: string;
68
+ /**
69
+ * Liveness callback invoked once this queued start has actually begun
70
+ * executing (its generator has been driven). Set only for `defer: false`
71
+ * launches, which await it before `engine.start()` resolves. Fired exactly
72
+ * once; a discarded start (engine disposed without draining) settles it via
73
+ * the dispose path so the `defer: false` awaiter never hangs.
74
+ */
75
+ onStarted?: () => void;
59
76
  };
@@ -6,6 +6,12 @@
6
6
  export type EngineCleanupIntervalDisposalTracker = {
7
7
  disposed: boolean;
8
8
  cleanupInterval: ReturnType<typeof setInterval> | null;
9
+ /**
10
+ * The optional second-instance detector interval, tracked alongside the
11
+ * cleanup interval so the same finalizer clears it when an engine is
12
+ * garbage-collected without `[Symbol.dispose]()`. Null when detection is off.
13
+ */
14
+ secondInstanceDetectionInterval: ReturnType<typeof setInterval> | null;
9
15
  testToken: symbol | undefined;
10
16
  };
11
17
  export declare const engineCleanupIntervalFinalizer: FinalizationRegistry<EngineCleanupIntervalDisposalTracker>;
@@ -6,6 +6,10 @@ export const engineCleanupIntervalFinalizer = new FinalizationRegistry((tracker)
6
6
  clearInterval(tracker.cleanupInterval);
7
7
  tracker.cleanupInterval = null;
8
8
  }
9
+ if (tracker.secondInstanceDetectionInterval !== null) {
10
+ clearInterval(tracker.secondInstanceDetectionInterval);
11
+ tracker.secondInstanceDetectionInterval = null;
12
+ }
9
13
  if (!tracker.disposed && shouldEmitEngineLeakWarning()) {
10
14
  if (tracker.testToken !== void 0)
11
15
  engineLeakWarningTokensForTesting.add(tracker.testToken);
@@ -2,7 +2,24 @@ import type { AnyActivityDefinition } from '../types.ts';
2
2
  import { type EngineCleanupIntervalDisposalTracker } from './engine-leak-warnings.ts';
3
3
  import type { Engine } from './index.ts';
4
4
  import { type EngineInternals } from './internals.ts';
5
+ import type { SecondInstanceDetector } from './second-instance-detector.ts';
5
6
  export declare function isActivityDefinition(value: unknown): value is AnyActivityDefinition;
6
7
  export declare function createQueuedInlineWorkflowStartHandler<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>, channel: MessageChannel): () => void;
8
+ /**
9
+ * Drain pending inline launches for `engine` before teardown. Built with the
10
+ * same inline-launch-queue callbacks as the scheduled flush handler so a drained
11
+ * start advances identically to a normally-flushed one. Called from
12
+ * `[Symbol.asyncDispose]` ahead of synchronous disposal (which aborts the signal
13
+ * and would otherwise discard the queue).
14
+ */
15
+ export declare function drainQueuedInlineWorkflowStartsForEngine<TWorkflows extends object, TActivities extends object>(engine: Engine<TWorkflows, TActivities>): Promise<void>;
7
16
  export declare function createCleanupIntervalTick<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>, tracker: EngineCleanupIntervalDisposalTracker): () => void;
17
+ /**
18
+ * Build the resolver the second-instance detection interval uses to find its live
19
+ * detector. Returns `null` when the engine has been garbage-collected or disposed
20
+ * (so the tick is skipped), otherwise the engine's current detector. Extracted
21
+ * here — like {@link createCleanupIntervalTick} — so the deref/disposed guard is
22
+ * directly testable without driving a real interval.
23
+ */
24
+ export declare function createSecondInstanceDetectorResolver<TWorkflows extends object, TActivities extends object>(weakEngine: WeakRef<Engine<TWorkflows, TActivities>>): () => SecondInstanceDetector | null;
8
25
  export declare function disposeEngineCleanupInterval(internals: EngineInternals): void;
@@ -3,12 +3,22 @@ import { createLifecycleCallbacks, createTerminationCallbacks } from "./callback
3
3
  import {
4
4
  engineCleanupIntervalFinalizer
5
5
  } from "./engine-leak-warnings.js";
6
- import { flushQueuedInlineWorkflowStarts } from "./inline-launch-queue.js";
6
+ import {
7
+ drainQueuedInlineWorkflowStarts,
8
+ flushQueuedInlineWorkflowStarts
9
+ } from "./inline-launch-queue.js";
7
10
  import { getInternals } from "./internals.js";
8
11
  import { swallowPromiseRejection } from "./strategy-helpers.js";
9
12
  export function isActivityDefinition(value) {
10
13
  return typeof value === "function" && typeof value.name === "string" && "execute" in value && typeof value.execute === "function";
11
14
  }
15
+ function inlineLaunchQueueCallbacksForEngine(engine) {
16
+ const lifecycleCallbacks = createLifecycleCallbacks(engine);
17
+ return {
18
+ processPendingUpdatesAfterInlineAdvance: (workflowId) => lifecycleCallbacks.processPendingUpdatesAfterInlineAdvance(workflowId),
19
+ swallowPromiseRejection: (promise) => swallowPromiseRejection(promise)
20
+ };
21
+ }
12
22
  export function createQueuedInlineWorkflowStartHandler(weakEngine, channel) {
13
23
  return function handleQueuedInlineWorkflowStart() {
14
24
  const engine = weakEngine.deref();
@@ -18,12 +28,12 @@ export function createQueuedInlineWorkflowStartHandler(weakEngine, channel) {
18
28
  return;
19
29
  }
20
30
  getInternals(engine).queuedInlineWorkflowStartFlushScheduled = !1;
21
- swallowPromiseRejection(flushQueuedInlineWorkflowStarts(getInternals(engine), {
22
- processPendingUpdatesAfterInlineAdvance: (workflowId) => createLifecycleCallbacks(engine).processPendingUpdatesAfterInlineAdvance(workflowId),
23
- swallowPromiseRejection: (promise) => swallowPromiseRejection(promise)
24
- }));
31
+ swallowPromiseRejection(flushQueuedInlineWorkflowStarts(getInternals(engine), inlineLaunchQueueCallbacksForEngine(engine)));
25
32
  };
26
33
  }
34
+ export async function drainQueuedInlineWorkflowStartsForEngine(engine) {
35
+ await drainQueuedInlineWorkflowStarts(getInternals(engine), inlineLaunchQueueCallbacksForEngine(engine));
36
+ }
27
37
  export function createCleanupIntervalTick(weakEngine, tracker) {
28
38
  return function cleanupExpiredResponsesForLiveEngine() {
29
39
  const engine = weakEngine.deref();
@@ -38,6 +48,17 @@ export function createCleanupIntervalTick(weakEngine, tracker) {
38
48
  createExpiredResponseCleanupTick(internals.updateCoordinator, (source, error) => createTerminationCallbacks(engine).handleCleanupError(source, error))();
39
49
  };
40
50
  }
51
+ export function createSecondInstanceDetectorResolver(weakEngine) {
52
+ return function resolveLiveSecondInstanceDetector() {
53
+ const engine = weakEngine.deref();
54
+ if (engine === void 0)
55
+ return null;
56
+ const internals = getInternals(engine);
57
+ if (internals.disposed)
58
+ return null;
59
+ return internals.secondInstanceDetector;
60
+ };
61
+ }
41
62
  export function disposeEngineCleanupInterval(internals) {
42
63
  if (internals.cleanupInterval !== null) {
43
64
  clearInterval(internals.cleanupInterval ?? void 0);
@@ -1,3 +1,4 @@
1
+ import type { WorkflowStatus } from '../types/identity.ts';
1
2
  import { WeftError } from '../weft-error.ts';
2
3
  /**
3
4
  * Thrown by {@link Engine.start} when a workflow with the requested ID already
@@ -186,6 +187,32 @@ export declare class WorkflowNotRegisteredError extends WeftError<'WorkflowNotRe
186
187
  readonly workflowType: string;
187
188
  constructor(workflowType: string);
188
189
  }
190
+ /**
191
+ * Thrown by {@link Engine.suspend} when invoked on an engine running in worker
192
+ * execution mode. Suspension parks the live run without aborting it, which the
193
+ * inline strategy supports directly; a worker run cannot be paused without
194
+ * sending it a cancellation, so the engine refuses rather than silently
195
+ * aborting. Suspend/resume in worker mode is a scoped follow-up.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * import { Engine, WorkflowSuspendNotSupportedError } from '@lostgradient/weft';
200
+ *
201
+ * async function trySuspend(engine: Engine, id: string) {
202
+ * try {
203
+ * await engine.suspend(id);
204
+ * } catch (err) {
205
+ * if (err instanceof WorkflowSuspendNotSupportedError) {
206
+ * // worker-mode engine: cancel instead, or run inline
207
+ * }
208
+ * }
209
+ * }
210
+ * void trySuspend;
211
+ * ```
212
+ */
213
+ export declare class WorkflowSuspendNotSupportedError extends WeftError<'WorkflowSuspendNotSupportedError'> {
214
+ constructor(message: string);
215
+ }
189
216
  /**
190
217
  * Thrown when the engine cannot resolve an activity name to a registered
191
218
  * function during dispatch. The per-workflow {@link ActivityRegistry} (built
@@ -212,4 +239,51 @@ export declare class ActivityResolutionError extends WeftError<'ActivityResoluti
212
239
  readonly activityName: string;
213
240
  constructor(workflowType: string, activityName: string);
214
241
  }
242
+ /**
243
+ * Thrown by {@link Engine.startOrSignal} when the target workflow already exists
244
+ * and is in a terminal state (completed, failed, cancelled, or timed out). A
245
+ * terminal run cannot accept a signal and must not be silently replaced, so
246
+ * `startOrSignal` surfaces this conflict instead of starting a fresh run under
247
+ * the same id or dropping the signal.
248
+ *
249
+ * Inspect `workflowId` to identify the conflicting run and `status` to see why
250
+ * it was rejected. To deliberately start a new run, choose a different id (or
251
+ * idempotency key); to deliver to a fresh run, terminal-state reuse is not a
252
+ * supported policy.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * import { StartOrSignalConflictError } from '@lostgradient/weft';
257
+ *
258
+ * function isTerminalStartOrSignalConflict(error: unknown): boolean {
259
+ * return error instanceof StartOrSignalConflictError;
260
+ * }
261
+ * ```
262
+ */
263
+ export declare class StartOrSignalConflictError extends WeftError<'StartOrSignalConflictError'> {
264
+ readonly workflowId: string;
265
+ readonly status: WorkflowStatus;
266
+ constructor(workflowId: string, status: WorkflowStatus);
267
+ }
268
+ /**
269
+ * Thrown by {@link Engine.start} / {@link Engine.startOrSignal} when an
270
+ * `idempotencyKey` resolves to a workflow that no longer exists — its run was
271
+ * purged or bulk-deleted while the durable `start-idem:` mapping (intentionally
272
+ * not swept on cleanup) lived on. The key is "spent": it can neither return the
273
+ * gone run nor safely start a fresh one under the same key (a concurrent caller
274
+ * may still hold the mapping). Use a different idempotency key to start anew.
275
+ *
276
+ * @example
277
+ * ```ts
278
+ * import { IdempotencyKeyPurgedError } from '@lostgradient/weft';
279
+ *
280
+ * function isPurgedIdempotencyKey(error: unknown): boolean {
281
+ * return error instanceof IdempotencyKeyPurgedError;
282
+ * }
283
+ * ```
284
+ */
285
+ export declare class IdempotencyKeyPurgedError extends WeftError<'IdempotencyKeyPurgedError'> {
286
+ readonly workflowId: string;
287
+ constructor(workflowId: string);
288
+ }
215
289
  export { PersistedDataIncompatibleError } from '../persisted-data-incompatible-error.ts';
@@ -78,13 +78,37 @@ export class WorkflowNotRegisteredError extends WeftError {
78
78
  }
79
79
  }
80
80
 
81
+ export class WorkflowSuspendNotSupportedError extends WeftError {
82
+ constructor(message) {
83
+ super("WorkflowSuspendNotSupportedError", message);
84
+ }
85
+ }
86
+
81
87
  export class ActivityResolutionError extends WeftError {
82
88
  workflowType;
83
89
  activityName;
84
90
  constructor(workflowType, activityName) {
85
- super("ActivityResolutionError", `No activity registered with name "${activityName}" for workflow type "${workflowType}". Register the activity via \`workflow({ name }).activities({ ... })\` on the workflow that runs it, or via \`engine.register(activityDefinition)\` for the legacy global registry.`);
91
+ super("ActivityResolutionError", `No activity registered with name "${activityName}" for workflow type "${workflowType}". Register the activity via \`workflow({ name }).activities({ ... })\` on the workflow that runs it, or via \`engine.register(activityDefinition)\` to make it available to every workflow on that engine instance.`);
86
92
  this.workflowType = workflowType;
87
93
  this.activityName = activityName;
88
94
  }
89
95
  }
96
+
97
+ export class StartOrSignalConflictError extends WeftError {
98
+ workflowId;
99
+ status;
100
+ constructor(workflowId, status) {
101
+ super("StartOrSignalConflictError", `Workflow "${workflowId}" is already in terminal state "${status}" and cannot accept a startOrSignal: a terminal run cannot be signalled and is not replaced. Use a different id or idempotency key to start a new run.`);
102
+ this.workflowId = workflowId;
103
+ this.status = status;
104
+ }
105
+ }
106
+
107
+ export class IdempotencyKeyPurgedError extends WeftError {
108
+ workflowId;
109
+ constructor(workflowId) {
110
+ super("IdempotencyKeyPurgedError", `The idempotency key maps to workflow "${workflowId}", which no longer exists (it was purged or deleted). Use a different idempotency key to start a new run.`);
111
+ this.workflowId = workflowId;
112
+ }
113
+ }
90
114
  export { PersistedDataIncompatibleError } from "../persisted-data-incompatible-error.js";
@@ -35,7 +35,7 @@ export async function bootstrapWorkflowResultResolver(internals, workflowId, wai
35
35
  waiter.reject(Error(`Workflow "${workflowId}" not found in storage`));
36
36
  return;
37
37
  }
38
- if (state.status === "running" || state.status === "pending")
38
+ if (state.status === "running" || state.status === "pending" || state.status === "suspended")
39
39
  return;
40
40
  try {
41
41
  const result = await loadWorkflowResult(internals, workflowId);