@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 +100 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +100 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
-
|
|
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 ??
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
5637
|
-
|
|
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) {
|