@dudousxd/nestjs-durable-core 0.52.0 → 0.53.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/dist/index.cjs +183 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +183 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -628,6 +628,16 @@ interface StateStore {
|
|
|
628
628
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
629
629
|
/** List waiters whose `token` starts with `prefix` — used to fan out an event to its subscribers. */
|
|
630
630
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
|
631
|
+
/**
|
|
632
|
+
* Delete the EXACT waiter row — `token` AND `runId` AND `seq` must all match — no-op if absent.
|
|
633
|
+
* Unlike {@link takeSignalWaiter}, which deletes ANY row for `token` (fine when the caller just won
|
|
634
|
+
* the race to consume it), this is for a caller removing its OWN registration after resolving the
|
|
635
|
+
* wait some other way (a buffered hit, a timeout): blind `takeSignalWaiter(token)` there could steal
|
|
636
|
+
* a DIFFERENT run's waiter that has since claimed the same token (`token` is the store's primary key,
|
|
637
|
+
* so a later `putSignalWaiter` for the same token replaces the row). The exact-match variant only
|
|
638
|
+
* ever removes the row this caller itself put there.
|
|
639
|
+
*/
|
|
640
|
+
removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
631
641
|
/**
|
|
632
642
|
* Buffer a signal whose waiter hasn't arrived yet, so the next `waitForSignal(token)` consumes it
|
|
633
643
|
* instead of it being lost (FIFO per token). Makes signals reliable regardless of timing and
|
|
@@ -2373,8 +2383,32 @@ declare class WorkflowEngine {
|
|
|
2373
2383
|
/**
|
|
2374
2384
|
* Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
|
|
2375
2385
|
* boot. A run still not due re-suspends cheaply without running new work.
|
|
2386
|
+
*
|
|
2387
|
+
* Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a crash
|
|
2388
|
+
* between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
|
|
2389
|
+
* `takeBufferedSignal`, or a signaler's `takeSignalWaiter` → `bufferSignal` → re-check — see
|
|
2390
|
+
* `signal`'s interleaving proof) can leave both rows stranded with nothing left to pair them: the
|
|
2391
|
+
* waiter's turn already suspended, the signaler that buffered it already returned. Event-wait
|
|
2392
|
+
* suspends already carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so
|
|
2393
|
+
* they show up HERE — that's what makes this due-timer pass the natural place for the sweep. One
|
|
2394
|
+
* bulk waiter scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
|
|
2376
2395
|
*/
|
|
2377
2396
|
resumeDueTimers(nowMs?: number): Promise<RunResult[]>;
|
|
2397
|
+
/**
|
|
2398
|
+
* The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
|
|
2399
|
+
* a registered signal waiter AND its token already has a buffered payload, pair and deliver them
|
|
2400
|
+
* directly instead of falling through to a full replay. Event/child/breakpoint waiter tokens are
|
|
2401
|
+
* never buffered (`publishEvent` doesn't buffer, and a child/breakpoint notifies via `signal` only
|
|
2402
|
+
* once its counterpart is already registered) — so `takeBufferedSignal` on those is a guaranteed
|
|
2403
|
+
* miss, and this is naturally signal-only without needing to classify the token.
|
|
2404
|
+
*
|
|
2405
|
+
* Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
|
|
2406
|
+
* no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
|
|
2407
|
+
* via {@link deliverSignal}, and even a `NamespaceMismatch` there is reported as a real (non-nullish)
|
|
2408
|
+
* result — returning `null` would read as "not settled" to `resumeLeased`'s `??` fallback and
|
|
2409
|
+
* trigger a second, redundant `resume` call on the same run.
|
|
2410
|
+
*/
|
|
2411
|
+
private reconcileSignalWaiter;
|
|
2378
2412
|
/**
|
|
2379
2413
|
* Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
|
|
2380
2414
|
* exactly one instance runs it), then resumes the body. Returns the result, or null if another
|
|
@@ -2447,6 +2481,37 @@ declare class WorkflowEngine {
|
|
|
2447
2481
|
publishEvent(name: string, payload: unknown, opts?: {
|
|
2448
2482
|
id?: string;
|
|
2449
2483
|
}): Promise<number>;
|
|
2484
|
+
/**
|
|
2485
|
+
* Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
|
|
2486
|
+
* resume the run. Shared by {@link signal}'s direct hit, its post-buffer re-check reclaim, and the
|
|
2487
|
+
* due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
|
|
2488
|
+
* same way.
|
|
2489
|
+
*/
|
|
2490
|
+
private deliverSignal;
|
|
2491
|
+
/**
|
|
2492
|
+
* Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
|
|
2493
|
+
* the run result — or null if no run is (or ends up) waiting for it.
|
|
2494
|
+
*
|
|
2495
|
+
* Race with the waiter side (`waitForSignal`'s check → `putSignalWaiter` → re-check, same shape in
|
|
2496
|
+
* the remote `waitSignal` command): a waiter can register in the sliver between this method's
|
|
2497
|
+
* initial `takeSignalWaiter` miss and its `bufferSignal` write, and the naive old flow (buffer and
|
|
2498
|
+
* return) would then leave BOTH a buffered payload and a registered waiter sitting in the store with
|
|
2499
|
+
* nothing left to pair them — a lost wake, forever suspended.
|
|
2500
|
+
*
|
|
2501
|
+
* The interleaving proof: the waiter side does check → put → re-check, this side does take → buffer
|
|
2502
|
+
* → re-check. Whichever side's SECOND look runs last is guaranteed to observe the other side's
|
|
2503
|
+
* write, because it always follows that write in real time:
|
|
2504
|
+
* - If the waiter's `put` lands before this method's first `take`, the take just finds it directly
|
|
2505
|
+
* (the pre-existing, non-racy path).
|
|
2506
|
+
* - If the waiter's `put` lands after this method's `bufferSignal`, our re-check below either sees
|
|
2507
|
+
* it (if `put` also lands before the re-check) or the waiter's OWN post-put re-check sees our
|
|
2508
|
+
* buffer (if `put` lands after our re-check but the waiter hasn't re-checked yet either — its
|
|
2509
|
+
* re-check strictly follows its own put).
|
|
2510
|
+
* - If the waiter's `put` lands between this method's `take` and its re-`take`, the re-check below
|
|
2511
|
+
* catches it.
|
|
2512
|
+
* Either way, the destructive `take*` ops (on the waiter row, on the buffer) arbitrate: whichever
|
|
2513
|
+
* side actually removes the row is the one that delivers, so a signal is never delivered twice.
|
|
2514
|
+
*/
|
|
2450
2515
|
signal(token: string, payload: unknown): Promise<RunResult | null>;
|
|
2451
2516
|
/**
|
|
2452
2517
|
* Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
|
|
@@ -3155,6 +3220,7 @@ declare class CodecStateStore implements StateStore {
|
|
|
3155
3220
|
putSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
3156
3221
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
3157
3222
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
|
3223
|
+
removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
3158
3224
|
bufferSignal(token: string, payload: unknown): Promise<void>;
|
|
3159
3225
|
takeBufferedSignal(token: string): Promise<{
|
|
3160
3226
|
payload: unknown;
|
|
@@ -3522,6 +3588,7 @@ declare class InMemoryStateStore implements StateStore {
|
|
|
3522
3588
|
putSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
3523
3589
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
3524
3590
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
|
3591
|
+
removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
3525
3592
|
transaction<T>(work: (tx: {
|
|
3526
3593
|
raw: unknown;
|
|
3527
3594
|
saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -628,6 +628,16 @@ interface StateStore {
|
|
|
628
628
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
629
629
|
/** List waiters whose `token` starts with `prefix` — used to fan out an event to its subscribers. */
|
|
630
630
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
|
631
|
+
/**
|
|
632
|
+
* Delete the EXACT waiter row — `token` AND `runId` AND `seq` must all match — no-op if absent.
|
|
633
|
+
* Unlike {@link takeSignalWaiter}, which deletes ANY row for `token` (fine when the caller just won
|
|
634
|
+
* the race to consume it), this is for a caller removing its OWN registration after resolving the
|
|
635
|
+
* wait some other way (a buffered hit, a timeout): blind `takeSignalWaiter(token)` there could steal
|
|
636
|
+
* a DIFFERENT run's waiter that has since claimed the same token (`token` is the store's primary key,
|
|
637
|
+
* so a later `putSignalWaiter` for the same token replaces the row). The exact-match variant only
|
|
638
|
+
* ever removes the row this caller itself put there.
|
|
639
|
+
*/
|
|
640
|
+
removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
631
641
|
/**
|
|
632
642
|
* Buffer a signal whose waiter hasn't arrived yet, so the next `waitForSignal(token)` consumes it
|
|
633
643
|
* instead of it being lost (FIFO per token). Makes signals reliable regardless of timing and
|
|
@@ -2373,8 +2383,32 @@ declare class WorkflowEngine {
|
|
|
2373
2383
|
/**
|
|
2374
2384
|
* Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
|
|
2375
2385
|
* boot. A run still not due re-suspends cheaply without running new work.
|
|
2386
|
+
*
|
|
2387
|
+
* Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a crash
|
|
2388
|
+
* between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
|
|
2389
|
+
* `takeBufferedSignal`, or a signaler's `takeSignalWaiter` → `bufferSignal` → re-check — see
|
|
2390
|
+
* `signal`'s interleaving proof) can leave both rows stranded with nothing left to pair them: the
|
|
2391
|
+
* waiter's turn already suspended, the signaler that buffered it already returned. Event-wait
|
|
2392
|
+
* suspends already carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so
|
|
2393
|
+
* they show up HERE — that's what makes this due-timer pass the natural place for the sweep. One
|
|
2394
|
+
* bulk waiter scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
|
|
2376
2395
|
*/
|
|
2377
2396
|
resumeDueTimers(nowMs?: number): Promise<RunResult[]>;
|
|
2397
|
+
/**
|
|
2398
|
+
* The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
|
|
2399
|
+
* a registered signal waiter AND its token already has a buffered payload, pair and deliver them
|
|
2400
|
+
* directly instead of falling through to a full replay. Event/child/breakpoint waiter tokens are
|
|
2401
|
+
* never buffered (`publishEvent` doesn't buffer, and a child/breakpoint notifies via `signal` only
|
|
2402
|
+
* once its counterpart is already registered) — so `takeBufferedSignal` on those is a guaranteed
|
|
2403
|
+
* miss, and this is naturally signal-only without needing to classify the token.
|
|
2404
|
+
*
|
|
2405
|
+
* Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
|
|
2406
|
+
* no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
|
|
2407
|
+
* via {@link deliverSignal}, and even a `NamespaceMismatch` there is reported as a real (non-nullish)
|
|
2408
|
+
* result — returning `null` would read as "not settled" to `resumeLeased`'s `??` fallback and
|
|
2409
|
+
* trigger a second, redundant `resume` call on the same run.
|
|
2410
|
+
*/
|
|
2411
|
+
private reconcileSignalWaiter;
|
|
2378
2412
|
/**
|
|
2379
2413
|
* Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
|
|
2380
2414
|
* exactly one instance runs it), then resumes the body. Returns the result, or null if another
|
|
@@ -2447,6 +2481,37 @@ declare class WorkflowEngine {
|
|
|
2447
2481
|
publishEvent(name: string, payload: unknown, opts?: {
|
|
2448
2482
|
id?: string;
|
|
2449
2483
|
}): Promise<number>;
|
|
2484
|
+
/**
|
|
2485
|
+
* Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
|
|
2486
|
+
* resume the run. Shared by {@link signal}'s direct hit, its post-buffer re-check reclaim, and the
|
|
2487
|
+
* due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
|
|
2488
|
+
* same way.
|
|
2489
|
+
*/
|
|
2490
|
+
private deliverSignal;
|
|
2491
|
+
/**
|
|
2492
|
+
* Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
|
|
2493
|
+
* the run result — or null if no run is (or ends up) waiting for it.
|
|
2494
|
+
*
|
|
2495
|
+
* Race with the waiter side (`waitForSignal`'s check → `putSignalWaiter` → re-check, same shape in
|
|
2496
|
+
* the remote `waitSignal` command): a waiter can register in the sliver between this method's
|
|
2497
|
+
* initial `takeSignalWaiter` miss and its `bufferSignal` write, and the naive old flow (buffer and
|
|
2498
|
+
* return) would then leave BOTH a buffered payload and a registered waiter sitting in the store with
|
|
2499
|
+
* nothing left to pair them — a lost wake, forever suspended.
|
|
2500
|
+
*
|
|
2501
|
+
* The interleaving proof: the waiter side does check → put → re-check, this side does take → buffer
|
|
2502
|
+
* → re-check. Whichever side's SECOND look runs last is guaranteed to observe the other side's
|
|
2503
|
+
* write, because it always follows that write in real time:
|
|
2504
|
+
* - If the waiter's `put` lands before this method's first `take`, the take just finds it directly
|
|
2505
|
+
* (the pre-existing, non-racy path).
|
|
2506
|
+
* - If the waiter's `put` lands after this method's `bufferSignal`, our re-check below either sees
|
|
2507
|
+
* it (if `put` also lands before the re-check) or the waiter's OWN post-put re-check sees our
|
|
2508
|
+
* buffer (if `put` lands after our re-check but the waiter hasn't re-checked yet either — its
|
|
2509
|
+
* re-check strictly follows its own put).
|
|
2510
|
+
* - If the waiter's `put` lands between this method's `take` and its re-`take`, the re-check below
|
|
2511
|
+
* catches it.
|
|
2512
|
+
* Either way, the destructive `take*` ops (on the waiter row, on the buffer) arbitrate: whichever
|
|
2513
|
+
* side actually removes the row is the one that delivers, so a signal is never delivered twice.
|
|
2514
|
+
*/
|
|
2450
2515
|
signal(token: string, payload: unknown): Promise<RunResult | null>;
|
|
2451
2516
|
/**
|
|
2452
2517
|
* Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
|
|
@@ -3155,6 +3220,7 @@ declare class CodecStateStore implements StateStore {
|
|
|
3155
3220
|
putSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
3156
3221
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
3157
3222
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
|
3223
|
+
removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
3158
3224
|
bufferSignal(token: string, payload: unknown): Promise<void>;
|
|
3159
3225
|
takeBufferedSignal(token: string): Promise<{
|
|
3160
3226
|
payload: unknown;
|
|
@@ -3522,6 +3588,7 @@ declare class InMemoryStateStore implements StateStore {
|
|
|
3522
3588
|
putSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
3523
3589
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
3524
3590
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
|
3591
|
+
removeSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
3525
3592
|
transaction<T>(work: (tx: {
|
|
3526
3593
|
raw: unknown;
|
|
3527
3594
|
saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -865,6 +865,47 @@ var RemoteWorkflowExecutor = class {
|
|
|
865
865
|
}
|
|
866
866
|
};
|
|
867
867
|
|
|
868
|
+
// src/run-waiting.ts
|
|
869
|
+
var WEBHOOK_PREFIX = "wh:";
|
|
870
|
+
var CHILD_PREFIX = "child:";
|
|
871
|
+
var BREAKPOINT_PREFIX = "bp:";
|
|
872
|
+
var EVENT_PREFIX = "event:";
|
|
873
|
+
function classifyWaiterToken(token) {
|
|
874
|
+
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
875
|
+
on: "webhook",
|
|
876
|
+
name: token
|
|
877
|
+
};
|
|
878
|
+
if (token.startsWith(BREAKPOINT_PREFIX)) return {
|
|
879
|
+
on: "breakpoint",
|
|
880
|
+
name: "breakpoint"
|
|
881
|
+
};
|
|
882
|
+
if (token.startsWith(CHILD_PREFIX)) return {
|
|
883
|
+
on: "child",
|
|
884
|
+
name: token.slice(CHILD_PREFIX.length)
|
|
885
|
+
};
|
|
886
|
+
if (token.startsWith(EVENT_PREFIX)) return {
|
|
887
|
+
on: "signal",
|
|
888
|
+
name: eventNameOf(token)
|
|
889
|
+
};
|
|
890
|
+
return {
|
|
891
|
+
on: "signal",
|
|
892
|
+
name: token
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
__name(classifyWaiterToken, "classifyWaiterToken");
|
|
896
|
+
function resolveRunWaiting(run, waiterByRun) {
|
|
897
|
+
if (run.status !== "suspended") return void 0;
|
|
898
|
+
const waiter = waiterByRun.get(run.id);
|
|
899
|
+
return waiter ? classifyWaiterToken(waiter.token) : void 0;
|
|
900
|
+
}
|
|
901
|
+
__name(resolveRunWaiting, "resolveRunWaiting");
|
|
902
|
+
function indexWaitersByRun(waiters) {
|
|
903
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
904
|
+
for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
|
|
905
|
+
return byRun;
|
|
906
|
+
}
|
|
907
|
+
__name(indexWaitersByRun, "indexWaitersByRun");
|
|
908
|
+
|
|
868
909
|
// src/singleton-gate.ts
|
|
869
910
|
var SINGLETON_RETRY_MS = 1e3;
|
|
870
911
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -1320,6 +1361,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1320
1361
|
runId,
|
|
1321
1362
|
seq: current
|
|
1322
1363
|
});
|
|
1364
|
+
const lateBuffered2 = await consumeBuffered(token, current);
|
|
1365
|
+
if (lateBuffered2) {
|
|
1366
|
+
await store.removeSignalWaiter({
|
|
1367
|
+
token,
|
|
1368
|
+
runId,
|
|
1369
|
+
seq: current
|
|
1370
|
+
});
|
|
1371
|
+
return lateBuffered2.value;
|
|
1372
|
+
}
|
|
1323
1373
|
throw new WorkflowSuspended();
|
|
1324
1374
|
}
|
|
1325
1375
|
const timeoutMs = opts.timeoutMs;
|
|
@@ -1331,7 +1381,11 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1331
1381
|
const buffered = await consumeBuffered(token, waitSeq);
|
|
1332
1382
|
if (buffered) return buffered.value;
|
|
1333
1383
|
if (host.clock() >= deadline) {
|
|
1334
|
-
await store.
|
|
1384
|
+
await store.removeSignalWaiter({
|
|
1385
|
+
token,
|
|
1386
|
+
runId,
|
|
1387
|
+
seq: waitSeq
|
|
1388
|
+
}).catch(() => void 0);
|
|
1335
1389
|
throw new SignalTimeoutError(token, timeoutMs);
|
|
1336
1390
|
}
|
|
1337
1391
|
await store.putSignalWaiter({
|
|
@@ -1339,6 +1393,15 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1339
1393
|
runId,
|
|
1340
1394
|
seq: waitSeq
|
|
1341
1395
|
});
|
|
1396
|
+
const lateBuffered = await consumeBuffered(token, waitSeq);
|
|
1397
|
+
if (lateBuffered) {
|
|
1398
|
+
await store.removeSignalWaiter({
|
|
1399
|
+
token,
|
|
1400
|
+
runId,
|
|
1401
|
+
seq: waitSeq
|
|
1402
|
+
});
|
|
1403
|
+
return lateBuffered.value;
|
|
1404
|
+
}
|
|
1342
1405
|
throw new WorkflowSuspended(deadline);
|
|
1343
1406
|
}, "waitForSignal");
|
|
1344
1407
|
const waitForEvent = /* @__PURE__ */ __name(async (name, opts) => {
|
|
@@ -1362,7 +1425,11 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
|
|
|
1362
1425
|
const waited = await readCheckpoint(waitSeq);
|
|
1363
1426
|
if (waited && waited.status === "completed") return waited.output;
|
|
1364
1427
|
if (host.clock() >= deadline) {
|
|
1365
|
-
await store.
|
|
1428
|
+
await store.removeSignalWaiter({
|
|
1429
|
+
token,
|
|
1430
|
+
runId,
|
|
1431
|
+
seq: waitSeq
|
|
1432
|
+
}).catch(() => void 0);
|
|
1366
1433
|
throw new SignalTimeoutError(`event:${name}`, timeoutMs);
|
|
1367
1434
|
}
|
|
1368
1435
|
await store.putSignalWaiter({
|
|
@@ -2508,9 +2575,46 @@ var WorkflowEngine = class {
|
|
|
2508
2575
|
/**
|
|
2509
2576
|
* Resume every suspended run whose durable timer is due. Call periodically (a poller) and on
|
|
2510
2577
|
* boot. A run still not due re-suspends cheaply without running new work.
|
|
2578
|
+
*
|
|
2579
|
+
* Piggy-backs the signal-waiter reconcile safety net (see {@link reconcileSignalWaiter}): a crash
|
|
2580
|
+
* between the waiter/buffer pairing ops on EITHER side (this run's own `putSignalWaiter` →
|
|
2581
|
+
* `takeBufferedSignal`, or a signaler's `takeSignalWaiter` → `bufferSignal` → re-check — see
|
|
2582
|
+
* `signal`'s interleaving proof) can leave both rows stranded with nothing left to pair them: the
|
|
2583
|
+
* waiter's turn already suspended, the signaler that buffered it already returned. Event-wait
|
|
2584
|
+
* suspends already carry a `reconcileMs` fallback `wakeAt` (see {@link reconcileWakeAt}) precisely so
|
|
2585
|
+
* they show up HERE — that's what makes this due-timer pass the natural place for the sweep. One
|
|
2586
|
+
* bulk waiter scan, filtered to just this tick's batch, keeps it cheap regardless of store size.
|
|
2511
2587
|
*/
|
|
2512
2588
|
async resumeDueTimers(nowMs = this.clock()) {
|
|
2513
|
-
|
|
2589
|
+
const due = await this.store.listDueTimers(nowMs, this.namespace);
|
|
2590
|
+
if (due.length === 0) return [];
|
|
2591
|
+
const waiterByRun = indexWaitersByRun(await this.store.listSignalWaiters(""));
|
|
2592
|
+
return this.resumeLeased(due, nowMs, (run) => this.reconcileSignalWaiter(run, waiterByRun.get(run.id)));
|
|
2593
|
+
}
|
|
2594
|
+
/**
|
|
2595
|
+
* The reconcile safety net's per-run check (see {@link resumeDueTimers}): if this suspended run has
|
|
2596
|
+
* a registered signal waiter AND its token already has a buffered payload, pair and deliver them
|
|
2597
|
+
* directly instead of falling through to a full replay. Event/child/breakpoint waiter tokens are
|
|
2598
|
+
* never buffered (`publishEvent` doesn't buffer, and a child/breakpoint notifies via `signal` only
|
|
2599
|
+
* once its counterpart is already registered) — so `takeBufferedSignal` on those is a guaranteed
|
|
2600
|
+
* miss, and this is naturally signal-only without needing to classify the token.
|
|
2601
|
+
*
|
|
2602
|
+
* Returns `undefined` (not settled — `resumeLeased` proceeds with its normal `resume`) when there's
|
|
2603
|
+
* no waiter or no buffered hit. Deliberately never returns `null`: a delivered signal always resumes
|
|
2604
|
+
* via {@link deliverSignal}, and even a `NamespaceMismatch` there is reported as a real (non-nullish)
|
|
2605
|
+
* result — returning `null` would read as "not settled" to `resumeLeased`'s `??` fallback and
|
|
2606
|
+
* trigger a second, redundant `resume` call on the same run.
|
|
2607
|
+
*/
|
|
2608
|
+
async reconcileSignalWaiter(run, waiter) {
|
|
2609
|
+
if (!waiter) return void 0;
|
|
2610
|
+
const buffered = await this.store.takeBufferedSignal(waiter.token);
|
|
2611
|
+
if (!buffered) return void 0;
|
|
2612
|
+
await this.store.removeSignalWaiter(waiter);
|
|
2613
|
+
const settled = await this.deliverSignal(waiter, buffered.payload);
|
|
2614
|
+
return settled ?? {
|
|
2615
|
+
runId: run.id,
|
|
2616
|
+
status: run.status
|
|
2617
|
+
};
|
|
2514
2618
|
}
|
|
2515
2619
|
/**
|
|
2516
2620
|
* Lease and execute one run by id — the worker side of dispatch. Acquires the recovery lease (so
|
|
@@ -2728,16 +2832,17 @@ var WorkflowEngine = class {
|
|
|
2728
2832
|
}
|
|
2729
2833
|
return touched;
|
|
2730
2834
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2835
|
+
/**
|
|
2836
|
+
* Deliver a signal to an already-known waiter: write its resolving `signal:<token>` checkpoint and
|
|
2837
|
+
* resume the run. Shared by {@link signal}'s direct hit, its post-buffer re-check reclaim, and the
|
|
2838
|
+
* due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
|
|
2839
|
+
* same way.
|
|
2840
|
+
*/
|
|
2841
|
+
async deliverSignal(waiter, payload) {
|
|
2737
2842
|
await this.store.saveCheckpoint(instantCheckpoint({
|
|
2738
2843
|
runId: waiter.runId,
|
|
2739
2844
|
seq: waiter.seq,
|
|
2740
|
-
name: `signal:${token}`,
|
|
2845
|
+
name: `signal:${waiter.token}`,
|
|
2741
2846
|
kind: "signal",
|
|
2742
2847
|
output: payload,
|
|
2743
2848
|
// Carry the awaiting command's fan group (set when a `ctx.gather_children`/`ctx.all` fan-out
|
|
@@ -2752,6 +2857,42 @@ var WorkflowEngine = class {
|
|
|
2752
2857
|
});
|
|
2753
2858
|
}
|
|
2754
2859
|
/**
|
|
2860
|
+
* Deliver an external signal to the run waiting on `token`, resume it with `payload`, and return
|
|
2861
|
+
* the run result — or null if no run is (or ends up) waiting for it.
|
|
2862
|
+
*
|
|
2863
|
+
* Race with the waiter side (`waitForSignal`'s check → `putSignalWaiter` → re-check, same shape in
|
|
2864
|
+
* the remote `waitSignal` command): a waiter can register in the sliver between this method's
|
|
2865
|
+
* initial `takeSignalWaiter` miss and its `bufferSignal` write, and the naive old flow (buffer and
|
|
2866
|
+
* return) would then leave BOTH a buffered payload and a registered waiter sitting in the store with
|
|
2867
|
+
* nothing left to pair them — a lost wake, forever suspended.
|
|
2868
|
+
*
|
|
2869
|
+
* The interleaving proof: the waiter side does check → put → re-check, this side does take → buffer
|
|
2870
|
+
* → re-check. Whichever side's SECOND look runs last is guaranteed to observe the other side's
|
|
2871
|
+
* write, because it always follows that write in real time:
|
|
2872
|
+
* - If the waiter's `put` lands before this method's first `take`, the take just finds it directly
|
|
2873
|
+
* (the pre-existing, non-racy path).
|
|
2874
|
+
* - If the waiter's `put` lands after this method's `bufferSignal`, our re-check below either sees
|
|
2875
|
+
* it (if `put` also lands before the re-check) or the waiter's OWN post-put re-check sees our
|
|
2876
|
+
* buffer (if `put` lands after our re-check but the waiter hasn't re-checked yet either — its
|
|
2877
|
+
* re-check strictly follows its own put).
|
|
2878
|
+
* - If the waiter's `put` lands between this method's `take` and its re-`take`, the re-check below
|
|
2879
|
+
* catches it.
|
|
2880
|
+
* Either way, the destructive `take*` ops (on the waiter row, on the buffer) arbitrate: whichever
|
|
2881
|
+
* side actually removes the row is the one that delivers, so a signal is never delivered twice.
|
|
2882
|
+
*/
|
|
2883
|
+
async signal(token, payload) {
|
|
2884
|
+
const waiter = await this.store.takeSignalWaiter(token);
|
|
2885
|
+
if (waiter) return this.deliverSignal(waiter, payload);
|
|
2886
|
+
await this.store.bufferSignal(token, payload);
|
|
2887
|
+
const lateWaiter = await this.store.takeSignalWaiter(token);
|
|
2888
|
+
if (!lateWaiter) return null;
|
|
2889
|
+
const reclaimed = await this.store.takeBufferedSignal(token);
|
|
2890
|
+
if (!reclaimed) {
|
|
2891
|
+
return null;
|
|
2892
|
+
}
|
|
2893
|
+
return this.deliverSignal(lateWaiter, reclaimed.payload);
|
|
2894
|
+
}
|
|
2895
|
+
/**
|
|
2755
2896
|
* Ensure a run exists for `runId`, then deliver a signal to it — atomically race-free thanks to
|
|
2756
2897
|
* signal buffering: if the run is new (or busy / not yet waiting), the signal is buffered and
|
|
2757
2898
|
* consumed when it reaches `waitForSignal(token)`. The canonical **durable-entity / accumulator**
|
|
@@ -3224,6 +3365,14 @@ var WorkflowEngine = class {
|
|
|
3224
3365
|
error: outcome.error
|
|
3225
3366
|
};
|
|
3226
3367
|
}
|
|
3368
|
+
const latest = await this.store.getRun(run.id);
|
|
3369
|
+
if (latest?.status === "cancelled") {
|
|
3370
|
+
return {
|
|
3371
|
+
runId: run.id,
|
|
3372
|
+
status: "cancelled",
|
|
3373
|
+
error: latest.error
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3227
3376
|
await this.store.updateRun(run.id, {
|
|
3228
3377
|
status: "suspended",
|
|
3229
3378
|
wakeAt: this.reconcileWakeAt(outcome.wakeAt),
|
|
@@ -3566,22 +3715,34 @@ var WorkflowEngine = class {
|
|
|
3566
3715
|
}));
|
|
3567
3716
|
wakeAt = wakeAt == null ? deadline : Math.min(wakeAt, deadline);
|
|
3568
3717
|
} else if (cmd.kind === "waitSignal") {
|
|
3569
|
-
const
|
|
3570
|
-
if (buffered) {
|
|
3718
|
+
const deliverBuffered = /* @__PURE__ */ __name(async (payload) => {
|
|
3571
3719
|
await this.store.saveCheckpoint(instantCheckpoint({
|
|
3572
3720
|
runId: run.id,
|
|
3573
3721
|
seq: cmd.seq,
|
|
3574
3722
|
name: `signal:${cmd.signal}`,
|
|
3575
3723
|
kind: "signal",
|
|
3576
|
-
output:
|
|
3724
|
+
output: payload
|
|
3577
3725
|
}));
|
|
3578
3726
|
setTimeout(() => void this.resume(run.id).catch(() => void 0), 0);
|
|
3727
|
+
}, "deliverBuffered");
|
|
3728
|
+
const buffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
3729
|
+
if (buffered) {
|
|
3730
|
+
await deliverBuffered(buffered.payload);
|
|
3579
3731
|
} else {
|
|
3580
3732
|
await this.store.putSignalWaiter({
|
|
3581
3733
|
token: cmd.signal,
|
|
3582
3734
|
runId: run.id,
|
|
3583
3735
|
seq: cmd.seq
|
|
3584
3736
|
});
|
|
3737
|
+
const lateBuffered = await this.store.takeBufferedSignal(cmd.signal);
|
|
3738
|
+
if (lateBuffered) {
|
|
3739
|
+
await this.store.removeSignalWaiter({
|
|
3740
|
+
token: cmd.signal,
|
|
3741
|
+
runId: run.id,
|
|
3742
|
+
seq: cmd.seq
|
|
3743
|
+
});
|
|
3744
|
+
await deliverBuffered(lateBuffered.payload);
|
|
3745
|
+
}
|
|
3585
3746
|
}
|
|
3586
3747
|
} else if (cmd.kind === "startChild") {
|
|
3587
3748
|
const childId = `${run.id}.child.${cmd.seq}`;
|
|
@@ -4441,47 +4602,6 @@ var TERMINAL_RUN_STATUSES = [
|
|
|
4441
4602
|
"dead"
|
|
4442
4603
|
];
|
|
4443
4604
|
|
|
4444
|
-
// src/run-waiting.ts
|
|
4445
|
-
var WEBHOOK_PREFIX = "wh:";
|
|
4446
|
-
var CHILD_PREFIX = "child:";
|
|
4447
|
-
var BREAKPOINT_PREFIX = "bp:";
|
|
4448
|
-
var EVENT_PREFIX = "event:";
|
|
4449
|
-
function classifyWaiterToken(token) {
|
|
4450
|
-
if (token.startsWith(WEBHOOK_PREFIX)) return {
|
|
4451
|
-
on: "webhook",
|
|
4452
|
-
name: token
|
|
4453
|
-
};
|
|
4454
|
-
if (token.startsWith(BREAKPOINT_PREFIX)) return {
|
|
4455
|
-
on: "breakpoint",
|
|
4456
|
-
name: "breakpoint"
|
|
4457
|
-
};
|
|
4458
|
-
if (token.startsWith(CHILD_PREFIX)) return {
|
|
4459
|
-
on: "child",
|
|
4460
|
-
name: token.slice(CHILD_PREFIX.length)
|
|
4461
|
-
};
|
|
4462
|
-
if (token.startsWith(EVENT_PREFIX)) return {
|
|
4463
|
-
on: "signal",
|
|
4464
|
-
name: eventNameOf(token)
|
|
4465
|
-
};
|
|
4466
|
-
return {
|
|
4467
|
-
on: "signal",
|
|
4468
|
-
name: token
|
|
4469
|
-
};
|
|
4470
|
-
}
|
|
4471
|
-
__name(classifyWaiterToken, "classifyWaiterToken");
|
|
4472
|
-
function resolveRunWaiting(run, waiterByRun) {
|
|
4473
|
-
if (run.status !== "suspended") return void 0;
|
|
4474
|
-
const waiter = waiterByRun.get(run.id);
|
|
4475
|
-
return waiter ? classifyWaiterToken(waiter.token) : void 0;
|
|
4476
|
-
}
|
|
4477
|
-
__name(resolveRunWaiting, "resolveRunWaiting");
|
|
4478
|
-
function indexWaitersByRun(waiters) {
|
|
4479
|
-
const byRun = /* @__PURE__ */ new Map();
|
|
4480
|
-
for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
|
|
4481
|
-
return byRun;
|
|
4482
|
-
}
|
|
4483
|
-
__name(indexWaitersByRun, "indexWaitersByRun");
|
|
4484
|
-
|
|
4485
4605
|
// src/codec-state-store.ts
|
|
4486
4606
|
var CodecStateStore = class {
|
|
4487
4607
|
static {
|
|
@@ -4589,6 +4709,9 @@ var CodecStateStore = class {
|
|
|
4589
4709
|
listSignalWaiters(prefix) {
|
|
4590
4710
|
return this.inner.listSignalWaiters(prefix);
|
|
4591
4711
|
}
|
|
4712
|
+
removeSignalWaiter(waiter) {
|
|
4713
|
+
return this.inner.removeSignalWaiter(waiter);
|
|
4714
|
+
}
|
|
4592
4715
|
bufferSignal(token, payload) {
|
|
4593
4716
|
return this.inner.bufferSignal(token, this.enc(payload));
|
|
4594
4717
|
}
|
|
@@ -5041,6 +5164,12 @@ var InMemoryStateStore = class {
|
|
|
5041
5164
|
...w
|
|
5042
5165
|
}));
|
|
5043
5166
|
}
|
|
5167
|
+
async removeSignalWaiter(waiter) {
|
|
5168
|
+
const current = this.signalWaiters.get(waiter.token);
|
|
5169
|
+
if (current && current.runId === waiter.runId && current.seq === waiter.seq) {
|
|
5170
|
+
this.signalWaiters.delete(waiter.token);
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5044
5173
|
// No real DB transaction (there's no DB) — run the work and save the checkpoint. Exactly-once
|
|
5045
5174
|
// across a crash needs a SQL store; this keeps `ctx.transaction` usable in tests / local dev.
|
|
5046
5175
|
async transaction(work) {
|