@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.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
- /** Per-step "reset the liveness timer" callbacks, called when a heartbeat arrives. */
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 step resets its liveness window (see callRemote).
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
- decision = await remote.executor.advance(run, history);
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 a remote result, but reject with `RemoteStepTimeout` if neither the result nor a heartbeat
3181
- * arrives within `timeoutMs`. Each heartbeat (delivered via `transport.onHeartbeat`) rearms the
3182
- * window, so a worker that keeps beating stays alive past `timeoutMs`.
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
- awaitWithHeartbeat(id, resultPromise, timeoutMs) {
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
- this.pending.delete(id);
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
- if (this.opts.timeoutMs) {
3411
+ const { timeoutMs } = this.opts;
3412
+ if (timeoutMs) {
3287
3413
  const timer = setTimeout(() => {
3288
3414
  this.pending.delete(taskId);
3289
- reject(new Error(`workflow task ${taskId} timed out after ${this.opts.timeoutMs}ms`));
3290
- }, this.opts.timeoutMs);
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(_handler) {
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,