@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 CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  Entities: () => Entities,
30
30
  EventAccumulators: () => EventAccumulators,
31
31
  FatalError: () => FatalError,
32
+ GatherError: () => GatherError,
32
33
  InMemoryAdmissionBackend: () => InMemoryAdmissionBackend,
33
34
  InMemoryStateStore: () => InMemoryStateStore,
34
35
  InMemoryTransport: () => InMemoryTransport,
@@ -37,6 +38,7 @@ __export(index_exports, {
37
38
  RemoteStepError: () => RemoteStepError,
38
39
  RemoteStepTimeout: () => RemoteStepTimeout,
39
40
  RemoteWorkflowExecutor: () => RemoteWorkflowExecutor,
41
+ RemoteWorkflowTimeout: () => RemoteWorkflowTimeout,
40
42
  STATE_STORE: () => STATE_STORE,
41
43
  STATE_STORE_CANONICAL: () => STATE_STORE_CANONICAL,
42
44
  SignalTimeoutError: () => SignalTimeoutError,
@@ -51,6 +53,7 @@ __export(index_exports, {
51
53
  attributeOperand: () => attributeOperand,
52
54
  breakpointToken: () => breakpointToken,
53
55
  collectMetrics: () => collectMetrics,
56
+ createStepLogger: () => createStepLogger,
54
57
  eventMatchOf: () => eventMatchOf,
55
58
  eventMatches: () => eventMatches,
56
59
  eventPrefix: () => eventPrefix,
@@ -453,6 +456,7 @@ function stepCheckpoint(p) {
453
456
  attempts: p.attempts,
454
457
  workerGroup: p.workerGroup,
455
458
  wakeAt: p.wakeAt,
459
+ parallelGroup: p.parallelGroup,
456
460
  enqueuedAt: p.enqueuedAt,
457
461
  startedAt: p.startedAt,
458
462
  finishedAt: p.finishedAt
@@ -470,6 +474,7 @@ function instantCheckpoint(p) {
470
474
  status: p.status ?? "completed",
471
475
  output: p.output,
472
476
  wakeAt: p.wakeAt,
477
+ parallelGroup: p.parallelGroup,
473
478
  attempts: 1,
474
479
  enqueuedAt: at,
475
480
  startedAt: at,
@@ -531,6 +536,19 @@ var RemoteStepTimeout = class extends Error {
531
536
  this.timeoutMs = timeoutMs;
532
537
  }
533
538
  };
539
+ var RemoteWorkflowTimeout = class extends Error {
540
+ static {
541
+ __name(this, "RemoteWorkflowTimeout");
542
+ }
543
+ taskId;
544
+ timeoutMs;
545
+ constructor(taskId, timeoutMs) {
546
+ super(`remote workflow task ${taskId} produced no decision within ${timeoutMs}ms \u2014 presumed dropped; re-driving via recovery`);
547
+ this.name = "RemoteWorkflowTimeout";
548
+ this.taskId = taskId;
549
+ this.timeoutMs = timeoutMs;
550
+ }
551
+ };
534
552
  var NonDeterminismError = class extends Error {
535
553
  static {
536
554
  __name(this, "NonDeterminismError");
@@ -544,6 +562,19 @@ var NonDeterminismError = class extends Error {
544
562
  this.seq = seq;
545
563
  }
546
564
  };
565
+ var GatherError = class extends Error {
566
+ static {
567
+ __name(this, "GatherError");
568
+ }
569
+ failures;
570
+ constructor(failures, total) {
571
+ const ids = failures.map((f) => f.id).join(", ");
572
+ const denom = total ?? failures.length;
573
+ super(`ctx.all: ${failures.length} of ${denom} child(ren) failed: ${ids}`);
574
+ this.name = "GatherError";
575
+ this.failures = failures;
576
+ }
577
+ };
547
578
  var WorkflowSuspended = class extends Error {
548
579
  static {
549
580
  __name(this, "WorkflowSuspended");
@@ -1213,6 +1244,72 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1213
1244
  }
1214
1245
  throw new WorkflowSuspended();
1215
1246
  }, "child");
1247
+ const all = /* @__PURE__ */ __name(async (workflow2, inputs, opts) => {
1248
+ if (inputs.length === 0) return [];
1249
+ const mode = opts?.mode ?? "waitAll";
1250
+ const name = workflowName(workflow2);
1251
+ const positions = inputs.map(() => pos.next());
1252
+ const group = `all:${positions[0]}`;
1253
+ const id = /* @__PURE__ */ __name((i) => `${runId}.all.${positions[0]}.${i}`, "id");
1254
+ const existing = await Promise.all(positions.map((seq) => readCheckpoint(seq)));
1255
+ if (mode === "failFast") {
1256
+ for (let i = 0; i < inputs.length; i += 1) {
1257
+ const cp = existing[i];
1258
+ const c = cp?.status === "completed" ? cp.output : null;
1259
+ if (c && c.ok === false) {
1260
+ throw new GatherError([
1261
+ {
1262
+ index: i,
1263
+ id: id(i),
1264
+ error: c.error ?? "unknown"
1265
+ }
1266
+ ], inputs.length);
1267
+ }
1268
+ }
1269
+ }
1270
+ let pending = false;
1271
+ for (let i = 0; i < inputs.length; i += 1) {
1272
+ const cp = existing[i];
1273
+ if (cp?.status === "completed") continue;
1274
+ pending = true;
1275
+ const seq = positions[i];
1276
+ const childId = id(i);
1277
+ await store.putSignalWaiter({
1278
+ token: `child:${childId}`,
1279
+ runId,
1280
+ seq
1281
+ });
1282
+ if (!await store.getRun(childId)) host.startChild(name, inputs[i], childId);
1283
+ if (!cp) {
1284
+ await writeCheckpoint(instantCheckpoint({
1285
+ runId,
1286
+ seq,
1287
+ name: `signal:child:${childId}`,
1288
+ kind: "signal",
1289
+ status: "running",
1290
+ parallelGroup: group
1291
+ }));
1292
+ }
1293
+ }
1294
+ if (pending) throw new WorkflowSuspended();
1295
+ const outputs = [];
1296
+ const failures = [];
1297
+ for (let i = 0; i < inputs.length; i += 1) {
1298
+ const c = existing[i]?.output;
1299
+ if (c && typeof c === "object" && "ok" in c && c.ok === false) {
1300
+ failures.push({
1301
+ index: i,
1302
+ id: id(i),
1303
+ error: c.error ?? "unknown"
1304
+ });
1305
+ outputs.push(void 0);
1306
+ } else {
1307
+ outputs.push(c?.value);
1308
+ }
1309
+ }
1310
+ if (failures.length > 0) throw new GatherError(failures, inputs.length);
1311
+ return outputs;
1312
+ }, "all");
1216
1313
  const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
1217
1314
  const { childId, priority } = normalizeChildOptions(options);
1218
1315
  const current = pos.next();
@@ -1347,6 +1444,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
1347
1444
  waitForEvent,
1348
1445
  task,
1349
1446
  child,
1447
+ all,
1350
1448
  startChild,
1351
1449
  breakpoint,
1352
1450
  webhook,
@@ -1404,6 +1502,7 @@ var WorkflowEngine = class {
1404
1502
  instanceId;
1405
1503
  leaseMs;
1406
1504
  maxRecoveryAttempts;
1505
+ remoteAdvanceSilenceMs;
1407
1506
  webhookUrl;
1408
1507
  traceparent;
1409
1508
  context;
@@ -1428,7 +1527,9 @@ var WorkflowEngine = class {
1428
1527
  accumulators;
1429
1528
  /** In-flight remote steps awaiting a worker result, keyed by stepId. */
1430
1529
  pending = /* @__PURE__ */ new Map();
1431
- /** Per-step "reset the liveness timer" callbacks, called when a heartbeat arrives. */
1530
+ /** "Reset the liveness timer" callbacks keyed by the beat's target — a stepId (in-flight remote step)
1531
+ * or a runId (in-flight remote workflow turn) — invoked when a matching heartbeat arrives. The two id
1532
+ * shapes don't collide in practice: a stepId is `${runId}:${seq}`, a run key is the bare runId. */
1432
1533
  heartbeatResets = /* @__PURE__ */ new Map();
1433
1534
  listeners = /* @__PURE__ */ new Set();
1434
1535
  /** Step interceptors (onion middleware around real local-step execution), first = outermost. */
@@ -1467,6 +1568,7 @@ var WorkflowEngine = class {
1467
1568
  this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
1468
1569
  this.leaseMs = deps.leaseMs ?? 3e4;
1469
1570
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
1571
+ this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
1470
1572
  this.webhookUrl = deps.webhookUrl;
1471
1573
  this.traceparent = deps.traceparent;
1472
1574
  this.context = deps.context;
@@ -1503,9 +1605,11 @@ var WorkflowEngine = class {
1503
1605
  }
1504
1606
  await this.completeRemoteResult(result);
1505
1607
  },
1506
- // A heartbeat for an in-flight long step resets its liveness window (see callRemote).
1608
+ // A heartbeat resets the liveness window for whatever it targets: an in-flight long STEP (keyed
1609
+ // by stepId, see callRemote) or — when stepId is absent — the in-flight workflow TURN (keyed by
1610
+ // runId, see runRemoteExecution's heartbeat-rearmed advance).
1507
1611
  async (beat) => {
1508
- this.heartbeatResets.get(beat.stepId)?.();
1612
+ this.heartbeatResets.get(beat.stepId ?? beat.runId)?.();
1509
1613
  },
1510
1614
  // A remote workflow worker streams each local step's lifecycle (running → completed/failed) so
1511
1615
  // it's checkpointed live, not all-at-once when the long turn ends.
@@ -2336,7 +2440,8 @@ var WorkflowEngine = class {
2336
2440
  attempts: 1,
2337
2441
  enqueuedAt: startedAt,
2338
2442
  startedAt,
2339
- finishedAt: startedAt
2443
+ finishedAt: startedAt,
2444
+ parallelGroup: event.parallelGroup
2340
2445
  }));
2341
2446
  this.emit({
2342
2447
  type: "step.started",
@@ -2360,7 +2465,8 @@ var WorkflowEngine = class {
2360
2465
  attempts: 1,
2361
2466
  enqueuedAt: startedAt,
2362
2467
  startedAt,
2363
- finishedAt: event.finishedAt != null ? new Date(event.finishedAt) : /* @__PURE__ */ new Date()
2468
+ finishedAt: event.finishedAt != null ? new Date(event.finishedAt) : /* @__PURE__ */ new Date(),
2469
+ parallelGroup: event.parallelGroup
2364
2470
  }));
2365
2471
  this.emit({
2366
2472
  type: failed ? "step.failed" : "step.completed",
@@ -2595,8 +2701,17 @@ var WorkflowEngine = class {
2595
2701
  const history = await this.remoteHistory(run.id);
2596
2702
  let decision;
2597
2703
  try {
2598
- decision = await remote.executor.advance(run, history);
2704
+ const silenceMs = this.remoteAdvanceSilenceMs;
2705
+ const advance = remote.executor.advance(run, history);
2706
+ decision = silenceMs != null ? await this.awaitWithLivenessDeadline(run.id, advance, silenceMs, () => new RemoteWorkflowTimeout(run.id, silenceMs)) : await advance;
2599
2707
  } catch (err) {
2708
+ if (err instanceof RemoteWorkflowTimeout) {
2709
+ await this.store.releaseRunLock(run.id);
2710
+ return {
2711
+ runId: run.id,
2712
+ status: "running"
2713
+ };
2714
+ }
2600
2715
  const error = {
2601
2716
  message: err instanceof Error ? err.message : String(err),
2602
2717
  stack: err instanceof Error ? err.stack : void 0
@@ -2716,7 +2831,11 @@ var WorkflowEngine = class {
2716
2831
  attempts: 1,
2717
2832
  enqueuedAt: startedAt,
2718
2833
  startedAt,
2719
- finishedAt
2834
+ finishedAt,
2835
+ // Carry the worker's `parallelGroup` (a Python `ctx.gather` tags every step in the fan with
2836
+ // the same group) onto the checkpoint, so the dashboard can render the steps as one parallel
2837
+ // group instead of a sequential list. Additive: undefined for ordinary sequential steps.
2838
+ parallelGroup: cmd.parallelGroup
2720
2839
  }));
2721
2840
  this.emit({
2722
2841
  type: cmd.error ? "step.failed" : "step.completed",
@@ -3245,11 +3364,13 @@ var WorkflowEngine = class {
3245
3364
  }
3246
3365
  }
3247
3366
  /**
3248
- * Await a remote result, but reject with `RemoteStepTimeout` if neither the result nor a heartbeat
3249
- * arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat`) rearms the
3250
- * window, so a worker that keeps beating stays alive past `timeoutMs`.
3367
+ * Await `resultPromise`, but reject with `onTimeout()` if neither it settles nor a heartbeat for `id`
3368
+ * arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat`, looked up in
3369
+ * `heartbeatResets` by `id`) REARMS the window, so a worker that keeps beating stays alive past
3370
+ * `timeoutMs`. Shared by the remote-STEP liveness (keyed by stepId) and the remote-WORKFLOW advance
3371
+ * liveness (keyed by runId); the caller supplies the timeout error and any extra cleanup via `onTimeout`.
3251
3372
  */
3252
- awaitWithHeartbeat(id, resultPromise, timeoutMs) {
3373
+ awaitWithLivenessDeadline(id, resultPromise, timeoutMs, onTimeout) {
3253
3374
  return new Promise((resolve, reject) => {
3254
3375
  let timer;
3255
3376
  const cleanup = /* @__PURE__ */ __name(() => {
@@ -3259,8 +3380,7 @@ var WorkflowEngine = class {
3259
3380
  const arm = /* @__PURE__ */ __name(() => {
3260
3381
  timer = setTimeout(() => {
3261
3382
  cleanup();
3262
- this.pending.delete(id);
3263
- reject(new RemoteStepTimeout(id, timeoutMs));
3383
+ reject(onTimeout());
3264
3384
  }, timeoutMs);
3265
3385
  timer.unref?.();
3266
3386
  }, "arm");
@@ -3278,6 +3398,14 @@ var WorkflowEngine = class {
3278
3398
  });
3279
3399
  });
3280
3400
  }
3401
+ /** Remote-STEP liveness: await a worker result, timing out (with the step's pending-map cleanup) to
3402
+ * `RemoteStepTimeout` if it neither lands nor beats within `timeoutMs`. See {@link awaitWithLivenessDeadline}. */
3403
+ awaitWithHeartbeat(id, resultPromise, timeoutMs) {
3404
+ return this.awaitWithLivenessDeadline(id, resultPromise, timeoutMs, () => {
3405
+ this.pending.delete(id);
3406
+ return new RemoteStepTimeout(id, timeoutMs);
3407
+ });
3408
+ }
3281
3409
  };
3282
3410
  var RemoteStepError = class extends Error {
3283
3411
  static {
@@ -3351,11 +3479,12 @@ var RemoteWorkflowExecutor = class {
3351
3479
  };
3352
3480
  const decision = new Promise((resolve, reject) => {
3353
3481
  this.pending.set(taskId, resolve);
3354
- if (this.opts.timeoutMs) {
3482
+ const { timeoutMs } = this.opts;
3483
+ if (timeoutMs) {
3355
3484
  const timer = setTimeout(() => {
3356
3485
  this.pending.delete(taskId);
3357
- reject(new Error(`workflow task ${taskId} timed out after ${this.opts.timeoutMs}ms`));
3358
- }, this.opts.timeoutMs);
3486
+ reject(new RemoteWorkflowTimeout(taskId, timeoutMs));
3487
+ }, timeoutMs);
3359
3488
  timer.unref?.();
3360
3489
  }
3361
3490
  });
@@ -4042,6 +4171,7 @@ var InMemoryTransport = class {
4042
4171
  }
4043
4172
  handlers = /* @__PURE__ */ new Map();
4044
4173
  resultHandler;
4174
+ heartbeatHandler;
4045
4175
  stepEventHandler;
4046
4176
  controlHandlers = /* @__PURE__ */ new Set();
4047
4177
  /** Register a fake worker handler for a step name. */
@@ -4056,7 +4186,12 @@ var InMemoryTransport = class {
4056
4186
  onResult(handler) {
4057
4187
  this.resultHandler = handler;
4058
4188
  }
4059
- onHeartbeat(_handler) {
4189
+ onHeartbeat(handler) {
4190
+ this.heartbeatHandler = handler;
4191
+ }
4192
+ /** Test hook: deliver a liveness heartbeat to the engine, as a real broker would on its channel. */
4193
+ async emitHeartbeat(beat) {
4194
+ await this.heartbeatHandler?.(beat);
4060
4195
  }
4061
4196
  /** Workflow step lifecycle: deliver to the engine's handler (async, mirroring a real broker). */
4062
4197
  async dispatchStepEvent(event) {
@@ -4083,6 +4218,7 @@ var InMemoryTransport = class {
4083
4218
  Entities,
4084
4219
  EventAccumulators,
4085
4220
  FatalError,
4221
+ GatherError,
4086
4222
  InMemoryAdmissionBackend,
4087
4223
  InMemoryStateStore,
4088
4224
  InMemoryTransport,
@@ -4091,6 +4227,7 @@ var InMemoryTransport = class {
4091
4227
  RemoteStepError,
4092
4228
  RemoteStepTimeout,
4093
4229
  RemoteWorkflowExecutor,
4230
+ RemoteWorkflowTimeout,
4094
4231
  STATE_STORE,
4095
4232
  STATE_STORE_CANONICAL,
4096
4233
  SignalTimeoutError,
@@ -4105,6 +4242,7 @@ var InMemoryTransport = class {
4105
4242
  attributeOperand,
4106
4243
  breakpointToken,
4107
4244
  collectMetrics,
4245
+ createStepLogger,
4108
4246
  eventMatchOf,
4109
4247
  eventMatches,
4110
4248
  eventPrefix,