@dudousxd/nestjs-durable-core 0.49.3 → 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
@@ -1814,6 +1814,8 @@ var WorkflowEngine = class {
1814
1814
  maxRecoveryAttempts;
1815
1815
  remoteAdvanceSilenceMs;
1816
1816
  reconcileMs;
1817
+ remoteRedispatchMs;
1818
+ remoteRedispatchMax;
1817
1819
  webhookUrl;
1818
1820
  traceparent;
1819
1821
  context;
@@ -1885,6 +1887,8 @@ var WorkflowEngine = class {
1885
1887
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
1886
1888
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
1887
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);
1888
1892
  this.webhookUrl = deps.webhookUrl;
1889
1893
  this.traceparent = deps.traceparent;
1890
1894
  this.context = deps.context;
@@ -2596,6 +2600,49 @@ var WorkflowEngine = class {
2596
2600
  };
2597
2601
  }
2598
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
+ /**
2599
2646
  * **Fix-and-replay**: re-run a run (typically a `dead`/`failed` one) with a corrected `input`, as a
2600
2647
  * fresh run with clean history. It's a NEW run — `newRunId` defaults to `<runId>~retry~<uuid>` — so
2601
2648
  * the original stays inspectable. Returns the new run's id, or null if `runId` is unknown.
@@ -4083,7 +4130,47 @@ var WorkflowEngine = class {
4083
4130
  if (existing?.status === "completed") return existing.output;
4084
4131
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
4085
4132
  if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport);
4086
- 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
+ }
4087
4174
  let attempt = 1;
4088
4175
  if (existing?.status === "failed") {
4089
4176
  const maxAttempts = Math.max(1, step.retries ?? 1);
@@ -4131,18 +4218,17 @@ var WorkflowEngine = class {
4131
4218
  startedAt: enqueuedAt,
4132
4219
  finishedAt: enqueuedAt
4133
4220
  });
4134
- await this.pool.dispatch({
4221
+ await this.dispatchRemoteTask({
4135
4222
  runId,
4136
4223
  seq,
4137
4224
  name: step.name,
4138
4225
  stepId: id,
4139
4226
  group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
4140
4227
  input,
4141
- traceparent: this.traceparent?.(),
4142
- context: this.context?.(),
4143
4228
  priority: admission?.priority,
4144
- attempt
4145
- }, transport);
4229
+ attempt,
4230
+ transport
4231
+ });
4146
4232
  this.emit({
4147
4233
  type: "step.started",
4148
4234
  runId,
@@ -4152,6 +4238,23 @@ var WorkflowEngine = class {
4152
4238
  });
4153
4239
  throw new WorkflowSuspended();
4154
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
+ }
4155
4258
  /**
4156
4259
  * Complete a durable remote step from its worker result and resume the run — runs on whichever
4157
4260
  * instance receives the result (the dispatching one may be gone), so the run is crash/scale-safe.