@dudousxd/nestjs-durable-core 0.54.0 → 0.55.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.d.cts CHANGED
@@ -2618,6 +2618,19 @@ declare class WorkflowEngine {
2618
2618
  * knowing about the child feature.
2619
2619
  */
2620
2620
  private notifyParent;
2621
+ /**
2622
+ * Deferred child start shared by the in-process ctx host and the remote `startChild` command.
2623
+ * Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
2624
+ * that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
2625
+ * failure) must NOT be swallowed: the parent is already suspended on `child:<childId>` (the waiter
2626
+ * is put BEFORE start on every path), so a silent drop parks it in suspended-forever, invisibly
2627
+ * re-attempting on every recovery wake — a misconfigured remote child looks exactly like a healthy
2628
+ * long wait. Instead the failure is delivered to that waiter like a failed child (notifyParent):
2629
+ * the parent resumes and fails loudly with the cause. For a fire-and-forget `ctx.startChild`
2630
+ * (spawn — no waiter) the completion is buffered; a later join by the same id consumes it and
2631
+ * correctly observes the failed start.
2632
+ */
2633
+ private startChildDeferred;
2621
2634
  /**
2622
2635
  * Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
2623
2636
  * `{ compensate: true }` to undo the saga first: the suspended run is resumed so its completed
package/dist/index.d.ts CHANGED
@@ -2618,6 +2618,19 @@ declare class WorkflowEngine {
2618
2618
  * knowing about the child feature.
2619
2619
  */
2620
2620
  private notifyParent;
2621
+ /**
2622
+ * Deferred child start shared by the in-process ctx host and the remote `startChild` command.
2623
+ * Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
2624
+ * that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
2625
+ * failure) must NOT be swallowed: the parent is already suspended on `child:<childId>` (the waiter
2626
+ * is put BEFORE start on every path), so a silent drop parks it in suspended-forever, invisibly
2627
+ * re-attempting on every recovery wake — a misconfigured remote child looks exactly like a healthy
2628
+ * long wait. Instead the failure is delivered to that waiter like a failed child (notifyParent):
2629
+ * the parent resumes and fails loudly with the cause. For a fire-and-forget `ctx.startChild`
2630
+ * (spawn — no waiter) the completion is buffered; a later join by the same id consumes it and
2631
+ * correctly observes the failed start.
2632
+ */
2633
+ private startChildDeferred;
2621
2634
  /**
2622
2635
  * Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
2623
2636
  * `{ compensate: true }` to undo the saga first: the suspended run is resumed so its completed
package/dist/index.js CHANGED
@@ -1099,6 +1099,21 @@ var TransportPool = class {
1099
1099
  ];
1100
1100
  }
1101
1101
  /**
1102
+ * The first transport (in pool order) reporting a LIVE heartbeat for `group` — the one a
1103
+ * convention-routed remote workflow must dispatch on. In a mixed pool (e.g. a tenant-scoped
1104
+ * control plane pairing its namespaced transport with a bare-prefix one for operator-convention
1105
+ * tenant workers), each transport sees a DIFFERENT keyspace: dispatching on `primary` while the
1106
+ * live worker heartbeats on another transport parks the task on a queue nobody consumes. Returns
1107
+ * `undefined` when no transport reports the group (mirrors the merged `listWorkerGroups` miss).
1108
+ */
1109
+ async transportWithLiveGroup(group) {
1110
+ for (const { transport } of this.transports) {
1111
+ if (!transport.listWorkerGroups) continue;
1112
+ if ((await transport.listWorkerGroups()).includes(group)) return transport;
1113
+ }
1114
+ return void 0;
1115
+ }
1116
+ /**
1102
1117
  * Propagate the engine's `namespace` to every transport that partitions by it — so the same
1103
1118
  * namespace that scopes the store also scopes each transport's queues/keys. A transport that
1104
1119
  * doesn't partition (no `useNamespace`) is skipped. Idempotent; a transport given an explicit
@@ -2414,10 +2429,10 @@ var WorkflowEngine = class {
2414
2429
  * liveness changes between calls, exactly as {@link synthesizeRemoteChild} does.
2415
2430
  */
2416
2431
  async resolveRemoteByConvention(run) {
2417
- const liveGroups = await this.pool.listWorkerGroups();
2418
2432
  const group = tenantGroup(sanitizeQueueToken(run.workflow), run.namespace);
2419
- if (!liveGroups.includes(group)) return void 0;
2420
- const executor = new RemoteWorkflowExecutor(this.pool.primary, run.workflow, run.namespace);
2433
+ const liveTransport = await this.pool.transportWithLiveGroup(group);
2434
+ if (!liveTransport) return void 0;
2435
+ const executor = new RemoteWorkflowExecutor(liveTransport, run.workflow, run.namespace);
2421
2436
  return {
2422
2437
  name: run.workflow,
2423
2438
  version: run.workflowVersion,
@@ -3036,6 +3051,27 @@ var WorkflowEngine = class {
3036
3051
  void this.signal(`child:${runId}`, completion).catch(() => void 0);
3037
3052
  }
3038
3053
  /**
3054
+ * Deferred child start shared by the in-process ctx host and the remote `startChild` command.
3055
+ * Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
3056
+ * that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
3057
+ * failure) must NOT be swallowed: the parent is already suspended on `child:<childId>` (the waiter
3058
+ * is put BEFORE start on every path), so a silent drop parks it in suspended-forever, invisibly
3059
+ * re-attempting on every recovery wake — a misconfigured remote child looks exactly like a healthy
3060
+ * long wait. Instead the failure is delivered to that waiter like a failed child (notifyParent):
3061
+ * the parent resumes and fails loudly with the cause. For a fire-and-forget `ctx.startChild`
3062
+ * (spawn — no waiter) the completion is buffered; a later join by the same id consumes it and
3063
+ * correctly observes the failed start.
3064
+ */
3065
+ startChildDeferred(workflow, input, childId, opts) {
3066
+ queueMicrotask(() => void this.start(workflow, input, childId, opts).catch((err) => {
3067
+ const message = err instanceof Error ? err.message : String(err);
3068
+ this.notifyParent(childId, {
3069
+ ok: false,
3070
+ error: `child workflow "${workflow}" failed to start: ${message}`
3071
+ });
3072
+ }));
3073
+ }
3074
+ /**
3039
3075
  * Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
3040
3076
  * `{ compensate: true }` to undo the saga first: the suspended run is resumed so its completed
3041
3077
  * steps' compensations run in reverse (visible as `compensate:<step>` events), then it's marked
@@ -3859,9 +3895,9 @@ var WorkflowEngine = class {
3859
3895
  parallelGroup: cmd.parallelGroup
3860
3896
  });
3861
3897
  if (!await this.store.getRun(childId)) {
3862
- queueMicrotask(() => void this.start(cmd.workflow, cmd.input, childId, {
3898
+ this.startChildDeferred(cmd.workflow, cmd.input, childId, {
3863
3899
  namespace: run.namespace
3864
- }).catch(() => void 0));
3900
+ });
3865
3901
  }
3866
3902
  } else {
3867
3903
  throw new Error(`remote workflow command '${cmd.kind}' is not supported yet`);
@@ -4240,10 +4276,10 @@ var WorkflowEngine = class {
4240
4276
  // `davi-local` pipeline would stamp its `processing` child `default` and it would leak off the
4241
4277
  // tenant's worker pool onto the shared/dev workers.
4242
4278
  startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
4243
- queueMicrotask(() => void this.start(workflow, input, id, {
4279
+ this.startChildDeferred(workflow, input, id, {
4244
4280
  priority,
4245
4281
  namespace: parentNamespace
4246
- }).catch(() => void 0));
4282
+ });
4247
4283
  }, "startChild"),
4248
4284
  // Deferred for the same reentrancy reason as `startChild` above. `cancel()` is already
4249
4285
  // idempotent on a terminal/cancelled run (returns its existing status without side effects), so