@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.cjs CHANGED
@@ -2396,6 +2396,10 @@ __name(mergeTags, "mergeTags");
2396
2396
  var BREAKPOINT = "breakpoint";
2397
2397
  var isBreakpoint = /* @__PURE__ */ __name((cp) => cp.status === "pending" && cp.name.startsWith(BREAKPOINT), "isBreakpoint");
2398
2398
  var versionKey = /* @__PURE__ */ __name((name, version) => `${name}@${version}`, "versionKey");
2399
+ function stepGroup(step, runNamespace) {
2400
+ return tenantGroup(sanitizeQueueToken(step.name), step.partition ?? runNamespace);
2401
+ }
2402
+ __name(stepGroup, "stepGroup");
2399
2403
  function nextContinuationId(runId) {
2400
2404
  const m = runId.match(/^(.*)~(\d+)$/);
2401
2405
  return m ? `${m[1]}~${Number(m[2]) + 1}` : `${runId}~1`;
@@ -3896,7 +3900,8 @@ var WorkflowEngine = class {
3896
3900
  async workerHealth(extra = []) {
3897
3901
  const groups = /* @__PURE__ */ new Set([
3898
3902
  ...this.knownGroups(extra),
3899
- ...await this.pool.listWorkerGroups()
3903
+ ...await this.pool.listWorkerGroups(),
3904
+ ...await this.inflightStepGroups()
3900
3905
  ]);
3901
3906
  const workflowTokens = /* @__PURE__ */ new Set();
3902
3907
  for (const def of this.workflows.values()) {
@@ -3917,6 +3922,38 @@ var WorkflowEngine = class {
3917
3922
  return out;
3918
3923
  }
3919
3924
  /**
3925
+ * The routing groups of every in-flight run's still-`pending` remote step — the queues where work is
3926
+ * dispatched and awaiting a worker. Folded into {@link workerHealth} so a step waiting on a group
3927
+ * that is NEITHER a registration NOR heartbeating (the exact shape of a run stuck on an offline
3928
+ * tenant pool) still reports its backlog: without this the health scan can't "see" that queue, and
3929
+ * the dashboard shows the run as running instead of no-worker. Only the BullMQ transport introspects
3930
+ * health, so on any other transport this is harmless (the groups just resolve to no `groupHealth`).
3931
+ *
3932
+ * Cost: one `listRuns` over the non-terminal runs plus a checkpoint read each — a diagnostic-path
3933
+ * scan, not a hot path. Scoped to `running`/`suspended`/`blocked` (a `pending` run has dispatched no
3934
+ * step yet) and deduped to distinct groups.
3935
+ */
3936
+ async inflightStepGroups() {
3937
+ const inflight = await this.store.listRuns({
3938
+ statuses: [
3939
+ "running",
3940
+ "suspended",
3941
+ "blocked"
3942
+ ]
3943
+ });
3944
+ const groups = /* @__PURE__ */ new Set();
3945
+ for (const run of inflight) {
3946
+ for (const cp of await this.store.listCheckpoints(run.id)) {
3947
+ if (cp.kind === "remote" && cp.status === "pending" && cp.workerGroup) {
3948
+ groups.add(cp.workerGroup);
3949
+ }
3950
+ }
3951
+ }
3952
+ return [
3953
+ ...groups
3954
+ ];
3955
+ }
3956
+ /**
3920
3957
  * Persist a streamed local-step lifecycle event from a remote workflow worker (see
3921
3958
  * {@link WorkflowStepEvent}). A Python `@workflow` runs its `ctx.step`s inline over one turn that
3922
3959
  * can last minutes; the worker streams each step's start/finish so the engine checkpoints it LIVE —
@@ -4880,7 +4917,7 @@ var WorkflowEngine = class {
4880
4917
  */
4881
4918
  async dispatchCompensationAttempt(run, seq, name, def, args, attempt, replay) {
4882
4919
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
4883
- const group = tenantGroup(sanitizeQueueToken(def.name), def.partition);
4920
+ const group = stepGroup(def, run.namespace);
4884
4921
  const enqueuedAt = /* @__PURE__ */ new Date();
4885
4922
  const cp = stepCheckpoint({
4886
4923
  runId: run.id,
@@ -4928,7 +4965,12 @@ var WorkflowEngine = class {
4928
4965
  startStep: /* @__PURE__ */ __name((s) => this.startStep(s), "startStep"),
4929
4966
  completeStep: /* @__PURE__ */ __name((s) => this.completeStep(s), "completeStep"),
4930
4967
  failStep: /* @__PURE__ */ __name((s) => this.failStep(s), "failStep"),
4931
- callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission), "callRemote"),
4968
+ // Route the dispatch by the RUN's tenant (same reason as `startChild` below): the executing
4969
+ // engine may be an operator driving every namespace, so the token must come from the run, not
4970
+ // from this engine. See {@link stepGroup}.
4971
+ callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission, {
4972
+ namespace: parentNamespace
4973
+ }), "callRemote"),
4932
4974
  // Defer so a fast child can't reentrantly resume a still-running parent. Stamp the child with
4933
4975
  // the PARENT run's namespace (not this engine's) so a child belongs to the tenant/partition of
4934
4976
  // the run it was spawned from — even when an operator (namespace: undefined, drives every
@@ -5110,14 +5152,15 @@ var WorkflowEngine = class {
5110
5152
  });
5111
5153
  return blocked.filter((r) => (this.namespace === void 0 || r.namespace === void 0 || r.namespace === this.namespace) && r.wakeAt !== void 0 && r.wakeAt <= nowMs);
5112
5154
  }
5113
- async callRemote(runId, seq, step, input, queue, transport, replay, admission) {
5155
+ async callRemote(runId, seq, step, input, queue, transport, replay, admission, routing) {
5156
+ const group = stepGroup(step, routing?.namespace);
5114
5157
  const existing = replay?.get(seq) ?? await this.store.getCheckpoint(runId, seq);
5115
5158
  if (existing && existing.name !== step.name) {
5116
5159
  throw new NonDeterminismError(runId, seq, step.name, existing.name);
5117
5160
  }
5118
5161
  if (existing?.status === "completed") return existing.output;
5119
5162
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
5120
- if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport);
5163
+ if (step.timeoutMs) return this.callRemoteInMemory(runId, seq, step, input, transport, group);
5121
5164
  if (existing?.status === "pending") {
5122
5165
  if (this.remoteRedispatchMs == null) throw new WorkflowSuspended();
5123
5166
  if (existing.wakeAt == null) {
@@ -5151,7 +5194,7 @@ var WorkflowEngine = class {
5151
5194
  seq,
5152
5195
  name: step.name,
5153
5196
  stepId: existing.stepId ?? stepId(runId, seq),
5154
- group: existing.workerGroup ?? tenantGroup(sanitizeQueueToken(step.name), step.partition),
5197
+ group: existing.workerGroup ?? group,
5155
5198
  input: existing.input,
5156
5199
  priority: admission?.priority,
5157
5200
  attempt: reAttempt,
@@ -5175,7 +5218,7 @@ var WorkflowEngine = class {
5175
5218
  if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
5176
5219
  attempt = existing.attempts + 1;
5177
5220
  }
5178
- await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
5221
+ await this.ensureRoutable(group, step.requires);
5179
5222
  const id = stepId(runId, seq);
5180
5223
  if (queue && this.admission.handles(queue)) {
5181
5224
  const decision = await this.admission.tryAdmit(queue, {
@@ -5202,7 +5245,7 @@ var WorkflowEngine = class {
5202
5245
  status: "pending",
5203
5246
  input,
5204
5247
  attempts: attempt,
5205
- workerGroup: tenantGroup(sanitizeQueueToken(step.name), step.partition),
5248
+ workerGroup: group,
5206
5249
  enqueuedAt,
5207
5250
  startedAt: enqueuedAt,
5208
5251
  finishedAt: enqueuedAt
@@ -5212,7 +5255,7 @@ var WorkflowEngine = class {
5212
5255
  seq,
5213
5256
  name: step.name,
5214
5257
  stepId: id,
5215
- group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
5258
+ group,
5216
5259
  input,
5217
5260
  priority: admission?.priority,
5218
5261
  attempt,
@@ -5360,9 +5403,9 @@ var WorkflowEngine = class {
5360
5403
  await this.admission.release(queue, id);
5361
5404
  }
5362
5405
  /** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
5363
- async callRemoteInMemory(runId, seq, step, input, transport) {
5406
+ async callRemoteInMemory(runId, seq, step, input, transport, group = stepGroup(step, void 0)) {
5364
5407
  if (this.pool.size === 0) throw new Error("remote steps require a Transport");
5365
- await this.ensureRoutable(tenantGroup(sanitizeQueueToken(step.name), step.partition), step.requires);
5408
+ await this.ensureRoutable(group, step.requires);
5366
5409
  const validInput = step.input ? step.input.parse(input) : input;
5367
5410
  const id = stepId(runId, seq);
5368
5411
  const enqueuedAt = /* @__PURE__ */ new Date();
@@ -5386,7 +5429,7 @@ var WorkflowEngine = class {
5386
5429
  seq,
5387
5430
  name: step.name,
5388
5431
  stepId: id,
5389
- group: tenantGroup(sanitizeQueueToken(step.name), step.partition),
5432
+ group,
5390
5433
  input: validInput,
5391
5434
  traceparent: this.traceparent?.(),
5392
5435
  context: this.context?.(),
@@ -5405,7 +5448,7 @@ var WorkflowEngine = class {
5405
5448
  output,
5406
5449
  events: resolution.events,
5407
5450
  attempts: attempt,
5408
- workerGroup: tenantGroup(sanitizeQueueToken(step.name), step.partition),
5451
+ workerGroup: group,
5409
5452
  enqueuedAt,
5410
5453
  startedAt
5411
5454
  });
@@ -5733,6 +5776,43 @@ __name(collectMetrics, "collectMetrics");
5733
5776
 
5734
5777
  // src/scheduler.ts
5735
5778
  var import_node_module = require("module");
5779
+
5780
+ // src/cron-compat.ts
5781
+ function isIndexable(value) {
5782
+ return typeof value === "object" && value !== null || typeof value === "function";
5783
+ }
5784
+ __name(isIndexable, "isIndexable");
5785
+ function unwrapDefault(module_) {
5786
+ if (!isIndexable(module_)) return module_;
5787
+ if (Reflect.get(module_, "parseExpression") !== void 0) return module_;
5788
+ if (Reflect.get(module_, "CronExpressionParser") !== void 0) return module_;
5789
+ const inner = Reflect.get(module_, "default");
5790
+ return inner === void 0 ? module_ : inner;
5791
+ }
5792
+ __name(unwrapDefault, "unwrapDefault");
5793
+ function v4Entry(module_) {
5794
+ if (!isIndexable(module_)) return void 0;
5795
+ const parse = Reflect.get(module_, "parseExpression");
5796
+ if (typeof parse !== "function") return void 0;
5797
+ return (expr, options) => parse.call(module_, expr, options);
5798
+ }
5799
+ __name(v4Entry, "v4Entry");
5800
+ function v5Entry(module_) {
5801
+ if (!isIndexable(module_)) return void 0;
5802
+ const parser = Reflect.get(module_, "CronExpressionParser");
5803
+ if (!isIndexable(parser)) return void 0;
5804
+ const parse = Reflect.get(parser, "parse");
5805
+ if (typeof parse !== "function") return void 0;
5806
+ return (expr, options) => parse.call(parser, expr, options);
5807
+ }
5808
+ __name(v5Entry, "v5Entry");
5809
+ function resolveCronParse(module_) {
5810
+ const resolved = unwrapDefault(module_);
5811
+ return v4Entry(resolved) ?? v5Entry(resolved);
5812
+ }
5813
+ __name(resolveCronParse, "resolveCronParse");
5814
+
5815
+ // src/scheduler.ts
5736
5816
  var nodeRequire = (0, import_node_module.createRequire)(__importMetaUrl);
5737
5817
  var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
5738
5818
  function scheduledRunId(key, everyMs, nowMs) {
@@ -5741,7 +5821,7 @@ function scheduledRunId(key, everyMs, nowMs) {
5741
5821
  __name(scheduledRunId, "scheduledRunId");
5742
5822
  var cronParser;
5743
5823
  function loadCronParser() {
5744
- if (cronParser) return cronParser;
5824
+ if (cronParser !== void 0) return cronParser;
5745
5825
  try {
5746
5826
  cronParser = nodeRequire("cron-parser");
5747
5827
  } catch {
@@ -5751,12 +5831,14 @@ function loadCronParser() {
5751
5831
  }
5752
5832
  __name(loadCronParser, "loadCronParser");
5753
5833
  function prevCronFireMs(expr, nowMs, timezone = "UTC") {
5754
- const parser = loadCronParser();
5755
- const it = parser.parseExpression(expr, {
5834
+ const parse = resolveCronParse(loadCronParser());
5835
+ if (!parse) {
5836
+ 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.`);
5837
+ }
5838
+ return parse(expr, {
5756
5839
  currentDate: new Date(nowMs + 1),
5757
5840
  tz: timezone
5758
- });
5759
- return it.prev().toDate().getTime();
5841
+ }).prev().toDate().getTime();
5760
5842
  }
5761
5843
  __name(prevCronFireMs, "prevCronFireMs");
5762
5844
  function scheduleRunIdAt(s, nowMs) {