@dudousxd/nestjs-durable-core 0.33.0 → 0.34.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 +34 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -6
- package/dist/index.d.ts +35 -6
- package/dist/index.js +34 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -577,7 +577,13 @@ interface StepResult {
|
|
|
577
577
|
interface Heartbeat {
|
|
578
578
|
runId: string;
|
|
579
579
|
seq: number;
|
|
580
|
-
|
|
580
|
+
/**
|
|
581
|
+
* The in-flight step this beat keeps alive. ABSENT for a RUN-scoped (workflow-turn) heartbeat: a
|
|
582
|
+
* worker replaying a long workflow turn beats for its `runId` so the engine rearms the run's
|
|
583
|
+
* `advance` deadline (see `WorkflowEngineDeps.remoteAdvanceSilenceMs`). The engine then keys the
|
|
584
|
+
* liveness reset by `runId` instead of `stepId`.
|
|
585
|
+
*/
|
|
586
|
+
stepId?: string | undefined;
|
|
581
587
|
group: string;
|
|
582
588
|
}
|
|
583
589
|
/** engine → workflow worker: advance this run one turn by replaying the function against `history`. */
|
|
@@ -1291,6 +1297,18 @@ interface WorkflowEngineDeps {
|
|
|
1291
1297
|
* Omit for unlimited (the default — recovery always retries).
|
|
1292
1298
|
*/
|
|
1293
1299
|
maxRecoveryAttempts?: number | undefined;
|
|
1300
|
+
/**
|
|
1301
|
+
* Opt-in liveness deadline (ms) for a remote workflow `advance`. If the worker neither returns a
|
|
1302
|
+
* decision NOR sends a run-scoped {@link Heartbeat} within this window, the engine presumes it dead:
|
|
1303
|
+
* it rejects the advance with {@link RemoteWorkflowTimeout}, releases the lease, and lets recovery
|
|
1304
|
+
* re-drive (replaying completed steps from history). Each beat REARMS the window, so a worker still
|
|
1305
|
+
* legitimately replaying a long turn (it keeps beating) is never re-driven — closing the duplicate-
|
|
1306
|
+
* side-effect hazard a fixed {@link RemoteWorkflowExecutor} `timeoutMs` cannot avoid. Omit (default)
|
|
1307
|
+
* for the prior unbounded await — byte-identical behavior when unset. If a `RemoteWorkflowExecutor` is
|
|
1308
|
+
* ALSO constructed with its own `timeoutMs`, the shorter of the two deadlines wins; prefer setting only
|
|
1309
|
+
* one (this rearmed window for heartbeat-aware workers, or the fixed `timeoutMs` otherwise).
|
|
1310
|
+
*/
|
|
1311
|
+
remoteAdvanceSilenceMs?: number | undefined;
|
|
1294
1312
|
/**
|
|
1295
1313
|
* Build the public callback URL for a `ctx.webhook()` token (e.g.
|
|
1296
1314
|
* ``(t) => `https://api.example.com/durable/webhooks/${t}` ``). Populates
|
|
@@ -1364,6 +1382,7 @@ declare class WorkflowEngine {
|
|
|
1364
1382
|
private readonly instanceId;
|
|
1365
1383
|
private readonly leaseMs;
|
|
1366
1384
|
private readonly maxRecoveryAttempts?;
|
|
1385
|
+
private readonly remoteAdvanceSilenceMs?;
|
|
1367
1386
|
private readonly webhookUrl?;
|
|
1368
1387
|
private readonly traceparent?;
|
|
1369
1388
|
private readonly context?;
|
|
@@ -1388,7 +1407,9 @@ declare class WorkflowEngine {
|
|
|
1388
1407
|
private readonly accumulators;
|
|
1389
1408
|
/** In-flight remote steps awaiting a worker result, keyed by stepId. */
|
|
1390
1409
|
private readonly pending;
|
|
1391
|
-
/**
|
|
1410
|
+
/** "Reset the liveness timer" callbacks keyed by the beat's target — a stepId (in-flight remote step)
|
|
1411
|
+
* or a runId (in-flight remote workflow turn) — invoked when a matching heartbeat arrives. The two id
|
|
1412
|
+
* shapes don't collide in practice: a stepId is `${runId}:${seq}`, a run key is the bare runId. */
|
|
1392
1413
|
private readonly heartbeatResets;
|
|
1393
1414
|
private readonly listeners;
|
|
1394
1415
|
/** Step interceptors (onion middleware around real local-step execution), first = outermost. */
|
|
@@ -1785,10 +1806,15 @@ declare class WorkflowEngine {
|
|
|
1785
1806
|
/** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
|
|
1786
1807
|
private callRemoteInMemory;
|
|
1787
1808
|
/**
|
|
1788
|
-
* Await
|
|
1789
|
-
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat
|
|
1790
|
-
* window, so a worker that keeps beating stays alive past
|
|
1809
|
+
* Await `resultPromise`, but reject with `onTimeout()` if neither it settles nor a heartbeat for `id`
|
|
1810
|
+
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat`, looked up in
|
|
1811
|
+
* `heartbeatResets` by `id`) REARMS the window, so a worker that keeps beating stays alive past
|
|
1812
|
+
* `timeoutMs`. Shared by the remote-STEP liveness (keyed by stepId) and the remote-WORKFLOW advance
|
|
1813
|
+
* liveness (keyed by runId); the caller supplies the timeout error and any extra cleanup via `onTimeout`.
|
|
1791
1814
|
*/
|
|
1815
|
+
private awaitWithLivenessDeadline;
|
|
1816
|
+
/** Remote-STEP liveness: await a worker result, timing out (with the step's pending-map cleanup) to
|
|
1817
|
+
* `RemoteStepTimeout` if it neither lands nor beats within `timeoutMs`. See {@link awaitWithLivenessDeadline}. */
|
|
1792
1818
|
private awaitWithHeartbeat;
|
|
1793
1819
|
}
|
|
1794
1820
|
/** Raised inside the workflow when a remote worker reports a step failure. */
|
|
@@ -2301,13 +2327,16 @@ declare class InMemoryStateStore implements StateStore {
|
|
|
2301
2327
|
declare class InMemoryTransport implements Transport, ControlPlane {
|
|
2302
2328
|
private readonly handlers;
|
|
2303
2329
|
private resultHandler?;
|
|
2330
|
+
private heartbeatHandler?;
|
|
2304
2331
|
private stepEventHandler?;
|
|
2305
2332
|
private readonly controlHandlers;
|
|
2306
2333
|
/** Register a fake worker handler for a step name. */
|
|
2307
2334
|
handle(name: string, fn: StepHandler): void;
|
|
2308
2335
|
dispatch(task: RemoteTask): Promise<void>;
|
|
2309
2336
|
onResult(handler: (result: StepResult) => Promise<void>): void;
|
|
2310
|
-
onHeartbeat(
|
|
2337
|
+
onHeartbeat(handler: (beat: Heartbeat) => Promise<void>): void;
|
|
2338
|
+
/** Test hook: deliver a liveness heartbeat to the engine, as a real broker would on its channel. */
|
|
2339
|
+
emitHeartbeat(beat: Heartbeat): Promise<void>;
|
|
2311
2340
|
/** Workflow step lifecycle: deliver to the engine's handler (async, mirroring a real broker). */
|
|
2312
2341
|
dispatchStepEvent(event: WorkflowStepEvent): Promise<void>;
|
|
2313
2342
|
onStepEvent(handler: (event: WorkflowStepEvent) => Promise<void>): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -577,7 +577,13 @@ interface StepResult {
|
|
|
577
577
|
interface Heartbeat {
|
|
578
578
|
runId: string;
|
|
579
579
|
seq: number;
|
|
580
|
-
|
|
580
|
+
/**
|
|
581
|
+
* The in-flight step this beat keeps alive. ABSENT for a RUN-scoped (workflow-turn) heartbeat: a
|
|
582
|
+
* worker replaying a long workflow turn beats for its `runId` so the engine rearms the run's
|
|
583
|
+
* `advance` deadline (see `WorkflowEngineDeps.remoteAdvanceSilenceMs`). The engine then keys the
|
|
584
|
+
* liveness reset by `runId` instead of `stepId`.
|
|
585
|
+
*/
|
|
586
|
+
stepId?: string | undefined;
|
|
581
587
|
group: string;
|
|
582
588
|
}
|
|
583
589
|
/** engine → workflow worker: advance this run one turn by replaying the function against `history`. */
|
|
@@ -1291,6 +1297,18 @@ interface WorkflowEngineDeps {
|
|
|
1291
1297
|
* Omit for unlimited (the default — recovery always retries).
|
|
1292
1298
|
*/
|
|
1293
1299
|
maxRecoveryAttempts?: number | undefined;
|
|
1300
|
+
/**
|
|
1301
|
+
* Opt-in liveness deadline (ms) for a remote workflow `advance`. If the worker neither returns a
|
|
1302
|
+
* decision NOR sends a run-scoped {@link Heartbeat} within this window, the engine presumes it dead:
|
|
1303
|
+
* it rejects the advance with {@link RemoteWorkflowTimeout}, releases the lease, and lets recovery
|
|
1304
|
+
* re-drive (replaying completed steps from history). Each beat REARMS the window, so a worker still
|
|
1305
|
+
* legitimately replaying a long turn (it keeps beating) is never re-driven — closing the duplicate-
|
|
1306
|
+
* side-effect hazard a fixed {@link RemoteWorkflowExecutor} `timeoutMs` cannot avoid. Omit (default)
|
|
1307
|
+
* for the prior unbounded await — byte-identical behavior when unset. If a `RemoteWorkflowExecutor` is
|
|
1308
|
+
* ALSO constructed with its own `timeoutMs`, the shorter of the two deadlines wins; prefer setting only
|
|
1309
|
+
* one (this rearmed window for heartbeat-aware workers, or the fixed `timeoutMs` otherwise).
|
|
1310
|
+
*/
|
|
1311
|
+
remoteAdvanceSilenceMs?: number | undefined;
|
|
1294
1312
|
/**
|
|
1295
1313
|
* Build the public callback URL for a `ctx.webhook()` token (e.g.
|
|
1296
1314
|
* ``(t) => `https://api.example.com/durable/webhooks/${t}` ``). Populates
|
|
@@ -1364,6 +1382,7 @@ declare class WorkflowEngine {
|
|
|
1364
1382
|
private readonly instanceId;
|
|
1365
1383
|
private readonly leaseMs;
|
|
1366
1384
|
private readonly maxRecoveryAttempts?;
|
|
1385
|
+
private readonly remoteAdvanceSilenceMs?;
|
|
1367
1386
|
private readonly webhookUrl?;
|
|
1368
1387
|
private readonly traceparent?;
|
|
1369
1388
|
private readonly context?;
|
|
@@ -1388,7 +1407,9 @@ declare class WorkflowEngine {
|
|
|
1388
1407
|
private readonly accumulators;
|
|
1389
1408
|
/** In-flight remote steps awaiting a worker result, keyed by stepId. */
|
|
1390
1409
|
private readonly pending;
|
|
1391
|
-
/**
|
|
1410
|
+
/** "Reset the liveness timer" callbacks keyed by the beat's target — a stepId (in-flight remote step)
|
|
1411
|
+
* or a runId (in-flight remote workflow turn) — invoked when a matching heartbeat arrives. The two id
|
|
1412
|
+
* shapes don't collide in practice: a stepId is `${runId}:${seq}`, a run key is the bare runId. */
|
|
1392
1413
|
private readonly heartbeatResets;
|
|
1393
1414
|
private readonly listeners;
|
|
1394
1415
|
/** Step interceptors (onion middleware around real local-step execution), first = outermost. */
|
|
@@ -1785,10 +1806,15 @@ declare class WorkflowEngine {
|
|
|
1785
1806
|
/** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
|
|
1786
1807
|
private callRemoteInMemory;
|
|
1787
1808
|
/**
|
|
1788
|
-
* Await
|
|
1789
|
-
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat
|
|
1790
|
-
* window, so a worker that keeps beating stays alive past
|
|
1809
|
+
* Await `resultPromise`, but reject with `onTimeout()` if neither it settles nor a heartbeat for `id`
|
|
1810
|
+
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat`, looked up in
|
|
1811
|
+
* `heartbeatResets` by `id`) REARMS the window, so a worker that keeps beating stays alive past
|
|
1812
|
+
* `timeoutMs`. Shared by the remote-STEP liveness (keyed by stepId) and the remote-WORKFLOW advance
|
|
1813
|
+
* liveness (keyed by runId); the caller supplies the timeout error and any extra cleanup via `onTimeout`.
|
|
1791
1814
|
*/
|
|
1815
|
+
private awaitWithLivenessDeadline;
|
|
1816
|
+
/** Remote-STEP liveness: await a worker result, timing out (with the step's pending-map cleanup) to
|
|
1817
|
+
* `RemoteStepTimeout` if it neither lands nor beats within `timeoutMs`. See {@link awaitWithLivenessDeadline}. */
|
|
1792
1818
|
private awaitWithHeartbeat;
|
|
1793
1819
|
}
|
|
1794
1820
|
/** Raised inside the workflow when a remote worker reports a step failure. */
|
|
@@ -2301,13 +2327,16 @@ declare class InMemoryStateStore implements StateStore {
|
|
|
2301
2327
|
declare class InMemoryTransport implements Transport, ControlPlane {
|
|
2302
2328
|
private readonly handlers;
|
|
2303
2329
|
private resultHandler?;
|
|
2330
|
+
private heartbeatHandler?;
|
|
2304
2331
|
private stepEventHandler?;
|
|
2305
2332
|
private readonly controlHandlers;
|
|
2306
2333
|
/** Register a fake worker handler for a step name. */
|
|
2307
2334
|
handle(name: string, fn: StepHandler): void;
|
|
2308
2335
|
dispatch(task: RemoteTask): Promise<void>;
|
|
2309
2336
|
onResult(handler: (result: StepResult) => Promise<void>): void;
|
|
2310
|
-
onHeartbeat(
|
|
2337
|
+
onHeartbeat(handler: (beat: Heartbeat) => Promise<void>): void;
|
|
2338
|
+
/** Test hook: deliver a liveness heartbeat to the engine, as a real broker would on its channel. */
|
|
2339
|
+
emitHeartbeat(beat: Heartbeat): Promise<void>;
|
|
2311
2340
|
/** Workflow step lifecycle: deliver to the engine's handler (async, mirroring a real broker). */
|
|
2312
2341
|
dispatchStepEvent(event: WorkflowStepEvent): Promise<void>;
|
|
2313
2342
|
onStepEvent(handler: (event: WorkflowStepEvent) => Promise<void>): void;
|
package/dist/index.js
CHANGED
|
@@ -1431,6 +1431,7 @@ var WorkflowEngine = class {
|
|
|
1431
1431
|
instanceId;
|
|
1432
1432
|
leaseMs;
|
|
1433
1433
|
maxRecoveryAttempts;
|
|
1434
|
+
remoteAdvanceSilenceMs;
|
|
1434
1435
|
webhookUrl;
|
|
1435
1436
|
traceparent;
|
|
1436
1437
|
context;
|
|
@@ -1455,7 +1456,9 @@ var WorkflowEngine = class {
|
|
|
1455
1456
|
accumulators;
|
|
1456
1457
|
/** In-flight remote steps awaiting a worker result, keyed by stepId. */
|
|
1457
1458
|
pending = /* @__PURE__ */ new Map();
|
|
1458
|
-
/**
|
|
1459
|
+
/** "Reset the liveness timer" callbacks keyed by the beat's target — a stepId (in-flight remote step)
|
|
1460
|
+
* or a runId (in-flight remote workflow turn) — invoked when a matching heartbeat arrives. The two id
|
|
1461
|
+
* shapes don't collide in practice: a stepId is `${runId}:${seq}`, a run key is the bare runId. */
|
|
1459
1462
|
heartbeatResets = /* @__PURE__ */ new Map();
|
|
1460
1463
|
listeners = /* @__PURE__ */ new Set();
|
|
1461
1464
|
/** Step interceptors (onion middleware around real local-step execution), first = outermost. */
|
|
@@ -1494,6 +1497,7 @@ var WorkflowEngine = class {
|
|
|
1494
1497
|
this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
|
|
1495
1498
|
this.leaseMs = deps.leaseMs ?? 3e4;
|
|
1496
1499
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
1500
|
+
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
1497
1501
|
this.webhookUrl = deps.webhookUrl;
|
|
1498
1502
|
this.traceparent = deps.traceparent;
|
|
1499
1503
|
this.context = deps.context;
|
|
@@ -1530,9 +1534,11 @@ var WorkflowEngine = class {
|
|
|
1530
1534
|
}
|
|
1531
1535
|
await this.completeRemoteResult(result);
|
|
1532
1536
|
},
|
|
1533
|
-
// A heartbeat for an in-flight long
|
|
1537
|
+
// A heartbeat resets the liveness window for whatever it targets: an in-flight long STEP (keyed
|
|
1538
|
+
// by stepId, see callRemote) or — when stepId is absent — the in-flight workflow TURN (keyed by
|
|
1539
|
+
// runId, see runRemoteExecution's heartbeat-rearmed advance).
|
|
1534
1540
|
async (beat) => {
|
|
1535
|
-
this.heartbeatResets.get(beat.stepId)?.();
|
|
1541
|
+
this.heartbeatResets.get(beat.stepId ?? beat.runId)?.();
|
|
1536
1542
|
},
|
|
1537
1543
|
// A remote workflow worker streams each local step's lifecycle (running → completed/failed) so
|
|
1538
1544
|
// it's checkpointed live, not all-at-once when the long turn ends.
|
|
@@ -2624,7 +2630,9 @@ var WorkflowEngine = class {
|
|
|
2624
2630
|
const history = await this.remoteHistory(run.id);
|
|
2625
2631
|
let decision;
|
|
2626
2632
|
try {
|
|
2627
|
-
|
|
2633
|
+
const silenceMs = this.remoteAdvanceSilenceMs;
|
|
2634
|
+
const advance = remote.executor.advance(run, history);
|
|
2635
|
+
decision = silenceMs != null ? await this.awaitWithLivenessDeadline(run.id, advance, silenceMs, () => new RemoteWorkflowTimeout(run.id, silenceMs)) : await advance;
|
|
2628
2636
|
} catch (err) {
|
|
2629
2637
|
if (err instanceof RemoteWorkflowTimeout) {
|
|
2630
2638
|
await this.store.releaseRunLock(run.id);
|
|
@@ -3285,11 +3293,13 @@ var WorkflowEngine = class {
|
|
|
3285
3293
|
}
|
|
3286
3294
|
}
|
|
3287
3295
|
/**
|
|
3288
|
-
* Await
|
|
3289
|
-
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat
|
|
3290
|
-
* window, so a worker that keeps beating stays alive past
|
|
3296
|
+
* Await `resultPromise`, but reject with `onTimeout()` if neither it settles nor a heartbeat for `id`
|
|
3297
|
+
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat`, looked up in
|
|
3298
|
+
* `heartbeatResets` by `id`) REARMS the window, so a worker that keeps beating stays alive past
|
|
3299
|
+
* `timeoutMs`. Shared by the remote-STEP liveness (keyed by stepId) and the remote-WORKFLOW advance
|
|
3300
|
+
* liveness (keyed by runId); the caller supplies the timeout error and any extra cleanup via `onTimeout`.
|
|
3291
3301
|
*/
|
|
3292
|
-
|
|
3302
|
+
awaitWithLivenessDeadline(id, resultPromise, timeoutMs, onTimeout) {
|
|
3293
3303
|
return new Promise((resolve, reject) => {
|
|
3294
3304
|
let timer;
|
|
3295
3305
|
const cleanup = /* @__PURE__ */ __name(() => {
|
|
@@ -3299,8 +3309,7 @@ var WorkflowEngine = class {
|
|
|
3299
3309
|
const arm = /* @__PURE__ */ __name(() => {
|
|
3300
3310
|
timer = setTimeout(() => {
|
|
3301
3311
|
cleanup();
|
|
3302
|
-
|
|
3303
|
-
reject(new RemoteStepTimeout(id, timeoutMs));
|
|
3312
|
+
reject(onTimeout());
|
|
3304
3313
|
}, timeoutMs);
|
|
3305
3314
|
timer.unref?.();
|
|
3306
3315
|
}, "arm");
|
|
@@ -3318,6 +3327,14 @@ var WorkflowEngine = class {
|
|
|
3318
3327
|
});
|
|
3319
3328
|
});
|
|
3320
3329
|
}
|
|
3330
|
+
/** Remote-STEP liveness: await a worker result, timing out (with the step's pending-map cleanup) to
|
|
3331
|
+
* `RemoteStepTimeout` if it neither lands nor beats within `timeoutMs`. See {@link awaitWithLivenessDeadline}. */
|
|
3332
|
+
awaitWithHeartbeat(id, resultPromise, timeoutMs) {
|
|
3333
|
+
return this.awaitWithLivenessDeadline(id, resultPromise, timeoutMs, () => {
|
|
3334
|
+
this.pending.delete(id);
|
|
3335
|
+
return new RemoteStepTimeout(id, timeoutMs);
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3321
3338
|
};
|
|
3322
3339
|
var RemoteStepError = class extends Error {
|
|
3323
3340
|
static {
|
|
@@ -4083,6 +4100,7 @@ var InMemoryTransport = class {
|
|
|
4083
4100
|
}
|
|
4084
4101
|
handlers = /* @__PURE__ */ new Map();
|
|
4085
4102
|
resultHandler;
|
|
4103
|
+
heartbeatHandler;
|
|
4086
4104
|
stepEventHandler;
|
|
4087
4105
|
controlHandlers = /* @__PURE__ */ new Set();
|
|
4088
4106
|
/** Register a fake worker handler for a step name. */
|
|
@@ -4097,7 +4115,12 @@ var InMemoryTransport = class {
|
|
|
4097
4115
|
onResult(handler) {
|
|
4098
4116
|
this.resultHandler = handler;
|
|
4099
4117
|
}
|
|
4100
|
-
onHeartbeat(
|
|
4118
|
+
onHeartbeat(handler) {
|
|
4119
|
+
this.heartbeatHandler = handler;
|
|
4120
|
+
}
|
|
4121
|
+
/** Test hook: deliver a liveness heartbeat to the engine, as a real broker would on its channel. */
|
|
4122
|
+
async emitHeartbeat(beat) {
|
|
4123
|
+
await this.heartbeatHandler?.(beat);
|
|
4101
4124
|
}
|
|
4102
4125
|
/** Workflow step lifecycle: deliver to the engine's handler (async, mirroring a real broker). */
|
|
4103
4126
|
async dispatchStepEvent(event) {
|