@lostgradient/weft 0.20.0 → 0.21.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 (138) hide show
  1. package/README.md +2 -2
  2. package/dist/core/engine/activity-reconciliation.d.ts +2 -2
  3. package/dist/core/engine/activity-reconciliation.js +12 -8
  4. package/dist/core/engine/async-activity-completion.js +12 -4
  5. package/dist/core/engine/async-activity-records.d.ts +11 -1
  6. package/dist/core/engine/async-activity-records.js +4 -3
  7. package/dist/core/engine/attributes-tags.d.ts +11 -1
  8. package/dist/core/engine/attributes-tags.js +4 -3
  9. package/dist/core/engine/bulk-operations-purge.js +5 -2
  10. package/dist/core/engine/bulk-operations.js +17 -5
  11. package/dist/core/engine/callback-creators-bundles.js +1 -1
  12. package/dist/core/engine/callback-creators-core.js +4 -2
  13. package/dist/core/engine/checkpoint-io.js +1 -1
  14. package/dist/core/engine/child-workflow.js +2 -1
  15. package/dist/core/engine/completed-review-storage.js +1 -1
  16. package/dist/core/engine/condition-waiters.d.ts +45 -3
  17. package/dist/core/engine/condition-waiters.js +12 -3
  18. package/dist/core/engine/disposal.js +6 -0
  19. package/dist/core/engine/engine-internal-types.d.ts +12 -4
  20. package/dist/core/engine/fenced-write.d.ts +46 -15
  21. package/dist/core/engine/fenced-write.js +39 -6
  22. package/dist/core/engine/finalizer-state.d.ts +3 -1
  23. package/dist/core/engine/handle-result.d.ts +49 -1
  24. package/dist/core/engine/handle-result.js +95 -20
  25. package/dist/core/engine/index.d.ts +13 -5
  26. package/dist/core/engine/index.js +155 -5
  27. package/dist/core/engine/inline-launch-queue.js +9 -0
  28. package/dist/core/engine/internals.d.ts +73 -0
  29. package/dist/core/engine/invoke-update-handler.d.ts +19 -0
  30. package/dist/core/engine/invoke-update-handler.js +7 -0
  31. package/dist/core/engine/lease-deposition.d.ts +132 -0
  32. package/dist/core/engine/lease-deposition.js +31 -0
  33. package/dist/core/engine/lease-errors.d.ts +80 -9
  34. package/dist/core/engine/lease-errors.js +28 -2
  35. package/dist/core/engine/lifecycle/resume.js +3 -1
  36. package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +33 -0
  37. package/dist/core/engine/lifecycle/standalone-claim-acquire.js +25 -0
  38. package/dist/core/engine/lifecycle/start-commit.js +20 -6
  39. package/dist/core/engine/lifecycle/transition.d.ts +15 -1
  40. package/dist/core/engine/lifecycle/transition.js +6 -3
  41. package/dist/core/engine/operations-activity.js +1 -1
  42. package/dist/core/engine/operations-time.js +21 -7
  43. package/dist/core/engine/owner-side-signal-poll.d.ts +132 -0
  44. package/dist/core/engine/owner-side-signal-poll.js +22 -0
  45. package/dist/core/engine/owner-side-update-poll.d.ts +116 -0
  46. package/dist/core/engine/owner-side-update-poll.js +22 -0
  47. package/dist/core/engine/ownership-bootstrap.d.ts +280 -0
  48. package/dist/core/engine/ownership-bootstrap.js +89 -0
  49. package/dist/core/engine/ownership-mode-marker.d.ts +82 -0
  50. package/dist/core/engine/ownership-mode-marker.js +52 -0
  51. package/dist/core/engine/ownership-options.d.ts +22 -6
  52. package/dist/core/engine/ownership-options.js +42 -12
  53. package/dist/core/engine/pending-updates.js +35 -24
  54. package/dist/core/engine/queries.d.ts +70 -0
  55. package/dist/core/engine/queries.js +58 -4
  56. package/dist/core/engine/schedules.js +1 -1
  57. package/dist/core/engine/signals.d.ts +11 -1
  58. package/dist/core/engine/signals.js +11 -7
  59. package/dist/core/engine/sleep-timer-acknowledgements.d.ts +68 -0
  60. package/dist/core/engine/sleep-timer-acknowledgements.js +19 -0
  61. package/dist/core/engine/storage-io.d.ts +58 -10
  62. package/dist/core/engine/storage-io.js +27 -9
  63. package/dist/core/engine/termination/cleanup.d.ts +5 -4
  64. package/dist/core/engine/termination/complete.js +23 -4
  65. package/dist/core/engine/termination/finalizer-claim.js +4 -4
  66. package/dist/core/engine/termination/state-commit-callbacks.d.ts +15 -0
  67. package/dist/core/engine/termination/state-commit-callbacks.js +0 -0
  68. package/dist/core/engine/termination/suspend.js +1 -1
  69. package/dist/core/engine/update-validation.d.ts +28 -0
  70. package/dist/core/engine/update-validation.js +37 -0
  71. package/dist/core/engine/updates.d.ts +17 -16
  72. package/dist/core/engine/updates.js +24 -47
  73. package/dist/core/engine/wake-ownership-check.d.ts +86 -0
  74. package/dist/core/engine/wake-ownership-check.js +19 -0
  75. package/dist/core/engine/wake-ownership-guard.d.ts +46 -0
  76. package/dist/core/engine/wake-ownership-guard.js +23 -0
  77. package/dist/core/engine/workflow-claim-codec.d.ts +64 -0
  78. package/dist/core/engine/workflow-claim-codec.js +44 -0
  79. package/dist/core/engine/workflow-claim-cooldown.d.ts +60 -0
  80. package/dist/core/engine/workflow-claim-cooldown.js +17 -0
  81. package/dist/core/engine/workflow-claim-fold.d.ts +82 -0
  82. package/dist/core/engine/workflow-claim-fold.js +53 -0
  83. package/dist/core/engine/workflow-claim-metrics.d.ts +131 -0
  84. package/dist/core/engine/workflow-claim-metrics.js +62 -0
  85. package/dist/core/engine/workflow-claim-reclaim-scan.d.ts +96 -0
  86. package/dist/core/engine/workflow-claim-reclaim-scan.js +66 -0
  87. package/dist/core/engine/workflow-claim-reclaim-target.d.ts +107 -0
  88. package/dist/core/engine/workflow-claim-reclaim-target.js +155 -0
  89. package/dist/core/engine/workflow-claim-registry.d.ts +227 -0
  90. package/dist/core/engine/workflow-claim-registry.js +198 -0
  91. package/dist/core/engine/workflow-claim-renewal-interval.d.ts +50 -0
  92. package/dist/core/engine/workflow-claim-renewal-interval.js +132 -0
  93. package/dist/core/engine/workflow-claim-renewal-subpasses.d.ts +265 -0
  94. package/dist/core/engine/workflow-claim-renewal-subpasses.js +104 -0
  95. package/dist/core/engine/workflow-claim-renewal-task.d.ts +159 -0
  96. package/dist/core/engine/workflow-claim-renewal-task.js +65 -0
  97. package/dist/core/engine/workflow-claim-transitions.d.ts +186 -0
  98. package/dist/core/engine/workflow-claim-transitions.js +120 -0
  99. package/dist/core/inline-execution-strategy.d.ts +20 -0
  100. package/dist/core/inline-execution-strategy.js +15 -4
  101. package/dist/core/scheduler/scheduler-class.js +3 -3
  102. package/dist/core/scheduler/timer-sources.d.ts +13 -0
  103. package/dist/core/scheduler/timer-sources.js +1 -1
  104. package/dist/core/types/options.d.ts +17 -1
  105. package/dist/core/weft-error.d.ts +1 -1
  106. package/dist/core/weft-error.js +2 -1
  107. package/dist/http.js +2 -2
  108. package/dist/index.d.ts +2 -1
  109. package/dist/index.js +8 -0
  110. package/dist/indexeddb.js +1 -1
  111. package/dist/json-schema.js +3 -3
  112. package/dist/observability/index.js +2 -2
  113. package/dist/storage/auto.js +1 -1
  114. package/dist/storage/bun-sql.js +113 -0
  115. package/dist/storage/compressed-storage.js +1 -1
  116. package/dist/storage/index.d.ts +38 -35
  117. package/dist/storage/interface.d.ts +87 -125
  118. package/dist/storage/interface.js +1 -1
  119. package/dist/storage/key-encoding.d.ts +51 -0
  120. package/dist/storage/key-encoding.js +13 -0
  121. package/dist/storage/key-prefixes.d.ts +1 -1
  122. package/dist/storage/key-prefixes.js +3 -0
  123. package/dist/storage/lmdb.js +1 -1
  124. package/dist/storage/memory.js +1 -1
  125. package/dist/storage/neon.js +2 -2
  126. package/dist/storage/node-sqlite.js +113 -0
  127. package/dist/storage/ownership-keys.d.ts +55 -0
  128. package/dist/storage/ownership-keys.js +6 -0
  129. package/dist/storage/postgres.js +2 -2
  130. package/dist/storage/resolve.js +1 -1
  131. package/dist/storage/scoped-storage.js +1 -1
  132. package/dist/storage/testing.js +1 -1
  133. package/dist/storage/turso.js +2 -2
  134. package/dist/version.d.ts +1 -1
  135. package/dist/version.js +1 -1
  136. package/dist/web-extension.js +1 -1
  137. package/dist/worker/protocol.js +1 -1
  138. package/package.json +1 -1
@@ -0,0 +1,89 @@
1
+ import { bootstrapOwnershipGates } from "./ownership-mode-marker.js";
2
+ import { WorkflowClaimMetricsCollector } from "./workflow-claim-metrics.js";
3
+ import { createWorkflowClaimReclaimTarget } from "./workflow-claim-reclaim-target.js";
4
+ import { WorkflowClaimRegistry } from "./workflow-claim-registry.js";
5
+ import {
6
+ createWorkflowClaimRenewalTask
7
+ } from "./workflow-claim-renewal-task.js";
8
+ export {
9
+ createWorkflowClaimReclaimTarget,
10
+ WORKFLOW_CLAIM_TAKEOVER_MAX_ATTEMPTS
11
+ } from "./workflow-claim-reclaim-target.js";
12
+ export function buildOwnerSideSignalPollTarget(sources) {
13
+ return {
14
+ listParkedSignalWaits() {
15
+ const waits = [];
16
+ for (const workflowId of sources.listParkedInlineWorkflowIds()) {
17
+ const signalName = sources.parkedSignalName(workflowId);
18
+ if (signalName !== void 0)
19
+ waits.push({ workflowId, signalName });
20
+ }
21
+ for (const [workflowId, waiterKey] of sources.listSignalWaiterEntries())
22
+ waits.push({ workflowId, signalName: waiterKey.slice(workflowId.length + 1) });
23
+ return waits;
24
+ },
25
+ hasBufferedSignal: (workflowId, signalName) => sources.hasBufferedSignal(workflowId, signalName),
26
+ async wakeWorkflow(workflowId) {
27
+ if (sources.isParkedInlineWorkflow(workflowId)) {
28
+ await sources.resumeParkedInlineWorkflow(workflowId);
29
+ return;
30
+ }
31
+ for (const [waiterWorkflowId, waiterKey] of sources.listSignalWaiterEntries()) {
32
+ if (waiterWorkflowId !== workflowId)
33
+ continue;
34
+ const signalName = waiterKey.slice(workflowId.length + 1);
35
+ if (await sources.hasBufferedSignal(workflowId, signalName)) {
36
+ if (await sources.confirmSignalWakeOwnership(workflowId) === "discard")
37
+ continue;
38
+ sources.wakeSignalWaiter(workflowId, waiterKey);
39
+ }
40
+ }
41
+ }
42
+ };
43
+ }
44
+ export function createWorkflowClaimRenewalTarget(registry, metrics) {
45
+ return {
46
+ listHeldWorkflowIds: () => registry.listHeldWorkflowIds(),
47
+ async renewWorkflowClaim(workflowId) {
48
+ if ((await registry.renew(workflowId)).status === "lost") {
49
+ metrics?.recordClaimAttempt("deposed");
50
+ throw Error(`workflow "${workflowId}" lost its ownership claim during renewal`);
51
+ }
52
+ }
53
+ };
54
+ }
55
+ export async function bootstrapWorkflowLeaseOwnership(options) {
56
+ await bootstrapOwnershipGates({
57
+ storage: options.storage,
58
+ ownershipMode: "workflow-lease",
59
+ getNow: options.getNow
60
+ });
61
+ const registry = new WorkflowClaimRegistry({
62
+ storage: options.storage,
63
+ engineId: crypto.randomUUID(),
64
+ getNow: options.getNow,
65
+ claimTtlMs: options.claimTtlMs,
66
+ claimRenewIntervalMs: options.claimRenewIntervalMs
67
+ }), metrics = new WorkflowClaimMetricsCollector, reclaimTarget = createWorkflowClaimReclaimTarget(registry, options.storage, metrics, options.onWorkflowClaimReclaimed, options.isWorkflowTypeRegistered), rawRenewalTask = createWorkflowClaimRenewalTask({
68
+ target: createWorkflowClaimRenewalTarget(registry, metrics),
69
+ reclaimTarget,
70
+ ...options.signalPollTarget === void 0 ? {} : { signalPollTarget: options.signalPollTarget },
71
+ ...options.updatePollTarget === void 0 ? {} : { updatePollTarget: options.updatePollTarget },
72
+ getNow: options.getNow,
73
+ intervalMs: options.claimRenewIntervalMs,
74
+ onPassComplete: (result) => {
75
+ for (const outcome of result.outcomes)
76
+ if (outcome.status === "failed")
77
+ metrics.recordClaimRenewalFailure();
78
+ metrics.setActiveClaims(registry.listHeldWorkflowIds().length);
79
+ }
80
+ });
81
+ return { registry, renewalTask: {
82
+ runOnce: () => rawRenewalTask.runOnce(),
83
+ start: () => rawRenewalTask.start(),
84
+ stop: () => {
85
+ reclaimTarget.markDisposing();
86
+ rawRenewalTask.stop();
87
+ }
88
+ }, metrics };
89
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The two construction-time capability gates described in
3
+ * [ADR 0002 § Construction-time capability gates](../../../documentation/contributing/architecture-decisions/0002-multiengine-per-workflow-ownership.md#construction-time-capability-gates):
4
+ *
5
+ * - **Gate 1 — storage capability.** Fails fast when the configured storage
6
+ * backend does not support `conditionalBatch`, naming the *configured*
7
+ * fencing mode in the diagnostic rather than a hardcoded one, so an operator
8
+ * troubleshooting the shipped `ownership: 'lease'` is not sent chasing
9
+ * `workflow-lease`, which is unimplemented until a later stage.
10
+ * - **Gate 2 — ownership-mode marker.** Stamps or verifies the store-wide
11
+ * `ownership-mode-marker` singleton so `ownership: 'lease'` and
12
+ * `ownership: 'workflow-lease'` remain mutually exclusive across engine
13
+ * *processes*, not merely within one process.
14
+ *
15
+ * Both gates fire only for the two fencing modes (`'lease'` and
16
+ * `'workflow-lease'`); `ownership: 'none'` never touches the marker or checks
17
+ * the capability. {@link bootstrapOwnershipGates} runs them in the ADR's
18
+ * required order (Gate 1 then Gate 2) and is the unit later wired in front of
19
+ * claim acquisition, recovery scanning, scheduler polling, and task polling —
20
+ * that wiring is a later stage and is deliberately not done here.
21
+ *
22
+ * @module core/engine/ownership-mode-marker
23
+ */
24
+ import { type Storage } from '../../storage/interface.ts';
25
+ import { type FencingOwnershipMode } from './workflow-claim-codec.ts';
26
+ /**
27
+ * Mirrors the full `ownership` discriminant (`EngineConstructorOptions.ownership`
28
+ * / `ResolvedOptions.ownershipMode`). Duplicated here as a literal union rather
29
+ * than imported, because neither call site exports a standalone name for it —
30
+ * importing `ResolvedOptions` would pull in the whole engine-internal-types
31
+ * surface for one field's type.
32
+ */
33
+ export type EngineOwnershipMode = 'none' | 'lease' | 'workflow-lease';
34
+ /**
35
+ * Gate 1 — storage capability. Fires for `ownership: 'lease'` or
36
+ * `ownership: 'workflow-lease'`; reuses the existing, untyped `Error` that
37
+ * {@link requireStorageCapability} already throws. The diagnostic names the
38
+ * mode that is actually configured — hardcoding `'workflow-lease'` into the
39
+ * message would misdirect an operator troubleshooting the shipped global
40
+ * lease toward an unimplemented feature.
41
+ */
42
+ export declare function assertOwnershipStorageCapability(storage: Storage, configuredMode: FencingOwnershipMode): void;
43
+ /** Input to {@link assertOwnershipModeMarker}. */
44
+ export type AssertOwnershipModeMarkerInput = {
45
+ storage: Storage;
46
+ /** This engine's configured fencing mode. */
47
+ configuredMode: FencingOwnershipMode;
48
+ /** Engine-clock source (ms), injected so tests can control `establishedAt` deterministically. */
49
+ getNow: () => number;
50
+ };
51
+ /**
52
+ * Gate 2 — ownership-mode marker. Fires immediately after Gate 1 passes, for
53
+ * the same trigger (`ownership: 'lease'` or `ownership: 'workflow-lease'`;
54
+ * `ownership: 'none'` never calls this).
55
+ *
56
+ * Reads `KEYS.ownershipModeMarker()`. If absent, `conditionalBatch`-puts
57
+ * `{ mode: configuredMode, establishedAt: now() }` with an expected value of
58
+ * `null` — the first fencing-mode engine against a fresh store establishes
59
+ * the mode every later one must agree with. On a CAS loss (another engine won
60
+ * the race to stamp it), re-reads and compares against that engine's mode
61
+ * instead, since it is now authoritative. If the stored mode — from either
62
+ * the initial read or the post-CAS-loss re-read — differs from this engine's
63
+ * configured mode, throws {@link OwnershipModeMismatchError} before any
64
+ * further construction proceeds.
65
+ */
66
+ export declare function assertOwnershipModeMarker(input: AssertOwnershipModeMarkerInput): Promise<void>;
67
+ /** Input to {@link bootstrapOwnershipGates}. */
68
+ export type BootstrapOwnershipGatesInput = {
69
+ storage: Storage;
70
+ ownershipMode: EngineOwnershipMode;
71
+ /** Engine-clock source (ms), injected so tests can control `establishedAt` deterministically. */
72
+ getNow: () => number;
73
+ };
74
+ /**
75
+ * Run Gate 1 then Gate 2, in that order, as ADR 0002 requires. No-op for
76
+ * `ownership: 'none'`, which never touches storage-capability enforcement or
77
+ * the mode marker. Both gates must complete successfully before any claim
78
+ * acquisition, recovery scan, scheduler poll, or task poll proceeds — but
79
+ * wiring this into `Engine` construction or any of those call sites is a
80
+ * later stage; this function is the standalone, testable unit only.
81
+ */
82
+ export declare function bootstrapOwnershipGates(input: BootstrapOwnershipGatesInput): Promise<void>;
@@ -0,0 +1,52 @@
1
+ import {
2
+ KEYS,
3
+ requireStorageCapability,
4
+ storageConditionalBatch
5
+ } from "../../storage/interface.js";
6
+ import { OwnershipModeMismatchError } from "./lease-errors.js";
7
+ import {
8
+ decodeOwnershipModeMarker,
9
+ encodeOwnershipModeMarker
10
+ } from "./workflow-claim-codec.js";
11
+ export function assertOwnershipStorageCapability(storage, configuredMode) {
12
+ requireStorageCapability(storage, "conditionalBatch", `ownership: '${configuredMode}'`);
13
+ }
14
+ async function readOwnershipModeMarkerOrThrowIfCorrupt(storage, markerKey) {
15
+ const bytes = await storage.get(markerKey);
16
+ if (bytes === null)
17
+ return null;
18
+ const decoded = decodeOwnershipModeMarker(bytes);
19
+ if (decoded === null)
20
+ throw Error(`The store's ownership-mode-marker ("${markerKey}") exists but does not decode as a valid { mode, establishedAt } record. Treating an undecodable marker as absent would let this engine silently overwrite whatever mode the store's real prior engines agreed on, defeating the marker's purpose of making a mixed fencing-mode deployment detectable. Resolve by operator repair: inspect the stored bytes and, only if certain no other engine relies on them, delete the key so a fresh marker can be established.`);
21
+ return decoded;
22
+ }
23
+ function assertOwnershipModeMatches(configuredMode, storedRecord) {
24
+ if (storedRecord.mode !== configuredMode)
25
+ throw new OwnershipModeMismatchError(configuredMode, storedRecord.mode, storedRecord.establishedAt);
26
+ }
27
+ export async function assertOwnershipModeMarker(input) {
28
+ const { storage, configuredMode, getNow } = input, markerKey = KEYS.ownershipModeMarker(), initial = await readOwnershipModeMarkerOrThrowIfCorrupt(storage, markerKey);
29
+ if (initial !== null) {
30
+ assertOwnershipModeMatches(configuredMode, initial);
31
+ return;
32
+ }
33
+ if (await storageConditionalBatch(storage, [{ key: markerKey, expectedValue: null }], [
34
+ {
35
+ type: "put",
36
+ key: markerKey,
37
+ value: encodeOwnershipModeMarker({ mode: configuredMode, establishedAt: getNow() })
38
+ }
39
+ ]))
40
+ return;
41
+ const reread = await readOwnershipModeMarkerOrThrowIfCorrupt(storage, markerKey);
42
+ if (reread === null)
43
+ throw Error(`Lost the compare-and-swap while establishing the store's ownership-mode-marker ("${markerKey}"), and a re-read then found it absent again. This indicates the marker was deleted concurrently during construction rather than raced by another engine's write (which would have left it present). Retry construction once the concurrent deletion has settled.`);
44
+ assertOwnershipModeMatches(configuredMode, reread);
45
+ }
46
+ export async function bootstrapOwnershipGates(input) {
47
+ const { storage, ownershipMode, getNow } = input;
48
+ if (ownershipMode === "none")
49
+ return;
50
+ assertOwnershipStorageCapability(storage, ownershipMode);
51
+ await assertOwnershipModeMarker({ storage, configuredMode: ownershipMode, getNow });
52
+ }
@@ -4,21 +4,37 @@
4
4
  * file under the `max-lines` ceiling; mirrors the second-instance resolver's
5
5
  * "only validate when the feature is enabled" discipline.
6
6
  *
7
+ * `ownership` is a three-member discriminated union — `'none'`, `'lease'`
8
+ * (a single store-wide lease), and `'workflow-lease'` (per-workflow fencing,
9
+ * ADR 0002 at
10
+ * `documentation/contributing/architecture-decisions/0002-multiengine-per-workflow-ownership.md`).
11
+ * This module resolves and validates the option surface only: it resolves the
12
+ * `workflowClaimTtl`/`workflowClaimRenewInterval` tuning fields and rejects
13
+ * incoherent configuration. The fencing behavior itself lives in the claim
14
+ * registry, the ownership bootstrap, and the fenced-write path.
15
+ *
7
16
  * @module core/engine/ownership-options
8
17
  */
9
18
  import type { EngineConstructorOptions, ResolvedOptions } from './engine-internal-types.ts';
19
+ import { WORKFLOW_CLAIM_TTL_SAFETY_MULTIPLIER } from './workflow-claim-transitions.ts';
10
20
  /** Default lease time-to-live for `ownership: 'lease'`. */
11
21
  export declare const DEFAULT_LEASE_TTL_MS = 30000;
12
22
  /** Default lease renewal interval for `ownership: 'lease'`. */
13
23
  export declare const DEFAULT_LEASE_RENEW_INTERVAL_MS = 5000;
14
24
  /** Default boot-time lease acquisition wait window for `ownership: 'lease'`. */
15
25
  export declare const DEFAULT_LEASE_WAIT_TIMEOUT_MS = 60000;
26
+ /** Default per-workflow claim time-to-live for `ownership: 'workflow-lease'`. */
27
+ export declare const DEFAULT_WORKFLOW_CLAIM_TTL_MS = 30000;
28
+ /** Default per-workflow claim renewal interval for `ownership: 'workflow-lease'`. */
29
+ export declare const DEFAULT_WORKFLOW_CLAIM_RENEW_INTERVAL_MS = 5000;
30
+ export { WORKFLOW_CLAIM_TTL_SAFETY_MULTIPLIER };
16
31
  export declare function resolveBackgroundTaskMode(options: EngineConstructorOptions | undefined): ResolvedOptions['backgroundTaskMode'];
17
32
  /**
18
- * Resolve the ownership posture and lease tuning into their `ResolvedOptions`
19
- * fields. Defaults to `'none'`. The lease durations are documented as "ignored
20
- * when ownership is not 'lease'", so they are only normalized (and thus only
21
- * able to throw on an invalid value) when lease ownership is actually enabled —
22
- * an invalid duration must not make an off-by-default config fatal at boot.
33
+ * Resolve the ownership posture and its per-mode tuning into their
34
+ * `ResolvedOptions` fields. Defaults to `'none'`. The lease and workflow-claim
35
+ * durations are documented as "ignored" outside their own mode, so they are
36
+ * only normalized (and thus only able to throw on an invalid value) when that
37
+ * mode is actually selected — an invalid duration must not make an off-by-
38
+ * default config fatal at boot.
23
39
  */
24
- export declare function resolveOwnershipFields(options: EngineConstructorOptions | undefined): Pick<ResolvedOptions, 'ownershipMode' | 'leaseTtlMs' | 'leaseRenewIntervalMs' | 'leaseWaitTimeoutMs'>;
40
+ export declare function resolveOwnershipFields(options: EngineConstructorOptions | undefined): Pick<ResolvedOptions, 'ownershipMode' | 'leaseTtlMs' | 'leaseRenewIntervalMs' | 'leaseWaitTimeoutMs' | 'workflowClaimTtlMs' | 'workflowClaimRenewIntervalMs'>;
@@ -1,5 +1,8 @@
1
1
  import { normalizeRetentionDuration } from "./validation.js";
2
- export const DEFAULT_LEASE_TTL_MS = 30000, DEFAULT_LEASE_RENEW_INTERVAL_MS = 5000, DEFAULT_LEASE_WAIT_TIMEOUT_MS = 60000;
2
+ import { WORKFLOW_CLAIM_TTL_SAFETY_MULTIPLIER } from "./workflow-claim-transitions.js";
3
+ export const DEFAULT_LEASE_TTL_MS = 30000, DEFAULT_LEASE_RENEW_INTERVAL_MS = 5000, DEFAULT_LEASE_WAIT_TIMEOUT_MS = 60000, DEFAULT_WORKFLOW_CLAIM_TTL_MS = 30000, DEFAULT_WORKFLOW_CLAIM_RENEW_INTERVAL_MS = 5000;
4
+
5
+ export { WORKFLOW_CLAIM_TTL_SAFETY_MULTIPLIER };
3
6
  export function resolveBackgroundTaskMode(options) {
4
7
  const mode = options?.backgroundTasks ?? "automatic";
5
8
  if (mode !== "automatic" && mode !== "manual")
@@ -15,26 +18,53 @@ function assertManualBackgroundTaskCompatibility(options) {
15
18
  throw Error('ownership cannot be "lease" when backgroundTasks is "manual"');
16
19
  }
17
20
  export function resolveOwnershipFields(options) {
18
- const ownershipMode = assertKnownOwnershipMode(options?.ownership ?? "none");
19
- if (ownershipMode !== "lease")
20
- return {
21
- ownershipMode,
22
- leaseTtlMs: DEFAULT_LEASE_TTL_MS,
23
- leaseRenewIntervalMs: DEFAULT_LEASE_RENEW_INTERVAL_MS,
24
- leaseWaitTimeoutMs: DEFAULT_LEASE_WAIT_TIMEOUT_MS
25
- };
21
+ const ownershipMode = assertKnownOwnershipMode(options?.ownership ?? "none"), leaseDefaults = {
22
+ leaseTtlMs: DEFAULT_LEASE_TTL_MS,
23
+ leaseRenewIntervalMs: DEFAULT_LEASE_RENEW_INTERVAL_MS,
24
+ leaseWaitTimeoutMs: DEFAULT_LEASE_WAIT_TIMEOUT_MS
25
+ }, workflowClaimDefaults = {
26
+ workflowClaimTtlMs: DEFAULT_WORKFLOW_CLAIM_TTL_MS,
27
+ workflowClaimRenewIntervalMs: DEFAULT_WORKFLOW_CLAIM_RENEW_INTERVAL_MS
28
+ };
29
+ if (ownershipMode === "lease")
30
+ return { ownershipMode, ...resolveLeaseTiming(options), ...workflowClaimDefaults };
31
+ if (ownershipMode === "workflow-lease") {
32
+ assertWorkflowLeaseCompatibleWithSecondInstanceDetection(options);
33
+ return { ownershipMode, ...leaseDefaults, ...resolveWorkflowClaimTiming(options) };
34
+ }
35
+ return { ownershipMode, ...leaseDefaults, ...workflowClaimDefaults };
36
+ }
37
+ function resolveLeaseTiming(options) {
26
38
  const leaseTtlMs = normalizeRetentionDuration(options?.leaseTtl, "options.leaseTtl") ?? DEFAULT_LEASE_TTL_MS, leaseRenewIntervalMs = normalizeRetentionDuration(options?.leaseRenewInterval, "options.leaseRenewInterval") ?? DEFAULT_LEASE_RENEW_INTERVAL_MS, leaseWaitTimeoutMs = normalizeRetentionDuration(options?.leaseWaitTimeout, "options.leaseWaitTimeout") ?? DEFAULT_LEASE_WAIT_TIMEOUT_MS;
27
39
  assertLeaseTimingCoherent(leaseTtlMs, leaseRenewIntervalMs, leaseWaitTimeoutMs);
28
- return { ownershipMode, leaseTtlMs, leaseRenewIntervalMs, leaseWaitTimeoutMs };
40
+ return { leaseTtlMs, leaseRenewIntervalMs, leaseWaitTimeoutMs };
41
+ }
42
+ function resolveWorkflowClaimTiming(options) {
43
+ const workflowClaimTtlMs = normalizeRetentionDuration(options?.workflowClaimTtl, "options.workflowClaimTtl") ?? DEFAULT_WORKFLOW_CLAIM_TTL_MS, workflowClaimRenewIntervalMs = normalizeRetentionDuration(options?.workflowClaimRenewInterval, "options.workflowClaimRenewInterval") ?? DEFAULT_WORKFLOW_CLAIM_RENEW_INTERVAL_MS;
44
+ assertWorkflowClaimTimingCoherent(workflowClaimTtlMs, workflowClaimRenewIntervalMs);
45
+ return { workflowClaimTtlMs, workflowClaimRenewIntervalMs };
29
46
  }
30
47
  function assertKnownOwnershipMode(mode) {
31
- if (mode !== "none" && mode !== "lease")
32
- throw Error(`Unknown ownership posture "${String(mode)}". Expected 'none' or 'lease'.`);
48
+ if (mode !== "none" && mode !== "lease" && mode !== "workflow-lease")
49
+ throw Error(`Unknown ownership posture "${mode}". Expected 'none', 'lease', or 'workflow-lease'.`);
33
50
  return mode;
34
51
  }
52
+ function assertWorkflowLeaseCompatibleWithSecondInstanceDetection(options) {
53
+ if (options?.detectSecondInstance === !0)
54
+ throw Error("ownership: 'workflow-lease' cannot be combined with detectSecondInstance: true " + "\u2014 second-instance detection assumes a single global owner.");
55
+ }
35
56
  function assertLeaseTimingCoherent(leaseTtlMs, leaseRenewIntervalMs, leaseWaitTimeoutMs) {
36
57
  if (leaseTtlMs <= 0 || leaseRenewIntervalMs <= 0 || leaseWaitTimeoutMs <= 0)
37
58
  throw Error(`ownership: 'lease' requires positive leaseTtl, leaseRenewInterval, and leaseWaitTimeout (got leaseTtl=${leaseTtlMs}ms, leaseRenewInterval=${leaseRenewIntervalMs}ms, leaseWaitTimeout=${leaseWaitTimeoutMs}ms).`);
38
59
  if (leaseRenewIntervalMs >= leaseTtlMs)
39
60
  throw Error(`ownership: 'lease' requires leaseRenewInterval (${leaseRenewIntervalMs}ms) to be strictly less than leaseTtl (${leaseTtlMs}ms), so a renewal fires before the lease can lapse.`);
40
61
  }
62
+ function assertWorkflowClaimTimingCoherent(workflowClaimTtlMs, workflowClaimRenewIntervalMs) {
63
+ if (workflowClaimTtlMs <= 0 || workflowClaimRenewIntervalMs <= 0)
64
+ throw Error(`ownership: 'workflow-lease' requires positive workflowClaimTtl and workflowClaimRenewInterval (got workflowClaimTtl=${workflowClaimTtlMs}ms, workflowClaimRenewInterval=${workflowClaimRenewIntervalMs}ms).`);
65
+ if (workflowClaimRenewIntervalMs >= workflowClaimTtlMs)
66
+ throw Error(`ownership: 'workflow-lease' requires workflowClaimRenewInterval (${workflowClaimRenewIntervalMs}ms) to be strictly less than workflowClaimTtl (${workflowClaimTtlMs}ms), so a renewal fires before the claim can lapse.`);
67
+ const requiredTtlMs = WORKFLOW_CLAIM_TTL_SAFETY_MULTIPLIER * workflowClaimRenewIntervalMs;
68
+ if (workflowClaimTtlMs < requiredTtlMs)
69
+ throw Error(`ownership: 'workflow-lease' requires workflowClaimTtl (${workflowClaimTtlMs}ms) to be at least WORKFLOW_CLAIM_TTL_SAFETY_MULTIPLIER (${WORKFLOW_CLAIM_TTL_SAFETY_MULTIPLIER}) times workflowClaimRenewInterval (${workflowClaimRenewIntervalMs}ms), i.e. at least ${requiredTtlMs}ms, so a stalled renewal has margin before the claim can be taken over.`);
70
+ }
@@ -2,10 +2,9 @@ import { UpdateCompletedEvent, UpdateReceivedEvent } from "../events.js";
2
2
  import { UpdateValidationError } from "../updates.js";
3
3
  import { notifyConditionWaiters } from "./condition-waiters.js";
4
4
  import { commitFencedEngineWrite } from "./fenced-write.js";
5
- import {
6
- extractStandardSchemaIssues,
7
- invokeUpdateHandler as invokeUpdateHandlerFromInternals
8
- } from "./updates.js";
5
+ import { invokeUpdateHandler as invokeUpdateHandlerFromInternals } from "./invoke-update-handler.js";
6
+ import { isLiveContextStale } from "./queries.js";
7
+ import { extractStandardSchemaIssues } from "./update-validation.js";
9
8
  async function waitForNextMacrotask() {
10
9
  await new Promise((resolve) => {
11
10
  setTimeout(resolve, 0);
@@ -63,32 +62,44 @@ async function runPendingUpdateValidator(validator, updateName, payload) {
63
62
  return new UpdateValidationError(updateName, issues).message;
64
63
  return null;
65
64
  }
65
+ async function resolveLiveHandlerContext(internals, workflowId) {
66
+ const stale = isLiveContextStale(internals, workflowId);
67
+ if (stale !== !1 && await stale)
68
+ return;
69
+ return internals.inlineStrategy?.getContext(workflowId);
70
+ }
71
+ async function processOnePendingUpdate(internals, workflowId, update, handlers, context, callbacks) {
72
+ const handler = handlers.get(update.name);
73
+ if (!handler)
74
+ return !1;
75
+ if (!claimPendingUpdateForDelivery(internals, workflowId, update.updateId))
76
+ return !1;
77
+ const validatorRejectionError = await runValidatorIfPresent(context, update);
78
+ if (validatorRejectionError !== null) {
79
+ await rejectPendingUpdate(internals, workflowId, update, validatorRejectionError, callbacks);
80
+ return !1;
81
+ }
82
+ await deliverPendingUpdate(internals, workflowId, update, handler, callbacks);
83
+ return !0;
84
+ }
66
85
  export async function processPendingUpdatesForHandlers(internals, workflowId, callbacks) {
67
- const context = internals.inlineStrategy?.getContext(workflowId);
86
+ const initialContext = internals.inlineStrategy?.getContext(workflowId);
87
+ if (!initialContext || initialContext.updateHandlers.size === 0)
88
+ return;
89
+ const pendingUpdates = await internals.updateCoordinator.getPendingUpdates(workflowId);
90
+ if (pendingUpdates.length === 0)
91
+ return;
92
+ const context = await resolveLiveHandlerContext(internals, workflowId);
68
93
  if (!context)
69
94
  return;
70
95
  const handlers = context.updateHandlers;
71
96
  if (handlers.size === 0)
72
97
  return;
73
- const pendingUpdates = await internals.updateCoordinator.getPendingUpdates(workflowId);
74
- if (pendingUpdates.length === 0)
75
- return;
76
98
  let handlerRan = !1;
77
99
  try {
78
- for (const update of pendingUpdates) {
79
- const handler = handlers.get(update.name);
80
- if (!handler)
81
- continue;
82
- if (!claimPendingUpdateForDelivery(internals, workflowId, update.updateId))
83
- continue;
84
- const validatorRejectionError = await runValidatorIfPresent(context, update);
85
- if (validatorRejectionError !== null) {
86
- await rejectPendingUpdate(internals, workflowId, update, validatorRejectionError, callbacks);
87
- continue;
88
- }
89
- handlerRan = !0;
90
- await deliverPendingUpdate(internals, workflowId, update, handler, callbacks);
91
- }
100
+ for (const update of pendingUpdates)
101
+ if (await processOnePendingUpdate(internals, workflowId, update, handlers, context, callbacks))
102
+ handlerRan = !0;
92
103
  } finally {
93
104
  if (handlerRan)
94
105
  notifyConditionWaiters(internals, workflowId);
@@ -113,7 +124,7 @@ async function runValidatorIfPresent(context, update) {
113
124
  }
114
125
  async function rejectPendingUpdate(internals, workflowId, update, errorMessage, callbacks) {
115
126
  const responseOperations = internals.updateCoordinator.buildResponseOperations(update.updateId, workflowId, void 0, errorMessage, update.idempotencyKey);
116
- await commitFencedEngineWrite(internals, responseOperations, [], () => Error(`Update rejection for workflow "${workflowId}" lost its CAS race.`));
127
+ await commitFencedEngineWrite(internals, workflowId, responseOperations, [], () => Error(`Update rejection for workflow "${workflowId}" lost its CAS race.`));
117
128
  callbacks.broadcast({ type: "update:completed", workflowId, updateId: update.updateId });
118
129
  }
119
130
  async function deliverPendingUpdate(internals, workflowId, update, handler, callbacks) {
@@ -125,7 +136,7 @@ async function deliverPendingUpdate(internals, workflowId, update, handler, call
125
136
  error = handlerError instanceof Error ? handlerError.message : String(handlerError);
126
137
  }
127
138
  const responseOperations = internals.updateCoordinator.buildResponseOperations(update.updateId, workflowId, result, error, update.idempotencyKey);
128
- await commitFencedEngineWrite(internals, responseOperations, [], () => Error(`Update response for workflow "${workflowId}" lost its CAS race.`));
139
+ await commitFencedEngineWrite(internals, workflowId, responseOperations, [], () => Error(`Update response for workflow "${workflowId}" lost its CAS race.`));
129
140
  callbacks.dispatchEvent(new UpdateCompletedEvent(update.updateId, workflowId, update.name, result, error));
130
141
  callbacks.broadcast({ type: "update:completed", workflowId, updateId: update.updateId });
131
142
  }
@@ -1,3 +1,73 @@
1
1
  import type { EngineInternals } from './internals.ts';
2
+ /**
3
+ * Thrown by `query()` when this engine has no live local execution context for
4
+ * `workflowId` AND a durable `ownership: 'workflow-lease'` claim shows another
5
+ * engine currently holds it — distinguishing "not owned here, retry against
6
+ * the owner" from a query handler that legitimately resolved to `undefined`.
7
+ * A workflow that is simply terminal, purged, or never existed still resolves
8
+ * to plain `undefined`, unchanged — this error fires only when a live holder
9
+ * record names a DIFFERENT engine.
10
+ *
11
+ * Not a public `WeftError`: ADR 0002 § Open questions leaves the cross-engine
12
+ * query/update ROUTING mechanism open (forward to the true owner vs. surface
13
+ * a retry-elsewhere error to the caller); this is the smaller, non-speculative
14
+ * fix for the silent-`undefined` bug alone, reusing the same "untyped `Error`"
15
+ * shape Gate 1 (`requireStorageCapability`) already ships for a similarly
16
+ * not-yet-typed diagnostic rather than adding a new public error code and its
17
+ * full registration surface for a routing question ADR 0002 has not settled.
18
+ * `query()` has no durable fallback path the way `update()`'s coordinated
19
+ * path does (see `updates.ts`), so surfacing an error here — rather than a
20
+ * value indistinguishable from a real `undefined` result — is the only
21
+ * correct option.
22
+ */
23
+ export declare class WorkflowNotLocallyOwnedError extends Error {
24
+ readonly workflowId: string;
25
+ constructor(workflowId: string);
26
+ }
27
+ /**
28
+ * Cheap, read-only check for whether `workflowId` is durably claimed by a
29
+ * DIFFERENT engine under `ownership: 'workflow-lease'` — a
30
+ * `wakeOwnershipCheck`-style storage read (one `storage.get`, no
31
+ * `conditionalBatch`), not the actual safety mechanism. Always `false`
32
+ * (inert) under `ownership: 'none'`/`'lease'`, where no claim registry is
33
+ * installed.
34
+ *
35
+ * Always re-reads the durable holder rather than trusting this engine's own
36
+ * cached `registry.currentEpoch(workflowId)`: after a takeover elsewhere,
37
+ * this engine's local claim entry stays populated (and its cached epoch
38
+ * stale) until its next renewal CAS independently detects the loss — a
39
+ * `registry.currentEpoch(workflowId) !== null` shortcut would let a stale
40
+ * engine keep answering `false` (and so keep serving queries/updates from a
41
+ * deposed context) for up to a full renewal interval. Deciding by the
42
+ * durable holder's `engineId` alone — not epoch — is sufficient here: this
43
+ * engine reacquiring the same workflow at a new epoch after a release still
44
+ * means the holder names THIS engine, correctly answering `false`.
45
+ */
46
+ export declare function isWorkflowClaimedByAnotherEngine(internals: EngineInternals, workflowId: string): Promise<boolean>;
47
+ /**
48
+ * `true` when `workflowId`'s persisted `WorkflowState` is missing (never
49
+ * existed, or already purged) or terminal. Used to ignore a durably stale
50
+ * `wf-owner-holder:<id>` record: a normal owner-side complete/fail commit is
51
+ * fenced but does not delete the holder record (only an explicit `release`
52
+ * — graceful shutdown or the reclaim scan's redrive-detects-terminal path —
53
+ * or a `takeover`/rotation does), so a contextless query for an already-
54
+ * terminal workflow can otherwise see a holder record naming a DIFFERENT
55
+ * engine and incorrectly throw `WorkflowNotLocallyOwnedError` for a workflow
56
+ * every engine agrees has already finished. Documented behavior is that
57
+ * terminal, purged, or unknown workflows keep resolving to plain
58
+ * `undefined` — this check restores that for the `workflow-lease` claim
59
+ * path specifically.
60
+ */
61
+ export declare function isWorkflowLocallyTerminalOrMissing(internals: EngineInternals, workflowId: string): Promise<boolean>;
62
+ /**
63
+ * `internals.workflowClaimRegistry !== null && (await isWorkflowClaimedByAnotherEngine(...))`,
64
+ * factored out so every "revalidate before serving a possibly-stale live
65
+ * Context" call site — `query()` below and `tryInlineUpdateHandler` in
66
+ * `updates.ts` — shares the exact same registry-gated short-circuit. The
67
+ * registry-null check is a plain property read, not an unconditional await,
68
+ * so `ownership: 'none'`/`'lease'` stay exactly as synchronous as they are
69
+ * today; only `'workflow-lease'` pays for the cheap durable read.
70
+ */
71
+ export declare function isLiveContextStale(internals: EngineInternals, workflowId: string): boolean | Promise<boolean>;
2
72
  /** Resolve a workflow query from built-in progress state or exposed inline accessors. */
3
73
  export declare function query(internals: EngineInternals, workflowId: string, name: string, input?: unknown): Promise<unknown>;
@@ -1,16 +1,70 @@
1
+ import { KEYS } from "../../storage/interface.js";
2
+ import { decodeWorkflowState, isTerminalWorkflowStatus } from "./validation.js";
3
+ import { decodeWorkflowClaimHolder } from "./workflow-claim-codec.js";
4
+
5
+ export class WorkflowNotLocallyOwnedError extends Error {
6
+ workflowId;
7
+ constructor(workflowId) {
8
+ super(`Workflow "${workflowId}" has no live query context on this engine, and a durable workflow-lease claim shows another engine currently owns it. Retry the query against the engine that currently holds the claim.`);
9
+ this.name = "WorkflowNotLocallyOwnedError";
10
+ this.workflowId = workflowId;
11
+ }
12
+ }
13
+ export async function isWorkflowClaimedByAnotherEngine(internals, workflowId) {
14
+ const registry = internals.workflowClaimRegistry;
15
+ if (registry === null)
16
+ return !1;
17
+ const raw = await internals.storage.get(KEYS.workflowOwnerHolder(workflowId));
18
+ if (raw === null)
19
+ return !1;
20
+ const holder = decodeWorkflowClaimHolder(raw);
21
+ if (holder === null)
22
+ return !1;
23
+ return holder.engineId !== registry.engineId;
24
+ }
25
+ export async function isWorkflowLocallyTerminalOrMissing(internals, workflowId) {
26
+ const stateBytes = await internals.storage.get(KEYS.workflow(workflowId));
27
+ if (stateBytes === null)
28
+ return !0;
29
+ const state = decodeWorkflowState(stateBytes);
30
+ return isTerminalWorkflowStatus(state.status);
31
+ }
32
+ export function isLiveContextStale(internals, workflowId) {
33
+ if (internals.workflowClaimRegistry === null)
34
+ return !1;
35
+ return isWorkflowClaimedByAnotherEngine(internals, workflowId);
36
+ }
37
+ function resolveDispatchContext(inlineStrategy, workflowId) {
38
+ return inlineStrategy.getContext(workflowId) ?? inlineStrategy.getParkedContext(workflowId);
39
+ }
40
+ async function resolveContextlessQueryResult(internals, workflowId) {
41
+ if (await isWorkflowClaimedByAnotherEngine(internals, workflowId) && !await isWorkflowLocallyTerminalOrMissing(internals, workflowId))
42
+ throw new WorkflowNotLocallyOwnedError(workflowId);
43
+ return;
44
+ }
1
45
  export async function query(internals, workflowId, name, input) {
2
46
  if (name === "activityProgress")
3
47
  return internals.heartbeatDetails.get(workflowId);
4
48
  const inlineStrategy = internals.inlineStrategy;
5
49
  if (!inlineStrategy)
6
50
  throw Error("Workflow queries are not supported when using the worker execution strategy.");
7
- const context = inlineStrategy.getContext(workflowId) ?? inlineStrategy.getParkedContext(workflowId);
51
+ const context = resolveDispatchContext(inlineStrategy, workflowId);
8
52
  if (!context)
9
- return;
10
- const queryHandler = context.queryHandlers.get(name);
53
+ return resolveContextlessQueryResult(internals, workflowId);
54
+ const staleCheck = isLiveContextStale(internals, workflowId);
55
+ let liveContext = context;
56
+ if (staleCheck !== !1) {
57
+ if (await staleCheck)
58
+ throw new WorkflowNotLocallyOwnedError(workflowId);
59
+ const refreshedContext = resolveDispatchContext(inlineStrategy, workflowId);
60
+ if (!refreshedContext)
61
+ return;
62
+ liveContext = refreshedContext;
63
+ }
64
+ const queryHandler = liveContext.queryHandlers.get(name);
11
65
  if (queryHandler)
12
66
  return queryHandler(input);
13
- const accessor = context.exposedAccessors.get(name);
67
+ const accessor = liveContext.exposedAccessors.get(name);
14
68
  if (!accessor)
15
69
  return;
16
70
  return accessor();
@@ -238,5 +238,5 @@ export async function handleScheduledWorkflowTerminal(internals, workflowId, cal
238
238
  });
239
239
  }
240
240
  async function deleteTransientScheduleRunMetadata(internals, workflowId) {
241
- await commitFencedEngineWrite(internals, [{ type: "delete", key: KEYS.scheduleRun(workflowId) }], [], () => Error(`Schedule-run cleanup for workflow "${workflowId}" lost its precondition.`));
241
+ await commitFencedEngineWrite(internals, workflowId, [{ type: "delete", key: KEYS.scheduleRun(workflowId) }], [], () => Error(`Schedule-run cleanup for workflow "${workflowId}" lost its precondition.`));
242
242
  }
@@ -30,7 +30,17 @@ export type ConsumedSignalResult = {
30
30
  payload: unknown;
31
31
  };
32
32
  export declare function signal(internals: EngineInternals, workflowId: string, name: string, payload: unknown, callbacks: SignalCallbacks, options?: SignalDeliveryOptions): Promise<void>;
33
- export declare function releaseSignalWaiter(internals: EngineInternals, workflowId: string, waiterKey: string, expectedResolve?: () => void): void;
33
+ /**
34
+ * Remove the signal waiter registered under `waiterKey`, optionally only when
35
+ * it is still `expectedResolve`.
36
+ *
37
+ * Returns whether THIS call removed that exact waiter. Callers that go on to
38
+ * invoke the resolver must gate on the return value: a waiter captured before
39
+ * an await can be replaced by replay or a fresh park while that await is in
40
+ * flight, and invoking the captured resolver anyway advances a superseded
41
+ * generator even though the replacement was correctly left in place.
42
+ */
43
+ export declare function releaseSignalWaiter(internals: EngineInternals, workflowId: string, waiterKey: string, expectedResolve?: () => void): boolean;
34
44
  export declare function bufferSignalPayloads(internals: EngineInternals, workflowId: string, deliveries: BufferedSignalDelivery[], callbacks: SignalCallbacks, defaultOptions?: SignalDeliveryOptions): Promise<void>;
35
45
  /**
36
46
  * Build the durable operations for a single keyed signal so it can be folded