@lostgradient/weft 0.19.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.
- package/README.md +2 -2
- package/dist/cli/generated/operation-catalog.snapshot.json +1334 -0
- package/dist/cli/generated/operation-client.generated.d.ts +9 -2
- package/dist/cli/generated/operation-client.generated.js +2 -0
- package/dist/core/engine/activity-reconciliation.d.ts +2 -2
- package/dist/core/engine/activity-reconciliation.js +12 -8
- package/dist/core/engine/async-activity-completion.js +12 -4
- package/dist/core/engine/async-activity-records.d.ts +11 -1
- package/dist/core/engine/async-activity-records.js +4 -3
- package/dist/core/engine/attributes-tags.d.ts +11 -1
- package/dist/core/engine/attributes-tags.js +4 -3
- package/dist/core/engine/bulk-operations-purge.js +5 -2
- package/dist/core/engine/bulk-operations.js +17 -5
- package/dist/core/engine/callback-creators-bundles.js +1 -1
- package/dist/core/engine/callback-creators-core.js +4 -2
- package/dist/core/engine/checkpoint-io.js +1 -1
- package/dist/core/engine/child-workflow.js +2 -1
- package/dist/core/engine/completed-review-storage.js +1 -1
- package/dist/core/engine/condition-waiters.d.ts +45 -3
- package/dist/core/engine/condition-waiters.js +12 -3
- package/dist/core/engine/disposal.js +6 -0
- package/dist/core/engine/engine-internal-types.d.ts +12 -4
- package/dist/core/engine/fenced-write.d.ts +46 -15
- package/dist/core/engine/fenced-write.js +39 -6
- package/dist/core/engine/finalizer-state.d.ts +3 -1
- package/dist/core/engine/handle-result.d.ts +49 -1
- package/dist/core/engine/handle-result.js +95 -20
- package/dist/core/engine/index.d.ts +13 -5
- package/dist/core/engine/index.js +155 -5
- package/dist/core/engine/inline-launch-queue.js +9 -0
- package/dist/core/engine/internals.d.ts +73 -0
- package/dist/core/engine/invoke-update-handler.d.ts +19 -0
- package/dist/core/engine/invoke-update-handler.js +7 -0
- package/dist/core/engine/lease-deposition.d.ts +132 -0
- package/dist/core/engine/lease-deposition.js +31 -0
- package/dist/core/engine/lease-errors.d.ts +80 -9
- package/dist/core/engine/lease-errors.js +28 -2
- package/dist/core/engine/lifecycle/resume.js +3 -1
- package/dist/core/engine/lifecycle/standalone-claim-acquire.d.ts +33 -0
- package/dist/core/engine/lifecycle/standalone-claim-acquire.js +25 -0
- package/dist/core/engine/lifecycle/start-commit.js +20 -6
- package/dist/core/engine/lifecycle/transition.d.ts +15 -1
- package/dist/core/engine/lifecycle/transition.js +6 -3
- package/dist/core/engine/operations-activity.js +1 -1
- package/dist/core/engine/operations-time.js +21 -7
- package/dist/core/engine/owner-side-signal-poll.d.ts +132 -0
- package/dist/core/engine/owner-side-signal-poll.js +22 -0
- package/dist/core/engine/owner-side-update-poll.d.ts +116 -0
- package/dist/core/engine/owner-side-update-poll.js +22 -0
- package/dist/core/engine/ownership-bootstrap.d.ts +280 -0
- package/dist/core/engine/ownership-bootstrap.js +89 -0
- package/dist/core/engine/ownership-mode-marker.d.ts +82 -0
- package/dist/core/engine/ownership-mode-marker.js +52 -0
- package/dist/core/engine/ownership-options.d.ts +22 -6
- package/dist/core/engine/ownership-options.js +42 -12
- package/dist/core/engine/pending-updates.js +35 -24
- package/dist/core/engine/queries.d.ts +70 -0
- package/dist/core/engine/queries.js +58 -4
- package/dist/core/engine/schedules.js +1 -1
- package/dist/core/engine/signals.d.ts +11 -1
- package/dist/core/engine/signals.js +11 -7
- package/dist/core/engine/sleep-timer-acknowledgements.d.ts +68 -0
- package/dist/core/engine/sleep-timer-acknowledgements.js +19 -0
- package/dist/core/engine/storage-io.d.ts +58 -10
- package/dist/core/engine/storage-io.js +27 -9
- package/dist/core/engine/termination/cleanup.d.ts +5 -4
- package/dist/core/engine/termination/complete.js +23 -4
- package/dist/core/engine/termination/finalizer-claim.js +4 -4
- package/dist/core/engine/termination/state-commit-callbacks.d.ts +15 -0
- package/dist/core/engine/termination/state-commit-callbacks.js +0 -0
- package/dist/core/engine/termination/suspend.js +1 -1
- package/dist/core/engine/update-validation.d.ts +28 -0
- package/dist/core/engine/update-validation.js +37 -0
- package/dist/core/engine/updates.d.ts +17 -16
- package/dist/core/engine/updates.js +24 -47
- package/dist/core/engine/wake-ownership-check.d.ts +86 -0
- package/dist/core/engine/wake-ownership-check.js +19 -0
- package/dist/core/engine/wake-ownership-guard.d.ts +46 -0
- package/dist/core/engine/wake-ownership-guard.js +23 -0
- package/dist/core/engine/workflow-claim-codec.d.ts +64 -0
- package/dist/core/engine/workflow-claim-codec.js +44 -0
- package/dist/core/engine/workflow-claim-cooldown.d.ts +60 -0
- package/dist/core/engine/workflow-claim-cooldown.js +17 -0
- package/dist/core/engine/workflow-claim-fold.d.ts +82 -0
- package/dist/core/engine/workflow-claim-fold.js +53 -0
- package/dist/core/engine/workflow-claim-metrics.d.ts +131 -0
- package/dist/core/engine/workflow-claim-metrics.js +62 -0
- package/dist/core/engine/workflow-claim-reclaim-scan.d.ts +96 -0
- package/dist/core/engine/workflow-claim-reclaim-scan.js +66 -0
- package/dist/core/engine/workflow-claim-reclaim-target.d.ts +107 -0
- package/dist/core/engine/workflow-claim-reclaim-target.js +155 -0
- package/dist/core/engine/workflow-claim-registry.d.ts +227 -0
- package/dist/core/engine/workflow-claim-registry.js +198 -0
- package/dist/core/engine/workflow-claim-renewal-interval.d.ts +50 -0
- package/dist/core/engine/workflow-claim-renewal-interval.js +132 -0
- package/dist/core/engine/workflow-claim-renewal-subpasses.d.ts +265 -0
- package/dist/core/engine/workflow-claim-renewal-subpasses.js +104 -0
- package/dist/core/engine/workflow-claim-renewal-task.d.ts +159 -0
- package/dist/core/engine/workflow-claim-renewal-task.js +65 -0
- package/dist/core/engine/workflow-claim-transitions.d.ts +186 -0
- package/dist/core/engine/workflow-claim-transitions.js +120 -0
- package/dist/core/inline-execution-strategy.d.ts +20 -0
- package/dist/core/inline-execution-strategy.js +15 -4
- package/dist/core/scheduler/scheduler-class.js +3 -3
- package/dist/core/scheduler/timer-sources.d.ts +13 -0
- package/dist/core/scheduler/timer-sources.js +1 -1
- package/dist/core/types/options.d.ts +17 -1
- package/dist/core/weft-error.d.ts +1 -1
- package/dist/core/weft-error.js +2 -1
- package/dist/http.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +8 -0
- package/dist/indexeddb.js +1 -1
- package/dist/json-schema.js +3 -3
- package/dist/observability/index.js +2 -2
- package/dist/server/operations/get-task-detail-schema.d.ts +341 -0
- package/dist/server/operations/get-task-detail-schema.js +118 -0
- package/dist/server/operations/get-task-detail.d.ts +346 -0
- package/dist/server/operations/get-task-detail.js +215 -0
- package/dist/server/operations/static-registrations.js +3 -0
- package/dist/storage/auto.js +1 -1
- package/dist/storage/bun-sql.js +113 -0
- package/dist/storage/compressed-storage.js +1 -1
- package/dist/storage/index.d.ts +38 -35
- package/dist/storage/interface.d.ts +87 -125
- package/dist/storage/interface.js +1 -1
- package/dist/storage/key-encoding.d.ts +51 -0
- package/dist/storage/key-encoding.js +13 -0
- package/dist/storage/key-prefixes.d.ts +1 -1
- package/dist/storage/key-prefixes.js +3 -0
- package/dist/storage/lmdb.js +1 -1
- package/dist/storage/memory.js +1 -1
- package/dist/storage/neon.js +2 -2
- package/dist/storage/node-sqlite.js +113 -0
- package/dist/storage/ownership-keys.d.ts +55 -0
- package/dist/storage/ownership-keys.js +6 -0
- package/dist/storage/postgres.js +2 -2
- package/dist/storage/resolve.js +1 -1
- package/dist/storage/scoped-storage.js +1 -1
- package/dist/storage/testing.js +1 -1
- package/dist/storage/turso.js +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/web-extension.js +1 -1
- package/dist/worker/protocol.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Step-2 epoch fencing for `ownership: 'lease'` (issue #470)
|
|
2
|
+
* Step-2 epoch fencing for `ownership: 'lease'` (issue #470), extended by ADR
|
|
3
|
+
* 0002 to per-workflow fencing under `ownership: 'workflow-lease'`.
|
|
3
4
|
*
|
|
4
5
|
* Step 1 made a rolling deploy a clean lease handoff but added no correctness
|
|
5
6
|
* backstop: a deposed zombie instance (GC pause, partition) that emerged after
|
|
@@ -15,6 +16,20 @@
|
|
|
15
16
|
* from silently spinning on swallowed write losses, surface the deposition to the
|
|
16
17
|
* operator, and release resources. They are not what makes the system safe.
|
|
17
18
|
*
|
|
19
|
+
* ADR 0002 adds a SECOND, independent fencing token: `wf-owner-epoch:<workflowId>`.
|
|
20
|
+
* Every caller of {@link commitFencedEngineWrite} / {@link
|
|
21
|
+
* commitFencedEngineWriteAllowingPreconditionFailure} now states, via the
|
|
22
|
+
* required `workflowId` parameter, whether the write is scoped to one
|
|
23
|
+
* workflow's execution (pass its id) or is engine-scoped/cross-workflow (pass
|
|
24
|
+
* `null`) — an optional parameter would let a call site silently forget the
|
|
25
|
+
* fence, which is exactly the correctness hole this mechanism exists to close.
|
|
26
|
+
* Under `ownership: 'workflow-lease'` with a non-null `workflowId`, the write
|
|
27
|
+
* is fenced on THAT workflow's claim epoch instead of the global lease epoch;
|
|
28
|
+
* losing that fence deposes only that one workflow (warn + throw), never the
|
|
29
|
+
* whole engine. Under `'lease'` and `'none'`, and under `'workflow-lease'`
|
|
30
|
+
* with `workflowId: null`, behavior is byte-for-byte unchanged from Step 2 —
|
|
31
|
+
* `workflowId` is only consulted in the one new branch.
|
|
32
|
+
*
|
|
18
33
|
* This module is allow-listed for import only from `src/core/engine/**`.
|
|
19
34
|
*/
|
|
20
35
|
import type { BatchOperation, ConditionalBatchCondition } from '../../storage/interface.ts';
|
|
@@ -31,37 +46,53 @@ import type { EngineInternals } from './internals.ts';
|
|
|
31
46
|
* `pauseSchedule`, `resumeSchedule`, `cancelSchedule`, and `updateSchedule`) would
|
|
32
47
|
* reach `resolveFenceEpochOrHalt` with no held epoch and be misreported as a
|
|
33
48
|
* deposition (warn + teardown) rather than the true "lease not held yet"
|
|
34
|
-
* condition. A no-op under `ownership: 'none'
|
|
49
|
+
* condition. A no-op under `ownership: 'none'` and under `ownership:
|
|
50
|
+
* 'workflow-lease'` — the analogous "claim not held yet" guard for a
|
|
51
|
+
* workflow-scoped write is a later stage's concern (folding `acquire()` into
|
|
52
|
+
* the enabling write), not this global-lease-only assertion.
|
|
35
53
|
*/
|
|
36
54
|
export declare function assertLeaseHeldForEngineWork(internals: EngineInternals): void;
|
|
37
55
|
/**
|
|
38
|
-
* Commit an engine-generator-owned durable write, fenced on the
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
56
|
+
* Commit an engine-generator-owned durable write, fenced on the applicable
|
|
57
|
+
* epoch: the global lease epoch under `ownership: 'lease'`, this workflow's
|
|
58
|
+
* claim epoch under `ownership: 'workflow-lease'` when `workflowId` is
|
|
59
|
+
* non-null, or — under `'none'` and every other `workflowId`/mode
|
|
60
|
+
* combination — byte-for-byte unfenced. A lost CAS race throws the
|
|
61
|
+
* caller-supplied `onLostRace` error so existing retry semantics apply; a
|
|
62
|
+
* deposition halts (the engine under global `'lease'`, or just this one
|
|
63
|
+
* workflow under `'workflow-lease'`) and throws {@link EngineDeposedError}.
|
|
64
|
+
* The helper owns the batch-vs-conditionalBatch decision — pass plain
|
|
65
|
+
* operations plus whatever base conditions you already need.
|
|
44
66
|
*
|
|
45
|
-
* @param internals - the engine internals (ownership mode, lease manager,
|
|
67
|
+
* @param internals - the engine internals (ownership mode, lease manager,
|
|
68
|
+
* workflow claim registry, deposed flag)
|
|
69
|
+
* @param workflowId - the workflow this write is scoped to, fenced on that
|
|
70
|
+
* workflow's claim epoch under `ownership: 'workflow-lease'`; or `null` for
|
|
71
|
+
* an engine-scoped/cross-workflow write, which is never fenced on a
|
|
72
|
+
* per-workflow claim regardless of ownership mode. Required — there is no
|
|
73
|
+
* default — so every call site states its scope explicitly.
|
|
46
74
|
* @param operations - the durable operations to commit atomically
|
|
47
75
|
* @param baseConditions - CAS conditions the caller already requires (may be empty)
|
|
48
76
|
* @param onLostRace - builds the error thrown on a same-epoch lost CAS race
|
|
49
77
|
*/
|
|
50
|
-
export declare function commitFencedEngineWrite(internals: EngineInternals, operations: BatchOperation[], baseConditions: ConditionalBatchCondition[], onLostRace: () => Error): Promise<void>;
|
|
78
|
+
export declare function commitFencedEngineWrite(internals: EngineInternals, workflowId: string | null, operations: BatchOperation[], baseConditions: ConditionalBatchCondition[], onLostRace: () => Error): Promise<void>;
|
|
51
79
|
/**
|
|
52
80
|
* Like {@link commitFencedEngineWrite}, but the caller treats a base-precondition
|
|
53
81
|
* failure as a legitimate outcome rather than an error — used by the idempotent
|
|
54
82
|
* start path, where a `false` means a concurrent same-key caller already wrote the
|
|
55
83
|
* record (resolve to the existing run) rather than "retry". Returns `true` when the
|
|
56
84
|
* batch committed and `false` when a base condition failed. Deposition is still a
|
|
57
|
-
* hard halt: if the epoch condition is the one that failed, this drives
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* and silently
|
|
85
|
+
* hard halt: if the epoch condition is the one that failed, this drives the
|
|
86
|
+
* applicable deposition path and throws {@link EngineDeposedError} — a deposed
|
|
87
|
+
* engine (or a deposed single workflow, under `workflow-lease`) must never report a
|
|
88
|
+
* precondition-failure the caller would read as "already exists" and silently
|
|
89
|
+
* move on.
|
|
61
90
|
*
|
|
62
91
|
* @param internals - the engine internals
|
|
92
|
+
* @param workflowId - see {@link commitFencedEngineWrite} — the workflow this
|
|
93
|
+
* write is scoped to, or `null` for an engine-scoped write
|
|
63
94
|
* @param operations - the durable operations to commit atomically
|
|
64
95
|
* @param baseConditions - the caller's required CAS conditions (non-empty in
|
|
65
96
|
* practice — this path is for conditional starts, which always carry one)
|
|
66
97
|
*/
|
|
67
|
-
export declare function commitFencedEngineWriteAllowingPreconditionFailure(internals: EngineInternals, operations: BatchOperation[], baseConditions: ConditionalBatchCondition[]): Promise<boolean>;
|
|
98
|
+
export declare function commitFencedEngineWriteAllowingPreconditionFailure(internals: EngineInternals, workflowId: string | null, operations: BatchOperation[], baseConditions: ConditionalBatchCondition[]): Promise<boolean>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KEYS, storageConditionalBatch } from "../../storage/interface.js";
|
|
2
2
|
import { decodeEpoch } from "./lease-codec.js";
|
|
3
|
-
import { handleDeposition } from "./lease-deposition.js";
|
|
3
|
+
import { emitWorkflowClaimLostWarning, handleDeposition } from "./lease-deposition.js";
|
|
4
4
|
import { EngineDeposedError, EngineLeaseNotHeldError } from "./lease-errors.js";
|
|
5
5
|
export function assertLeaseHeldForEngineWork(internals) {
|
|
6
6
|
if (internals.options.ownershipMode !== "lease")
|
|
@@ -8,7 +8,9 @@ export function assertLeaseHeldForEngineWork(internals) {
|
|
|
8
8
|
if (!(!internals.deposed && internals.leaseManager !== null && internals.leaseManager.currentEpochBytes() !== null))
|
|
9
9
|
throw new EngineLeaseNotHeldError;
|
|
10
10
|
}
|
|
11
|
-
async function fencedCommit(internals, operations, baseConditions) {
|
|
11
|
+
async function fencedCommit(internals, workflowId, operations, baseConditions) {
|
|
12
|
+
if (internals.options.ownershipMode === "workflow-lease" && workflowId !== null)
|
|
13
|
+
return fencedCommitForWorkflow(internals, workflowId, operations, baseConditions);
|
|
12
14
|
const epochBytes = resolveFenceEpochOrHalt(internals);
|
|
13
15
|
if (epochBytes === null) {
|
|
14
16
|
if (baseConditions.length === 0) {
|
|
@@ -41,12 +43,30 @@ function resolveFenceEpochOrHalt(internals) {
|
|
|
41
43
|
}
|
|
42
44
|
return epochBytes;
|
|
43
45
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
async function fencedCommitForWorkflow(internals, workflowId, operations, baseConditions) {
|
|
47
|
+
const epochBytes = internals.workflowClaimRegistry?.currentEpochBytes(workflowId) ?? null;
|
|
48
|
+
if (epochBytes === null)
|
|
49
|
+
haltWorkflowClaim(workflowId);
|
|
50
|
+
const conditions = [
|
|
51
|
+
...baseConditions,
|
|
52
|
+
{ key: KEYS.workflowOwnerEpoch(workflowId), expectedValue: epochBytes }
|
|
53
|
+
];
|
|
54
|
+
if (await storageConditionalBatch(internals.storage, conditions, operations))
|
|
55
|
+
return "committed";
|
|
56
|
+
if (await isWorkflowDeposed(internals, workflowId, epochBytes))
|
|
57
|
+
haltWorkflowClaim(workflowId);
|
|
58
|
+
return "lost-race";
|
|
59
|
+
}
|
|
60
|
+
function haltWorkflowClaim(workflowId) {
|
|
61
|
+
emitWorkflowClaimLostWarning(workflowId);
|
|
62
|
+
throw new EngineDeposedError(workflowId);
|
|
63
|
+
}
|
|
64
|
+
export async function commitFencedEngineWrite(internals, workflowId, operations, baseConditions, onLostRace) {
|
|
65
|
+
if (await fencedCommit(internals, workflowId, operations, baseConditions) === "lost-race")
|
|
46
66
|
throw onLostRace();
|
|
47
67
|
}
|
|
48
|
-
export async function commitFencedEngineWriteAllowingPreconditionFailure(internals, operations, baseConditions) {
|
|
49
|
-
return await fencedCommit(internals, operations, baseConditions) === "committed";
|
|
68
|
+
export async function commitFencedEngineWriteAllowingPreconditionFailure(internals, workflowId, operations, baseConditions) {
|
|
69
|
+
return await fencedCommit(internals, workflowId, operations, baseConditions) === "committed";
|
|
50
70
|
}
|
|
51
71
|
function epochBytesEqual(a, b) {
|
|
52
72
|
if (b === null || a.byteLength !== b.byteLength)
|
|
@@ -69,3 +89,16 @@ async function isDeposed(internals, heldEpochBytes) {
|
|
|
69
89
|
return !0;
|
|
70
90
|
return !epochBytesEqual(heldEpochBytes, currentEpochRaw);
|
|
71
91
|
}
|
|
92
|
+
async function isWorkflowDeposed(internals, workflowId, heldEpochBytes) {
|
|
93
|
+
let currentEpochRaw;
|
|
94
|
+
try {
|
|
95
|
+
currentEpochRaw = await internals.storage.get(KEYS.workflowOwnerEpoch(workflowId));
|
|
96
|
+
} catch {
|
|
97
|
+
return !0;
|
|
98
|
+
}
|
|
99
|
+
if (currentEpochRaw === null)
|
|
100
|
+
return !0;
|
|
101
|
+
if (decodeEpoch(currentEpochRaw) === null)
|
|
102
|
+
return !0;
|
|
103
|
+
return !epochBytesEqual(heldEpochBytes, currentEpochRaw);
|
|
104
|
+
}
|
|
@@ -16,7 +16,9 @@ import type { EngineInternals } from './internals.ts';
|
|
|
16
16
|
* the next checkpoint still flushes the staged op in the terminal batch, so the
|
|
17
17
|
* finalizer always sees the resource id. The staged op inherits the lease-epoch
|
|
18
18
|
* fence (#470) for free, since checkpoint and terminal commits route through
|
|
19
|
-
* `
|
|
19
|
+
* `commitSelfWorkflowStateOperations` (completion) or
|
|
20
|
+
* `commitExternalTerminalWorkflowStateOperations` (cancel/timeout/suspend —
|
|
21
|
+
* ADR 0002), both in `storage-io.ts`.
|
|
20
22
|
*
|
|
21
23
|
* Oversized payloads are rejected before staging (the same hostile-input guard
|
|
22
24
|
* activity results use). A call made once the workflow is already terminalizing
|
|
@@ -4,5 +4,53 @@ import type { EngineInternals } from './internals.ts';
|
|
|
4
4
|
export declare function createWorkflowHandleWithResultPromise(internals: EngineInternals, workflowId: string): WorkflowHandle;
|
|
5
5
|
export declare function createWorkflowResultWaiter(internals: EngineInternals, workflowId: string): WorkflowResultWaiter;
|
|
6
6
|
export declare function getWorkflowResultPromise(internals: EngineInternals, workflowId: string): Promise<unknown>;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Result promise for a parent generator parked on `ctx.startChild()`'s
|
|
9
|
+
* default `parentClosePolicy: 'await'` — the ONLY intended caller (see
|
|
10
|
+
* `child-workflow.ts`'s `executeChildWorkflow`, which must call this instead
|
|
11
|
+
* of the plain `childHandle.result()` / `handle.result()` path). Identical to
|
|
12
|
+
* {@link getWorkflowResultPromise} except the waiter this settles is marked
|
|
13
|
+
* generator-owned for `parentWorkflowId`: under `ownership: 'workflow-lease'`
|
|
14
|
+
* a settle attempt first confirms `parentWorkflowId` still holds the claim
|
|
15
|
+
* generation it parked under (`confirmWakeOwnership`, `'child-completion'`),
|
|
16
|
+
* fencing duplicate generator advancement (WFT-79 F1) — see
|
|
17
|
+
* {@link bootstrapWorkflowResultResolver}. Top-level, non-generator callers
|
|
18
|
+
* must keep using {@link getWorkflowResultPromise}, which stays unfenced by
|
|
19
|
+
* design: cross-engine `handle.result()` polling has no generator to
|
|
20
|
+
* duplicate and must remain settleable from durable state alone.
|
|
21
|
+
*
|
|
22
|
+
* If `internals.resultResolvers` already holds a waiter for `workflowId` —
|
|
23
|
+
* e.g. an observational `handle.result()` caller got there first — that
|
|
24
|
+
* SHARED waiter is untouched: {@link fenceResultOnParentGeneration} wraps
|
|
25
|
+
* only THIS caller's derived promise, so the observational caller's own
|
|
26
|
+
* promise settles normally from the shared waiter regardless of the parent's
|
|
27
|
+
* claim generation. See that function's doc for why fencing lives on the
|
|
28
|
+
* parent's view instead of the shared waiter itself.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getGeneratorOwnedWorkflowResultPromise(internals: EngineInternals, workflowId: string, parentWorkflowId: string): Promise<unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Settle result waiters for workflows this engine does not own, whose terminal
|
|
33
|
+
* transition lands on another engine and so never touches this engine's
|
|
34
|
+
* in-memory resolver map.
|
|
35
|
+
*
|
|
36
|
+
* Exported for `backgroundTasks: 'manual'`, where no timer runs and the host
|
|
37
|
+
* drives every background step through an awaited `runMaintenance()`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function pollPendingCrossEngineResultWaiters(internals: EngineInternals): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Outcome of one {@link bootstrapWorkflowResultResolver} attempt, used by
|
|
42
|
+
* this file's callers to decide whether to keep polling:
|
|
43
|
+
*
|
|
44
|
+
* - `'settled'`: the waiter was resolved or rejected.
|
|
45
|
+
* - `'pending'`: no terminal result yet (still running, or a transient read
|
|
46
|
+
* failure under a guaranteed-retry ownership mode). Keep polling.
|
|
47
|
+
*
|
|
48
|
+
* A generator-owned caller's discarded parent generation needs no outcome
|
|
49
|
+
* here at all: `fenceResultOnParentGeneration` withholds settlement on that
|
|
50
|
+
* caller's own derived promise, independent of whether the SHARED waiter
|
|
51
|
+
* this function settles ever resolves.
|
|
52
|
+
*/
|
|
53
|
+
type ResultResolutionOutcome = 'settled' | 'pending';
|
|
54
|
+
export declare function bootstrapWorkflowResultResolver(internals: EngineInternals, workflowId: string, waiter: WorkflowResultWaiter): Promise<ResultResolutionOutcome>;
|
|
8
55
|
export declare function cacheHandle(internals: EngineInternals, workflowId: string, handle: WorkflowHandle): void;
|
|
56
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EngineDisposedError } from "./errors.js";
|
|
2
2
|
import { WorkflowHandle } from "./handles.js";
|
|
3
|
-
import {
|
|
3
|
+
import { deriveWorkflowResultFromState, loadWorkflowState } from "./storage-io.js";
|
|
4
|
+
import { confirmWakeOwnership } from "./wake-ownership-guard.js";
|
|
4
5
|
export function createWorkflowHandleWithResultPromise(internals, workflowId) {
|
|
5
6
|
const handle = new WorkflowHandle(workflowId, internals.engine);
|
|
6
7
|
cacheHandle(internals, workflowId, handle);
|
|
@@ -22,32 +23,106 @@ export function getWorkflowResultPromise(internals, workflowId) {
|
|
|
22
23
|
return rejected;
|
|
23
24
|
}
|
|
24
25
|
const waiter = createWorkflowResultWaiter(internals, workflowId);
|
|
25
|
-
bootstrapWorkflowResultResolver(internals, workflowId, waiter);
|
|
26
|
+
bootstrapWorkflowResultResolver(internals, workflowId, waiter).then(() => scheduleCrossEngineResultPollIfPending(internals, workflowId, waiter));
|
|
26
27
|
return waiter.promise;
|
|
27
28
|
}
|
|
28
|
-
export
|
|
29
|
+
export function getGeneratorOwnedWorkflowResultPromise(internals, workflowId, parentWorkflowId) {
|
|
30
|
+
return fenceResultOnParentGeneration(internals, parentWorkflowId, getWorkflowResultPromise(internals, workflowId));
|
|
31
|
+
}
|
|
32
|
+
function fenceResultOnParentGeneration(internals, parentWorkflowId, promise) {
|
|
33
|
+
const gate = Promise.withResolvers();
|
|
34
|
+
promise.then(async (value) => {
|
|
35
|
+
if (await parentStillOwnsGeneration(internals, parentWorkflowId))
|
|
36
|
+
gate.resolve(value);
|
|
37
|
+
}, async (error) => {
|
|
38
|
+
if (await parentStillOwnsGeneration(internals, parentWorkflowId))
|
|
39
|
+
gate.reject(error);
|
|
40
|
+
});
|
|
41
|
+
return gate.promise;
|
|
42
|
+
}
|
|
43
|
+
async function parentStillOwnsGeneration(internals, parentWorkflowId) {
|
|
29
44
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
return await confirmWakeOwnership(internals, parentWorkflowId, "child-completion") === "proceed";
|
|
46
|
+
} catch {
|
|
47
|
+
return !0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function scheduleCrossEngineResultPollIfPending(internals, workflowId, waiter) {
|
|
51
|
+
if (internals.workflowClaimRegistry === null)
|
|
52
|
+
return;
|
|
53
|
+
if (internals.resultResolvers.get(workflowId) !== waiter)
|
|
54
|
+
return;
|
|
55
|
+
if (internals.options.backgroundTaskMode !== "automatic")
|
|
56
|
+
return;
|
|
57
|
+
const handle = setTimeout(() => {
|
|
58
|
+
internals.pendingResultPollTimers.delete(handle);
|
|
59
|
+
bootstrapWorkflowResultResolver(internals, workflowId, waiter).then(() => scheduleCrossEngineResultPollIfPending(internals, workflowId, waiter));
|
|
60
|
+
}, internals.options.workflowClaimRenewIntervalMs);
|
|
61
|
+
internals.pendingResultPollTimers.add(handle);
|
|
62
|
+
handle.unref?.();
|
|
63
|
+
}
|
|
64
|
+
export async function pollPendingCrossEngineResultWaiters(internals) {
|
|
65
|
+
if (internals.workflowClaimRegistry === null)
|
|
66
|
+
return;
|
|
67
|
+
const pending = Array.from(internals.resultResolvers.entries());
|
|
68
|
+
for (const [workflowId, waiter] of pending) {
|
|
69
|
+
if (await deferToLocalTerminalDeliveryIfPending(internals, workflowId, waiter))
|
|
70
|
+
continue;
|
|
40
71
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
await bootstrapWorkflowResultResolver(internals, workflowId, waiter);
|
|
73
|
+
} catch {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function settleOrRetryOnTransientReadFailure(internals, workflowId, waiter, error) {
|
|
77
|
+
if (internals.workflowClaimRegistry === null) {
|
|
78
|
+
clearResultWaiter(internals, workflowId, waiter);
|
|
79
|
+
waiter.reject(error);
|
|
80
|
+
return "settled";
|
|
81
|
+
}
|
|
82
|
+
return "pending";
|
|
83
|
+
}
|
|
84
|
+
async function deferToLocalTerminalDeliveryIfPending(internals, workflowId, waiter) {
|
|
85
|
+
if (internals.workflowClaimRegistry?.currentEpoch(workflowId) == null)
|
|
86
|
+
return !1;
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
88
|
+
return internals.resultResolvers.get(workflowId) !== waiter;
|
|
89
|
+
}
|
|
90
|
+
async function prepareToSettleTerminalWaiter(internals, workflowId, waiter) {
|
|
91
|
+
if (await deferToLocalTerminalDeliveryIfPending(internals, workflowId, waiter))
|
|
92
|
+
return "settled";
|
|
93
|
+
return "proceed";
|
|
94
|
+
}
|
|
95
|
+
export async function bootstrapWorkflowResultResolver(internals, workflowId, waiter) {
|
|
96
|
+
let state;
|
|
97
|
+
try {
|
|
98
|
+
state = await loadWorkflowState(internals, workflowId);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return settleOrRetryOnTransientReadFailure(internals, workflowId, waiter, error);
|
|
101
|
+
}
|
|
102
|
+
if (linkToReplacementWaiter(internals, workflowId, waiter))
|
|
103
|
+
return "settled";
|
|
104
|
+
if (!state) {
|
|
105
|
+
const outcome = await prepareToSettleTerminalWaiter(internals, workflowId, waiter);
|
|
106
|
+
if (outcome !== "proceed")
|
|
107
|
+
return outcome;
|
|
108
|
+
internals.resultResolvers.delete(workflowId);
|
|
109
|
+
waiter.reject(Error(`Workflow "${workflowId}" not found in storage`));
|
|
110
|
+
return "settled";
|
|
111
|
+
}
|
|
112
|
+
if (state.status === "running" || state.status === "pending" || state.status === "suspended")
|
|
113
|
+
return "pending";
|
|
114
|
+
const outcome = await prepareToSettleTerminalWaiter(internals, workflowId, waiter);
|
|
115
|
+
if (outcome !== "proceed")
|
|
116
|
+
return outcome;
|
|
117
|
+
try {
|
|
118
|
+
const result = deriveWorkflowResultFromState(state);
|
|
119
|
+
clearResultWaiter(internals, workflowId, waiter);
|
|
120
|
+
waiter.resolve(result);
|
|
121
|
+
return "settled";
|
|
48
122
|
} catch (error) {
|
|
49
123
|
clearResultWaiter(internals, workflowId, waiter);
|
|
50
124
|
waiter.reject(error);
|
|
125
|
+
return "settled";
|
|
51
126
|
}
|
|
52
127
|
}
|
|
53
128
|
function linkToReplacementWaiter(internals, workflowId, waiter) {
|
|
@@ -27,7 +27,9 @@ export type { PendingAsyncActivity } from './async-activity-records.ts';
|
|
|
27
27
|
export type { PendingTimelineEntry, RegistrationEntry, ResolvedOptions, TrackedWaiterKeys, WorkflowResultWaiter, } from './engine-internal-types.ts';
|
|
28
28
|
export { ActivityResolutionError, BulkDeleteRequiresTerminalWorkflowsError, BulkOperationConfirmationError, EngineCreateNameMismatchError, EngineDisposalError, EngineDisposedError, IdempotencyKeyPurgedError, PersistedDataIncompatibleError, StartOrSignalConflictError, WorkflowAlreadyExistsError, WorkflowConcurrencyLimitExceededError, WorkflowNotFoundError, WorkflowNotRegisteredError, WorkflowSuspendNotSupportedError, WorkflowTeardownPendingError, WorkflowTypeNotRegisteredForRecoveryError, } from './errors.ts';
|
|
29
29
|
export { HANDLE_RESULT_PROMISE, WorkflowHandle } from './handles.ts';
|
|
30
|
-
export {
|
|
30
|
+
export { WeftWorkflowClaimLostWarning, WeftWorkflowWakeDiscardedWarning, WORKFLOW_CLAIM_LOST_WARNING_NAME, WORKFLOW_WAKE_DISCARDED_WARNING_NAME, } from './lease-deposition.ts';
|
|
31
|
+
export type { WorkflowWakeKind } from './lease-deposition.ts';
|
|
32
|
+
export { EngineLeaseAcquisitionTimeoutError, EngineLeaseCorruptedError, EngineLeaseNotHeldError, OwnershipModeMismatchError, WorkflowClaimUnavailableError, } from './lease-errors.ts';
|
|
31
33
|
export type { EngineLeaseHealth, LeaseLostReason } from './lease-health.ts';
|
|
32
34
|
export type { RecoverAllOptions, RecoveredWorkflowInfo } from './lifecycle.ts';
|
|
33
35
|
export { ScheduleHandle } from './schedule-handle.ts';
|
|
@@ -272,13 +274,19 @@ export declare class Engine<TWorkflows extends object = DefaultWorkflowRegistry,
|
|
|
272
274
|
aggregate(filter: ListFilter | undefined, options: AggregateOptions): Promise<AggregateResult>;
|
|
273
275
|
getRetentionOverview(): RetentionOverview;
|
|
274
276
|
/**
|
|
275
|
-
* Run one host-driven maintenance cycle. This
|
|
276
|
-
*
|
|
277
|
-
*
|
|
277
|
+
* Run one host-driven maintenance cycle. This runs the `ownership:
|
|
278
|
+
* 'workflow-lease'` construction gates (idempotent — a no-op once already
|
|
279
|
+
* bootstrapped) and one per-workflow claim-renewal pass, fires due durable
|
|
280
|
+
* timers, deletes expired update responses, applies configured retention,
|
|
281
|
+
* and re-evaluates alert rules without relying on process-local intervals.
|
|
278
282
|
*
|
|
279
283
|
* Use with `backgroundTasks: 'manual'` from a serverless alarm, Cron trigger,
|
|
280
284
|
* or another externally scheduled wake-up. Concurrent calls are safe, but a
|
|
281
|
-
* host should await each cycle before scheduling another.
|
|
285
|
+
* host should await each cycle before scheduling another. Under `ownership:
|
|
286
|
+
* 'workflow-lease'`, this is also the ONLY thing that renews this engine's
|
|
287
|
+
* per-workflow claims in manual mode — no interval is ever started for
|
|
288
|
+
* `backgroundTasks: 'manual'`, so a host that calls this less often than
|
|
289
|
+
* `workflowClaimRenewInterval` will see its own live claims lapse.
|
|
282
290
|
*/
|
|
283
291
|
runMaintenance(now?: number): Promise<void>;
|
|
284
292
|
purge(filter?: ListFilter): Promise<PurgeResult>;
|