@dudousxd/nestjs-durable-core 0.49.2 → 0.49.4

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
@@ -57,17 +57,21 @@ __export(index_exports, {
57
57
  attributeOperand: () => attributeOperand,
58
58
  bindWorkflowClass: () => bindWorkflowClass,
59
59
  breakpointToken: () => breakpointToken,
60
+ classifyWaiterToken: () => classifyWaiterToken,
60
61
  collectMetrics: () => collectMetrics,
61
62
  createStepLogger: () => createStepLogger,
62
63
  currentWorkflowCtx: () => currentWorkflowCtx,
63
64
  eventMatchOf: () => eventMatchOf,
64
65
  eventMatches: () => eventMatches,
66
+ eventNameOf: () => eventNameOf,
65
67
  eventPrefix: () => eventPrefix,
66
68
  eventToken: () => eventToken,
69
+ indexWaitersByRun: () => indexWaitersByRun,
67
70
  matchesAttributes: () => matchesAttributes,
68
71
  normalizeAttributeRows: () => normalizeAttributeRows,
69
72
  parseDuration: () => parseDuration,
70
73
  prevCronFireMs: () => prevCronFireMs,
74
+ resolveRunWaiting: () => resolveRunWaiting,
71
75
  runInWorkflowCtx: () => runInWorkflowCtx,
72
76
  runSchedules: () => runSchedules,
73
77
  runStepHandler: () => runStepHandler,
@@ -861,6 +865,14 @@ function eventToken(name, match, runId, seq) {
861
865
  return `${eventPrefix(name)}${enc(JSON.stringify(match ?? {}))}:${runId}#${seq}`;
862
866
  }
863
867
  __name(eventToken, "eventToken");
868
+ function eventNameOf(token) {
869
+ try {
870
+ return dec(token.split(":")[1] ?? "");
871
+ } catch {
872
+ return token;
873
+ }
874
+ }
875
+ __name(eventNameOf, "eventNameOf");
864
876
  function eventMatchOf(token) {
865
877
  try {
866
878
  return JSON.parse(dec(token.split(":")[2] ?? ""));
@@ -1802,6 +1814,8 @@ var WorkflowEngine = class {
1802
1814
  maxRecoveryAttempts;
1803
1815
  remoteAdvanceSilenceMs;
1804
1816
  reconcileMs;
1817
+ remoteRedispatchMs;
1818
+ remoteRedispatchMax;
1805
1819
  webhookUrl;
1806
1820
  traceparent;
1807
1821
  context;
@@ -1873,6 +1887,8 @@ var WorkflowEngine = class {
1873
1887
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
1874
1888
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
1875
1889
  this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
1890
+ this.remoteRedispatchMs = deps.remoteRedispatchMs || void 0;
1891
+ this.remoteRedispatchMax = Math.max(1, deps.remoteRedispatchMax ?? 10);
1876
1892
  this.webhookUrl = deps.webhookUrl;
1877
1893
  this.traceparent = deps.traceparent;
1878
1894
  this.context = deps.context;
@@ -2584,6 +2600,49 @@ var WorkflowEngine = class {
2584
2600
  };
2585
2601
  }
2586
2602
  /**
2603
+ * Explicitly re-dispatch every remote step of `runId` stuck `pending` — the OPERATOR escape hatch for
2604
+ * a run whose dispatched step job was LOST (worker crashed with no result, or a transport that dropped
2605
+ * the job). No automatic recovery re-drives these: a reconcile re-drive re-suspends a still-pending
2606
+ * step by design (unless `remoteRedispatchMs` is set), `recoverIncomplete` only reclaims LEASED runs,
2607
+ * and `requeue` just replays back to the pending guard. This re-enqueues the same `stepId` so the
2608
+ * (idempotent) step re-runs and its result resumes the run. Safe to call on a healthy run — it re-runs
2609
+ * only in-flight `pending` remote steps, which are idempotent by the durable contract. Returns the
2610
+ * run's current status (with the count re-dispatched), or null if the run is unknown.
2611
+ */
2612
+ async redispatchPending(runId) {
2613
+ const run = await this.store.getRun(runId);
2614
+ if (!run) return null;
2615
+ const pending = (await this.store.listCheckpoints(runId)).filter((cp) => cp.kind === "remote" && cp.status === "pending");
2616
+ let redispatched = 0;
2617
+ for (const cp of pending) {
2618
+ if (!cp.stepId) continue;
2619
+ const reAttempt = cp.attempts + 1;
2620
+ const reEnqueuedAt = /* @__PURE__ */ new Date();
2621
+ await this.store.saveCheckpoint({
2622
+ ...cp,
2623
+ attempts: reAttempt,
2624
+ enqueuedAt: reEnqueuedAt,
2625
+ startedAt: reEnqueuedAt,
2626
+ finishedAt: reEnqueuedAt
2627
+ });
2628
+ await this.dispatchRemoteTask({
2629
+ runId,
2630
+ seq: cp.seq,
2631
+ name: cp.name,
2632
+ stepId: cp.stepId,
2633
+ group: cp.workerGroup ?? sanitizeQueueToken(cp.name),
2634
+ input: cp.input,
2635
+ attempt: reAttempt
2636
+ });
2637
+ redispatched += 1;
2638
+ }
2639
+ return {
2640
+ runId,
2641
+ status: run.status,
2642
+ redispatched
2643
+ };
2644
+ }
2645
+ /**
2587
2646
  * **Fix-and-replay**: re-run a run (typically a `dead`/`failed` one) with a corrected `input`, as a
2588
2647
  * fresh run with clean history. It's a NEW run — `newRunId` defaults to `<runId>~retry~<uuid>` — so
2589
2648
  * the original stays inspectable. Returns the new run's id, or null if `runId` is unknown.
@@ -4071,7 +4130,47 @@ var WorkflowEngine = class {
4071
4130
  if (existing?.status === "completed") return existing.output;
4072
4131
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
4073
4132
  if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport);
4074
- if (existing?.status === "pending") throw new WorkflowSuspended();
4133
+ if (existing?.status === "pending") {
4134
+ if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
4135
+ if (existing.wakeAt == null) {
4136
+ const wakeAt = this.clock() + this.remoteRedispatchMs;
4137
+ await this.store.saveCheckpoint({
4138
+ ...existing,
4139
+ wakeAt
4140
+ });
4141
+ throw new WorkflowSuspended(wakeAt);
4142
+ }
4143
+ if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
4144
+ if (existing.attempts >= this.remoteRedispatchMax) {
4145
+ throw new RemoteStepError({
4146
+ message: `remote step "${step.name}" lost \u2014 no result after ${existing.attempts} re-dispatch(es)`,
4147
+ code: "remote_step_lost"
4148
+ });
4149
+ }
4150
+ const reAttempt = existing.attempts + 1;
4151
+ const nextDeadline = this.clock() + this.remoteRedispatchMs;
4152
+ const reEnqueuedAt = /* @__PURE__ */ new Date();
4153
+ await this.store.saveCheckpoint({
4154
+ ...existing,
4155
+ attempts: reAttempt,
4156
+ wakeAt: nextDeadline,
4157
+ enqueuedAt: reEnqueuedAt,
4158
+ startedAt: reEnqueuedAt,
4159
+ finishedAt: reEnqueuedAt
4160
+ });
4161
+ await this.dispatchRemoteTask({
4162
+ runId,
4163
+ seq,
4164
+ name: step.name,
4165
+ stepId: existing.stepId ?? stepId(runId, seq),
4166
+ group: existing.workerGroup ?? tenantGroup(sanitizeQueueToken(step.name), step.partition),
4167
+ input: existing.input,
4168
+ priority: admission?.priority,
4169
+ attempt: reAttempt,
4170
+ transport
4171
+ });
4172
+ throw new WorkflowSuspended(nextDeadline);
4173
+ }
4075
4174
  let attempt = 1;
4076
4175
  if (existing?.status === "failed") {
4077
4176
  const maxAttempts = Math.max(1, step.retries ?? 1);
@@ -4119,18 +4218,17 @@ var WorkflowEngine = class {
4119
4218
  startedAt: enqueuedAt,
4120
4219
  finishedAt: enqueuedAt
4121
4220
  });
4122
- await this.pool.dispatch({
4221
+ await this.dispatchRemoteTask({
4123
4222
  runId,
4124
4223
  seq,
4125
4224
  name: step.name,
4126
4225
  stepId: id,
4127
4226
  group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
4128
4227
  input,
4129
- traceparent: this.traceparent?.(),
4130
- context: this.context?.(),
4131
4228
  priority: admission?.priority,
4132
- attempt
4133
- }, transport);
4229
+ attempt,
4230
+ transport
4231
+ });
4134
4232
  this.emit({
4135
4233
  type: "step.started",
4136
4234
  runId,
@@ -4140,6 +4238,23 @@ var WorkflowEngine = class {
4140
4238
  });
4141
4239
  throw new WorkflowSuspended();
4142
4240
  }
4241
+ /** Enqueue a remote step task to its worker group via the transport pool. Shared by the initial
4242
+ * dispatch, the opt-in self-heal re-dispatch (see `remoteRedispatchMs`), and the explicit
4243
+ * {@link redispatchPending}. Carries the current trace/context so a re-dispatch is traceable. */
4244
+ dispatchRemoteTask(task) {
4245
+ return this.pool.dispatch({
4246
+ runId: task.runId,
4247
+ seq: task.seq,
4248
+ name: task.name,
4249
+ stepId: task.stepId,
4250
+ group: task.group,
4251
+ input: task.input,
4252
+ traceparent: this.traceparent?.(),
4253
+ context: this.context?.(),
4254
+ priority: task.priority,
4255
+ attempt: task.attempt
4256
+ }, task.transport);
4257
+ }
4143
4258
  /**
4144
4259
  * Complete a durable remote step from its worker result and resume the run — runs on whichever
4145
4260
  * instance receives the result (the dispatching one may be gone), so the run is crash/scale-safe.
@@ -4376,6 +4491,42 @@ var TERMINAL_RUN_STATUSES = [
4376
4491
  "dead"
4377
4492
  ];
4378
4493
 
4494
+ // src/run-waiting.ts
4495
+ var WEBHOOK_PREFIX = "wh:";
4496
+ var CHILD_PREFIX = "child:";
4497
+ var EVENT_PREFIX = "event:";
4498
+ function classifyWaiterToken(token) {
4499
+ if (token.startsWith(WEBHOOK_PREFIX)) return {
4500
+ on: "webhook",
4501
+ name: token
4502
+ };
4503
+ if (token.startsWith(CHILD_PREFIX)) return {
4504
+ on: "child",
4505
+ name: token.slice(CHILD_PREFIX.length)
4506
+ };
4507
+ if (token.startsWith(EVENT_PREFIX)) return {
4508
+ on: "signal",
4509
+ name: eventNameOf(token)
4510
+ };
4511
+ return {
4512
+ on: "signal",
4513
+ name: token
4514
+ };
4515
+ }
4516
+ __name(classifyWaiterToken, "classifyWaiterToken");
4517
+ function resolveRunWaiting(run, waiterByRun) {
4518
+ if (run.status !== "suspended") return void 0;
4519
+ const waiter = waiterByRun.get(run.id);
4520
+ return waiter ? classifyWaiterToken(waiter.token) : void 0;
4521
+ }
4522
+ __name(resolveRunWaiting, "resolveRunWaiting");
4523
+ function indexWaitersByRun(waiters) {
4524
+ const byRun = /* @__PURE__ */ new Map();
4525
+ for (const w of waiters) if (!byRun.has(w.runId)) byRun.set(w.runId, w);
4526
+ return byRun;
4527
+ }
4528
+ __name(indexWaitersByRun, "indexWaitersByRun");
4529
+
4379
4530
  // src/codec-state-store.ts
4380
4531
  var CodecStateStore = class {
4381
4532
  static {
@@ -5131,17 +5282,21 @@ var InMemoryTransport = class {
5131
5282
  attributeOperand,
5132
5283
  bindWorkflowClass,
5133
5284
  breakpointToken,
5285
+ classifyWaiterToken,
5134
5286
  collectMetrics,
5135
5287
  createStepLogger,
5136
5288
  currentWorkflowCtx,
5137
5289
  eventMatchOf,
5138
5290
  eventMatches,
5291
+ eventNameOf,
5139
5292
  eventPrefix,
5140
5293
  eventToken,
5294
+ indexWaitersByRun,
5141
5295
  matchesAttributes,
5142
5296
  normalizeAttributeRows,
5143
5297
  parseDuration,
5144
5298
  prevCronFireMs,
5299
+ resolveRunWaiting,
5145
5300
  runInWorkflowCtx,
5146
5301
  runSchedules,
5147
5302
  runStepHandler,