@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
@@ -4,7 +4,7 @@ import {
4
4
  PersistedDataIncompatibleError
5
5
  } from "../persisted-data-incompatible-error.js";
6
6
  const SCHEMA_VERSION_PATTERN = /^(?:0|[1-9]\d*)$/, USER_DATA_PREFIXES = ["wf:", "op:", "schedule:", "ev:", "sig:", "upd:", "idx:"];
7
- export async function assertCompatiblePersistedDataVersion(storage, options = {}) {
7
+ export async function assertCompatiblePersistedDataVersion(storage) {
8
8
  const raw = await storage.get(PERSISTED_DATA_SCHEMA_VERSION_KEY);
9
9
  if (raw !== null) {
10
10
  const text = new TextDecoder().decode(raw);
@@ -17,9 +17,8 @@ export async function assertCompatiblePersistedDataVersion(storage, options = {}
17
17
  throw new PersistedDataIncompatibleError(parsed, CURRENT_PERSISTED_DATA_SCHEMA_VERSION);
18
18
  return;
19
19
  }
20
- if (!options.allowLegacyData)
21
- for (const prefix of USER_DATA_PREFIXES)
22
- for await (const _entry of storage.scan(prefix, { limit: 1 }))
23
- throw new PersistedDataIncompatibleError(null, CURRENT_PERSISTED_DATA_SCHEMA_VERSION);
20
+ for (const prefix of USER_DATA_PREFIXES)
21
+ for await (const _entry of storage.scan(prefix, { limit: 1 }))
22
+ throw new PersistedDataIncompatibleError(null, CURRENT_PERSISTED_DATA_SCHEMA_VERSION);
24
23
  await storage.put(PERSISTED_DATA_SCHEMA_VERSION_KEY, new TextEncoder().encode(String(CURRENT_PERSISTED_DATA_SCHEMA_VERSION)));
25
24
  }
@@ -0,0 +1,45 @@
1
+ import type { ScheduleSpec, ScheduleSummary } from '../types.ts';
2
+ /**
3
+ * Narrow engine view a {@link ScheduleHandle} delegates to. The full
4
+ * {@link Engine} implements it; the handle only depends on these schedule
5
+ * lifecycle operations.
6
+ */
7
+ export interface ScheduleHandleEngine {
8
+ pauseSchedule(scheduleId: string): Promise<void>;
9
+ resumeSchedule(scheduleId: string): Promise<void>;
10
+ cancelSchedule(scheduleId: string): Promise<void>;
11
+ updateSchedule(scheduleId: string, newSpec: string | ScheduleSpec): Promise<void>;
12
+ getSchedule(scheduleId: string): Promise<ScheduleSummary | null>;
13
+ }
14
+ /**
15
+ * Handle to a recurring schedule created by {@link Engine.schedule}. Use
16
+ * `handle.pause()`, `handle.resume()`, `handle.cancel()`, or
17
+ * `handle.update(cronExpression)` to manage the schedule lifecycle.
18
+ * `handle.describe()` returns the current {@link ScheduleSummary}.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { workflow, Engine, ScheduleHandle } from '@lostgradient/weft';
23
+ *
24
+ * const engine = new Engine();
25
+ * engine.register(workflow({ name: 'daily-report' }).execute(async function* () { return 'ok'; }));
26
+ *
27
+ * const handle = await engine.schedule('daily-report', null, '0 9 * * *');
28
+ * const typedHandle: ScheduleHandle = handle;
29
+ * await handle.pause();
30
+ * const summary = await handle.describe();
31
+ * void typedHandle;
32
+ * console.log(summary.status); // 'paused'
33
+ * await handle.cancel();
34
+ * ```
35
+ */
36
+ export declare class ScheduleHandle {
37
+ #private;
38
+ readonly id: string;
39
+ constructor(id: string, engine: ScheduleHandleEngine);
40
+ pause(): Promise<void>;
41
+ resume(): Promise<void>;
42
+ cancel(): Promise<void>;
43
+ update(newSpec: string | ScheduleSpec): Promise<void>;
44
+ describe(): Promise<ScheduleSummary>;
45
+ }
@@ -0,0 +1,26 @@
1
+ export class ScheduleHandle {
2
+ id;
3
+ #engine;
4
+ constructor(id, engine) {
5
+ this.id = id;
6
+ this.#engine = engine;
7
+ }
8
+ async pause() {
9
+ await this.#engine.pauseSchedule(this.id);
10
+ }
11
+ async resume() {
12
+ await this.#engine.resumeSchedule(this.id);
13
+ }
14
+ async cancel() {
15
+ await this.#engine.cancelSchedule(this.id);
16
+ }
17
+ async update(newSpec) {
18
+ await this.#engine.updateSchedule(this.id, newSpec);
19
+ }
20
+ async describe() {
21
+ const schedule = await this.#engine.getSchedule(this.id);
22
+ if (!schedule)
23
+ throw Error(`Schedule "${this.id}" not found`);
24
+ return schedule;
25
+ }
26
+ }
@@ -1,7 +1,7 @@
1
1
  import type { BatchOperation } from '../../storage/interface.ts';
2
2
  import type { PaginatedResult, ScheduleFilter, ScheduleOptions, ScheduleSpec, ScheduleState, ScheduleSummary, WorkflowState } from '../types.ts';
3
- import { ScheduleHandle } from './handles.ts';
4
3
  import type { EngineInternals } from './internals.ts';
4
+ import { ScheduleHandle } from './schedule-handle.ts';
5
5
  export { handleScheduleTimer } from './schedule-timer.ts';
6
6
  export type RefreshedScheduleState = {
7
7
  state: ScheduleState;
@@ -1,7 +1,7 @@
1
1
  import { KEYS } from "../../storage/interface.js";
2
2
  import { decode, encode } from "../codec.js";
3
3
  import { WorkflowNotRegisteredError } from "./errors.js";
4
- import { ScheduleHandle } from "./handles.js";
4
+ import { ScheduleHandle } from "./schedule-handle.js";
5
5
  import { getNextScheduleOccurrence } from "./schedule-occurrence.js";
6
6
  import {
7
7
  clearScheduleCurrentWorkflow,
@@ -123,11 +123,15 @@ export async function updateSchedule(internals, scheduleId, newSpec) {
123
123
  };
124
124
  await writeScheduleState(internals, updatedState, { includeTimer: state.status === "active" });
125
125
  }
126
+ function scheduledRunOccupiesSlot(currentWorkflowState) {
127
+ const status = currentWorkflowState?.status;
128
+ return status === "running" || status === "pending" || status === "suspended";
129
+ }
126
130
  export async function refreshScheduledWorkflowState(internals, state, callbacks) {
127
131
  if (!state.currentWorkflowId)
128
132
  return { state, currentWorkflowState: null };
129
133
  const currentWorkflowState = await callbacks.loadWorkflowState(state.currentWorkflowId);
130
- if (currentWorkflowState?.status === "running" || currentWorkflowState?.status === "pending")
134
+ if (scheduledRunOccupiesSlot(currentWorkflowState))
131
135
  return { state, currentWorkflowState };
132
136
  await internals.storage.delete(KEYS.scheduleRun(state.currentWorkflowId));
133
137
  return {
@@ -141,7 +145,7 @@ export async function startScheduledRun(_internals, state, callbacks) {
141
145
  return workflowId;
142
146
  }
143
147
  function hasActiveScheduledWorkflow(currentWorkflowState) {
144
- return currentWorkflowState?.status === "running" || currentWorkflowState?.status === "pending";
148
+ return scheduledRunOccupiesSlot(currentWorkflowState);
145
149
  }
146
150
  async function applyBlockedScheduleOccurrence(state, hasActiveWorkflow, callbacks) {
147
151
  if (!hasActiveWorkflow)
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Best-effort, warn-only detection of a SECOND engine instance writing to the
3
+ * same durable store — a smoke alarm for singleton-deployment misconfiguration
4
+ * (an autoscaler accidentally set above one replica, or overlapping rolling
5
+ * deploys), NOT a correctness mechanism.
6
+ *
7
+ * **This is liveness, not fencing.** Weft's supported model is one engine process
8
+ * per durable store (see the recovery-and-deploys guide); fenced ownership is a
9
+ * future `MultiEngine` capability that does not exist yet. This detector never
10
+ * blocks boot, gates recovery, refuses a write, or claims ownership. It only
11
+ * observes whether another instance's heartbeat is *advancing while this instance
12
+ * is also running* and emits a warning if so. It does not prevent duplicate
13
+ * execution — infrastructure-level enforcement (`replicas: 1` + a `Recreate`
14
+ * deploy strategy, or a single systemd unit) is the real control.
15
+ *
16
+ * **Why liveness, not a boot check.** A boot-time check cannot distinguish a
17
+ * rolling-deploy handoff from a genuine second instance: both leave a recent
18
+ * heartbeat record in the store. Only observing a *foreign* heartbeat advance
19
+ * across several of our own ticks separates a live peer (autoscaling=2 → both
20
+ * heartbeats advance forever → both warn) from a dead-but-recent predecessor (a
21
+ * clean `Recreate` deploy → old heartbeat never advances after handoff → quiet).
22
+ *
23
+ * **Advance is measured by sequence, not wall clock.** Each heartbeat carries a
24
+ * per-instance monotonic `sequence`; a peer counts as advancing only when its
25
+ * `sequence` grows between two of *our* ticks. A peer's sequence cannot increase
26
+ * unless it is alive and ticking in our own time frame, so detection never
27
+ * compares clocks across hosts — it stays correct even if a peer's clock is
28
+ * frozen, skewed, or stepped backward. `heartbeatAt` exists only for the boot
29
+ * staleness sweep that garbage-collects long-dead instances' keys.
30
+ *
31
+ * Each engine writes its own heartbeat under `liveness:<instanceId>` and scans
32
+ * the `liveness:` prefix to observe peers. Per-instance keys (not one shared,
33
+ * clobbered key) keep every heartbeat independently observable and the sequence
34
+ * monotonic per writer.
35
+ *
36
+ * @module core/engine/second-instance-detector
37
+ */
38
+ import { type Storage } from '../../storage/interface.ts';
39
+ import type { EngineCleanupIntervalDisposalTracker } from './engine-leak-warnings.ts';
40
+ /** Options for {@link createSecondInstanceDetector}. */
41
+ export type SecondInstanceDetectorOptions = {
42
+ storage: Storage;
43
+ /** This engine's unique instance id. */
44
+ instanceId: string;
45
+ /** Wall-clock source (ms), injected so tests can advance time deterministically. */
46
+ getNow: () => number;
47
+ /**
48
+ * Heartbeat interval in ms. The staleness window is derived from this, so the
49
+ * interval also sets how long a deploy overlap must last before it warns.
50
+ */
51
+ intervalMs: number;
52
+ /**
53
+ * Emit a warning. Defaults to `process.emitWarning(message, WARNING_NAME)`,
54
+ * so the emitted `Warning.name` is `WeftSecondInstanceWarning` and consumers
55
+ * can filter on `warning.name` rather than scraping the message. Injected for
56
+ * tests; the seam is message-only because the name is a fixed constant.
57
+ */
58
+ warn?: (message: string) => void;
59
+ };
60
+ /**
61
+ * The `name` of the emitted warning. A stable, filterable identifier on the
62
+ * `Warning` object — consumers subscribe to the process `warning` event and
63
+ * match `warning.name === WARNING_NAME` (see the singleton-deployment guide).
64
+ */
65
+ export declare const SECOND_INSTANCE_WARNING_NAME = "WeftSecondInstanceWarning";
66
+ /**
67
+ * A running detector. `tick()` runs one heartbeat round (exposed for tests and
68
+ * driven by an interval in production); `stop()` clears the interval and
69
+ * best-effort removes this instance's heartbeat so the next boot starts quiet.
70
+ */
71
+ export type SecondInstanceDetector = {
72
+ /** Run one heartbeat round: observe peers, then write our own heartbeat. */
73
+ tick(): Promise<void>;
74
+ /** Stop the interval and best-effort delete this instance's heartbeat key. */
75
+ stop(): Promise<void>;
76
+ };
77
+ /**
78
+ * Create a best-effort second-instance detector. Does not start an interval
79
+ * itself — the engine owns timer lifecycle so disposal can clear it through the
80
+ * same path as its other intervals. Call {@link SecondInstanceDetector.tick} on
81
+ * an interval and {@link SecondInstanceDetector.stop} on dispose.
82
+ */
83
+ export declare function createSecondInstanceDetector(options: SecondInstanceDetectorOptions): SecondInstanceDetector;
84
+ /**
85
+ * Build the `setInterval` callback that drives a detector tick. `resolveDetector`
86
+ * returns the live detector, or `null` when the engine has been garbage-collected
87
+ * or disposed. When it returns `null` the tick SELF-CLEARS its own interval (via
88
+ * `tracker.secondInstanceDetectionInterval`) and returns — mirroring
89
+ * {@link createCleanupIntervalTick}. This is the prompt cleanup path: a leaked
90
+ * engine's first post-GC tick clears the timer immediately, rather than relying
91
+ * on the `FinalizationRegistry` backstop, whose callbacks are not guaranteed to
92
+ * run promptly (or at all). Extracted so the skip/clear guard is directly
93
+ * testable without a timer. A tick failure is swallowed: the detector is a smoke
94
+ * alarm, never a correctness path, so it must not surface as an unhandled rejection.
95
+ */
96
+ export declare function createSecondInstanceDetectionTick(resolveDetector: () => SecondInstanceDetector | null, tracker: EngineCleanupIntervalDisposalTracker): () => void;
@@ -0,0 +1,108 @@
1
+ import { KEYS } from "../../storage/interface.js";
2
+ const textEncoder = new TextEncoder, textDecoder = new TextDecoder;
3
+ function encodeHeartbeat(heartbeat) {
4
+ return textEncoder.encode(JSON.stringify(heartbeat));
5
+ }
6
+ async function bestEffort(operation) {
7
+ try {
8
+ await operation();
9
+ } catch {}
10
+ }
11
+ function isUsableHeartbeat(candidate) {
12
+ const { instanceId, heartbeatAt, sequence } = candidate;
13
+ return typeof instanceId === "string" && instanceId.length > 0 && typeof heartbeatAt === "number" && Number.isFinite(heartbeatAt) && typeof sequence === "number" && Number.isInteger(sequence) && sequence >= 0;
14
+ }
15
+ function decodeHeartbeat(raw) {
16
+ let parsed;
17
+ try {
18
+ parsed = JSON.parse(textDecoder.decode(raw));
19
+ } catch {
20
+ return null;
21
+ }
22
+ if (typeof parsed !== "object" || parsed === null)
23
+ return null;
24
+ const candidate = parsed;
25
+ return isUsableHeartbeat(candidate) ? candidate : null;
26
+ }
27
+ export const SECOND_INSTANCE_WARNING_NAME = "WeftSecondInstanceWarning";
28
+ const STALE_SWEEP_WINDOW_MULTIPLE = 10, ADVANCE_TICKS_BEFORE_WARN = 2;
29
+ export function createSecondInstanceDetector(options) {
30
+ const { storage, instanceId, getNow, intervalMs } = options, warn = options.warn ?? ((message) => process.emitWarning(message, SECOND_INSTANCE_WARNING_NAME)), stalenessWindowMs = intervalMs * (ADVANCE_TICKS_BEFORE_WARN + 1);
31
+ let sequence = 0, swept = !1, stopped = !1;
32
+ const observed = new Map, warnedInstanceIds = new Set;
33
+ async function readPeers() {
34
+ const peers = [];
35
+ for await (const [key, value] of storage.scan(KEYS.livenessPrefix())) {
36
+ const heartbeat = decodeHeartbeat(value);
37
+ if (heartbeat !== null && heartbeat.instanceId !== instanceId)
38
+ peers.push({ key, heartbeat });
39
+ }
40
+ return peers;
41
+ }
42
+ async function sweepStaleHeartbeats(peers, now) {
43
+ const staleBefore = now - stalenessWindowMs * STALE_SWEEP_WINDOW_MULTIPLE;
44
+ for (const peer of peers)
45
+ if (peer.heartbeat.heartbeatAt < staleBefore)
46
+ await bestEffort(() => storage.delete(peer.key));
47
+ }
48
+ function evaluatePeers(peers) {
49
+ const seenThisTick = new Set;
50
+ for (const { key, heartbeat } of peers) {
51
+ if (KEYS.liveness(heartbeat.instanceId) !== key)
52
+ continue;
53
+ seenThisTick.add(heartbeat.instanceId);
54
+ const previous = observed.get(heartbeat.instanceId), advances = previous !== void 0 && heartbeat.sequence > previous.sequence ? previous.advances + 1 : 0;
55
+ observed.set(heartbeat.instanceId, { sequence: heartbeat.sequence, advances });
56
+ if (advances >= ADVANCE_TICKS_BEFORE_WARN && !warnedInstanceIds.has(heartbeat.instanceId)) {
57
+ warnedInstanceIds.add(heartbeat.instanceId);
58
+ warn(`another engine instance (${heartbeat.instanceId}) is writing to this durable store. Weft supports one engine process per store; running two can cause duplicate workflow execution. Enforce a single instance at the infrastructure layer (for example, one replica with a Recreate deploy strategy).`);
59
+ }
60
+ }
61
+ for (const knownInstanceId of Array.from(observed.keys()))
62
+ if (!seenThisTick.has(knownInstanceId))
63
+ observed.delete(knownInstanceId);
64
+ }
65
+ let tickInFlight = !1;
66
+ async function tick() {
67
+ if (stopped || tickInFlight)
68
+ return;
69
+ tickInFlight = !0;
70
+ let wroteHeartbeat = !1;
71
+ try {
72
+ const now = getNow(), peers = await readPeers();
73
+ if (!swept) {
74
+ swept = !0;
75
+ await sweepStaleHeartbeats(peers, now);
76
+ }
77
+ evaluatePeers(peers);
78
+ if (stopped)
79
+ return;
80
+ sequence += 1;
81
+ const heartbeat = { instanceId, heartbeatAt: now, sequence };
82
+ wroteHeartbeat = !0;
83
+ await bestEffort(() => storage.put(KEYS.liveness(instanceId), encodeHeartbeat(heartbeat)));
84
+ } finally {
85
+ tickInFlight = !1;
86
+ if (wroteHeartbeat && stopped)
87
+ await bestEffort(() => storage.delete(KEYS.liveness(instanceId)));
88
+ }
89
+ }
90
+ async function stop() {
91
+ stopped = !0;
92
+ await bestEffort(() => storage.delete(KEYS.liveness(instanceId)));
93
+ }
94
+ return { tick, stop };
95
+ }
96
+ export function createSecondInstanceDetectionTick(resolveDetector, tracker) {
97
+ return function secondInstanceDetectionTick() {
98
+ const detector = resolveDetector();
99
+ if (detector === null) {
100
+ if (tracker.secondInstanceDetectionInterval !== null) {
101
+ clearInterval(tracker.secondInstanceDetectionInterval);
102
+ tracker.secondInstanceDetectionInterval = null;
103
+ }
104
+ return;
105
+ }
106
+ detector.tick().catch(() => {});
107
+ };
108
+ }
@@ -1,3 +1,4 @@
1
+ import type { BatchOperation } from '../../storage/interface.ts';
1
2
  import type { ComposedWorkflowInterceptor } from '../interceptor.ts';
2
3
  import type { SignalDeliveryOptions, WorkflowState } from '../types.ts';
3
4
  import type { EngineInternals } from './internals.ts';
@@ -31,6 +32,27 @@ export type ConsumedSignalResult = {
31
32
  export declare function signal(internals: EngineInternals, workflowId: string, name: string, payload: unknown, callbacks: SignalCallbacks, options?: SignalDeliveryOptions): Promise<void>;
32
33
  export declare function releaseSignalWaiter(internals: EngineInternals, workflowId: string, waiterKey: string, expectedResolve?: () => void): void;
33
34
  export declare function bufferSignalPayloads(internals: EngineInternals, workflowId: string, deliveries: BufferedSignalDelivery[], callbacks: SignalCallbacks, defaultOptions?: SignalDeliveryOptions): Promise<void>;
35
+ /**
36
+ * Build the durable operations for a single keyed signal so it can be folded
37
+ * into a workflow's create batch by {@link startOrSignal}. Writes the same pair
38
+ * the live signal path writes — the `sig:` payload (consumed on first drive by
39
+ * `processWaitSignalOperation`) and the `sigres:` accepted-response marker — so a
40
+ * concurrent caller that falls back to the standard signal path dedups against
41
+ * the SAME `signalId`. The accepted-response marker is consumption-independent:
42
+ * even after the winning run consumes the `sig:` payload, a late loser finds the
43
+ * `sigres:` key and short-circuits instead of re-delivering, which is what
44
+ * guarantees "one signal per signalId" across the create and signal paths.
45
+ *
46
+ * Returns both the put operations and the CAS condition (the `sig:` key must be
47
+ * absent) so the caller can gate the create batch on it.
48
+ */
49
+ export declare function buildCreateBatchSignalOperations(internals: EngineInternals, workflowId: string, signalName: string, payload: unknown, signalId: string): {
50
+ operations: BatchOperation[];
51
+ condition: {
52
+ key: string;
53
+ expectedValue: null;
54
+ };
55
+ };
34
56
  export declare function hasBufferedSignal(internals: EngineInternals, workflowId: string, signalName: string): Promise<boolean>;
35
57
  export declare function consumeSignal(internals: EngineInternals, workflowId: string, signalName: string): Promise<ConsumedSignalResult>;
36
58
  /** Register a waiter key in a workflow-keyed reverse index. */
@@ -83,6 +83,21 @@ function createExplicitSignalOperations(internals, workflowId, deliveries, signa
83
83
  value: encodePayloadWithinLimit(payload, internals.options.payloadSizePolicy.maxBytes, "signal payload")
84
84
  }));
85
85
  }
86
+ export function buildCreateBatchSignalOperations(internals, workflowId, signalName, payload, signalId) {
87
+ validateSignalId(signalId);
88
+ const signalKey = KEYS.signal(workflowId, signalName, signalId), acceptedResponseKey = KEYS.signalAcceptedResponse(workflowId, signalName, signalId);
89
+ return {
90
+ operations: [
91
+ {
92
+ type: "put",
93
+ key: signalKey,
94
+ value: encodePayloadWithinLimit(payload, internals.options.payloadSizePolicy.maxBytes, "signal payload")
95
+ },
96
+ { type: "put", key: acceptedResponseKey, value: encode(SIGNAL_ACCEPTED_RESPONSE) }
97
+ ],
98
+ condition: { key: signalKey, expectedValue: null }
99
+ };
100
+ }
86
101
  function appendTerminalCleanupOperation(internals, workflowId, operations) {
87
102
  if (internals.workflowsNeedingTerminalCleanup.has(workflowId))
88
103
  return;
@@ -17,12 +17,37 @@ export type TerminationCallbacks = {
17
17
  cleanupReviews: (workflowId: string) => Promise<void>;
18
18
  };
19
19
  export declare const TERMINAL_WORKFLOW_STATUSES: ReadonlySet<WorkflowStatus>;
20
+ /**
21
+ * Non-terminal statuses a workflow can be *forcibly* terminated from — cancel/
22
+ * timeout (`terminateWorkflow`) or system fail (`failWorkflow`). Both read this
23
+ * single constant so they cannot drift. `'suspended'` is included (a paused run
24
+ * must still be reachable by cancel/fail, else the CAS no-ops and the run is
25
+ * stranded); `completeWorkflow` deliberately excludes it (a suspended run's
26
+ * generator is evicted, so it can never reach normal completion).
27
+ */
28
+ export declare const FORCIBLY_TERMINABLE_STATUSES: readonly ["running", "pending", "suspended"];
20
29
  /**
21
30
  * Remove any pending signal, update, and sleep waiters for a workflow. This
22
31
  * prevents memory leaks and ensures that cancelled/completed/failed workflows
23
32
  * cannot accept new signals, updates, or resolve orphaned sleep timers.
24
33
  */
25
34
  export declare function cleanupWaiters(internals: EngineInternals, workflowId: string, callbacks: Pick<TerminationCallbacks, 'swallowPromiseRejection'>): void;
35
+ /**
36
+ * Evict only the in-flight OPERATION waiters for a workflow that is being
37
+ * suspended (signal/update/review waiters, review escalations, and sleep
38
+ * resolvers), WITHOUT resolving them and WITHOUT touching the non-serialized
39
+ * `services`, headers, type, or nesting-depth bookkeeping.
40
+ *
41
+ * Suspend parks the inline run (evicting its context/generator), so a signal or
42
+ * update arriving afterwards must NOT wake the dormant operation loop and drive
43
+ * the gone generator — it should buffer durably and be replayed when the
44
+ * workflow resumes. Deleting (not resolving) each waiter leaves the loop dormant
45
+ * and severs that wake path; the durable signal/sleep state in storage is
46
+ * untouched, so resume replays it. This is the suspend-specific complement to
47
+ * {@link cleanupWaiters}, which is for terminal transitions and additionally
48
+ * drops `services` and resolves sleep resolvers — both wrong for a pause.
49
+ */
50
+ export declare function evictSuspendedWorkflowWaiters(internals: EngineInternals, workflowId: string, callbacks: Pick<TerminationCallbacks, 'swallowPromiseRejection'>): void;
26
51
  /**
27
52
  * Remove durable records keyed by `workflowId` that otherwise leak after a
28
53
  * workflow reaches a terminal state.
@@ -8,7 +8,11 @@ export const TERMINAL_WORKFLOW_STATUSES = new Set([
8
8
  "failed",
9
9
  "cancelled",
10
10
  "timed-out"
11
- ]);
11
+ ]), FORCIBLY_TERMINABLE_STATUSES = [
12
+ "running",
13
+ "pending",
14
+ "suspended"
15
+ ];
12
16
  function selectTrackedWaiterMaps(internals, kind) {
13
17
  return {
14
18
  signal: () => ({
@@ -48,6 +52,14 @@ function cleanupSleepResolvers(internals, workflowId) {
48
52
  }
49
53
  internals.sleepResolversByWorkflow.delete(workflowId);
50
54
  }
55
+ function evictSleepResolversWithoutResolving(internals, workflowId) {
56
+ const sleepOps = internals.sleepResolversByWorkflow.get(workflowId);
57
+ if (!sleepOps)
58
+ return;
59
+ for (const operationId of sleepOps)
60
+ internals.sleepResolvers.delete(`${workflowId}:${operationId}`);
61
+ internals.sleepResolversByWorkflow.delete(workflowId);
62
+ }
51
63
  function cleanupReviewEscalations(internals, workflowId, callbacks) {
52
64
  const reviewIds = internals.workflowReviewIds.get(workflowId);
53
65
  if (!reviewIds)
@@ -71,8 +83,15 @@ export function cleanupWaiters(internals, workflowId, callbacks) {
71
83
  cleanupReviewEscalations(internals, workflowId, callbacks);
72
84
  internals.workflowNestingDepths.delete(workflowId);
73
85
  internals.workflowHeaders.delete(workflowId);
86
+ internals.workflowServices.delete(workflowId);
74
87
  internals.workflowTypeByWorkflowId.delete(workflowId);
75
88
  }
89
+ export function evictSuspendedWorkflowWaiters(internals, workflowId, callbacks) {
90
+ for (const kind of TRACKED_WAITER_KINDS)
91
+ cleanupTrackedWaiter(internals, workflowId, kind);
92
+ evictSleepResolversWithoutResolving(internals, workflowId);
93
+ cleanupReviewEscalations(internals, workflowId, callbacks);
94
+ }
76
95
  export async function cleanupWorkflowStorage(internals, workflowId, includeOutputArtifacts) {
77
96
  const encodedWorkflowId = encodeStorageKeyComponent(workflowId), prefixes = [
78
97
  KEYS.activityReconciliationPrefix(workflowId),
@@ -86,6 +105,7 @@ export async function cleanupWorkflowStorage(internals, workflowId, includeOutpu
86
105
  prefixes.push(`offload:${encodedWorkflowId}:`, `blob:${encodedWorkflowId}:`);
87
106
  await internals.storage.delete(KEYS.workflowHeaders(workflowId));
88
107
  await internals.storage.delete(KEYS.signalSequence(workflowId));
108
+ await internals.storage.delete(KEYS.workflowHasServices(workflowId));
89
109
  if (internals.storage.deletePrefix) {
90
110
  for (const prefix of prefixes)
91
111
  await internals.storage.deletePrefix(prefix);
@@ -26,7 +26,8 @@ import { buildWorkflowVisibilityIndexTransition } from "../workflow-indexes.js";
26
26
  import {
27
27
  cleanupTerminalWorkflowImmediately,
28
28
  cleanupTerminalWorkflowSynchronously,
29
- finalizeScheduledWorkflowTerminal
29
+ finalizeScheduledWorkflowTerminal,
30
+ FORCIBLY_TERMINABLE_STATUSES
30
31
  } from "./cleanup.js";
31
32
  async function runCancelHandlers(handlers, callbacks, workflowId) {
32
33
  for (const handler of handlers)
@@ -54,7 +55,7 @@ export async function terminateWorkflow(internals, workflowId, status, callbacks
54
55
  internals.strategy.cancelWorkflow(workflowId);
55
56
  try {
56
57
  const attributeBytes = await internals.storage.get(KEYS.attribute(workflowId)), attributes = attributeBytes ? decode(attributeBytes) : {}, retainedAttributes = buildRetainedTerminalSearchAttributes(attributes), terminationMessage = status === "timed-out" ? "Workflow timed out" : "Workflow cancelled", terminationResult = await updateWorkflowState(internals, workflowId, { status, ...reason !== void 0 ? { terminationReason: reason } : {} }, {
57
- allowedStatuses: ["running", "pending"],
58
+ allowedStatuses: FORCIBLY_TERMINABLE_STATUSES,
58
59
  buildAdditionalOperations: (_previousState, updatedAt) => {
59
60
  finalizePendingTimelineEntry(internals, workflowId, status, terminationMessage, updatedAt);
60
61
  const pendingTimelineOperation = buildPendingTimelineOperation(internals, workflowId);
@@ -182,7 +183,7 @@ export async function failWorkflow(internals, workflowId, error, callbacks, fail
182
183
  if (error.stack !== void 0)
183
184
  stateUpdate.errorStack = error.stack;
184
185
  if (!await updateWorkflowState(internals, workflowId, stateUpdate, {
185
- allowedStatuses: ["running", "pending"],
186
+ allowedStatuses: FORCIBLY_TERMINABLE_STATUSES,
186
187
  buildAdditionalOperations: (_previousState, updatedAt) => {
187
188
  finalizePendingTimelineEntry(internals, workflowId, "failed", error.message, updatedAt);
188
189
  const pendingTimelineOperation = buildPendingTimelineOperation(internals, workflowId);
@@ -0,0 +1,68 @@
1
+ import type { EngineInternals } from '../internals.ts';
2
+ import { type TerminationCallbacks } from './cleanup.ts';
3
+ /**
4
+ * Suspend a running workflow without terminating it: a non-terminal cousin of
5
+ * {@link terminateWorkflow}. The workflow's status transitions `running →
6
+ * suspended`, its durable checkpoint is preserved, and it becomes resumable via
7
+ * `engine.resume(id)` / `handle.resume()`. Suspension is client-driven
8
+ * preemption, so — unlike a fault — a suspended workflow is NOT auto-recovered
9
+ * by `engine.recoverAll()`.
10
+ *
11
+ * Contrast with cancel/timeout, which this deliberately does NOT do:
12
+ * - does NOT abort the workflow's `AbortController` — suspend is a pause, not a
13
+ * cancellation, so user code observing `ctx.signal.aborted` or registered
14
+ * abort listeners must not fire. The live inline run is *parked*
15
+ * (`parkWorkflow`: evict execution state without aborting), the same primitive
16
+ * the engine uses for signal-parking,
17
+ * - does NOT run cancel handlers,
18
+ * - does NOT settle the result promise (`handle.result()` stays pending until a
19
+ * later `resume()` drives the run to completion, or a `cancel()` terminates it),
20
+ * - does NOT clean up durable output artifacts or in-memory services (the
21
+ * `services` value is preserved so an in-process `resume()` can reuse it),
22
+ * - does NOT schedule terminal cleanup.
23
+ *
24
+ * The CAS status flip and the in-memory teardown both run inside one serialized
25
+ * per-workflow write section with `allowedStatuses: ['running']`. If the
26
+ * workflow already left `running` (it completed, failed, or a concurrent cancel
27
+ * won the race), the flip is skipped and suspend is a no-op — and because the
28
+ * teardown is gated on the flip succeeding, a workflow that lost the race keeps
29
+ * its execution state intact.
30
+ *
31
+ * The teardown evicts every piece of in-memory execution state that could let a
32
+ * post-suspend operation drive the parked run: the inline context/generator (via
33
+ * `parkWorkflow`), the in-memory checkpoint, the parked-inline marker, and the
34
+ * in-flight operation waiters (signal/update/sleep/review — deleted, NOT
35
+ * resolved, so a signal arriving after suspend buffers durably and is replayed
36
+ * on resume instead of waking a dormant operation loop against the gone
37
+ * generator). The durable checkpoint, durable buffered signals, durable sleep
38
+ * timers, and `workflowServices` are all left intact for resume.
39
+ *
40
+ * A signal that races the in-lock teardown is benign: `continueWorkflow` no-ops
41
+ * for an evicted generator, and `persistCheckpoint` no-ops when the context and
42
+ * in-memory checkpoint are gone — so no step can commit past the suspend point.
43
+ *
44
+ * `'suspended'` is neither `'running'` nor `'pending'`, and both local-ownership
45
+ * predicates (`isInlineWorkflowLocallyOwned`, `hasLocalCheckpointOwnership`) are
46
+ * gated on those two statuses. So once the status flips, the workflow stops
47
+ * registering as locally owned — which is exactly what makes `recoverAll()` skip
48
+ * it AND what lets `engine.resume()` re-drive it from storage instead of taking
49
+ * its local-ownership early return.
50
+ *
51
+ * The execution deadline is absolute wall-clock time: suspension does NOT extend
52
+ * it. The pending `deadline:` timer is deleted durably IN THE SAME COMMIT BATCH
53
+ * as the status flip, and re-armed at the same absolute fire time on resume (or
54
+ * fires immediately if already past). It is a durable delete rather than a
55
+ * `scheduler.cancel()` call because the scheduler is durable-scan-based and
56
+ * resume's re-arm is likewise durable-only (`buildTimerBatchOperations`); folding
57
+ * the delete into the commit makes it atomic with the flip and ordered before any
58
+ * concurrent resume, so an immediate resume cannot have its freshly re-armed
59
+ * deadline deleted by a late fire-and-forget cancel.
60
+ *
61
+ * Worker execution mode is not supported: a worker run cannot be parked without
62
+ * sending it a cancellation. To keep the contract state-dependent (suspend on a
63
+ * non-running workflow is always a no-op), the mode check runs only AFTER the
64
+ * status load confirms the workflow is `running`; a `running` worker workflow
65
+ * throws {@link WorkflowSuspendNotSupportedError}, while a completed or unknown
66
+ * one is a no-op regardless of execution mode.
67
+ */
68
+ export declare function suspendWorkflow(internals: EngineInternals, workflowId: string, callbacks: TerminationCallbacks): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import { KEYS } from "../../../storage/interface.js";
2
+ import { encode } from "../../codec.js";
3
+ import { WorkflowSuspendedEvent } from "../../events.js";
4
+ import { WorkflowSuspendNotSupportedError } from "../errors.js";
5
+ import { dropQueuedInlineWorkflowStart } from "../inline-launch-queue.js";
6
+ import { buildWorkflowVisibilityIndexTransition } from "../workflow-indexes.js";
7
+ import { evictSuspendedWorkflowWaiters } from "./cleanup.js";
8
+ export async function suspendWorkflow(internals, workflowId, callbacks) {
9
+ if (!await callbacks.runSerializedWorkflowStateWrite(workflowId, async () => {
10
+ const state = await callbacks.loadWorkflowState(workflowId);
11
+ if (!state || state.status !== "running")
12
+ return !1;
13
+ if (internals.inlineStrategy === null)
14
+ throw new WorkflowSuspendNotSupportedError("suspend is only supported in inline execution mode; a worker run cannot be paused without cancelling it.");
15
+ internals.inlineStrategy.parkWorkflow(workflowId);
16
+ dropQueuedInlineWorkflowStart(internals, workflowId);
17
+ internals.checkpoints.delete(workflowId);
18
+ internals.parkedInlineWorkflows.delete(workflowId);
19
+ evictSuspendedWorkflowWaiters(internals, workflowId, callbacks);
20
+ const updatedAt = internals.options.getNow(), updatedState = { ...state, status: "suspended", updatedAt };
21
+ await callbacks.commitWorkflowStateOperations(state, [
22
+ { type: "put", key: KEYS.workflow(workflowId), value: encode(updatedState) },
23
+ ...buildWorkflowVisibilityIndexTransition(workflowId, state, updatedState).batchOps,
24
+ ...buildDeadlineTimerDeleteOperations(workflowId, state.executionDeadline)
25
+ ]);
26
+ return !0;
27
+ }))
28
+ return;
29
+ const event = new WorkflowSuspendedEvent(workflowId);
30
+ callbacks.dispatchEvent(event);
31
+ callbacks.forwardEventToHandle(workflowId, event);
32
+ }
33
+ function buildDeadlineTimerDeleteOperations(workflowId, executionDeadline) {
34
+ if (executionDeadline === void 0)
35
+ return [];
36
+ const timerId = `deadline:${workflowId}`;
37
+ return [
38
+ { type: "delete", key: KEYS.deadline(executionDeadline, timerId) },
39
+ { type: "delete", key: `timer-idx:${timerId}` }
40
+ ];
41
+ }
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Thin barrel over the split termination modules. Keeps existing import paths
3
3
  * (`./termination.ts`) working while the implementation lives in
4
- * `./termination/cleanup.ts` and `./termination/complete.ts`.
4
+ * `./termination/cleanup.ts`, `./termination/complete.ts`, and
5
+ * `./termination/suspend.ts`.
5
6
  */
6
- export { TERMINAL_WORKFLOW_STATUSES, cleanupTerminalWorkflowDurableState, cleanupTerminalWorkflowImmediately, cleanupTerminalWorkflowMemory, cleanupTerminalWorkflowSynchronously, cleanupWaiters, cleanupWorkflowStorage, finalizeScheduledWorkflowTerminal, handleCleanupError, runDeferredTerminalCleanup, type TerminationCallbacks, } from './termination/cleanup.ts';
7
+ export { TERMINAL_WORKFLOW_STATUSES, cleanupTerminalWorkflowDurableState, cleanupTerminalWorkflowImmediately, cleanupTerminalWorkflowMemory, cleanupTerminalWorkflowSynchronously, cleanupWaiters, cleanupWorkflowStorage, evictSuspendedWorkflowWaiters, finalizeScheduledWorkflowTerminal, handleCleanupError, runDeferredTerminalCleanup, type TerminationCallbacks, } from './termination/cleanup.ts';
7
8
  export { buildPendingTimelineOperation, buildTerminalCleanupTimerOperations, cancelWorkflow, completeWorkflow, ensureTerminalCleanupTracked, failWorkflow, finalizePendingTimelineEntry, terminateWorkflow, timeoutWorkflow, } from './termination/complete.ts';
9
+ export { suspendWorkflow } from './termination/suspend.ts';