@dudousxd/nestjs-durable-core 0.61.0 → 0.62.1

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
@@ -3050,6 +3050,19 @@ declare class WorkflowEngine {
3050
3050
  * from registrations, shows once its workers beat). Empty when no transport can introspect health
3051
3051
  * (only the BullMQ transport implements `groupHealth`). */
3052
3052
  workerHealth(extra?: string[]): Promise<GroupHealth[]>;
3053
+ /**
3054
+ * The routing groups of every in-flight run's still-`pending` remote step — the queues where work is
3055
+ * dispatched and awaiting a worker. Folded into {@link workerHealth} so a step waiting on a group
3056
+ * that is NEITHER a registration NOR heartbeating (the exact shape of a run stuck on an offline
3057
+ * tenant pool) still reports its backlog: without this the health scan can't "see" that queue, and
3058
+ * the dashboard shows the run as running instead of no-worker. Only the BullMQ transport introspects
3059
+ * health, so on any other transport this is harmless (the groups just resolve to no `groupHealth`).
3060
+ *
3061
+ * Cost: one `listRuns` over the non-terminal runs plus a checkpoint read each — a diagnostic-path
3062
+ * scan, not a hot path. Scoped to `running`/`suspended`/`blocked` (a `pending` run has dispatched no
3063
+ * step yet) and deduped to distinct groups.
3064
+ */
3065
+ private inflightStepGroups;
3053
3066
  /**
3054
3067
  * Persist a streamed local-step lifecycle event from a remote workflow worker (see
3055
3068
  * {@link WorkflowStepEvent}). A Python `@workflow` runs its `ctx.step`s inline over one turn that
package/dist/index.d.ts CHANGED
@@ -3050,6 +3050,19 @@ declare class WorkflowEngine {
3050
3050
  * from registrations, shows once its workers beat). Empty when no transport can introspect health
3051
3051
  * (only the BullMQ transport implements `groupHealth`). */
3052
3052
  workerHealth(extra?: string[]): Promise<GroupHealth[]>;
3053
+ /**
3054
+ * The routing groups of every in-flight run's still-`pending` remote step — the queues where work is
3055
+ * dispatched and awaiting a worker. Folded into {@link workerHealth} so a step waiting on a group
3056
+ * that is NEITHER a registration NOR heartbeating (the exact shape of a run stuck on an offline
3057
+ * tenant pool) still reports its backlog: without this the health scan can't "see" that queue, and
3058
+ * the dashboard shows the run as running instead of no-worker. Only the BullMQ transport introspects
3059
+ * health, so on any other transport this is harmless (the groups just resolve to no `groupHealth`).
3060
+ *
3061
+ * Cost: one `listRuns` over the non-terminal runs plus a checkpoint read each — a diagnostic-path
3062
+ * scan, not a hot path. Scoped to `running`/`suspended`/`blocked` (a `pending` run has dispatched no
3063
+ * step yet) and deduped to distinct groups.
3064
+ */
3065
+ private inflightStepGroups;
3053
3066
  /**
3054
3067
  * Persist a streamed local-step lifecycle event from a remote workflow worker (see
3055
3068
  * {@link WorkflowStepEvent}). A Python `@workflow` runs its `ctx.step`s inline over one turn that
package/dist/index.js CHANGED
@@ -2278,6 +2278,10 @@ __name(mergeTags, "mergeTags");
2278
2278
  var BREAKPOINT = "breakpoint";
2279
2279
  var isBreakpoint = /* @__PURE__ */ __name((cp) => cp.status === "pending" && cp.name.startsWith(BREAKPOINT), "isBreakpoint");
2280
2280
  var versionKey = /* @__PURE__ */ __name((name, version) => `${name}@${version}`, "versionKey");
2281
+ function stepGroup(step, runNamespace) {
2282
+ return tenantGroup(sanitizeQueueToken(step.name), step.partition ?? runNamespace);
2283
+ }
2284
+ __name(stepGroup, "stepGroup");
2281
2285
  function nextContinuationId(runId) {
2282
2286
  const m = runId.match(/^(.*)~(\d+)$/);
2283
2287
  return m ? `${m[1]}~${Number(m[2]) + 1}` : `${runId}~1`;
@@ -3778,7 +3782,8 @@ var WorkflowEngine = class {
3778
3782
  async workerHealth(extra = []) {
3779
3783
  const groups = /* @__PURE__ */ new Set([
3780
3784
  ...this.knownGroups(extra),
3781
- ...await this.pool.listWorkerGroups()
3785
+ ...await this.pool.listWorkerGroups(),
3786
+ ...await this.inflightStepGroups()
3782
3787
  ]);
3783
3788
  const workflowTokens = /* @__PURE__ */ new Set();
3784
3789
  for (const def of this.workflows.values()) {
@@ -3799,6 +3804,38 @@ var WorkflowEngine = class {
3799
3804
  return out;
3800
3805
  }
3801
3806
  /**
3807
+ * The routing groups of every in-flight run's still-`pending` remote step — the queues where work is
3808
+ * dispatched and awaiting a worker. Folded into {@link workerHealth} so a step waiting on a group
3809
+ * that is NEITHER a registration NOR heartbeating (the exact shape of a run stuck on an offline
3810
+ * tenant pool) still reports its backlog: without this the health scan can't "see" that queue, and
3811
+ * the dashboard shows the run as running instead of no-worker. Only the BullMQ transport introspects
3812
+ * health, so on any other transport this is harmless (the groups just resolve to no `groupHealth`).
3813
+ *
3814
+ * Cost: one `listRuns` over the non-terminal runs plus a checkpoint read each — a diagnostic-path
3815
+ * scan, not a hot path. Scoped to `running`/`suspended`/`blocked` (a `pending` run has dispatched no
3816
+ * step yet) and deduped to distinct groups.
3817
+ */
3818
+ async inflightStepGroups() {
3819
+ const inflight = await this.store.listRuns({
3820
+ statuses: [
3821
+ "running",
3822
+ "suspended",
3823
+ "blocked"
3824
+ ]
3825
+ });
3826
+ const groups = /* @__PURE__ */ new Set();
3827
+ for (const run of inflight) {
3828
+ for (const cp of await this.store.listCheckpoints(run.id)) {
3829
+ if (cp.kind === "remote" && cp.status === "pending" && cp.workerGroup) {
3830
+ groups.add(cp.workerGroup);
3831
+ }
3832
+ }
3833
+ }
3834
+ return [
3835
+ ...groups
3836
+ ];
3837
+ }
3838
+ /**
3802
3839
  * Persist a streamed local-step lifecycle event from a remote workflow worker (see
3803
3840
  * {@link WorkflowStepEvent}). A Python `@workflow` runs its `ctx.step`s inline over one turn that
3804
3841
  * can last minutes; the worker streams each step's start/finish so the engine checkpoints it LIVE —
@@ -4762,7 +4799,7 @@ var WorkflowEngine = class {
4762
4799
  */
4763
4800
  async dispatchCompensationAttempt(run, seq, name, def, args, attempt, replay) {
4764
4801
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
4765
- const group = tenantGroup(sanitizeQueueToken(def.name), def.partition);
4802
+ const group = stepGroup(def, run.namespace);
4766
4803
  const enqueuedAt = /* @__PURE__ */ new Date();
4767
4804
  const cp = stepCheckpoint({
4768
4805
  runId: run.id,
@@ -4810,7 +4847,12 @@ var WorkflowEngine = class {
4810
4847
  startStep: /* @__PURE__ */ __name((s) => this.startStep(s), "startStep"),
4811
4848
  completeStep: /* @__PURE__ */ __name((s) => this.completeStep(s), "completeStep"),
4812
4849
  failStep: /* @__PURE__ */ __name((s) => this.failStep(s), "failStep"),
4813
- callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission), "callRemote"),
4850
+ // Route the dispatch by the RUN's tenant (same reason as `startChild` below): the executing
4851
+ // engine may be an operator driving every namespace, so the token must come from the run, not
4852
+ // from this engine. See {@link stepGroup}.
4853
+ callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission, {
4854
+ namespace: parentNamespace
4855
+ }), "callRemote"),
4814
4856
  // Defer so a fast child can't reentrantly resume a still-running parent. Stamp the child with
4815
4857
  // the PARENT run's namespace (not this engine's) so a child belongs to the tenant/partition of
4816
4858
  // the run it was spawned from — even when an operator (namespace: undefined, drives every
@@ -4992,14 +5034,15 @@ var WorkflowEngine = class {
4992
5034
  });
4993
5035
  return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
4994
5036
  }
4995
- async callRemote(runId, seq, step, input, queue, transport, replay, admission) {
5037
+ async callRemote(runId, seq, step, input, queue, transport, replay, admission, routing) {
5038
+ const group = stepGroup(step, routing?.namespace);
4996
5039
  const existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
4997
5040
  if (existing && existing.name !== step.name) {
4998
5041
  throw new NonDeterminismError(runId, seq, step.name, existing.name);
4999
5042
  }
5000
5043
  if (existing?.status === "completed") return existing.output;
5001
5044
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
5002
- if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport);
5045
+ if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport, group);
5003
5046
  if (existing?.status === "pending") {
5004
5047
  if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
5005
5048
  if (existing.wakeAt == null) {
@@ -5033,7 +5076,7 @@ var WorkflowEngine = class {
5033
5076
  seq,
5034
5077
  name: step.name,
5035
5078
  stepId: existing.stepId ?? stepId(runId, seq),
5036
- group: existing.workerGroup ?? tenantGroup(sanitizeQueueToken(step.name), step.partition),
5079
+ group: existing.workerGroup ?? group,
5037
5080
  input: existing.input,
5038
5081
  priority: admission?.priority,
5039
5082
  attempt: reAttempt,
@@ -5057,7 +5100,7 @@ var WorkflowEngine = class {
5057
5100
  if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
5058
5101
  attempt = existing.attempts + 1;
5059
5102
  }
5060
- await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
5103
+ await this.ensureRoutable(group, step.requires);
5061
5104
  const id = stepId(runId, seq);
5062
5105
  if (queue && this.admission.handles(queue)) {
5063
5106
  const decision = await this.admission.tryAdmit(queue, {
@@ -5084,7 +5127,7 @@ var WorkflowEngine = class {
5084
5127
  status: "pending",
5085
5128
  input,
5086
5129
  attempts: attempt,
5087
- workerGroup: tenantGroup(sanitizeQueueToken(step.name), step.partition),
5130
+ workerGroup: group,
5088
5131
  enqueuedAt,
5089
5132
  startedAt: enqueuedAt,
5090
5133
  finishedAt: enqueuedAt
@@ -5094,7 +5137,7 @@ var WorkflowEngine = class {
5094
5137
  seq,
5095
5138
  name: step.name,
5096
5139
  stepId: id,
5097
- group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
5140
+ group,
5098
5141
  input,
5099
5142
  priority: admission?.priority,
5100
5143
  attempt,
@@ -5242,9 +5285,9 @@ var WorkflowEngine = class {
5242
5285
  await this.admission.release(queue, id);
5243
5286
  }
5244
5287
  /** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
5245
- async callRemoteInMemory(runId, seq, step, input, transport) {
5288
+ async callRemoteInMemory(runId, seq, step, input, transport, group = stepGroup(step, void 0)) {
5246
5289
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
5247
- await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
5290
+ await this.ensureRoutable(group, step.requires);
5248
5291
  const validInput = step.input ? step.input.parse(input) : input;
5249
5292
  const id = stepId(runId, seq);
5250
5293
  const enqueuedAt = /* @__PURE__ */ new Date();
@@ -5268,7 +5311,7 @@ var WorkflowEngine = class {
5268
5311
  seq,
5269
5312
  name: step.name,
5270
5313
  stepId: id,
5271
- group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
5314
+ group,
5272
5315
  input: validInput,
5273
5316
  traceparent: this.traceparent?.(),
5274
5317
  context: this.context?.(),
@@ -5287,7 +5330,7 @@ var WorkflowEngine = class {
5287
5330
  output,
5288
5331
  events: resolution.events,
5289
5332
  attempts: attempt,
5290
- workerGroup: tenantGroup(sanitizeQueueToken(step.name), step.partition),
5333
+ workerGroup: group,
5291
5334
  enqueuedAt,
5292
5335
  startedAt
5293
5336
  });
@@ -5615,6 +5658,43 @@ __name(collectMetrics, "collectMetrics");
5615
5658
 
5616
5659
  // src/scheduler.ts
5617
5660
  import { createRequire } from "node:module";
5661
+
5662
+ // src/cron-compat.ts
5663
+ function isIndexable(value) {
5664
+ return typeof value === "object" && value !== null || typeof value === "function";
5665
+ }
5666
+ __name(isIndexable, "isIndexable");
5667
+ function unwrapDefault(module_) {
5668
+ if (!isIndexable(module_)) return module_;
5669
+ if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
5670
+ if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
5671
+ const inner = Reflect.get(module_, "default");
5672
+ return inner === void 0 ? module_ : inner;
5673
+ }
5674
+ __name(unwrapDefault, "unwrapDefault");
5675
+ function v4Entry(module_) {
5676
+ if (!isIndexable(module_)) return void 0;
5677
+ const parse = Reflect.get(module_, "parseExpression");
5678
+ if (typeof parse !== "function") return void 0;
5679
+ return (expr, options) => parse.call(module_, expr, options);
5680
+ }
5681
+ __name(v4Entry, "v4Entry");
5682
+ function v5Entry(module_) {
5683
+ if (!isIndexable(module_)) return void 0;
5684
+ const parser = Reflect.get(module_, "CronExpressionParser");
5685
+ if (!isIndexable(parser)) return void 0;
5686
+ const parse = Reflect.get(parser, "parse");
5687
+ if (typeof parse !== "function") return void 0;
5688
+ return (expr, options) => parse.call(parser, expr, options);
5689
+ }
5690
+ __name(v5Entry, "v5Entry");
5691
+ function resolveCronParse(module_) {
5692
+ const resolved = unwrapDefault(module_);
5693
+ return v4Entry(resolved) ?? v5Entry(resolved);
5694
+ }
5695
+ __name(resolveCronParse, "resolveCronParse");
5696
+
5697
+ // src/scheduler.ts
5618
5698
  var nodeRequire = createRequire(import.meta.url);
5619
5699
  var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
5620
5700
  function scheduledRunId(key, everyMs, nowMs) {
@@ -5623,7 +5703,7 @@ function scheduledRunId(key, everyMs, nowMs) {
5623
5703
  __name(scheduledRunId, "scheduledRunId");
5624
5704
  var cronParser;
5625
5705
  function loadCronParser() {
5626
- if (cronParser) return cronParser;
5706
+ if (cronParser !== void 0) return cronParser;
5627
5707
  try {
5628
5708
  cronParser = nodeRequire("cron-parser");
5629
5709
  } catch {
@@ -5633,12 +5713,14 @@ function loadCronParser() {
5633
5713
  }
5634
5714
  __name(loadCronParser, "loadCronParser");
5635
5715
  function prevCronFireMs(expr, nowMs, timezone = "UTC") {
5636
- const parser = loadCronParser();
5637
- const it = parser.parseExpression(expr, {
5716
+ const parse = resolveCronParse(loadCronParser());
5717
+ if (!parse) {
5718
+ throw new Error(`the installed "cron-parser" exposes neither v4's parseExpression nor v5's CronExpressionParser.parse \u2014 the supported range is ^4.0.0 || ^5.0.0.`);
5719
+ }
5720
+ return parse(expr, {
5638
5721
  currentDate: new Date(nowMs + 1),
5639
5722
  tz: timezone
5640
- });
5641
- return it.prev().toDate().getTime();
5723
+ }).prev().toDate().getTime();
5642
5724
  }
5643
5725
  __name(prevCronFireMs, "prevCronFireMs");
5644
5726
  function scheduleRunIdAt(s, nowMs) {