@dudousxd/nestjs-durable-core 0.29.1 → 0.31.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.cjs +242 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +243 -114
- package/dist/index.d.ts +243 -114
- package/dist/index.js +241 -143
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,161 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
|
+
// src/queue.ts
|
|
5
|
+
var QueueController = class {
|
|
6
|
+
static {
|
|
7
|
+
__name(this, "QueueController");
|
|
8
|
+
}
|
|
9
|
+
config;
|
|
10
|
+
clock;
|
|
11
|
+
inFlight = 0;
|
|
12
|
+
windowStart = 0;
|
|
13
|
+
windowCount = 0;
|
|
14
|
+
/** Registered waiters by id (only used when priority/fairness ordering is in play). */
|
|
15
|
+
waiters = /* @__PURE__ */ new Map();
|
|
16
|
+
/** Monotonic counter stamping each newly-seen waiter for a stable FIFO tiebreak. */
|
|
17
|
+
waiterSeq = 0;
|
|
18
|
+
/** Per-key "last served" order for round-robin fairness — higher = more recently served. */
|
|
19
|
+
keyServed = /* @__PURE__ */ new Map();
|
|
20
|
+
servedTick = 0;
|
|
21
|
+
constructor(config, clock) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.clock = clock;
|
|
24
|
+
}
|
|
25
|
+
/** Whether this controller does any priority/fairness ordering (else it's a plain FIFO gate). */
|
|
26
|
+
get ordered() {
|
|
27
|
+
return this.config.fairness === "key" || this.config.order === "lifo";
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Try to admit one unit of work. On `ok`, the caller has taken a concurrency slot and must call
|
|
31
|
+
* {@link release} when the step settles. On block, `retryAt` is when admission may next succeed.
|
|
32
|
+
*
|
|
33
|
+
* `item` carries optional priority/fairness hints; omitting it is the legacy FIFO path. When
|
|
34
|
+
* ordering is configured (or a priority is given), a blocked call REGISTERS as a waiter and is only
|
|
35
|
+
* admitted on a later retry once it is the rightful next under (priority desc, fairness, FIFO).
|
|
36
|
+
*/
|
|
37
|
+
tryAdmit(item) {
|
|
38
|
+
const now = this.clock();
|
|
39
|
+
const rl = this.config.rateLimit;
|
|
40
|
+
if (rl) {
|
|
41
|
+
if (now - this.windowStart >= rl.periodMs) {
|
|
42
|
+
this.windowStart = now;
|
|
43
|
+
this.windowCount = 0;
|
|
44
|
+
}
|
|
45
|
+
if (this.windowCount >= rl.limit) return {
|
|
46
|
+
ok: false,
|
|
47
|
+
retryAt: this.windowStart + rl.periodMs
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const usingOrder = this.ordered || item?.priority != null || item?.waiterId != null;
|
|
51
|
+
const waiterId = item?.waiterId;
|
|
52
|
+
const full = this.config.concurrency != null && this.inFlight >= this.config.concurrency;
|
|
53
|
+
if (full) {
|
|
54
|
+
if (usingOrder && waiterId != null) this.register(waiterId, item);
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
retryAt: now + (this.config.retryMs ?? 1e3)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (usingOrder && this.waiters.size > 0) {
|
|
61
|
+
if (waiterId != null) this.register(waiterId, item);
|
|
62
|
+
if (!this.isNext(waiterId, item)) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
retryAt: now + (this.config.retryMs ?? 1e3)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (rl) this.windowCount += 1;
|
|
70
|
+
this.inFlight += 1;
|
|
71
|
+
if (waiterId != null) this.waiters.delete(waiterId);
|
|
72
|
+
if (item?.key != null) this.keyServed.set(item.key, ++this.servedTick);
|
|
73
|
+
return {
|
|
74
|
+
ok: true
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Release a concurrency slot taken by a successful {@link tryAdmit}. */
|
|
78
|
+
release() {
|
|
79
|
+
if (this.inFlight > 0) this.inFlight -= 1;
|
|
80
|
+
}
|
|
81
|
+
/** Track (or refresh) a blocked call as a waiter; its FIFO seq is stamped only on first sight. */
|
|
82
|
+
register(waiterId, item) {
|
|
83
|
+
const existing = this.waiters.get(waiterId);
|
|
84
|
+
if (existing) {
|
|
85
|
+
existing.priority = item?.priority ?? 0;
|
|
86
|
+
existing.key = item?.key;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.waiters.set(waiterId, {
|
|
90
|
+
priority: item?.priority ?? 0,
|
|
91
|
+
key: item?.key,
|
|
92
|
+
seq: this.waiterSeq++
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Is the calling item the best candidate among all registered waiters? Ordering, best first:
|
|
97
|
+
* 1. higher `priority`;
|
|
98
|
+
* 2. fairness: with `fairness: 'key'`, the least-recently-served key wins (round-robin);
|
|
99
|
+
* 3. earliest registration (FIFO) — stable tiebreak.
|
|
100
|
+
* A bare item (no waiterId) is compared on its hints directly.
|
|
101
|
+
*/
|
|
102
|
+
isNext(waiterId, item) {
|
|
103
|
+
const self = (waiterId != null ? this.waiters.get(waiterId) : void 0) ?? {
|
|
104
|
+
priority: item?.priority ?? 0,
|
|
105
|
+
key: item?.key,
|
|
106
|
+
seq: Number.POSITIVE_INFINITY
|
|
107
|
+
};
|
|
108
|
+
let best = self;
|
|
109
|
+
for (const w of this.waiters.values()) {
|
|
110
|
+
if (this.better(w, best)) best = w;
|
|
111
|
+
}
|
|
112
|
+
return best === self;
|
|
113
|
+
}
|
|
114
|
+
/** Strict "a should be admitted before b" comparison under the active policy. */
|
|
115
|
+
better(a, b) {
|
|
116
|
+
if (a.priority !== b.priority) return a.priority > b.priority;
|
|
117
|
+
if (this.config.fairness === "key") {
|
|
118
|
+
const sa = this.servedOf(a.key);
|
|
119
|
+
const sb = this.servedOf(b.key);
|
|
120
|
+
if (sa !== sb) return sa < sb;
|
|
121
|
+
}
|
|
122
|
+
return this.config.order === "lifo" ? a.seq > b.seq : a.seq < b.seq;
|
|
123
|
+
}
|
|
124
|
+
/** Last-served tick for a key (0 = never served, so an unseen key is admitted before a busy one). */
|
|
125
|
+
servedOf(key) {
|
|
126
|
+
if (key == null) return 0;
|
|
127
|
+
return this.keyServed.get(key) ?? 0;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/admission.ts
|
|
132
|
+
var InMemoryAdmissionBackend = class {
|
|
133
|
+
static {
|
|
134
|
+
__name(this, "InMemoryAdmissionBackend");
|
|
135
|
+
}
|
|
136
|
+
clock;
|
|
137
|
+
controllers = /* @__PURE__ */ new Map();
|
|
138
|
+
constructor(clock) {
|
|
139
|
+
this.clock = clock;
|
|
140
|
+
}
|
|
141
|
+
register(config) {
|
|
142
|
+
this.controllers.set(config.name, new QueueController(config, this.clock));
|
|
143
|
+
}
|
|
144
|
+
handles(queue) {
|
|
145
|
+
return this.controllers.has(queue);
|
|
146
|
+
}
|
|
147
|
+
async tryAdmit(queue, item) {
|
|
148
|
+
const controller = this.controllers.get(queue);
|
|
149
|
+
if (!controller) return {
|
|
150
|
+
ok: true
|
|
151
|
+
};
|
|
152
|
+
return controller.tryAdmit(item);
|
|
153
|
+
}
|
|
154
|
+
async release(queue, _slotId) {
|
|
155
|
+
this.controllers.get(queue)?.release();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
4
159
|
// src/duration.ts
|
|
5
160
|
var UNIT_MS = {
|
|
6
161
|
ms: 1,
|
|
@@ -517,133 +672,6 @@ function eventMatches(payload, match) {
|
|
|
517
672
|
}
|
|
518
673
|
__name(eventMatches, "eventMatches");
|
|
519
674
|
|
|
520
|
-
// src/queue.ts
|
|
521
|
-
var QueueController = class {
|
|
522
|
-
static {
|
|
523
|
-
__name(this, "QueueController");
|
|
524
|
-
}
|
|
525
|
-
config;
|
|
526
|
-
clock;
|
|
527
|
-
inFlight = 0;
|
|
528
|
-
windowStart = 0;
|
|
529
|
-
windowCount = 0;
|
|
530
|
-
/** Registered waiters by id (only used when priority/fairness ordering is in play). */
|
|
531
|
-
waiters = /* @__PURE__ */ new Map();
|
|
532
|
-
/** Monotonic counter stamping each newly-seen waiter for a stable FIFO tiebreak. */
|
|
533
|
-
waiterSeq = 0;
|
|
534
|
-
/** Per-key "last served" order for round-robin fairness — higher = more recently served. */
|
|
535
|
-
keyServed = /* @__PURE__ */ new Map();
|
|
536
|
-
servedTick = 0;
|
|
537
|
-
constructor(config, clock) {
|
|
538
|
-
this.config = config;
|
|
539
|
-
this.clock = clock;
|
|
540
|
-
}
|
|
541
|
-
/** Whether this controller does any priority/fairness ordering (else it's a plain FIFO gate). */
|
|
542
|
-
get ordered() {
|
|
543
|
-
return this.config.fairness === "key";
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* Try to admit one unit of work. On `ok`, the caller has taken a concurrency slot and must call
|
|
547
|
-
* {@link release} when the step settles. On block, `retryAt` is when admission may next succeed.
|
|
548
|
-
*
|
|
549
|
-
* `item` carries optional priority/fairness hints; omitting it is the legacy FIFO path. When
|
|
550
|
-
* ordering is configured (or a priority is given), a blocked call REGISTERS as a waiter and is only
|
|
551
|
-
* admitted on a later retry once it is the rightful next under (priority desc, fairness, FIFO).
|
|
552
|
-
*/
|
|
553
|
-
tryAdmit(item) {
|
|
554
|
-
const now = this.clock();
|
|
555
|
-
const rl = this.config.rateLimit;
|
|
556
|
-
if (rl) {
|
|
557
|
-
if (now - this.windowStart >= rl.periodMs) {
|
|
558
|
-
this.windowStart = now;
|
|
559
|
-
this.windowCount = 0;
|
|
560
|
-
}
|
|
561
|
-
if (this.windowCount >= rl.limit) return {
|
|
562
|
-
ok: false,
|
|
563
|
-
retryAt: this.windowStart + rl.periodMs
|
|
564
|
-
};
|
|
565
|
-
}
|
|
566
|
-
const usingOrder = this.ordered || item?.priority != null || item?.waiterId != null;
|
|
567
|
-
const waiterId = item?.waiterId;
|
|
568
|
-
const full = this.config.concurrency != null && this.inFlight >= this.config.concurrency;
|
|
569
|
-
if (full) {
|
|
570
|
-
if (usingOrder && waiterId != null) this.register(waiterId, item);
|
|
571
|
-
return {
|
|
572
|
-
ok: false,
|
|
573
|
-
retryAt: now + (this.config.retryMs ?? 1e3)
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
if (usingOrder && this.waiters.size > 0) {
|
|
577
|
-
if (waiterId != null) this.register(waiterId, item);
|
|
578
|
-
if (!this.isNext(waiterId, item)) {
|
|
579
|
-
return {
|
|
580
|
-
ok: false,
|
|
581
|
-
retryAt: now + (this.config.retryMs ?? 1e3)
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
if (rl) this.windowCount += 1;
|
|
586
|
-
this.inFlight += 1;
|
|
587
|
-
if (waiterId != null) this.waiters.delete(waiterId);
|
|
588
|
-
if (item?.key != null) this.keyServed.set(item.key, ++this.servedTick);
|
|
589
|
-
return {
|
|
590
|
-
ok: true
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
/** Release a concurrency slot taken by a successful {@link tryAdmit}. */
|
|
594
|
-
release() {
|
|
595
|
-
if (this.inFlight > 0) this.inFlight -= 1;
|
|
596
|
-
}
|
|
597
|
-
/** Track (or refresh) a blocked call as a waiter; its FIFO seq is stamped only on first sight. */
|
|
598
|
-
register(waiterId, item) {
|
|
599
|
-
const existing = this.waiters.get(waiterId);
|
|
600
|
-
if (existing) {
|
|
601
|
-
existing.priority = item?.priority ?? 0;
|
|
602
|
-
existing.key = item?.key;
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
this.waiters.set(waiterId, {
|
|
606
|
-
priority: item?.priority ?? 0,
|
|
607
|
-
key: item?.key,
|
|
608
|
-
seq: this.waiterSeq++
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Is the calling item the best candidate among all registered waiters? Ordering, best first:
|
|
613
|
-
* 1. higher `priority`;
|
|
614
|
-
* 2. fairness: with `fairness: 'key'`, the least-recently-served key wins (round-robin);
|
|
615
|
-
* 3. earliest registration (FIFO) — stable tiebreak.
|
|
616
|
-
* A bare item (no waiterId) is compared on its hints directly.
|
|
617
|
-
*/
|
|
618
|
-
isNext(waiterId, item) {
|
|
619
|
-
const self = (waiterId != null ? this.waiters.get(waiterId) : void 0) ?? {
|
|
620
|
-
priority: item?.priority ?? 0,
|
|
621
|
-
key: item?.key,
|
|
622
|
-
seq: Number.POSITIVE_INFINITY
|
|
623
|
-
};
|
|
624
|
-
let best = self;
|
|
625
|
-
for (const w of this.waiters.values()) {
|
|
626
|
-
if (this.better(w, best)) best = w;
|
|
627
|
-
}
|
|
628
|
-
return best === self;
|
|
629
|
-
}
|
|
630
|
-
/** Strict "a should be admitted before b" comparison under the active policy. */
|
|
631
|
-
better(a, b) {
|
|
632
|
-
if (a.priority !== b.priority) return a.priority > b.priority;
|
|
633
|
-
if (this.config.fairness === "key") {
|
|
634
|
-
const sa = this.servedOf(a.key);
|
|
635
|
-
const sb = this.servedOf(b.key);
|
|
636
|
-
if (sa !== sb) return sa < sb;
|
|
637
|
-
}
|
|
638
|
-
return a.seq < b.seq;
|
|
639
|
-
}
|
|
640
|
-
/** Last-served tick for a key (0 = never served, so an unseen key is admitted before a busy one). */
|
|
641
|
-
servedOf(key) {
|
|
642
|
-
if (key == null) return 0;
|
|
643
|
-
return this.keyServed.get(key) ?? 0;
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
|
|
647
675
|
// src/singleton-gate.ts
|
|
648
676
|
var SINGLETON_RETRY_MS = 1e3;
|
|
649
677
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -837,6 +865,12 @@ function workflowName(ref) {
|
|
|
837
865
|
__name(workflowName, "workflowName");
|
|
838
866
|
|
|
839
867
|
// src/workflow-ctx.ts
|
|
868
|
+
function normalizeChildOptions(options) {
|
|
869
|
+
return typeof options === "string" ? {
|
|
870
|
+
childId: options
|
|
871
|
+
} : options ?? {};
|
|
872
|
+
}
|
|
873
|
+
__name(normalizeChildOptions, "normalizeChildOptions");
|
|
840
874
|
var Position = class Position2 {
|
|
841
875
|
static {
|
|
842
876
|
__name(this, "Position");
|
|
@@ -1086,7 +1120,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1086
1120
|
await step(`task:dispatch:${name}`, dispatch, options);
|
|
1087
1121
|
return unwrapCompletion(await waitForSignal(`task:${runId}:${name}`), `task "${name}"`);
|
|
1088
1122
|
}, "task");
|
|
1089
|
-
const child = /* @__PURE__ */ __name(async (workflow2, input,
|
|
1123
|
+
const child = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
1124
|
+
const { childId, priority } = normalizeChildOptions(options);
|
|
1090
1125
|
const current = pos.next();
|
|
1091
1126
|
const id = childId ?? `${runId}.child.${current}`;
|
|
1092
1127
|
const existing = await readCheckpoint(current);
|
|
@@ -1098,7 +1133,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1098
1133
|
runId,
|
|
1099
1134
|
seq: current
|
|
1100
1135
|
});
|
|
1101
|
-
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id);
|
|
1136
|
+
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
|
|
1102
1137
|
if (!existing) {
|
|
1103
1138
|
await writeCheckpoint(instantCheckpoint({
|
|
1104
1139
|
runId,
|
|
@@ -1110,12 +1145,13 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1110
1145
|
}
|
|
1111
1146
|
throw new WorkflowSuspended();
|
|
1112
1147
|
}, "child");
|
|
1113
|
-
const startChild = /* @__PURE__ */ __name(async (workflow2, input,
|
|
1148
|
+
const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
1149
|
+
const { childId, priority } = normalizeChildOptions(options);
|
|
1114
1150
|
const current = pos.next();
|
|
1115
1151
|
const id = childId ?? `${runId}.child.${current}`;
|
|
1116
1152
|
const existing = await readCheckpoint(current);
|
|
1117
1153
|
if (existing && existing.status === "completed") return existing.output;
|
|
1118
|
-
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id);
|
|
1154
|
+
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
|
|
1119
1155
|
await writeCheckpoint(instantCheckpoint({
|
|
1120
1156
|
runId,
|
|
1121
1157
|
seq: current,
|
|
@@ -1339,8 +1375,10 @@ var WorkflowEngine = class {
|
|
|
1339
1375
|
updateValidators = /* @__PURE__ */ new Map();
|
|
1340
1376
|
/** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
|
|
1341
1377
|
cancelRequested = /* @__PURE__ */ new Set();
|
|
1342
|
-
/** Flow-control
|
|
1343
|
-
|
|
1378
|
+
/** Flow-control admission backend for remote steps (see {@link registerQueue}). */
|
|
1379
|
+
admission;
|
|
1380
|
+
/** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
|
|
1381
|
+
queueWaiters = /* @__PURE__ */ new Map();
|
|
1344
1382
|
/** Which queue a dispatched step took a slot from, by stepId — so the result can release it. */
|
|
1345
1383
|
stepQueue = /* @__PURE__ */ new Map();
|
|
1346
1384
|
/** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
|
|
@@ -1356,6 +1394,8 @@ var WorkflowEngine = class {
|
|
|
1356
1394
|
] : []));
|
|
1357
1395
|
this.controlPlane = deps.controlPlane;
|
|
1358
1396
|
this.clock = deps.clock ?? Date.now;
|
|
1397
|
+
this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
|
|
1398
|
+
this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
|
|
1359
1399
|
this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
|
|
1360
1400
|
this.leaseMs = deps.leaseMs ?? 3e4;
|
|
1361
1401
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
@@ -1519,7 +1559,7 @@ var WorkflowEngine = class {
|
|
|
1519
1559
|
* durable. Per engine instance (see {@link QueueConfig}). Registering the same name replaces it.
|
|
1520
1560
|
*/
|
|
1521
1561
|
registerQueue(config) {
|
|
1522
|
-
this.
|
|
1562
|
+
this.admission.register(config);
|
|
1523
1563
|
}
|
|
1524
1564
|
/** Subscribe to lifecycle events. Returns an unsubscribe function. */
|
|
1525
1565
|
subscribe(listener) {
|
|
@@ -1640,6 +1680,7 @@ var WorkflowEngine = class {
|
|
|
1640
1680
|
input,
|
|
1641
1681
|
tags,
|
|
1642
1682
|
searchAttributes: opts?.searchAttributes,
|
|
1683
|
+
priority: opts?.priority,
|
|
1643
1684
|
createdAt: now,
|
|
1644
1685
|
updatedAt: now
|
|
1645
1686
|
};
|
|
@@ -2115,6 +2156,26 @@ var WorkflowEngine = class {
|
|
|
2115
2156
|
return results;
|
|
2116
2157
|
}
|
|
2117
2158
|
/**
|
|
2159
|
+
* Hard-delete a run and its entire subtree. Unlike {@link cancel} (which marks a run `cancelled`
|
|
2160
|
+
* but keeps it as history), delete REMOVES the run and all its rows (checkpoints, signal waiters,
|
|
2161
|
+
* search-attribute rows) — it vanishes from {@link getRun} and {@link listRuns}. Use it to purge a
|
|
2162
|
+
* finished run whose data is being deleted (e.g. a pipeline run whose result rows are cleared).
|
|
2163
|
+
*
|
|
2164
|
+
* Cascades depth-first via {@link getRunChildren} so children are gone before the parent (no
|
|
2165
|
+
* orphaned child runs). Prefer {@link cancel} first for a live run — deleting one mid-flight orphans
|
|
2166
|
+
* its worker (its next checkpoint write fails). Returns the number of runs deleted (0 if absent).
|
|
2167
|
+
*/
|
|
2168
|
+
async deleteRun(runId) {
|
|
2169
|
+
const run = await this.store.getRun(runId);
|
|
2170
|
+
if (!run) return 0;
|
|
2171
|
+
let deleted = 0;
|
|
2172
|
+
for (const childId of await this.getRunChildren(runId)) {
|
|
2173
|
+
deleted += await this.deleteRun(childId);
|
|
2174
|
+
}
|
|
2175
|
+
await this.store.deleteRun(runId);
|
|
2176
|
+
return deleted + 1;
|
|
2177
|
+
}
|
|
2178
|
+
/**
|
|
2118
2179
|
* Cascade cancellation to a run's children — both awaited (`ctx.child`, found via its live
|
|
2119
2180
|
* `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
|
|
2120
2181
|
* checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
|
|
@@ -2846,8 +2907,10 @@ var WorkflowEngine = class {
|
|
|
2846
2907
|
failStep: /* @__PURE__ */ __name((s) => this.failStep(s), "failStep"),
|
|
2847
2908
|
callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission), "callRemote"),
|
|
2848
2909
|
// Defer so a fast child can't reentrantly resume a still-running parent.
|
|
2849
|
-
startChild: /* @__PURE__ */ __name((workflow, input, id) => {
|
|
2850
|
-
queueMicrotask(() => void this.start(workflow, input, id
|
|
2910
|
+
startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
|
|
2911
|
+
queueMicrotask(() => void this.start(workflow, input, id, {
|
|
2912
|
+
priority
|
|
2913
|
+
}).catch(() => void 0));
|
|
2851
2914
|
}, "startChild"),
|
|
2852
2915
|
signalEntity: /* @__PURE__ */ __name((name, key, op, arg, reply) => {
|
|
2853
2916
|
queueMicrotask(() => void this.entities.dispatch(name, key, op, arg, reply).catch(() => void 0));
|
|
@@ -2948,14 +3011,19 @@ var WorkflowEngine = class {
|
|
|
2948
3011
|
attempt = existing.attempts + 1;
|
|
2949
3012
|
}
|
|
2950
3013
|
const id = stepId(runId, seq);
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
const decision = controller.tryAdmit({
|
|
3014
|
+
if (queue && this.admission.handles(queue)) {
|
|
3015
|
+
const decision = await this.admission.tryAdmit(queue, {
|
|
2954
3016
|
priority: admission?.priority,
|
|
2955
3017
|
key: admission?.fairnessKey ?? runId,
|
|
2956
3018
|
waiterId: id
|
|
2957
3019
|
});
|
|
2958
|
-
if (!decision.ok)
|
|
3020
|
+
if (!decision.ok) {
|
|
3021
|
+
const waiters = this.queueWaiters.get(queue) ?? /* @__PURE__ */ new Set();
|
|
3022
|
+
waiters.add(runId);
|
|
3023
|
+
this.queueWaiters.set(queue, waiters);
|
|
3024
|
+
throw new WorkflowSuspended(decision.retryAt);
|
|
3025
|
+
}
|
|
3026
|
+
this.queueWaiters.get(queue)?.delete(runId);
|
|
2959
3027
|
this.stepQueue.set(id, queue);
|
|
2960
3028
|
}
|
|
2961
3029
|
const validInput = step.input.parse(input);
|
|
@@ -2983,6 +3051,7 @@ var WorkflowEngine = class {
|
|
|
2983
3051
|
input: validInput,
|
|
2984
3052
|
traceparent: this.traceparent?.(),
|
|
2985
3053
|
context: this.context?.(),
|
|
3054
|
+
priority: admission?.priority,
|
|
2986
3055
|
attempt
|
|
2987
3056
|
}, transport);
|
|
2988
3057
|
this.emit({
|
|
@@ -3002,7 +3071,7 @@ var WorkflowEngine = class {
|
|
|
3002
3071
|
async completeRemoteResult(result) {
|
|
3003
3072
|
const cp = await this.store.getCheckpoint(result.runId, result.seq);
|
|
3004
3073
|
if (!cp || cp.status !== "pending") return;
|
|
3005
|
-
this.releaseQueueSlot(cp.stepId);
|
|
3074
|
+
void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
|
|
3006
3075
|
const run = await this.store.getRun(result.runId);
|
|
3007
3076
|
if (run && (run.status === "cancelled" || run.status === "completed")) return;
|
|
3008
3077
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
@@ -3029,12 +3098,26 @@ var WorkflowEngine = class {
|
|
|
3029
3098
|
});
|
|
3030
3099
|
await this.resume(result.runId);
|
|
3031
3100
|
}
|
|
3101
|
+
/**
|
|
3102
|
+
* A slot freed on `queue` (a fleet-wide signal): resume this instance's runs blocked on it so they
|
|
3103
|
+
* re-contend now instead of at their retry tick. Snapshot-and-clear — a run still blocked after the
|
|
3104
|
+
* retry re-registers itself, and one that's gone (cancelled/admitted) is dropped. Best-effort.
|
|
3105
|
+
*/
|
|
3106
|
+
wakeQueueWaiters(queue) {
|
|
3107
|
+
const waiters = this.queueWaiters.get(queue);
|
|
3108
|
+
if (!waiters || waiters.size === 0) return;
|
|
3109
|
+
const runIds = [
|
|
3110
|
+
...waiters
|
|
3111
|
+
];
|
|
3112
|
+
waiters.clear();
|
|
3113
|
+
for (const runId of runIds) void this.resume(runId).catch(() => void 0);
|
|
3114
|
+
}
|
|
3032
3115
|
/** Release the flow-control slot a dispatched step held (if any), by its stepId. */
|
|
3033
|
-
releaseQueueSlot(id) {
|
|
3116
|
+
async releaseQueueSlot(id) {
|
|
3034
3117
|
const queue = this.stepQueue.get(id);
|
|
3035
3118
|
if (queue === void 0) return;
|
|
3036
3119
|
this.stepQueue.delete(id);
|
|
3037
|
-
this.
|
|
3120
|
+
await this.admission.release(queue, id);
|
|
3038
3121
|
}
|
|
3039
3122
|
/** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
|
|
3040
3123
|
async callRemoteInMemory(runId, seq, step, input, transport) {
|
|
@@ -3195,6 +3278,7 @@ var RemoteWorkflowExecutor = class {
|
|
|
3195
3278
|
input: run.input,
|
|
3196
3279
|
history,
|
|
3197
3280
|
group: this.group,
|
|
3281
|
+
priority: run.priority,
|
|
3198
3282
|
attempt: 1
|
|
3199
3283
|
};
|
|
3200
3284
|
const decision = new Promise((resolve, reject) => {
|
|
@@ -3274,6 +3358,9 @@ var CodecStateStore = class {
|
|
|
3274
3358
|
async getRun(runId) {
|
|
3275
3359
|
return this.decRun(await this.inner.getRun(runId));
|
|
3276
3360
|
}
|
|
3361
|
+
deleteRun(runId) {
|
|
3362
|
+
return this.inner.deleteRun(runId);
|
|
3363
|
+
}
|
|
3277
3364
|
async getCheckpoint(runId, seq) {
|
|
3278
3365
|
return this.decCp(await this.inner.getCheckpoint(runId, seq));
|
|
3279
3366
|
}
|
|
@@ -3668,6 +3755,16 @@ var InMemoryStateStore = class {
|
|
|
3668
3755
|
...run
|
|
3669
3756
|
} : null;
|
|
3670
3757
|
}
|
|
3758
|
+
async deleteRun(runId) {
|
|
3759
|
+
this.runs.delete(runId);
|
|
3760
|
+
for (const [key, cp] of this.checkpoints) {
|
|
3761
|
+
if (cp.runId === runId) this.checkpoints.delete(key);
|
|
3762
|
+
}
|
|
3763
|
+
for (const [token, waiter] of this.signalWaiters) {
|
|
3764
|
+
if (waiter.runId === runId) this.signalWaiters.delete(token);
|
|
3765
|
+
}
|
|
3766
|
+
this.reindexAttributes(runId, void 0);
|
|
3767
|
+
}
|
|
3671
3768
|
async getCheckpoint(runId, seq) {
|
|
3672
3769
|
const cp = this.checkpoints.get(this.key(runId, seq));
|
|
3673
3770
|
return cp ? {
|
|
@@ -3913,6 +4010,7 @@ export {
|
|
|
3913
4010
|
Entities,
|
|
3914
4011
|
EventAccumulators,
|
|
3915
4012
|
FatalError,
|
|
4013
|
+
InMemoryAdmissionBackend,
|
|
3916
4014
|
InMemoryStateStore,
|
|
3917
4015
|
InMemoryTransport,
|
|
3918
4016
|
NonDeterminismError,
|