@dudousxd/nestjs-durable-core 0.49.0 → 0.49.2

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.d.cts CHANGED
@@ -1113,6 +1113,16 @@ interface GroupHealth {
1113
1113
  depth: number;
1114
1114
  /** Workers with a non-expired heartbeat for this group. */
1115
1115
  liveWorkers: WorkerHeartbeat[];
1116
+ /**
1117
+ * Whether this group serves a `@Workflow` body or a `@Step`/handler — route-by-handler gives each
1118
+ * its own queue, so a health list mixes both. Classified by the CONTROL PLANE from its authoritative
1119
+ * registry (`workerHealth()`): a group whose base name is a registered workflow is `'workflow'`,
1120
+ * everything else (in-process steps, remote `handle_*`) is `'step'`. `undefined` only when no
1121
+ * control-plane registry was available to classify (e.g. a transport that reports health with no
1122
+ * engine). Lets a dashboard summarise the fleet in domain terms ("N workflows · M steps") instead of
1123
+ * leaking the raw queue count.
1124
+ */
1125
+ kind?: 'workflow' | 'step';
1116
1126
  }
1117
1127
  /**
1118
1128
  * The **control plane** — a broadcast pub/sub across ALL engine instances (every pod), separate
@@ -1774,6 +1784,19 @@ interface WorkflowEngineDeps {
1774
1784
  * one (this rearmed window for heartbeat-aware workers, or the fixed `timeoutMs` otherwise).
1775
1785
  */
1776
1786
  remoteAdvanceSilenceMs?: number | undefined;
1787
+ /**
1788
+ * Fallback re-drive window (ms) for a run that SUSPENDS with no natural timer — i.e. it's waiting on
1789
+ * an EVENT to wake it (a child's completion, a signal, a remote step with no `timeoutMs`) rather than
1790
+ * a `ctx.sleep`. Those events re-drive the run directly; but if the wake is LOST (the delivering pod
1791
+ * crashed / rolled mid-handoff), the run would otherwise sit `suspended` forever with `wakeAt: null` —
1792
+ * invisible to both the timer poller (no `wakeAt`) and crash-recovery (no lease). This stamps a
1793
+ * fallback `wakeAt` so {@link resumeDueTimers} re-drives it after the window; the re-drive is an
1794
+ * idempotent replay (a still-pending dependency re-suspends via the checkpoint guard, a settled one
1795
+ * advances), so it's a safe self-heal, not a retry that can double-dispatch. The primary event path
1796
+ * still wins first for a healthy run (it re-drives long before this fires), so this only ever fires
1797
+ * for a genuinely-orphaned run. Default 5min; set `0` to disable (the prior wake-forever behavior).
1798
+ */
1799
+ reconcileMs?: number | undefined;
1777
1800
  /**
1778
1801
  * Build the public callback URL for a `ctx.webhook()` token (e.g.
1779
1802
  * ``(t) => `https://api.example.com/durable/webhooks/${t}` ``). Populates
@@ -1849,6 +1872,7 @@ declare class WorkflowEngine {
1849
1872
  private readonly leaseMs;
1850
1873
  private readonly maxRecoveryAttempts?;
1851
1874
  private readonly remoteAdvanceSilenceMs?;
1875
+ private readonly reconcileMs;
1852
1876
  private readonly webhookUrl?;
1853
1877
  private readonly traceparent?;
1854
1878
  private readonly context?;
@@ -2314,6 +2338,13 @@ declare class WorkflowEngine {
2314
2338
  * keeps its own lease handling (the TS executor's `finally`, the remote path's caller).
2315
2339
  */
2316
2340
  private settleRun;
2341
+ /**
2342
+ * A wakeAt to persist on a suspend: the natural timer if the turn produced one (a `ctx.sleep`, a
2343
+ * remote-step deadline), else the {@link reconcileMs} fallback so an event-waiting run (child/signal/
2344
+ * timeout-less remote step) can't be orphaned forever if its wake is lost. `undefined` only when the
2345
+ * fallback is disabled (`reconcileMs: 0`). See {@link WorkflowEngineDeps.reconcileMs}.
2346
+ */
2347
+ private reconcileWakeAt;
2317
2348
  private runRemoteExecution;
2318
2349
  /**
2319
2350
  * Apply a remote workflow turn's {@link WorkflowDecision}: persist the local steps it ran, then
package/dist/index.d.ts CHANGED
@@ -1113,6 +1113,16 @@ interface GroupHealth {
1113
1113
  depth: number;
1114
1114
  /** Workers with a non-expired heartbeat for this group. */
1115
1115
  liveWorkers: WorkerHeartbeat[];
1116
+ /**
1117
+ * Whether this group serves a `@Workflow` body or a `@Step`/handler — route-by-handler gives each
1118
+ * its own queue, so a health list mixes both. Classified by the CONTROL PLANE from its authoritative
1119
+ * registry (`workerHealth()`): a group whose base name is a registered workflow is `'workflow'`,
1120
+ * everything else (in-process steps, remote `handle_*`) is `'step'`. `undefined` only when no
1121
+ * control-plane registry was available to classify (e.g. a transport that reports health with no
1122
+ * engine). Lets a dashboard summarise the fleet in domain terms ("N workflows · M steps") instead of
1123
+ * leaking the raw queue count.
1124
+ */
1125
+ kind?: 'workflow' | 'step';
1116
1126
  }
1117
1127
  /**
1118
1128
  * The **control plane** — a broadcast pub/sub across ALL engine instances (every pod), separate
@@ -1774,6 +1784,19 @@ interface WorkflowEngineDeps {
1774
1784
  * one (this rearmed window for heartbeat-aware workers, or the fixed `timeoutMs` otherwise).
1775
1785
  */
1776
1786
  remoteAdvanceSilenceMs?: number | undefined;
1787
+ /**
1788
+ * Fallback re-drive window (ms) for a run that SUSPENDS with no natural timer — i.e. it's waiting on
1789
+ * an EVENT to wake it (a child's completion, a signal, a remote step with no `timeoutMs`) rather than
1790
+ * a `ctx.sleep`. Those events re-drive the run directly; but if the wake is LOST (the delivering pod
1791
+ * crashed / rolled mid-handoff), the run would otherwise sit `suspended` forever with `wakeAt: null` —
1792
+ * invisible to both the timer poller (no `wakeAt`) and crash-recovery (no lease). This stamps a
1793
+ * fallback `wakeAt` so {@link resumeDueTimers} re-drives it after the window; the re-drive is an
1794
+ * idempotent replay (a still-pending dependency re-suspends via the checkpoint guard, a settled one
1795
+ * advances), so it's a safe self-heal, not a retry that can double-dispatch. The primary event path
1796
+ * still wins first for a healthy run (it re-drives long before this fires), so this only ever fires
1797
+ * for a genuinely-orphaned run. Default 5min; set `0` to disable (the prior wake-forever behavior).
1798
+ */
1799
+ reconcileMs?: number | undefined;
1777
1800
  /**
1778
1801
  * Build the public callback URL for a `ctx.webhook()` token (e.g.
1779
1802
  * ``(t) => `https://api.example.com/durable/webhooks/${t}` ``). Populates
@@ -1849,6 +1872,7 @@ declare class WorkflowEngine {
1849
1872
  private readonly leaseMs;
1850
1873
  private readonly maxRecoveryAttempts?;
1851
1874
  private readonly remoteAdvanceSilenceMs?;
1875
+ private readonly reconcileMs;
1852
1876
  private readonly webhookUrl?;
1853
1877
  private readonly traceparent?;
1854
1878
  private readonly context?;
@@ -2314,6 +2338,13 @@ declare class WorkflowEngine {
2314
2338
  * keeps its own lease handling (the TS executor's `finally`, the remote path's caller).
2315
2339
  */
2316
2340
  private settleRun;
2341
+ /**
2342
+ * A wakeAt to persist on a suspend: the natural timer if the turn produced one (a `ctx.sleep`, a
2343
+ * remote-step deadline), else the {@link reconcileMs} fallback so an event-waiting run (child/signal/
2344
+ * timeout-less remote step) can't be orphaned forever if its wake is lost. `undefined` only when the
2345
+ * fallback is disabled (`reconcileMs: 0`). See {@link WorkflowEngineDeps.reconcileMs}.
2346
+ */
2347
+ private reconcileWakeAt;
2317
2348
  private runRemoteExecution;
2318
2349
  /**
2319
2350
  * Apply a remote workflow turn's {@link WorkflowDecision}: persist the local steps it ran, then
package/dist/index.js CHANGED
@@ -1720,6 +1720,7 @@ var WorkflowEngine = class {
1720
1720
  leaseMs;
1721
1721
  maxRecoveryAttempts;
1722
1722
  remoteAdvanceSilenceMs;
1723
+ reconcileMs;
1723
1724
  webhookUrl;
1724
1725
  traceparent;
1725
1726
  context;
@@ -1790,6 +1791,7 @@ var WorkflowEngine = class {
1790
1791
  this.leaseMs = deps.leaseMs ?? 3e4;
1791
1792
  this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
1792
1793
  this.remoteAdvanceSilenceMs = deps.remoteAdvanceSilenceMs;
1794
+ this.reconcileMs = deps.reconcileMs === void 0 ? 3e5 : deps.reconcileMs || void 0;
1793
1795
  this.webhookUrl = deps.webhookUrl;
1794
1796
  this.traceparent = deps.traceparent;
1795
1797
  this.context = deps.context;
@@ -2879,10 +2881,21 @@ var WorkflowEngine = class {
2879
2881
  ...this.knownGroups(extra),
2880
2882
  ...await this.pool.listWorkerGroups()
2881
2883
  ]);
2884
+ const workflowTokens = /* @__PURE__ */ new Set();
2885
+ for (const def of this.workflows.values()) {
2886
+ workflowTokens.add(sanitizeQueueToken(def.name));
2887
+ if (def.remote?.group) workflowTokens.add(def.remote.group);
2888
+ }
2882
2889
  const out = [];
2883
2890
  for (const group of groups) {
2884
2891
  const health = await this.pool.groupHealth(group);
2885
- if (health) out.push(health);
2892
+ if (!health) continue;
2893
+ const at = group.lastIndexOf("@");
2894
+ const baseToken = at === -1 ? group : group.slice(0, at);
2895
+ out.push({
2896
+ ...health,
2897
+ kind: workflowTokens.has(baseToken) ? "workflow" : "step"
2898
+ });
2886
2899
  }
2887
2900
  return out;
2888
2901
  }
@@ -3123,7 +3136,7 @@ var WorkflowEngine = class {
3123
3136
  }
3124
3137
  await this.store.updateRun(run.id, {
3125
3138
  status: "suspended",
3126
- wakeAt: outcome.wakeAt,
3139
+ wakeAt: this.reconcileWakeAt(outcome.wakeAt),
3127
3140
  updatedAt
3128
3141
  });
3129
3142
  this.emit({
@@ -3137,6 +3150,16 @@ var WorkflowEngine = class {
3137
3150
  status: "suspended"
3138
3151
  };
3139
3152
  }
3153
+ /**
3154
+ * A wakeAt to persist on a suspend: the natural timer if the turn produced one (a `ctx.sleep`, a
3155
+ * remote-step deadline), else the {@link reconcileMs} fallback so an event-waiting run (child/signal/
3156
+ * timeout-less remote step) can't be orphaned forever if its wake is lost. `undefined` only when the
3157
+ * fallback is disabled (`reconcileMs: 0`). See {@link WorkflowEngineDeps.reconcileMs}.
3158
+ */
3159
+ reconcileWakeAt(wakeAt) {
3160
+ if (wakeAt != null) return wakeAt;
3161
+ return this.reconcileMs != null ? this.clock() + this.reconcileMs : void 0;
3162
+ }
3140
3163
  async runRemoteExecution(run, registered) {
3141
3164
  if (run.status === "cancelling") {
3142
3165
  const error = {