@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.cjs
CHANGED
|
@@ -28,6 +28,7 @@ __export(index_exports, {
|
|
|
28
28
|
Entities: () => Entities,
|
|
29
29
|
EventAccumulators: () => EventAccumulators,
|
|
30
30
|
FatalError: () => FatalError,
|
|
31
|
+
InMemoryAdmissionBackend: () => InMemoryAdmissionBackend,
|
|
31
32
|
InMemoryStateStore: () => InMemoryStateStore,
|
|
32
33
|
InMemoryTransport: () => InMemoryTransport,
|
|
33
34
|
NonDeterminismError: () => NonDeterminismError,
|
|
@@ -65,6 +66,161 @@ __export(index_exports, {
|
|
|
65
66
|
});
|
|
66
67
|
module.exports = __toCommonJS(index_exports);
|
|
67
68
|
|
|
69
|
+
// src/queue.ts
|
|
70
|
+
var QueueController = class {
|
|
71
|
+
static {
|
|
72
|
+
__name(this, "QueueController");
|
|
73
|
+
}
|
|
74
|
+
config;
|
|
75
|
+
clock;
|
|
76
|
+
inFlight = 0;
|
|
77
|
+
windowStart = 0;
|
|
78
|
+
windowCount = 0;
|
|
79
|
+
/** Registered waiters by id (only used when priority/fairness ordering is in play). */
|
|
80
|
+
waiters = /* @__PURE__ */ new Map();
|
|
81
|
+
/** Monotonic counter stamping each newly-seen waiter for a stable FIFO tiebreak. */
|
|
82
|
+
waiterSeq = 0;
|
|
83
|
+
/** Per-key "last served" order for round-robin fairness — higher = more recently served. */
|
|
84
|
+
keyServed = /* @__PURE__ */ new Map();
|
|
85
|
+
servedTick = 0;
|
|
86
|
+
constructor(config, clock) {
|
|
87
|
+
this.config = config;
|
|
88
|
+
this.clock = clock;
|
|
89
|
+
}
|
|
90
|
+
/** Whether this controller does any priority/fairness ordering (else it's a plain FIFO gate). */
|
|
91
|
+
get ordered() {
|
|
92
|
+
return this.config.fairness === "key" || this.config.order === "lifo";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Try to admit one unit of work. On `ok`, the caller has taken a concurrency slot and must call
|
|
96
|
+
* {@link release} when the step settles. On block, `retryAt` is when admission may next succeed.
|
|
97
|
+
*
|
|
98
|
+
* `item` carries optional priority/fairness hints; omitting it is the legacy FIFO path. When
|
|
99
|
+
* ordering is configured (or a priority is given), a blocked call REGISTERS as a waiter and is only
|
|
100
|
+
* admitted on a later retry once it is the rightful next under (priority desc, fairness, FIFO).
|
|
101
|
+
*/
|
|
102
|
+
tryAdmit(item) {
|
|
103
|
+
const now = this.clock();
|
|
104
|
+
const rl = this.config.rateLimit;
|
|
105
|
+
if (rl) {
|
|
106
|
+
if (now - this.windowStart >= rl.periodMs) {
|
|
107
|
+
this.windowStart = now;
|
|
108
|
+
this.windowCount = 0;
|
|
109
|
+
}
|
|
110
|
+
if (this.windowCount >= rl.limit) return {
|
|
111
|
+
ok: false,
|
|
112
|
+
retryAt: this.windowStart + rl.periodMs
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const usingOrder = this.ordered || item?.priority != null || item?.waiterId != null;
|
|
116
|
+
const waiterId = item?.waiterId;
|
|
117
|
+
const full = this.config.concurrency != null && this.inFlight >= this.config.concurrency;
|
|
118
|
+
if (full) {
|
|
119
|
+
if (usingOrder && waiterId != null) this.register(waiterId, item);
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
retryAt: now + (this.config.retryMs ?? 1e3)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (usingOrder && this.waiters.size > 0) {
|
|
126
|
+
if (waiterId != null) this.register(waiterId, item);
|
|
127
|
+
if (!this.isNext(waiterId, item)) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
retryAt: now + (this.config.retryMs ?? 1e3)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (rl) this.windowCount += 1;
|
|
135
|
+
this.inFlight += 1;
|
|
136
|
+
if (waiterId != null) this.waiters.delete(waiterId);
|
|
137
|
+
if (item?.key != null) this.keyServed.set(item.key, ++this.servedTick);
|
|
138
|
+
return {
|
|
139
|
+
ok: true
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Release a concurrency slot taken by a successful {@link tryAdmit}. */
|
|
143
|
+
release() {
|
|
144
|
+
if (this.inFlight > 0) this.inFlight -= 1;
|
|
145
|
+
}
|
|
146
|
+
/** Track (or refresh) a blocked call as a waiter; its FIFO seq is stamped only on first sight. */
|
|
147
|
+
register(waiterId, item) {
|
|
148
|
+
const existing = this.waiters.get(waiterId);
|
|
149
|
+
if (existing) {
|
|
150
|
+
existing.priority = item?.priority ?? 0;
|
|
151
|
+
existing.key = item?.key;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.waiters.set(waiterId, {
|
|
155
|
+
priority: item?.priority ?? 0,
|
|
156
|
+
key: item?.key,
|
|
157
|
+
seq: this.waiterSeq++
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Is the calling item the best candidate among all registered waiters? Ordering, best first:
|
|
162
|
+
* 1. higher `priority`;
|
|
163
|
+
* 2. fairness: with `fairness: 'key'`, the least-recently-served key wins (round-robin);
|
|
164
|
+
* 3. earliest registration (FIFO) — stable tiebreak.
|
|
165
|
+
* A bare item (no waiterId) is compared on its hints directly.
|
|
166
|
+
*/
|
|
167
|
+
isNext(waiterId, item) {
|
|
168
|
+
const self = (waiterId != null ? this.waiters.get(waiterId) : void 0) ?? {
|
|
169
|
+
priority: item?.priority ?? 0,
|
|
170
|
+
key: item?.key,
|
|
171
|
+
seq: Number.POSITIVE_INFINITY
|
|
172
|
+
};
|
|
173
|
+
let best = self;
|
|
174
|
+
for (const w of this.waiters.values()) {
|
|
175
|
+
if (this.better(w, best)) best = w;
|
|
176
|
+
}
|
|
177
|
+
return best === self;
|
|
178
|
+
}
|
|
179
|
+
/** Strict "a should be admitted before b" comparison under the active policy. */
|
|
180
|
+
better(a, b) {
|
|
181
|
+
if (a.priority !== b.priority) return a.priority > b.priority;
|
|
182
|
+
if (this.config.fairness === "key") {
|
|
183
|
+
const sa = this.servedOf(a.key);
|
|
184
|
+
const sb = this.servedOf(b.key);
|
|
185
|
+
if (sa !== sb) return sa < sb;
|
|
186
|
+
}
|
|
187
|
+
return this.config.order === "lifo" ? a.seq > b.seq : a.seq < b.seq;
|
|
188
|
+
}
|
|
189
|
+
/** Last-served tick for a key (0 = never served, so an unseen key is admitted before a busy one). */
|
|
190
|
+
servedOf(key) {
|
|
191
|
+
if (key == null) return 0;
|
|
192
|
+
return this.keyServed.get(key) ?? 0;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/admission.ts
|
|
197
|
+
var InMemoryAdmissionBackend = class {
|
|
198
|
+
static {
|
|
199
|
+
__name(this, "InMemoryAdmissionBackend");
|
|
200
|
+
}
|
|
201
|
+
clock;
|
|
202
|
+
controllers = /* @__PURE__ */ new Map();
|
|
203
|
+
constructor(clock) {
|
|
204
|
+
this.clock = clock;
|
|
205
|
+
}
|
|
206
|
+
register(config) {
|
|
207
|
+
this.controllers.set(config.name, new QueueController(config, this.clock));
|
|
208
|
+
}
|
|
209
|
+
handles(queue) {
|
|
210
|
+
return this.controllers.has(queue);
|
|
211
|
+
}
|
|
212
|
+
async tryAdmit(queue, item) {
|
|
213
|
+
const controller = this.controllers.get(queue);
|
|
214
|
+
if (!controller) return {
|
|
215
|
+
ok: true
|
|
216
|
+
};
|
|
217
|
+
return controller.tryAdmit(item);
|
|
218
|
+
}
|
|
219
|
+
async release(queue, _slotId) {
|
|
220
|
+
this.controllers.get(queue)?.release();
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
68
224
|
// src/duration.ts
|
|
69
225
|
var UNIT_MS = {
|
|
70
226
|
ms: 1,
|
|
@@ -581,133 +737,6 @@ function eventMatches(payload, match) {
|
|
|
581
737
|
}
|
|
582
738
|
__name(eventMatches, "eventMatches");
|
|
583
739
|
|
|
584
|
-
// src/queue.ts
|
|
585
|
-
var QueueController = class {
|
|
586
|
-
static {
|
|
587
|
-
__name(this, "QueueController");
|
|
588
|
-
}
|
|
589
|
-
config;
|
|
590
|
-
clock;
|
|
591
|
-
inFlight = 0;
|
|
592
|
-
windowStart = 0;
|
|
593
|
-
windowCount = 0;
|
|
594
|
-
/** Registered waiters by id (only used when priority/fairness ordering is in play). */
|
|
595
|
-
waiters = /* @__PURE__ */ new Map();
|
|
596
|
-
/** Monotonic counter stamping each newly-seen waiter for a stable FIFO tiebreak. */
|
|
597
|
-
waiterSeq = 0;
|
|
598
|
-
/** Per-key "last served" order for round-robin fairness — higher = more recently served. */
|
|
599
|
-
keyServed = /* @__PURE__ */ new Map();
|
|
600
|
-
servedTick = 0;
|
|
601
|
-
constructor(config, clock) {
|
|
602
|
-
this.config = config;
|
|
603
|
-
this.clock = clock;
|
|
604
|
-
}
|
|
605
|
-
/** Whether this controller does any priority/fairness ordering (else it's a plain FIFO gate). */
|
|
606
|
-
get ordered() {
|
|
607
|
-
return this.config.fairness === "key";
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* Try to admit one unit of work. On `ok`, the caller has taken a concurrency slot and must call
|
|
611
|
-
* {@link release} when the step settles. On block, `retryAt` is when admission may next succeed.
|
|
612
|
-
*
|
|
613
|
-
* `item` carries optional priority/fairness hints; omitting it is the legacy FIFO path. When
|
|
614
|
-
* ordering is configured (or a priority is given), a blocked call REGISTERS as a waiter and is only
|
|
615
|
-
* admitted on a later retry once it is the rightful next under (priority desc, fairness, FIFO).
|
|
616
|
-
*/
|
|
617
|
-
tryAdmit(item) {
|
|
618
|
-
const now = this.clock();
|
|
619
|
-
const rl = this.config.rateLimit;
|
|
620
|
-
if (rl) {
|
|
621
|
-
if (now - this.windowStart >= rl.periodMs) {
|
|
622
|
-
this.windowStart = now;
|
|
623
|
-
this.windowCount = 0;
|
|
624
|
-
}
|
|
625
|
-
if (this.windowCount >= rl.limit) return {
|
|
626
|
-
ok: false,
|
|
627
|
-
retryAt: this.windowStart + rl.periodMs
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
const usingOrder = this.ordered || item?.priority != null || item?.waiterId != null;
|
|
631
|
-
const waiterId = item?.waiterId;
|
|
632
|
-
const full = this.config.concurrency != null && this.inFlight >= this.config.concurrency;
|
|
633
|
-
if (full) {
|
|
634
|
-
if (usingOrder && waiterId != null) this.register(waiterId, item);
|
|
635
|
-
return {
|
|
636
|
-
ok: false,
|
|
637
|
-
retryAt: now + (this.config.retryMs ?? 1e3)
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
if (usingOrder && this.waiters.size > 0) {
|
|
641
|
-
if (waiterId != null) this.register(waiterId, item);
|
|
642
|
-
if (!this.isNext(waiterId, item)) {
|
|
643
|
-
return {
|
|
644
|
-
ok: false,
|
|
645
|
-
retryAt: now + (this.config.retryMs ?? 1e3)
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
if (rl) this.windowCount += 1;
|
|
650
|
-
this.inFlight += 1;
|
|
651
|
-
if (waiterId != null) this.waiters.delete(waiterId);
|
|
652
|
-
if (item?.key != null) this.keyServed.set(item.key, ++this.servedTick);
|
|
653
|
-
return {
|
|
654
|
-
ok: true
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
/** Release a concurrency slot taken by a successful {@link tryAdmit}. */
|
|
658
|
-
release() {
|
|
659
|
-
if (this.inFlight > 0) this.inFlight -= 1;
|
|
660
|
-
}
|
|
661
|
-
/** Track (or refresh) a blocked call as a waiter; its FIFO seq is stamped only on first sight. */
|
|
662
|
-
register(waiterId, item) {
|
|
663
|
-
const existing = this.waiters.get(waiterId);
|
|
664
|
-
if (existing) {
|
|
665
|
-
existing.priority = item?.priority ?? 0;
|
|
666
|
-
existing.key = item?.key;
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
this.waiters.set(waiterId, {
|
|
670
|
-
priority: item?.priority ?? 0,
|
|
671
|
-
key: item?.key,
|
|
672
|
-
seq: this.waiterSeq++
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
/**
|
|
676
|
-
* Is the calling item the best candidate among all registered waiters? Ordering, best first:
|
|
677
|
-
* 1. higher `priority`;
|
|
678
|
-
* 2. fairness: with `fairness: 'key'`, the least-recently-served key wins (round-robin);
|
|
679
|
-
* 3. earliest registration (FIFO) — stable tiebreak.
|
|
680
|
-
* A bare item (no waiterId) is compared on its hints directly.
|
|
681
|
-
*/
|
|
682
|
-
isNext(waiterId, item) {
|
|
683
|
-
const self = (waiterId != null ? this.waiters.get(waiterId) : void 0) ?? {
|
|
684
|
-
priority: item?.priority ?? 0,
|
|
685
|
-
key: item?.key,
|
|
686
|
-
seq: Number.POSITIVE_INFINITY
|
|
687
|
-
};
|
|
688
|
-
let best = self;
|
|
689
|
-
for (const w of this.waiters.values()) {
|
|
690
|
-
if (this.better(w, best)) best = w;
|
|
691
|
-
}
|
|
692
|
-
return best === self;
|
|
693
|
-
}
|
|
694
|
-
/** Strict "a should be admitted before b" comparison under the active policy. */
|
|
695
|
-
better(a, b) {
|
|
696
|
-
if (a.priority !== b.priority) return a.priority > b.priority;
|
|
697
|
-
if (this.config.fairness === "key") {
|
|
698
|
-
const sa = this.servedOf(a.key);
|
|
699
|
-
const sb = this.servedOf(b.key);
|
|
700
|
-
if (sa !== sb) return sa < sb;
|
|
701
|
-
}
|
|
702
|
-
return a.seq < b.seq;
|
|
703
|
-
}
|
|
704
|
-
/** Last-served tick for a key (0 = never served, so an unseen key is admitted before a busy one). */
|
|
705
|
-
servedOf(key) {
|
|
706
|
-
if (key == null) return 0;
|
|
707
|
-
return this.keyServed.get(key) ?? 0;
|
|
708
|
-
}
|
|
709
|
-
};
|
|
710
|
-
|
|
711
740
|
// src/singleton-gate.ts
|
|
712
741
|
var SINGLETON_RETRY_MS = 1e3;
|
|
713
742
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -901,6 +930,12 @@ function workflowName(ref) {
|
|
|
901
930
|
__name(workflowName, "workflowName");
|
|
902
931
|
|
|
903
932
|
// src/workflow-ctx.ts
|
|
933
|
+
function normalizeChildOptions(options) {
|
|
934
|
+
return typeof options === "string" ? {
|
|
935
|
+
childId: options
|
|
936
|
+
} : options ?? {};
|
|
937
|
+
}
|
|
938
|
+
__name(normalizeChildOptions, "normalizeChildOptions");
|
|
904
939
|
var Position = class Position2 {
|
|
905
940
|
static {
|
|
906
941
|
__name(this, "Position");
|
|
@@ -1150,7 +1185,8 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1150
1185
|
await step(`task:dispatch:${name}`, dispatch, options);
|
|
1151
1186
|
return unwrapCompletion(await waitForSignal(`task:${runId}:${name}`), `task "${name}"`);
|
|
1152
1187
|
}, "task");
|
|
1153
|
-
const child = /* @__PURE__ */ __name(async (workflow2, input,
|
|
1188
|
+
const child = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
1189
|
+
const { childId, priority } = normalizeChildOptions(options);
|
|
1154
1190
|
const current = pos.next();
|
|
1155
1191
|
const id = childId ?? `${runId}.child.${current}`;
|
|
1156
1192
|
const existing = await readCheckpoint(current);
|
|
@@ -1162,7 +1198,7 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1162
1198
|
runId,
|
|
1163
1199
|
seq: current
|
|
1164
1200
|
});
|
|
1165
|
-
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id);
|
|
1201
|
+
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
|
|
1166
1202
|
if (!existing) {
|
|
1167
1203
|
await writeCheckpoint(instantCheckpoint({
|
|
1168
1204
|
runId,
|
|
@@ -1174,12 +1210,13 @@ function createWorkflowCtx(host, runId, compensations, workflow = "") {
|
|
|
1174
1210
|
}
|
|
1175
1211
|
throw new WorkflowSuspended();
|
|
1176
1212
|
}, "child");
|
|
1177
|
-
const startChild = /* @__PURE__ */ __name(async (workflow2, input,
|
|
1213
|
+
const startChild = /* @__PURE__ */ __name(async (workflow2, input, options) => {
|
|
1214
|
+
const { childId, priority } = normalizeChildOptions(options);
|
|
1178
1215
|
const current = pos.next();
|
|
1179
1216
|
const id = childId ?? `${runId}.child.${current}`;
|
|
1180
1217
|
const existing = await readCheckpoint(current);
|
|
1181
1218
|
if (existing && existing.status === "completed") return existing.output;
|
|
1182
|
-
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id);
|
|
1219
|
+
if (!await store.getRun(id)) host.startChild(workflowName(workflow2), input, id, priority);
|
|
1183
1220
|
await writeCheckpoint(instantCheckpoint({
|
|
1184
1221
|
runId,
|
|
1185
1222
|
seq: current,
|
|
@@ -1403,8 +1440,10 @@ var WorkflowEngine = class {
|
|
|
1403
1440
|
updateValidators = /* @__PURE__ */ new Map();
|
|
1404
1441
|
/** Runs being cancelled WITH saga compensation — see `cancel({ compensate: true })`. */
|
|
1405
1442
|
cancelRequested = /* @__PURE__ */ new Set();
|
|
1406
|
-
/** Flow-control
|
|
1407
|
-
|
|
1443
|
+
/** Flow-control admission backend for remote steps (see {@link registerQueue}). */
|
|
1444
|
+
admission;
|
|
1445
|
+
/** Runs on THIS instance blocked on admission, by queue — woken early on a freed-slot signal. */
|
|
1446
|
+
queueWaiters = /* @__PURE__ */ new Map();
|
|
1408
1447
|
/** Which queue a dispatched step took a slot from, by stepId — so the result can release it. */
|
|
1409
1448
|
stepQueue = /* @__PURE__ */ new Map();
|
|
1410
1449
|
/** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
|
|
@@ -1420,6 +1459,8 @@ var WorkflowEngine = class {
|
|
|
1420
1459
|
] : []));
|
|
1421
1460
|
this.controlPlane = deps.controlPlane;
|
|
1422
1461
|
this.clock = deps.clock ?? Date.now;
|
|
1462
|
+
this.admission = deps.admission ?? new InMemoryAdmissionBackend(this.clock);
|
|
1463
|
+
this.admission.onFreed?.((queue) => this.wakeQueueWaiters(queue));
|
|
1423
1464
|
this.instanceId = deps.instanceId ?? globalThis.crypto.randomUUID();
|
|
1424
1465
|
this.leaseMs = deps.leaseMs ?? 3e4;
|
|
1425
1466
|
this.maxRecoveryAttempts = deps.maxRecoveryAttempts;
|
|
@@ -1583,7 +1624,7 @@ var WorkflowEngine = class {
|
|
|
1583
1624
|
* durable. Per engine instance (see {@link QueueConfig}). Registering the same name replaces it.
|
|
1584
1625
|
*/
|
|
1585
1626
|
registerQueue(config) {
|
|
1586
|
-
this.
|
|
1627
|
+
this.admission.register(config);
|
|
1587
1628
|
}
|
|
1588
1629
|
/** Subscribe to lifecycle events. Returns an unsubscribe function. */
|
|
1589
1630
|
subscribe(listener) {
|
|
@@ -1704,6 +1745,7 @@ var WorkflowEngine = class {
|
|
|
1704
1745
|
input,
|
|
1705
1746
|
tags,
|
|
1706
1747
|
searchAttributes: opts?.searchAttributes,
|
|
1748
|
+
priority: opts?.priority,
|
|
1707
1749
|
createdAt: now,
|
|
1708
1750
|
updatedAt: now
|
|
1709
1751
|
};
|
|
@@ -2179,6 +2221,26 @@ var WorkflowEngine = class {
|
|
|
2179
2221
|
return results;
|
|
2180
2222
|
}
|
|
2181
2223
|
/**
|
|
2224
|
+
* Hard-delete a run and its entire subtree. Unlike {@link cancel} (which marks a run `cancelled`
|
|
2225
|
+
* but keeps it as history), delete REMOVES the run and all its rows (checkpoints, signal waiters,
|
|
2226
|
+
* search-attribute rows) — it vanishes from {@link getRun} and {@link listRuns}. Use it to purge a
|
|
2227
|
+
* finished run whose data is being deleted (e.g. a pipeline run whose result rows are cleared).
|
|
2228
|
+
*
|
|
2229
|
+
* Cascades depth-first via {@link getRunChildren} so children are gone before the parent (no
|
|
2230
|
+
* orphaned child runs). Prefer {@link cancel} first for a live run — deleting one mid-flight orphans
|
|
2231
|
+
* its worker (its next checkpoint write fails). Returns the number of runs deleted (0 if absent).
|
|
2232
|
+
*/
|
|
2233
|
+
async deleteRun(runId) {
|
|
2234
|
+
const run = await this.store.getRun(runId);
|
|
2235
|
+
if (!run) return 0;
|
|
2236
|
+
let deleted = 0;
|
|
2237
|
+
for (const childId of await this.getRunChildren(runId)) {
|
|
2238
|
+
deleted += await this.deleteRun(childId);
|
|
2239
|
+
}
|
|
2240
|
+
await this.store.deleteRun(runId);
|
|
2241
|
+
return deleted + 1;
|
|
2242
|
+
}
|
|
2243
|
+
/**
|
|
2182
2244
|
* Cascade cancellation to a run's children — both awaited (`ctx.child`, found via its live
|
|
2183
2245
|
* `child:<id>` waiter) and fire-and-forget (`ctx.startChild`, found via its `spawn:<id>`
|
|
2184
2246
|
* checkpoint). Recursive, so a whole subtree is cancelled; the terminal guard in `cancel` stops it
|
|
@@ -2910,8 +2972,10 @@ var WorkflowEngine = class {
|
|
|
2910
2972
|
failStep: /* @__PURE__ */ __name((s) => this.failStep(s), "failStep"),
|
|
2911
2973
|
callRemote: /* @__PURE__ */ __name((runId, seq, step, input, queue, transport, admission) => this.callRemote(runId, seq, step, input, queue, transport, replay, admission), "callRemote"),
|
|
2912
2974
|
// Defer so a fast child can't reentrantly resume a still-running parent.
|
|
2913
|
-
startChild: /* @__PURE__ */ __name((workflow, input, id) => {
|
|
2914
|
-
queueMicrotask(() => void this.start(workflow, input, id
|
|
2975
|
+
startChild: /* @__PURE__ */ __name((workflow, input, id, priority) => {
|
|
2976
|
+
queueMicrotask(() => void this.start(workflow, input, id, {
|
|
2977
|
+
priority
|
|
2978
|
+
}).catch(() => void 0));
|
|
2915
2979
|
}, "startChild"),
|
|
2916
2980
|
signalEntity: /* @__PURE__ */ __name((name, key, op, arg, reply) => {
|
|
2917
2981
|
queueMicrotask(() => void this.entities.dispatch(name, key, op, arg, reply).catch(() => void 0));
|
|
@@ -3012,14 +3076,19 @@ var WorkflowEngine = class {
|
|
|
3012
3076
|
attempt = existing.attempts + 1;
|
|
3013
3077
|
}
|
|
3014
3078
|
const id = stepId(runId, seq);
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
const decision = controller.tryAdmit({
|
|
3079
|
+
if (queue && this.admission.handles(queue)) {
|
|
3080
|
+
const decision = await this.admission.tryAdmit(queue, {
|
|
3018
3081
|
priority: admission?.priority,
|
|
3019
3082
|
key: admission?.fairnessKey ?? runId,
|
|
3020
3083
|
waiterId: id
|
|
3021
3084
|
});
|
|
3022
|
-
if (!decision.ok)
|
|
3085
|
+
if (!decision.ok) {
|
|
3086
|
+
const waiters = this.queueWaiters.get(queue) ?? /* @__PURE__ */ new Set();
|
|
3087
|
+
waiters.add(runId);
|
|
3088
|
+
this.queueWaiters.set(queue, waiters);
|
|
3089
|
+
throw new WorkflowSuspended(decision.retryAt);
|
|
3090
|
+
}
|
|
3091
|
+
this.queueWaiters.get(queue)?.delete(runId);
|
|
3023
3092
|
this.stepQueue.set(id, queue);
|
|
3024
3093
|
}
|
|
3025
3094
|
const validInput = step.input.parse(input);
|
|
@@ -3047,6 +3116,7 @@ var WorkflowEngine = class {
|
|
|
3047
3116
|
input: validInput,
|
|
3048
3117
|
traceparent: this.traceparent?.(),
|
|
3049
3118
|
context: this.context?.(),
|
|
3119
|
+
priority: admission?.priority,
|
|
3050
3120
|
attempt
|
|
3051
3121
|
}, transport);
|
|
3052
3122
|
this.emit({
|
|
@@ -3066,7 +3136,7 @@ var WorkflowEngine = class {
|
|
|
3066
3136
|
async completeRemoteResult(result) {
|
|
3067
3137
|
const cp = await this.store.getCheckpoint(result.runId, result.seq);
|
|
3068
3138
|
if (!cp || cp.status !== "pending") return;
|
|
3069
|
-
this.releaseQueueSlot(cp.stepId);
|
|
3139
|
+
void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
|
|
3070
3140
|
const run = await this.store.getRun(result.runId);
|
|
3071
3141
|
if (run && (run.status === "cancelled" || run.status === "completed")) return;
|
|
3072
3142
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
@@ -3093,12 +3163,26 @@ var WorkflowEngine = class {
|
|
|
3093
3163
|
});
|
|
3094
3164
|
await this.resume(result.runId);
|
|
3095
3165
|
}
|
|
3166
|
+
/**
|
|
3167
|
+
* A slot freed on `queue` (a fleet-wide signal): resume this instance's runs blocked on it so they
|
|
3168
|
+
* re-contend now instead of at their retry tick. Snapshot-and-clear — a run still blocked after the
|
|
3169
|
+
* retry re-registers itself, and one that's gone (cancelled/admitted) is dropped. Best-effort.
|
|
3170
|
+
*/
|
|
3171
|
+
wakeQueueWaiters(queue) {
|
|
3172
|
+
const waiters = this.queueWaiters.get(queue);
|
|
3173
|
+
if (!waiters || waiters.size === 0) return;
|
|
3174
|
+
const runIds = [
|
|
3175
|
+
...waiters
|
|
3176
|
+
];
|
|
3177
|
+
waiters.clear();
|
|
3178
|
+
for (const runId of runIds) void this.resume(runId).catch(() => void 0);
|
|
3179
|
+
}
|
|
3096
3180
|
/** Release the flow-control slot a dispatched step held (if any), by its stepId. */
|
|
3097
|
-
releaseQueueSlot(id) {
|
|
3181
|
+
async releaseQueueSlot(id) {
|
|
3098
3182
|
const queue = this.stepQueue.get(id);
|
|
3099
3183
|
if (queue === void 0) return;
|
|
3100
3184
|
this.stepQueue.delete(id);
|
|
3101
|
-
this.
|
|
3185
|
+
await this.admission.release(queue, id);
|
|
3102
3186
|
}
|
|
3103
3187
|
/** In-memory await path for a remote step with a liveness `timeoutMs` (re-dispatch on timeout). */
|
|
3104
3188
|
async callRemoteInMemory(runId, seq, step, input, transport) {
|
|
@@ -3259,6 +3343,7 @@ var RemoteWorkflowExecutor = class {
|
|
|
3259
3343
|
input: run.input,
|
|
3260
3344
|
history,
|
|
3261
3345
|
group: this.group,
|
|
3346
|
+
priority: run.priority,
|
|
3262
3347
|
attempt: 1
|
|
3263
3348
|
};
|
|
3264
3349
|
const decision = new Promise((resolve, reject) => {
|
|
@@ -3338,6 +3423,9 @@ var CodecStateStore = class {
|
|
|
3338
3423
|
async getRun(runId) {
|
|
3339
3424
|
return this.decRun(await this.inner.getRun(runId));
|
|
3340
3425
|
}
|
|
3426
|
+
deleteRun(runId) {
|
|
3427
|
+
return this.inner.deleteRun(runId);
|
|
3428
|
+
}
|
|
3341
3429
|
async getCheckpoint(runId, seq) {
|
|
3342
3430
|
return this.decCp(await this.inner.getCheckpoint(runId, seq));
|
|
3343
3431
|
}
|
|
@@ -3732,6 +3820,16 @@ var InMemoryStateStore = class {
|
|
|
3732
3820
|
...run
|
|
3733
3821
|
} : null;
|
|
3734
3822
|
}
|
|
3823
|
+
async deleteRun(runId) {
|
|
3824
|
+
this.runs.delete(runId);
|
|
3825
|
+
for (const [key, cp] of this.checkpoints) {
|
|
3826
|
+
if (cp.runId === runId) this.checkpoints.delete(key);
|
|
3827
|
+
}
|
|
3828
|
+
for (const [token, waiter] of this.signalWaiters) {
|
|
3829
|
+
if (waiter.runId === runId) this.signalWaiters.delete(token);
|
|
3830
|
+
}
|
|
3831
|
+
this.reindexAttributes(runId, void 0);
|
|
3832
|
+
}
|
|
3735
3833
|
async getCheckpoint(runId, seq) {
|
|
3736
3834
|
const cp = this.checkpoints.get(this.key(runId, seq));
|
|
3737
3835
|
return cp ? {
|
|
@@ -3978,6 +4076,7 @@ var InMemoryTransport = class {
|
|
|
3978
4076
|
Entities,
|
|
3979
4077
|
EventAccumulators,
|
|
3980
4078
|
FatalError,
|
|
4079
|
+
InMemoryAdmissionBackend,
|
|
3981
4080
|
InMemoryStateStore,
|
|
3982
4081
|
InMemoryTransport,
|
|
3983
4082
|
NonDeterminismError,
|