@dudousxd/nestjs-durable-core 0.32.1 → 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 +155 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -8
- package/dist/index.d.ts +120 -8
- package/dist/index.js +152 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -385,6 +385,7 @@ function stepCheckpoint(p) {
|
|
|
385
385
|
attempts: p.attempts,
|
|
386
386
|
workerGroup: p.workerGroup,
|
|
387
387
|
wakeAt: p.wakeAt,
|
|
388
|
+
parallelGroup: p.parallelGroup,
|
|
388
389
|
enqueuedAt: p.enqueuedAt,
|
|
389
390
|
startedAt: p.startedAt,
|
|
390
391
|
finishedAt: p.finishedAt
|
|
@@ -402,6 +403,7 @@ function instantCheckpoint(p) {
|
|
|
402
403
|
status: p.status ?? "completed",
|
|
403
404
|
output: p.output,
|
|
404
405
|
wakeAt: p.wakeAt,
|
|
406
|
+
parallelGroup: p.parallelGroup,
|
|
405
407
|
attempts: 1,
|
|
406
408
|
enqueuedAt: at,
|
|
407
409
|
startedAt: at,
|
|
@@ -463,6 +465,19 @@ var RemoteStepTimeout = class extends Error {
|
|
|
463
465
|
this.timeoutMs = timeoutMs;
|
|
464
466
|
}
|
|
465
467
|
};
|
|
468
|
+
var RemoteWorkflowTimeout = class extends Error {
|
|
469
|
+
static {
|
|
470
|
+
__name(this, "RemoteWorkflowTimeout");
|
|
471
|
+
}
|
|
472
|
+
taskId;
|
|
473
|
+
timeoutMs;
|
|
474
|
+
constructor(taskId, timeoutMs) {
|
|
475
|
+
super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
|
|
476
|
+
this.name = "RemoteWorkflowTimeout";
|
|
477
|
+
this.taskId = taskId;
|
|
478
|
+
this.timeoutMs = timeoutMs;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
466
481
|
var NonDeterminismError = class extends Error {
|
|
467
482
|
static {
|
|
468
483
|
__name(this, "NonDeterminismError");
|
|
@@ -476,6 +491,19 @@ var NonDeterminismError = class extends Error {
|
|
|
476
491
|
this.seq = seq;
|
|
477
492
|
}
|
|
478
493
|
};
|
|
494
|
+
var GatherError = class extends Error {
|
|
495
|
+
static {
|
|
496
|
+
__name(this, "GatherError");
|
|
497
|
+
}
|
|
498
|
+
failures;
|
|
499
|
+
constructor(failures, total) {
|
|
500
|
+
const ids = failures.map((f) => f.id).join(", ");
|
|
501
|
+
const denom = total ?? failures.length;
|
|
502
|
+
super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
|
|
503
|
+
this.name = "GatherError";
|
|
504
|
+
this.failures = failures;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
479
507
|
var WorkflowSuspended = class extends Error {
|
|
480
508
|
static {
|
|
481
509
|
__name(this, "WorkflowSuspended");
|
|
@@ -1145,6 +1173,72 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1145
1173
|
}
|
|
1146
1174
|
throw new WorkflowSuspended();
|
|
1147
1175
|
}, "child");
|
|
1176
|
+
const all = /* @__PURE__ */ __name(async (workflow2, inputs, opts) => {
|
|
1177
|
+
if (inputs.length === 0) return [];
|
|
1178
|
+
const mode = opts?.mode ?? "waitAll";
|
|
1179
|
+
const name = workflowName(workflow2);
|
|
1180
|
+
const positions = inputs.map(() => pos.next());
|
|
1181
|
+
const group = `all:${positions[0]}`;
|
|
1182
|
+
const id = /* @__PURE__ */ __name((i) => `${runId}.all.${positions[0]}.${i}`, "id");
|
|
1183
|
+
const existing = await Promise.all(positions.map((seq) => readCheckpoint(seq)));
|
|
1184
|
+
if (mode === "failFast") {
|
|
1185
|
+
for (let i = 0; i < inputs.length; i += 1) {
|
|
1186
|
+
const cp = existing[i];
|
|
1187
|
+
const c = cp?.status === "completed" ? cp.output : null;
|
|
1188
|
+
if (c && c.ok === false) {
|
|
1189
|
+
throw new GatherError([
|
|
1190
|
+
{
|
|
1191
|
+
index: i,
|
|
1192
|
+
id: id(i),
|
|
1193
|
+
error: c.error ?? "unknown"
|
|
1194
|
+
}
|
|
1195
|
+
], inputs.length);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
let pending = false;
|
|
1200
|
+
for (let i = 0; i < inputs.length; i += 1) {
|
|
1201
|
+
const cp = existing[i];
|
|
1202
|
+
if (cp?.status === "completed") continue;
|
|
1203
|
+
pending = true;
|
|
1204
|
+
const seq = positions[i];
|
|
1205
|
+
const childId = id(i);
|
|
1206
|
+
await store.putSignalWaiter({
|
|
1207
|
+
token: `child:${childId}`,
|
|
1208
|
+
runId,
|
|
1209
|
+
seq
|
|
1210
|
+
});
|
|
1211
|
+
if (!await store.getRun(childId)) host.startChild(name, inputs[i], childId);
|
|
1212
|
+
if (!cp) {
|
|
1213
|
+
await writeCheckpoint(instantCheckpoint({
|
|
1214
|
+
runId,
|
|
1215
|
+
seq,
|
|
1216
|
+
name: `signal:child:${childId}`,
|
|
1217
|
+
kind: "signal",
|
|
1218
|
+
status: "running",
|
|
1219
|
+
parallelGroup: group
|
|
1220
|
+
}));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (pending) throw new WorkflowSuspended();
|
|
1224
|
+
const outputs = [];
|
|
1225
|
+
const failures = [];
|
|
1226
|
+
for (let i = 0; i < inputs.length; i += 1) {
|
|
1227
|
+
const c = existing[i]?.output;
|
|
1228
|
+
if (c && typeof c === "object" && "ok" in c && c.ok === false) {
|
|
1229
|
+
failures.push({
|
|
1230
|
+
index: i,
|
|
1231
|
+
id: id(i),
|
|
1232
|
+
error: c.error ?? "unknown"
|
|
1233
|
+
});
|
|
1234
|
+
outputs.push(void 0);
|
|
1235
|
+
} else {
|
|
1236
|
+
outputs.push(c?.value);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (failures.length > 0) throw new GatherError(failures, inputs.length);
|
|
1240
|
+
return outputs;
|
|
1241
|
+
}, "all");
|
|
1148
1242
|
const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
1149
1243
|
const { childId, priority } = normalizeChildOptions(options);
|
|
1150
1244
|
const current = pos.next();
|
|
@@ -1279,6 +1373,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1279
1373
|
waitForEvent,
|
|
1280
1374
|
task,
|
|
1281
1375
|
child,
|
|
1376
|
+
all,
|
|
1282
1377
|
startChild,
|
|
1283
1378
|
breakpoint,
|
|
1284
1379
|
webhook,
|
|
@@ -1336,6 +1431,7 @@ var WorkflowEngine = class {
|
|
|
1336
1431
|
instanceId;
|
|
1337
1432
|
leaseMs;
|
|
1338
1433
|
maxRecoveryAttempts;
|
|
1434
|
+
remoteAdvanceSilenceMs;
|
|
1339
1435
|
webhookUrl;
|
|
1340
1436
|
traceparent;
|
|
1341
1437
|
context;
|
|
@@ -1360,7 +1456,9 @@ var WorkflowEngine = class {
|
|
|
1360
1456
|
accumulators;
|
|
1361
1457
|
/** In-flight remote steps awaiting a worker result, keyed by stepId. */
|
|
1362
1458
|
pending = /* @__PURE__ */ new Map();
|
|
1363
|
-
/**
|
|
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. */
|
|
1364
1462
|
heartbeatResets = /* @__PURE__ */ new Map();
|
|
1365
1463
|
listeners = /* @__PURE__ */ new Set();
|
|
1366
1464
|
/** Step interceptors (onion middleware around real local-step execution), first = outermost. */
|
|
@@ -1399,6 +1497,7 @@ var WorkflowEngine = class {
|
|
|
1399
1497
|
this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
|
|
1400
1498
|
this.leaseMs = deps.leaseMs ?? 3e4;
|
|
1401
1499
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
1500
|
+
this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
|
|
1402
1501
|
this.webhookUrl = deps.webhookUrl;
|
|
1403
1502
|
this.traceparent = deps.traceparent;
|
|
1404
1503
|
this.context = deps.context;
|
|
@@ -1435,9 +1534,11 @@ var WorkflowEngine = class {
|
|
|
1435
1534
|
}
|
|
1436
1535
|
await this.completeRemoteResult(result);
|
|
1437
1536
|
},
|
|
1438
|
-
// 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).
|
|
1439
1540
|
async (beat) => {
|
|
1440
|
-
this.heartbeatResets.get(beat.stepId)?.();
|
|
1541
|
+
this.heartbeatResets.get(beat.stepId ?? beat.runId)?.();
|
|
1441
1542
|
},
|
|
1442
1543
|
// A remote workflow worker streams each local step's lifecycle (running → completed/failed) so
|
|
1443
1544
|
// it's checkpointed live, not all-at-once when the long turn ends.
|
|
@@ -2268,7 +2369,8 @@ var WorkflowEngine = class {
|
|
|
2268
2369
|
attempts: 1,
|
|
2269
2370
|
enqueuedAt: startedAt,
|
|
2270
2371
|
startedAt,
|
|
2271
|
-
finishedAt: startedAt
|
|
2372
|
+
finishedAt: startedAt,
|
|
2373
|
+
parallelGroup: event.parallelGroup
|
|
2272
2374
|
}));
|
|
2273
2375
|
this.emit({
|
|
2274
2376
|
type: "step.started",
|
|
@@ -2292,7 +2394,8 @@ var WorkflowEngine = class {
|
|
|
2292
2394
|
attempts: 1,
|
|
2293
2395
|
enqueuedAt: startedAt,
|
|
2294
2396
|
startedAt,
|
|
2295
|
-
finishedAt: event.finishedAt != null ? new Date(event.finishedAt) : /* @__PURE__ */ new Date()
|
|
2397
|
+
finishedAt: event.finishedAt != null ? new Date(event.finishedAt) : /* @__PURE__ */ new Date(),
|
|
2398
|
+
parallelGroup: event.parallelGroup
|
|
2296
2399
|
}));
|
|
2297
2400
|
this.emit({
|
|
2298
2401
|
type: failed ? "step.failed" : "step.completed",
|
|
@@ -2527,8 +2630,17 @@ var WorkflowEngine = class {
|
|
|
2527
2630
|
const history = await this.remoteHistory(run.id);
|
|
2528
2631
|
let decision;
|
|
2529
2632
|
try {
|
|
2530
|
-
|
|
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;
|
|
2531
2636
|
} catch (err) {
|
|
2637
|
+
if (err instanceof RemoteWorkflowTimeout) {
|
|
2638
|
+
await this.store.releaseRunLock(run.id);
|
|
2639
|
+
return {
|
|
2640
|
+
runId: run.id,
|
|
2641
|
+
status: "running"
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2532
2644
|
const error = {
|
|
2533
2645
|
message: err instanceof Error ? err.message : String(err),
|
|
2534
2646
|
stack: err instanceof Error ? err.stack : void 0
|
|
@@ -2648,7 +2760,11 @@ var WorkflowEngine = class {
|
|
|
2648
2760
|
attempts: 1,
|
|
2649
2761
|
enqueuedAt: startedAt,
|
|
2650
2762
|
startedAt,
|
|
2651
|
-
finishedAt
|
|
2763
|
+
finishedAt,
|
|
2764
|
+
// Carry the worker's `parallelGroup` (a Python `ctx.gather` tags every step in the fan with
|
|
2765
|
+
// the same group) onto the checkpoint, so the dashboard can render the steps as one parallel
|
|
2766
|
+
// group instead of a sequential list. Additive: undefined for ordinary sequential steps.
|
|
2767
|
+
parallelGroup: cmd.parallelGroup
|
|
2652
2768
|
}));
|
|
2653
2769
|
this.emit({
|
|
2654
2770
|
type: cmd.error ? "step.failed" : "step.completed",
|
|
@@ -3177,11 +3293,13 @@ var WorkflowEngine = class {
|
|
|
3177
3293
|
}
|
|
3178
3294
|
}
|
|
3179
3295
|
/**
|
|
3180
|
-
* Await
|
|
3181
|
-
* arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat
|
|
3182
|
-
* 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`.
|
|
3183
3301
|
*/
|
|
3184
|
-
|
|
3302
|
+
awaitWithLivenessDeadline(id, resultPromise, timeoutMs, onTimeout) {
|
|
3185
3303
|
return new Promise((resolve, reject) => {
|
|
3186
3304
|
let timer;
|
|
3187
3305
|
const cleanup = /* @__PURE__ */ __name(() => {
|
|
@@ -3191,8 +3309,7 @@ var WorkflowEngine = class {
|
|
|
3191
3309
|
const arm = /* @__PURE__ */ __name(() => {
|
|
3192
3310
|
timer = setTimeout(() => {
|
|
3193
3311
|
cleanup();
|
|
3194
|
-
|
|
3195
|
-
reject(new RemoteStepTimeout(id, timeoutMs));
|
|
3312
|
+
reject(onTimeout());
|
|
3196
3313
|
}, timeoutMs);
|
|
3197
3314
|
timer.unref?.();
|
|
3198
3315
|
}, "arm");
|
|
@@ -3210,6 +3327,14 @@ var WorkflowEngine = class {
|
|
|
3210
3327
|
});
|
|
3211
3328
|
});
|
|
3212
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
|
+
}
|
|
3213
3338
|
};
|
|
3214
3339
|
var RemoteStepError = class extends Error {
|
|
3215
3340
|
static {
|
|
@@ -3283,11 +3408,12 @@ var RemoteWorkflowExecutor = class {
|
|
|
3283
3408
|
};
|
|
3284
3409
|
const decision = new Promise((resolve, reject) => {
|
|
3285
3410
|
this.pending.set(taskId, resolve);
|
|
3286
|
-
|
|
3411
|
+
const { timeoutMs } = this.opts;
|
|
3412
|
+
if (timeoutMs) {
|
|
3287
3413
|
const timer = setTimeout(() => {
|
|
3288
3414
|
this.pending.delete(taskId);
|
|
3289
|
-
reject(new
|
|
3290
|
-
},
|
|
3415
|
+
reject(new RemoteWorkflowTimeout(taskId, timeoutMs));
|
|
3416
|
+
}, timeoutMs);
|
|
3291
3417
|
timer.unref?.();
|
|
3292
3418
|
}
|
|
3293
3419
|
});
|
|
@@ -3974,6 +4100,7 @@ var InMemoryTransport = class {
|
|
|
3974
4100
|
}
|
|
3975
4101
|
handlers = /* @__PURE__ */ new Map();
|
|
3976
4102
|
resultHandler;
|
|
4103
|
+
heartbeatHandler;
|
|
3977
4104
|
stepEventHandler;
|
|
3978
4105
|
controlHandlers = /* @__PURE__ */ new Set();
|
|
3979
4106
|
/** Register a fake worker handler for a step name. */
|
|
@@ -3988,7 +4115,12 @@ var InMemoryTransport = class {
|
|
|
3988
4115
|
onResult(handler) {
|
|
3989
4116
|
this.resultHandler = handler;
|
|
3990
4117
|
}
|
|
3991
|
-
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);
|
|
3992
4124
|
}
|
|
3993
4125
|
/** Workflow step lifecycle: deliver to the engine's handler (async, mirroring a real broker). */
|
|
3994
4126
|
async dispatchStepEvent(event) {
|
|
@@ -4014,6 +4146,7 @@ export {
|
|
|
4014
4146
|
Entities,
|
|
4015
4147
|
EventAccumulators,
|
|
4016
4148
|
FatalError,
|
|
4149
|
+
GatherError,
|
|
4017
4150
|
InMemoryAdmissionBackend,
|
|
4018
4151
|
InMemoryStateStore,
|
|
4019
4152
|
InMemoryTransport,
|
|
@@ -4022,6 +4155,7 @@ export {
|
|
|
4022
4155
|
RemoteStepError,
|
|
4023
4156
|
RemoteStepTimeout,
|
|
4024
4157
|
RemoteWorkflowExecutor,
|
|
4158
|
+
RemoteWorkflowTimeout,
|
|
4025
4159
|
STATE_STORE,
|
|
4026
4160
|
STATE_STORE_CANONICAL,
|
|
4027
4161
|
SignalTimeoutError,
|
|
@@ -4036,6 +4170,7 @@ export {
|
|
|
4036
4170
|
attributeOperand,
|
|
4037
4171
|
breakpointToken,
|
|
4038
4172
|
collectMetrics,
|
|
4173
|
+
createStepLogger,
|
|
4039
4174
|
eventMatchOf,
|
|
4040
4175
|
eventMatches,
|
|
4041
4176
|
eventPrefix,
|