@dudousxd/nestjs-durable-core 0.84.1 → 0.85.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
@@ -47,6 +47,7 @@ __export(index_exports, {
47
47
  GatherError: () => GatherError,
48
48
  HANDOFF_SEQ_BASE: () => HANDOFF_SEQ_BASE,
49
49
  InMemoryAdmissionBackend: () => InMemoryAdmissionBackend,
50
+ InMemoryRunQueue: () => InMemoryRunQueue,
50
51
  InMemoryStateStore: () => InMemoryStateStore,
51
52
  InMemoryTransport: () => InMemoryTransport,
52
53
  LEGACY_V1_CAPABILITIES: () => LEGACY_V1_CAPABILITIES,
@@ -131,6 +132,7 @@ __export(index_exports, {
131
132
  fanPageEtag: () => fanPageEtag,
132
133
  findStalledRuns: () => findStalledRuns,
133
134
  foldedCount: () => foldedCount,
135
+ getRunsByIds: () => getRunsByIds,
134
136
  handoffHandlerId: () => handoffHandlerId,
135
137
  handoffName: () => handoffName,
136
138
  hasDurableExecutionWrappers: () => hasDurableExecutionWrappers,
@@ -168,6 +170,7 @@ __export(index_exports, {
168
170
  poolNamespace: () => poolNamespace,
169
171
  prevCronFireMs: () => prevCronFireMs,
170
172
  queryCheckpointsOf: () => queryCheckpointsOf,
173
+ readRunsByIds: () => readRunsByIds,
171
174
  readSearchAttributes: () => readSearchAttributes,
172
175
  requiredCapabilities: () => requiredCapabilities,
173
176
  resolveConcurrency: () => resolveConcurrency,
@@ -780,6 +783,82 @@ function backoffDelay(attempt, options) {
780
783
  }
781
784
  __name(backoffDelay, "backoffDelay");
782
785
 
786
+ // src/bounded-runner.ts
787
+ var BoundedRunner = class {
788
+ static {
789
+ __name(this, "BoundedRunner");
790
+ }
791
+ limit;
792
+ active = 0;
793
+ waiting = /* @__PURE__ */ new Map();
794
+ seq = 0;
795
+ constructor(limit) {
796
+ this.limit = limit;
797
+ }
798
+ /** Tasks running now plus tasks waiting for a slot. */
799
+ get size() {
800
+ return this.active + this.waiting.size;
801
+ }
802
+ /** Tasks running now. */
803
+ get running() {
804
+ return this.active;
805
+ }
806
+ submit(key, task) {
807
+ if (this.active < this.limit) {
808
+ this.active += 1;
809
+ queueMicrotask(() => this.execute(task));
810
+ return;
811
+ }
812
+ if (!this.waiting.has(key)) this.waiting.set(key, {
813
+ task
814
+ });
815
+ }
816
+ /**
817
+ * Run `task` under the bound and resolve once it settled, or once it was dropped by
818
+ * {@link clear} before it got a slot. Never deduplicated.
819
+ */
820
+ run(task) {
821
+ return new Promise((resolve) => {
822
+ const done = /* @__PURE__ */ __name(() => resolve(), "done");
823
+ const wrapped = /* @__PURE__ */ __name(() => task().then(done, done), "wrapped");
824
+ if (this.active < this.limit) {
825
+ this.active += 1;
826
+ queueMicrotask(() => this.execute(wrapped));
827
+ return;
828
+ }
829
+ this.seq += 1;
830
+ this.waiting.set(`\0run:${this.seq}`, {
831
+ task: wrapped,
832
+ dropped: done
833
+ });
834
+ });
835
+ }
836
+ /** Forget every task still waiting for a slot. */
837
+ clear() {
838
+ for (const { dropped } of this.waiting.values()) dropped?.();
839
+ this.waiting.clear();
840
+ }
841
+ execute(task) {
842
+ let settled;
843
+ try {
844
+ settled = task();
845
+ } catch {
846
+ settled = Promise.resolve();
847
+ }
848
+ void settled.catch(() => void 0).then(() => {
849
+ const next = this.waiting.entries().next();
850
+ if (next.done) {
851
+ this.active -= 1;
852
+ return;
853
+ }
854
+ const [key, waiting] = next.value;
855
+ this.waiting.delete(key);
856
+ this.execute(waiting.task);
857
+ });
858
+ }
859
+ };
860
+ var LOCAL_RUN_CONCURRENCY = 256;
861
+
783
862
  // src/execution-hooks.ts
784
863
  var wrappers = [];
785
864
  function useDurableExecution(wrapper) {
@@ -2340,6 +2419,61 @@ var RemoteWorkflowExecutor = class {
2340
2419
  }
2341
2420
  };
2342
2421
 
2422
+ // src/run-dispatch-batcher.ts
2423
+ var RUN_DISPATCH_BATCH = 500;
2424
+ var RUN_DISPATCH_WINDOW_MS = 5;
2425
+ var RunDispatchBatcher = class {
2426
+ static {
2427
+ __name(this, "RunDispatchBatcher");
2428
+ }
2429
+ send;
2430
+ onError;
2431
+ buffer = /* @__PURE__ */ new Map();
2432
+ timer;
2433
+ sending = /* @__PURE__ */ new Set();
2434
+ constructor(send, onError) {
2435
+ this.send = send;
2436
+ this.onError = onError;
2437
+ }
2438
+ /** Nothing buffered and nothing being sent. */
2439
+ get idle() {
2440
+ return this.buffer.size === 0 && this.sending.size === 0;
2441
+ }
2442
+ /** Buffer `run`. A wake for a run already buffered replaces its start: a wake also runs a `pending` run. */
2443
+ add(run) {
2444
+ const prior = this.buffer.get(run.runId);
2445
+ if (prior && run.reason !== "wake") return;
2446
+ this.buffer.set(run.runId, run);
2447
+ if (this.buffer.size >= RUN_DISPATCH_BATCH) {
2448
+ this.flush();
2449
+ return;
2450
+ }
2451
+ if (!this.timer) {
2452
+ this.timer = setTimeout(() => this.flush(), RUN_DISPATCH_WINDOW_MS);
2453
+ this.timer.unref?.();
2454
+ }
2455
+ }
2456
+ /** Send what is buffered now. */
2457
+ flush() {
2458
+ if (this.timer) clearTimeout(this.timer);
2459
+ this.timer = void 0;
2460
+ if (this.buffer.size === 0) return;
2461
+ const runs = [
2462
+ ...this.buffer.values()
2463
+ ];
2464
+ this.buffer = /* @__PURE__ */ new Map();
2465
+ const sent = this.send(runs).catch((err) => this.onError(runs, err)).finally(() => this.sending.delete(sent));
2466
+ this.sending.add(sent);
2467
+ }
2468
+ /** Send what is buffered and wait for every send in flight. */
2469
+ async settle() {
2470
+ this.flush();
2471
+ await Promise.allSettled([
2472
+ ...this.sending
2473
+ ]);
2474
+ }
2475
+ };
2476
+
2343
2477
  // src/run-waiting.ts
2344
2478
  var WEBHOOK_PREFIX = "wh:";
2345
2479
  var CHILD_PREFIX = "child:";
@@ -2410,6 +2544,51 @@ function indexWaitersByRun(waiters) {
2410
2544
  return byRun;
2411
2545
  }
2412
2546
  __name(indexWaitersByRun, "indexWaitersByRun");
2547
+ var GET_RUN_FALLBACK_PARALLEL = 50;
2548
+ async function getRunsByIds(store, runIds) {
2549
+ const { runs, unread, error: error2 } = await readRunsByIds(store, runIds);
2550
+ if (unread.length > 0) throw error2;
2551
+ return runs;
2552
+ }
2553
+ __name(getRunsByIds, "getRunsByIds");
2554
+ async function readRunsByIds(store, runIds) {
2555
+ const unique = [
2556
+ ...new Set(runIds)
2557
+ ];
2558
+ const runs = /* @__PURE__ */ new Map();
2559
+ const unread = [];
2560
+ let error2;
2561
+ const batch = store.getRuns ? RUN_ID_BATCH : GET_RUN_FALLBACK_PARALLEL;
2562
+ for (let at = 0; at < unique.length; at += batch) {
2563
+ const ids = unique.slice(at, at + batch);
2564
+ if (store.getRuns) {
2565
+ try {
2566
+ for (const run of await store.getRuns(ids)) runs.set(run.id, run);
2567
+ } catch (err) {
2568
+ error2 ??= err;
2569
+ unread.push(...ids);
2570
+ }
2571
+ continue;
2572
+ }
2573
+ const reads = await Promise.allSettled(ids.map((id) => store.getRun(id)));
2574
+ reads.forEach((read, k) => {
2575
+ if (read.status === "fulfilled") {
2576
+ if (read.value) runs.set(read.value.id, read.value);
2577
+ return;
2578
+ }
2579
+ error2 ??= read.reason;
2580
+ unread.push(ids[k]);
2581
+ });
2582
+ }
2583
+ return {
2584
+ runs,
2585
+ unread,
2586
+ ...unread.length > 0 ? {
2587
+ error: error2
2588
+ } : {}
2589
+ };
2590
+ }
2591
+ __name(readRunsByIds, "readRunsByIds");
2413
2592
 
2414
2593
  // src/scheduler.ts
2415
2594
  var import_node_module = require("module");
@@ -2451,7 +2630,31 @@ __name(resolveCronParse, "resolveCronParse");
2451
2630
 
2452
2631
  // src/scheduler.ts
2453
2632
  var nodeRequire = (0, import_node_module.createRequire)(__importMetaUrl);
2454
- var defaultSleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "defaultSleep");
2633
+ var jitterFireAt = /* @__PURE__ */ new WeakMap();
2634
+ function deferredWindows(engine, key) {
2635
+ return [
2636
+ ...jitterFireAt.get(engine)?.get(key)?.keys() ?? []
2637
+ ];
2638
+ }
2639
+ __name(deferredWindows, "deferredWindows");
2640
+ function jitterDue(engine, s, candidates, nowMs, random) {
2641
+ let byKey = jitterFireAt.get(engine);
2642
+ if (byKey === void 0) {
2643
+ byKey = /* @__PURE__ */ new Map();
2644
+ jitterFireAt.set(engine, byKey);
2645
+ }
2646
+ const previous = byKey.get(s.key);
2647
+ const current = /* @__PURE__ */ new Map();
2648
+ const due = [];
2649
+ for (const runId of candidates) {
2650
+ const at = previous?.get(runId) ?? nowMs + Math.floor(random() * s.jitter);
2651
+ if (nowMs >= at) due.push(runId);
2652
+ else current.set(runId, at);
2653
+ }
2654
+ byKey.set(s.key, current);
2655
+ return due;
2656
+ }
2657
+ __name(jitterDue, "jitterDue");
2455
2658
  function scheduledRunId(key, everyMs, nowMs) {
2456
2659
  return `sched:${key}:${Math.floor(nowMs / everyMs)}`;
2457
2660
  }
@@ -2521,13 +2724,19 @@ function scheduleRunIdsAt(s, nowMs) {
2521
2724
  __name(scheduleRunIdsAt, "scheduleRunIdsAt");
2522
2725
  async function runSchedules(engine, schedules, nowMs, opts) {
2523
2726
  const random = opts?.random ?? Math.random;
2524
- const sleep = opts?.sleep ?? defaultSleep;
2525
2727
  const settled = settledIdsFor(engine);
2526
2728
  const ids = [];
2527
2729
  for (const s of schedules) {
2528
2730
  if (engine.isDraining?.()) break;
2529
2731
  if (s.paused) continue;
2530
- const candidates = scheduleRunIdsAt(s, nowMs).filter((id) => !settled.get(s.key)?.has(id));
2732
+ const due = scheduleRunIdsAt(s, nowMs);
2733
+ const windows = s.jitter && s.jitter > 0 ? [
2734
+ .../* @__PURE__ */ new Set([
2735
+ ...deferredWindows(engine, s.key),
2736
+ ...due
2737
+ ])
2738
+ ] : due;
2739
+ const candidates = windows.filter((id) => !settled.get(s.key)?.has(id));
2531
2740
  if (candidates.length === 0) continue;
2532
2741
  if (s.overlap === "skip" && s.everyMs) {
2533
2742
  const prevBucket = Math.floor(nowMs / s.everyMs) - 1;
@@ -2536,8 +2745,9 @@ async function runSchedules(engine, schedules, nowMs, opts) {
2536
2745
  if (prev && (prev.status === "running" || prev.status === "suspended")) continue;
2537
2746
  }
2538
2747
  }
2539
- if (s.jitter && s.jitter > 0) await sleep(Math.floor(random() * s.jitter));
2540
- for (const runId of candidates) {
2748
+ const firing = s.jitter && s.jitter > 0 ? jitterDue(engine, s, candidates, nowMs, random) : candidates;
2749
+ if (firing.length === 0) continue;
2750
+ for (const runId of firing) {
2541
2751
  if (engine.isDraining?.()) break;
2542
2752
  const existing = await engine.getRun(runId);
2543
2753
  remember(settled, s.key, runId);
@@ -3302,6 +3512,39 @@ var TransportPool = class {
3302
3512
  transport.onStartRun?.(handler);
3303
3513
  }
3304
3514
  }
3515
+ /** Does any transport here carry a run queue ({@link Transport.dispatchRuns} + `onDispatchedRun`)? */
3516
+ get dispatchesRuns() {
3517
+ return this.transports.some(({ transport }) => transport.dispatchRuns && transport.onDispatchedRun);
3518
+ }
3519
+ /**
3520
+ * Queue `runs` on the first transport that carries a run queue, failing over to the next on an
3521
+ * error, like {@link dispatch}. Throws if none accepted them.
3522
+ */
3523
+ async dispatchRuns(runs, opts) {
3524
+ let lastErr;
3525
+ for (const { transport } of this.transports) {
3526
+ if (!transport.dispatchRuns || !transport.onDispatchedRun) continue;
3527
+ try {
3528
+ await transport.dispatchRuns(runs, opts);
3529
+ return;
3530
+ } catch (err) {
3531
+ lastErr = err;
3532
+ }
3533
+ }
3534
+ throw lastErr ?? new Error("no transport carries a run queue");
3535
+ }
3536
+ /**
3537
+ * Consume the run queue of every transport that carries one (a run may have been queued on any of
3538
+ * them after a failover). Returns one stop function per consumer.
3539
+ */
3540
+ onDispatchedRun(handler, opts) {
3541
+ const stops = [];
3542
+ for (const { transport } of this.transports) {
3543
+ if (!transport.dispatchRuns || !transport.onDispatchedRun) continue;
3544
+ stops.push(transport.onDispatchedRun(handler, opts));
3545
+ }
3546
+ return stops;
3547
+ }
3305
3548
  /**
3306
3549
  * The live handshake descriptors advertised for `group` (design §7.2), merged across every transport
3307
3550
  * that can report them. Returns `[]` when NO transport implements
@@ -3834,21 +4077,28 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3834
4077
  return inputs.map((_, i) => i).slice(from, from + waves.waveSize);
3835
4078
  }, "waveItems");
3836
4079
  if (mode === "failFast") {
3837
- for (let i = 0; i < inputs.length; i += 1) {
3838
- const cp = existing[i];
3839
- const c = cp?.status === "completed" ? cp.output : null;
3840
- if (c && c.ok === false) {
3841
- for (let j = 0; j < inputs.length; j += 1) {
3842
- if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
3843
- }
3844
- throw new GatherError([
3845
- {
3846
- index: i,
3847
- id: id(i),
3848
- error: c.error ?? "unknown"
3849
- }
3850
- ], inputs.length);
4080
+ const failure = /* @__PURE__ */ __name((i2) => {
4081
+ const cp = existing[i2];
4082
+ const c2 = cp?.status === "completed" ? cp.output : null;
4083
+ return c2 && c2.ok === false ? c2 : null;
4084
+ }, "failure");
4085
+ const first = /* @__PURE__ */ __name((cancelled) => inputs.findIndex((_, i2) => {
4086
+ const c2 = failure(i2);
4087
+ return c2 !== null && c2.code === "cancelled" === cancelled;
4088
+ }), "first");
4089
+ const i = first(false) !== -1 ? first(false) : first(true);
4090
+ const c = i === -1 ? null : failure(i);
4091
+ if (c) {
4092
+ for (let j = 0; j < inputs.length; j += 1) {
4093
+ if (j !== i && existing[j]?.status !== "completed") host.cancelChild?.(id(j));
3851
4094
  }
4095
+ throw new GatherError([
4096
+ {
4097
+ index: i,
4098
+ id: id(i),
4099
+ error: c.error ?? "unknown"
4100
+ }
4101
+ ], inputs.length);
3852
4102
  }
3853
4103
  }
3854
4104
  if (store.compactCheckpoints && inputs.length >= FOLD_MIN_FAN) {
@@ -3927,10 +4177,12 @@ function createWorkflowCtx(host, runId, compensations, workflow = "", searchAttr
3927
4177
  await registerChildWaits(entries);
3928
4178
  for (const i of chunk) host.startChild(name, inputs[i], id(i));
3929
4179
  }
3930
- for (let at = 0; at < toEnsure.length; at += FAN_CHUNK) {
3931
- await Promise.all(toEnsure.slice(at, at + FAN_CHUNK).map(async (i) => {
3932
- if (!await store.getRun(id(i))) host.startChild(name, inputs[i], id(i));
3933
- }));
4180
+ if (toEnsure.length > 0) {
4181
+ const { runs: started, unread } = await readRunsByIds(store, toEnsure.map((i) => id(i)));
4182
+ const skip = new Set(unread);
4183
+ for (const i of toEnsure) {
4184
+ if (!started.has(id(i)) && !skip.has(id(i))) host.startChild(name, inputs[i], id(i));
4185
+ }
3934
4186
  }
3935
4187
  }
3936
4188
  if (active.length > 0) {
@@ -4390,6 +4642,56 @@ var ANNOUNCED_ROUTES_TTL_MS = 5e3;
4390
4642
  var VERSION_UNDECLARED_TAG = "version:undeclared";
4391
4643
  var versionKey = /* @__PURE__ */ __name((name, version) => `${name}@${version}`, "versionKey");
4392
4644
  var STEP_FAN_CHUNK = 200;
4645
+ var PARENT_NOTIFY_ATTEMPTS = 4;
4646
+ var PARENT_NOTIFY_BACKOFF_MS = 250;
4647
+ var LEASE_RECHECK_MAX_MS = 2e3;
4648
+ var LEASE_RECHECK_SLACK_MS = 100;
4649
+ var LEASE_RECHECK_ATTEMPTS = 30;
4650
+ function deferred() {
4651
+ let resolve;
4652
+ let reject;
4653
+ const promise = new Promise((res, rej) => {
4654
+ resolve = res;
4655
+ reject = rej;
4656
+ });
4657
+ promise.catch(() => void 0);
4658
+ return {
4659
+ promise,
4660
+ resolve,
4661
+ reject
4662
+ };
4663
+ }
4664
+ __name(deferred, "deferred");
4665
+ var pendingStateKey = /* @__PURE__ */ __name((at) => String(new Date(at).getTime()), "pendingStateKey");
4666
+ var freshPendingKey = /* @__PURE__ */ __name(() => `${Date.now()}-${globalThis.crypto.randomUUID().slice(0, 8)}`, "freshPendingKey");
4667
+ var CHILD_SIGNAL_PREFIX = "signal:child:";
4668
+ var DELIVER_CHUNK = 50;
4669
+ function childCompletion(child) {
4670
+ if (child.status === "completed") return {
4671
+ ok: true,
4672
+ value: child.output
4673
+ };
4674
+ if (child.status === "failed") return failedCompletion(child.error ?? {
4675
+ message: "failed"
4676
+ });
4677
+ if (child.status === "dead") return failedCompletion(child.error ?? {
4678
+ message: "dead"
4679
+ });
4680
+ if (child.status === "cancelled") return cancelledCompletion(child.error);
4681
+ return void 0;
4682
+ }
4683
+ __name(childCompletion, "childCompletion");
4684
+ var CHILD_CANCELLED = "cancelled";
4685
+ function cancelledCompletion(error2) {
4686
+ const base = error2 ?? {
4687
+ message: "cancelled"
4688
+ };
4689
+ return failedCompletion({
4690
+ ...base,
4691
+ code: base.code ?? CHILD_CANCELLED
4692
+ });
4693
+ }
4694
+ __name(cancelledCompletion, "cancelledCompletion");
4393
4695
  var RESULT_BATCH_MAX = 256;
4394
4696
  function shuffle(items) {
4395
4697
  for (let i = items.length - 1; i > 0; i -= 1) {
@@ -4566,6 +4868,35 @@ var WorkflowEngine = class {
4566
4868
  /** Executions currently in flight, so a graceful shutdown can wait for them to settle. */
4567
4869
  inflight = /* @__PURE__ */ new Set();
4568
4870
  /**
4871
+ * Work a settled turn leaves running in the background (a parent notification, a post-release
4872
+ * re-drive), so a graceful shutdown waits for it too: dropping one on exit strands the run until
4873
+ * its reconcile timer.
4874
+ */
4875
+ background = /* @__PURE__ */ new Set();
4876
+ /**
4877
+ * Runs with an implicit wake executing on this instance, and whether another wake arrived for the
4878
+ * run meanwhile (see {@link resumeImplicit}).
4879
+ */
4880
+ wakeTurns = /* @__PURE__ */ new Map();
4881
+ /** Re-checks scheduled for runs whose wake found another instance's lease (see {@link execute}). */
4882
+ leaseRetries = /* @__PURE__ */ new Map();
4883
+ /**
4884
+ * Every run turn and child start this instance takes on itself, under one budget of
4885
+ * {@link LOCAL_RUN_CONCURRENCY}: the runs it dispatches to itself, a fan's deferred child starts,
4886
+ * enqueued pickups and the runs it takes off the transport's run queue.
4887
+ */
4888
+ localWork;
4889
+ /** The dispatcher the caller supplied, if any; without one the engine picks (see {@link dispatchRun}). */
4890
+ customDispatcher;
4891
+ /** Runs on their way to the transport's run queue (see {@link consumeRuns}). */
4892
+ runBatcher;
4893
+ /** Set while this engine consumes a transport's run queue, and so dispatches its runs to it. */
4894
+ dispatchesOverTransport = false;
4895
+ /** Stops for the run-queue consumers started by {@link consumeRuns}. */
4896
+ runConsumerStops = [];
4897
+ /** The consumers being stopped, so {@link drain} waits for their runs in flight. */
4898
+ runConsumersStopping;
4899
+ /**
4569
4900
  * The run lease of each execution in flight on THIS instance, by run id — renewed while it runs
4570
4901
  * and checked by every settle (the zombie fence, see {@link ExecutionLease}). Also what `drain()`
4571
4902
  * releases when its budget runs out.
@@ -4621,15 +4952,27 @@ var WorkflowEngine = class {
4621
4952
  this.rehydrate = deps.rehydrate ?? ((_carrier, fn) => fn());
4622
4953
  this.compensationRetries = Math.max(1, deps.compensationRetries ?? 1);
4623
4954
  this.trackStepStart = deps.trackStepStart ?? true;
4624
- this.runDispatcher = deps.runDispatcher ?? {
4625
- dispatch: /* @__PURE__ */ __name((runId) => queueMicrotask(() => void this.runOne(runId).catch(() => {
4626
- })), "dispatch")
4955
+ this.localWork = new BoundedRunner(LOCAL_RUN_CONCURRENCY);
4956
+ this.customDispatcher = deps.runDispatcher;
4957
+ this.runBatcher = new RunDispatchBatcher((runs) => this.pool.dispatchRuns(runs, {
4958
+ namespace: this.namespace
4959
+ }), (runs, err) => {
4960
+ for (const run of runs) {
4961
+ if (run.reason === "wake" && !this.draining) {
4962
+ this.inBackground(this.resumeImplicit(run.runId));
4963
+ }
4964
+ }
4965
+ console.error(`[nestjs-durable] could not queue ${runs.length} run(s) on the transport; starts stay pending for the next poll, wakes resume here:`, err);
4966
+ });
4967
+ this.runDispatcher = {
4968
+ dispatch: /* @__PURE__ */ __name((runId) => this.dispatchRun(runId), "dispatch")
4627
4969
  };
4628
4970
  this.singletons = new SingletonGate({
4629
4971
  store: this.store,
4630
4972
  clock: this.clock,
4631
4973
  dispatch: /* @__PURE__ */ __name((runId) => {
4632
- this.runDispatcher.dispatch(runId);
4974
+ if (this.queuesRunsOverTransport) void this.wake(runId);
4975
+ else this.runDispatcher.dispatch(runId);
4633
4976
  }, "dispatch"),
4634
4977
  configFor: /* @__PURE__ */ __name((run) => (this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? this.latest.get(run.workflow))?.singleton, "configFor")
4635
4978
  });
@@ -4754,13 +5097,224 @@ var WorkflowEngine = class {
4754
5097
  }
4755
5098
  /**
4756
5099
  * Be notified when a run is enqueued on ANOTHER instance (via the control plane), so a worker can
4757
- * pick it up at once — e.g. `engine.onEnqueued((runId) => engine.runOne(runId))`. Returns an
5100
+ * pick it up at once — e.g. `engine.onEnqueued((runId) => engine.runEnqueued(runId))`. Returns an
4758
5101
  * unsubscribe function. Only wire this on instances that should execute runs (workers).
4759
5102
  */
4760
5103
  onEnqueued(listener) {
4761
5104
  this.enqueuedListeners.add(listener);
4762
5105
  return () => this.enqueuedListeners.delete(listener);
4763
5106
  }
5107
+ /**
5108
+ * Pick up a run another instance announced as enqueued: run it here if it is still `pending`.
5109
+ * Bounded by {@link LOCAL_RUN_CONCURRENCY}, and a run another instance already took is skipped
5110
+ * after one read, instead of being leased and replayed here once more.
5111
+ */
5112
+ runEnqueued(runId) {
5113
+ this.localWork.submit(`run:${runId}`, () => this.runIfPending(runId));
5114
+ }
5115
+ /**
5116
+ * Consume the run queue of every transport that carries one ({@link Transport.dispatchRuns} and
5117
+ * {@link Transport.onDispatchedRun}), and from then on dispatch the runs this engine starts there
5118
+ * rather than executing them in-process: a run, a `ctx.all` fan's children included, executes on
5119
+ * whichever consuming instance takes it, at most {@link LOCAL_RUN_CONCURRENCY} at once per
5120
+ * instance. Call it on every instance that drives runs; the Nest module does on a driving operator.
5121
+ *
5122
+ * Returns a function that stops consuming and resolves once the runs in flight settled, after
5123
+ * which runs are executed in-process again; {@link beginDrain} stops consuming too. Undefined
5124
+ * when no transport carries a run queue: runs keep executing in-process.
5125
+ */
5126
+ consumeRuns() {
5127
+ if (!this.pool.dispatchesRuns) return void 0;
5128
+ if (this.runConsumerStops.length === 0) {
5129
+ this.runConsumerStops = this.pool.onDispatchedRun((run) => this.localWork.run(() => this.runFromQueue(run)), {
5130
+ concurrency: LOCAL_RUN_CONCURRENCY,
5131
+ namespace: this.namespace
5132
+ });
5133
+ }
5134
+ this.dispatchesOverTransport = true;
5135
+ return async () => {
5136
+ this.dispatchesOverTransport = false;
5137
+ await this.runBatcher.settle();
5138
+ await this.stopRunConsumers();
5139
+ };
5140
+ }
5141
+ /** Stop every run-queue consumer; resolves once their runs in flight settled. */
5142
+ stopRunConsumers() {
5143
+ if (this.runConsumerStops.length > 0) {
5144
+ const stops = this.runConsumerStops;
5145
+ this.runConsumerStops = [];
5146
+ this.runConsumersStopping = Promise.allSettled(stops.map((stop) => stop()));
5147
+ }
5148
+ return this.runConsumersStopping ?? Promise.resolve();
5149
+ }
5150
+ /** Whether {@link dispatchRun} queues runs on the transport (see {@link consumeRuns}). */
5151
+ get queuesRunsOverTransport() {
5152
+ return this.customDispatcher === void 0 && this.dispatchesOverTransport;
5153
+ }
5154
+ /**
5155
+ * Hand a `pending` run to whatever executes it: the caller's {@link WorkflowEngineDeps.runDispatcher};
5156
+ * else the transport's run queue while this engine consumes it, batched; else this instance,
5157
+ * bounded by {@link LOCAL_RUN_CONCURRENCY}. A run whose queueing fails stays `pending` for a
5158
+ * `runPending` poll.
5159
+ */
5160
+ dispatchRun(runId, priority, key) {
5161
+ if (this.customDispatcher) return this.customDispatcher.dispatch(runId);
5162
+ if (this.dispatchesOverTransport) {
5163
+ this.runBatcher.add({
5164
+ runId,
5165
+ reason: "start",
5166
+ ...priority !== void 0 ? {
5167
+ priority
5168
+ } : {},
5169
+ ...key !== void 0 ? {
5170
+ key
5171
+ } : {}
5172
+ });
5173
+ return;
5174
+ }
5175
+ this.localWork.submit(`run:${runId}`, () => this.runOne(runId));
5176
+ }
5177
+ /**
5178
+ * Wake a suspended run that has something new to see (a delivered signal or child completion, a
5179
+ * lease that came free, a freed slot): while this engine consumes the transport's run queue, on
5180
+ * whichever instance takes the wake (the result then only says it was handed over); otherwise
5181
+ * here, now.
5182
+ */
5183
+ wake(runId) {
5184
+ if (this.queuesRunsOverTransport) {
5185
+ this.runBatcher.add({
5186
+ runId,
5187
+ reason: "wake"
5188
+ });
5189
+ return Promise.resolve({
5190
+ runId,
5191
+ status: "suspended"
5192
+ });
5193
+ }
5194
+ return this.resumeImplicit(runId);
5195
+ }
5196
+ /**
5197
+ * Take a turn of a run taken off the transport's run queue. A `start` runs only while the run is
5198
+ * still `pending` (or `cancelling`, a recovered cancel): any other state means another instance
5199
+ * already took it. A `wake` leases and resumes the run unless it is finished. The run lease admits
5200
+ * one executor either way. Never rejects.
5201
+ */
5202
+ async runFromQueue(dispatch) {
5203
+ if (this.draining) return;
5204
+ const { runId } = dispatch;
5205
+ try {
5206
+ const run = await this.store.getRun(runId);
5207
+ if (!run) return;
5208
+ if (run.status === "pending" || run.status === "cancelling") {
5209
+ await this.runOne(runId);
5210
+ return;
5211
+ }
5212
+ if (dispatch.reason !== "wake" || TERMINAL_RUN_STATUSES.includes(run.status)) return;
5213
+ const nowMs = this.clock();
5214
+ if (run.wakeAt !== void 0 && run.wakeAt <= nowMs) {
5215
+ const waiters = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
5216
+ runId
5217
+ ]));
5218
+ const resumed = await this.resumeLeased([
5219
+ run
5220
+ ], nowMs, (locked) => this.reconcileSignalWaiter(locked, waiters.get(locked.id)));
5221
+ if (resumed.length === 0) this.recheckAfterLease(runId, run.lockedUntil);
5222
+ return;
5223
+ }
5224
+ await this.resumeImplicit(runId).catch((err) => {
5225
+ if (!(err instanceof NamespaceMismatch)) throw err;
5226
+ });
5227
+ } catch (error2) {
5228
+ console.error(`[nestjs-durable] could not run queued run ${runId}:`, error2);
5229
+ }
5230
+ }
5231
+ /**
5232
+ * Hand due runs (timers, blocked runs due for a fleet re-check) to whichever instance takes them
5233
+ * off the run queue. Each is claimed first, under its lease, by pushing its `wakeAt` one lease
5234
+ * period out, so the next poll (here or elsewhere) does not queue it again while it waits; a wake
5235
+ * that is lost brings it back due after that period. False when the transport refused the wakes,
5236
+ * so the caller resumes them itself.
5237
+ */
5238
+ async handOffDue(runs, nowMs) {
5239
+ const claimed = [];
5240
+ for (const run of runs) {
5241
+ if (this.draining) break;
5242
+ if (!await this.store.tryLockRun(run.id, this.instanceId, nowMs + this.leaseMs, nowMs)) {
5243
+ continue;
5244
+ }
5245
+ try {
5246
+ const patch = {
5247
+ wakeAt: nowMs + this.leaseMs
5248
+ };
5249
+ if (this.store.updateRunIf) {
5250
+ if (await this.store.updateRunIf(run.id, [
5251
+ run.status
5252
+ ], patch)) claimed.push(run);
5253
+ } else {
5254
+ await this.store.updateRun(run.id, patch);
5255
+ claimed.push(run);
5256
+ }
5257
+ } finally {
5258
+ await this.store.releaseRunLock(run.id);
5259
+ }
5260
+ }
5261
+ try {
5262
+ for (let at = 0; at < claimed.length; at += RUN_DISPATCH_BATCH) {
5263
+ await this.pool.dispatchRuns(claimed.slice(at, at + RUN_DISPATCH_BATCH).map((run) => ({
5264
+ runId: run.id,
5265
+ reason: "wake",
5266
+ ...run.priority !== void 0 ? {
5267
+ priority: run.priority
5268
+ } : {}
5269
+ })), {
5270
+ namespace: this.namespace
5271
+ });
5272
+ }
5273
+ return true;
5274
+ } catch (error2) {
5275
+ console.error("[nestjs-durable] could not queue due runs on the transport:", error2);
5276
+ return false;
5277
+ }
5278
+ }
5279
+ /**
5280
+ * Queue `runs` on the transport for whichever consuming instance takes them, in batches. False
5281
+ * when the transport refused them, so the caller executes them itself instead.
5282
+ */
5283
+ async queueOnTransport(runs) {
5284
+ try {
5285
+ for (let at = 0; at < runs.length; at += RUN_DISPATCH_BATCH) {
5286
+ await this.pool.dispatchRuns(runs.slice(at, at + RUN_DISPATCH_BATCH).map((run) => ({
5287
+ runId: run.id,
5288
+ reason: "start",
5289
+ key: pendingStateKey(run.updatedAt),
5290
+ ...run.priority !== void 0 ? {
5291
+ priority: run.priority
5292
+ } : {}
5293
+ })), {
5294
+ namespace: this.namespace
5295
+ });
5296
+ }
5297
+ return true;
5298
+ } catch (error2) {
5299
+ console.error("[nestjs-durable] could not queue runs on the transport; running them here:", error2);
5300
+ return false;
5301
+ }
5302
+ }
5303
+ /**
5304
+ * Execute a run announced by another instance, if it is still `pending`: a
5305
+ * run another instance already took is skipped after one read instead of being leased and
5306
+ * replayed here once more. Never rejects.
5307
+ */
5308
+ async runIfPending(runId) {
5309
+ if (this.draining) return;
5310
+ try {
5311
+ const run = await this.store.getRun(runId);
5312
+ if (run?.status !== "pending") return;
5313
+ await this.runOne(runId);
5314
+ } catch (error2) {
5315
+ console.error(`[nestjs-durable] could not run enqueued run ${runId}:`, error2);
5316
+ }
5317
+ }
4764
5318
  /** Fire cooperative-cancellation listeners for `runId` (a worker bridge aborts in-flight work). */
4765
5319
  notifyCancelled(runId) {
4766
5320
  for (const fn of this.cancelListeners) {
@@ -5156,8 +5710,8 @@ var WorkflowEngine = class {
5156
5710
  status: "suspended"
5157
5711
  };
5158
5712
  }
5159
- await this.runDispatcher.dispatch(runId);
5160
- if (this.controlPlane) {
5713
+ await this.dispatchRun(runId, run.priority, pendingStateKey(run.updatedAt));
5714
+ if (this.controlPlane && !this.queuesRunsOverTransport) {
5161
5715
  void this.controlPlane.publishControl({
5162
5716
  kind: "enqueued",
5163
5717
  runId,
@@ -5196,7 +5750,43 @@ var WorkflowEngine = class {
5196
5750
  * stale waiter or a late result left behind by a failed run silently re-executes its body.
5197
5751
  */
5198
5752
  resumeImplicit(runId) {
5199
- return this.resumeRun(runId, false);
5753
+ const turn = this.wakeTurns.get(runId);
5754
+ if (turn) {
5755
+ turn.again ??= deferred();
5756
+ if (currentWorkflowCtx()?.runId === runId) {
5757
+ return Promise.resolve({
5758
+ runId,
5759
+ status: "running"
5760
+ });
5761
+ }
5762
+ return turn.again.promise;
5763
+ }
5764
+ const mine = {};
5765
+ this.wakeTurns.set(runId, mine);
5766
+ const finish = /* @__PURE__ */ __name(() => {
5767
+ if (this.wakeTurns.get(runId) === mine) this.wakeTurns.delete(runId);
5768
+ const again = mine.again;
5769
+ if (!again) return;
5770
+ if (this.draining) {
5771
+ again.resolve({
5772
+ runId,
5773
+ status: "running"
5774
+ });
5775
+ return;
5776
+ }
5777
+ const next = this.resumeImplicit(runId);
5778
+ next.then(again.resolve, again.reject);
5779
+ this.inBackground(next);
5780
+ }, "finish");
5781
+ const result = this.resumeRun(runId, false);
5782
+ void result.then(finish, finish);
5783
+ return result;
5784
+ }
5785
+ /** Keep `work` in {@link background} until it settles; its failure is swallowed. */
5786
+ inBackground(work) {
5787
+ const tracked = work.catch(() => void 0);
5788
+ this.background.add(tracked);
5789
+ void tracked.then(() => this.background.delete(tracked));
5200
5790
  }
5201
5791
  /**
5202
5792
  * One execution of a run at a time in this process. A resume that lands while the run executes
@@ -5483,6 +6073,10 @@ var WorkflowEngine = class {
5483
6073
  */
5484
6074
  beginDrain(spentMs = 0) {
5485
6075
  this.draining = true;
6076
+ if (this.runConsumerStops.length > 0) {
6077
+ this.runBatcher.flush();
6078
+ void this.stopRunConsumers();
6079
+ }
5486
6080
  if (Number.isFinite(spentMs) && spentMs > 0) {
5487
6081
  this.drainSpentMs += spentMs;
5488
6082
  for (const rearm of this.drainRearms) rearm();
@@ -5503,7 +6097,14 @@ var WorkflowEngine = class {
5503
6097
  */
5504
6098
  async drain(timeoutMs = 1e4) {
5505
6099
  this.beginDrain();
5506
- if (this.inflight.size === 0) return;
6100
+ this.localWork.clear();
6101
+ for (const timer2 of this.leaseRetries.values()) clearTimeout(timer2);
6102
+ this.leaseRetries.clear();
6103
+ const consumers = this.runConsumersStopping;
6104
+ if (this.inflight.size === 0 && this.background.size === 0 && !consumers && this.runBatcher.idle) {
6105
+ return;
6106
+ }
6107
+ const queued = this.runBatcher.settle();
5507
6108
  const start = Date.now();
5508
6109
  let rearm = /* @__PURE__ */ __name(() => {
5509
6110
  }, "rearm");
@@ -5522,7 +6123,10 @@ var WorkflowEngine = class {
5522
6123
  try {
5523
6124
  await Promise.race([
5524
6125
  Promise.allSettled([
5525
- ...this.inflight
6126
+ ...this.inflight,
6127
+ ...this.background,
6128
+ queued,
6129
+ consumers
5526
6130
  ]).then(() => {
5527
6131
  settled = true;
5528
6132
  }),
@@ -5629,6 +6233,7 @@ var WorkflowEngine = class {
5629
6233
  error: error2
5630
6234
  });
5631
6235
  await this.cancelChildren(run.id).catch(() => void 0);
6236
+ this.notifyParent(run.id, cancelledCompletion(error2));
5632
6237
  }
5633
6238
  }
5634
6239
  }
@@ -5643,7 +6248,9 @@ var WorkflowEngine = class {
5643
6248
  if (this.draining) break;
5644
6249
  const outcome = this.isOwnLiveLease(listed, nowMs) ? await this.reclaimOwnLease(listed, nowMs) : await this.recoverOrphan(listed, nowMs);
5645
6250
  if (!outcome) continue;
5646
- if (outcome.dispatch) await this.runDispatcher.dispatch(outcome.result.runId);
6251
+ if (outcome.dispatch) {
6252
+ await this.dispatchRun(outcome.result.runId, listed.priority, freshPendingKey());
6253
+ }
5647
6254
  results.push(outcome.result);
5648
6255
  }
5649
6256
  return results;
@@ -5835,6 +6442,18 @@ var WorkflowEngine = class {
5835
6442
  const due = shuffle(await this.store.listDueTimers(nowMs, this.namespace, this.timerBatchSize));
5836
6443
  const blocked = await this.dueBlockedRuns(nowMs);
5837
6444
  if (due.length === 0 && blocked.length === 0) return [];
6445
+ if (this.queuesRunsOverTransport && !this.draining) {
6446
+ const runs = [
6447
+ ...due,
6448
+ ...blocked
6449
+ ];
6450
+ if (await this.handOffDue(runs, nowMs)) {
6451
+ return runs.map((run) => ({
6452
+ runId: run.id,
6453
+ status: run.status
6454
+ }));
6455
+ }
6456
+ }
5838
6457
  const waiterByRun = indexWaitersByRun(await listSignalWaitersOfRuns(this.store, [
5839
6458
  ...due,
5840
6459
  ...blocked
@@ -5868,7 +6487,7 @@ var WorkflowEngine = class {
5868
6487
  const buffered = await this.store.takeBufferedSignal(waiter.token);
5869
6488
  if (!buffered) return void 0;
5870
6489
  await this.store.removeSignalWaiter(waiter);
5871
- const settled = await this.deliverSignal(waiter, buffered.payload);
6490
+ const settled = await this.deliverSignal(waiter, buffered.payload, true);
5872
6491
  return settled ?? {
5873
6492
  runId: run.id,
5874
6493
  status: run.status
@@ -5894,7 +6513,7 @@ var WorkflowEngine = class {
5894
6513
  if (!eventMatches(candidate.payload, match)) continue;
5895
6514
  if (!await this.store.removeBufferedEvent(candidate.id)) continue;
5896
6515
  await this.store.removeSignalWaiter(waiter);
5897
- const settled = await this.deliverSignal(waiter, candidate.payload);
6516
+ const settled = await this.deliverSignal(waiter, candidate.payload, true);
5898
6517
  return settled ?? {
5899
6518
  runId: run.id,
5900
6519
  status: run.status
@@ -6014,7 +6633,7 @@ var WorkflowEngine = class {
6014
6633
  } : {},
6015
6634
  updatedAt: /* @__PURE__ */ new Date()
6016
6635
  });
6017
- await this.runDispatcher.dispatch(runId);
6636
+ await this.dispatchRun(runId, run.priority, freshPendingKey());
6018
6637
  return {
6019
6638
  runId,
6020
6639
  status: "pending"
@@ -6089,7 +6708,16 @@ var WorkflowEngine = class {
6089
6708
  * {@link resumeDueTimers}.
6090
6709
  */
6091
6710
  async runPending(nowMs = this.clock()) {
6092
- return this.resumeLeased(await this.store.listPendingRuns(100, this.namespace), nowMs);
6711
+ const pending = await this.store.listPendingRuns(100, this.namespace);
6712
+ if (this.queuesRunsOverTransport && !this.draining && pending.length > 0) {
6713
+ if (await this.queueOnTransport(pending)) {
6714
+ return pending.map((run) => ({
6715
+ runId: run.id,
6716
+ status: run.status
6717
+ }));
6718
+ }
6719
+ }
6720
+ return this.resumeLeased(pending, nowMs);
6093
6721
  }
6094
6722
  /**
6095
6723
  * Resolve once `runId` reaches a settled state — terminal (completed/failed/cancelled/dead) or
@@ -6266,7 +6894,7 @@ var WorkflowEngine = class {
6266
6894
  * due-timer reconcile sweep ({@link reconcileSignalWaiter}) — all three resolve a waiter the exact
6267
6895
  * same way.
6268
6896
  */
6269
- async deliverSignal(waiter, payload) {
6897
+ async deliverSignal(waiter, payload, resumeHere = false) {
6270
6898
  await this.store.saveCheckpoint(instantCheckpoint({
6271
6899
  runId: waiter.runId,
6272
6900
  seq: waiter.seq,
@@ -6285,7 +6913,7 @@ var WorkflowEngine = class {
6285
6913
  status: "suspended"
6286
6914
  };
6287
6915
  }
6288
- return this.resumeImplicit(waiter.runId).catch((err) => {
6916
+ return (resumeHere ? this.resumeImplicit(waiter.runId) : this.wake(waiter.runId)).catch((err) => {
6289
6917
  if (err instanceof NamespaceMismatch) return null;
6290
6918
  throw err;
6291
6919
  });
@@ -6372,6 +7000,101 @@ var WorkflowEngine = class {
6372
7000
  return;
6373
7001
  }
6374
7002
  }
7003
+ /**
7004
+ * Deliver the outcome of every awaited child that already finished but whose completion never
7005
+ * reached this run, into `checkpoints` (this turn's snapshot) and the store.
7006
+ *
7007
+ * A child persists its terminal status first and tells its parent after ({@link notifyParent}). A
7008
+ * crash, a scale-down or a store error between the two leaves the parent's `running` placeholder
7009
+ * and `child:<id>` waiter behind for a child that is done, and nothing else would ever settle
7010
+ * them: the parent's reconcile wake replays straight back to the same wait. So each turn looks up
7011
+ * the children of its `running` placeholders (one batched read per 500) and settles the finished
7012
+ * ones exactly as the child's own notification would have: the `signal:child:<id>` checkpoint
7013
+ * with the child's completion, then the waiter removed. A notification arriving late writes the
7014
+ * same checkpoint and finds no waiter. Best effort: a store error leaves the rest to the next turn.
7015
+ */
7016
+ async deliverFinishedChildren(runId, checkpoints) {
7017
+ const awaited = [];
7018
+ for (const cp of checkpoints) {
7019
+ if (cp.kind !== "signal" || cp.status !== "running") continue;
7020
+ if (!cp.name.startsWith(CHILD_SIGNAL_PREFIX)) continue;
7021
+ awaited.push({
7022
+ childId: cp.name.slice(CHILD_SIGNAL_PREFIX.length),
7023
+ seq: cp.seq,
7024
+ parallelGroup: cp.parallelGroup
7025
+ });
7026
+ }
7027
+ await this.settleFinishedChildren(runId, awaited, checkpoints);
7028
+ }
7029
+ /**
7030
+ * {@link deliverFinishedChildren} for a remote workflow, whose awaited children (`startChild`
7031
+ * commands) leave a `child:<id>` waiter but no placeholder checkpoint: the run's own waiters say
7032
+ * which children it is waiting on. One indexed read per turn of a remote run.
7033
+ */
7034
+ async deliverFinishedChildWaiters(runId, checkpoints) {
7035
+ let waiters;
7036
+ try {
7037
+ waiters = await listSignalWaitersOfRuns(this.store, [
7038
+ runId
7039
+ ]);
7040
+ } catch {
7041
+ return;
7042
+ }
7043
+ const settled = new Set(checkpoints.filter((cp) => cp.status === "completed").map((cp) => cp.seq));
7044
+ const awaited = waiters.filter((w) => w.token.startsWith("child:") && !settled.has(w.seq)).map((w) => ({
7045
+ childId: w.token.slice("child:".length),
7046
+ seq: w.seq,
7047
+ parallelGroup: w.parallelGroup
7048
+ }));
7049
+ await this.settleFinishedChildren(runId, awaited, checkpoints);
7050
+ }
7051
+ /**
7052
+ * Settle every child in `awaited` that has finished: its `signal:child:<id>` checkpoint with the
7053
+ * child's completion, then its waiter removed, exactly as the child's own notification does, and
7054
+ * the checkpoint put into `checkpoints`. Children are read in batches of 500.
7055
+ */
7056
+ async settleFinishedChildren(runId, awaited, checkpoints) {
7057
+ if (awaited.length === 0) return;
7058
+ try {
7059
+ const { runs: children } = await readRunsByIds(this.store, awaited.map((a) => a.childId));
7060
+ const finished = awaited.flatMap((a) => {
7061
+ const child = children.get(a.childId);
7062
+ const completion = child ? childCompletion(child) : void 0;
7063
+ return completion ? [
7064
+ {
7065
+ ...a,
7066
+ completion
7067
+ }
7068
+ ] : [];
7069
+ });
7070
+ for (let at = 0; at < finished.length; at += DELIVER_CHUNK) {
7071
+ await Promise.all(finished.slice(at, at + DELIVER_CHUNK).map(async (a) => {
7072
+ const settled = instantCheckpoint({
7073
+ runId,
7074
+ seq: a.seq,
7075
+ name: `${CHILD_SIGNAL_PREFIX}${a.childId}`,
7076
+ kind: "signal",
7077
+ output: a.completion,
7078
+ parallelGroup: a.parallelGroup
7079
+ });
7080
+ await this.store.saveCheckpoint(settled);
7081
+ await this.store.removeSignalWaiter({
7082
+ token: `child:${a.childId}`,
7083
+ runId,
7084
+ seq: a.seq,
7085
+ ...a.parallelGroup !== void 0 ? {
7086
+ parallelGroup: a.parallelGroup
7087
+ } : {}
7088
+ });
7089
+ const index = checkpoints.findIndex((cp) => cp.seq === a.seq);
7090
+ if (index === -1) checkpoints.push(settled);
7091
+ else checkpoints[index] = settled;
7092
+ }));
7093
+ }
7094
+ checkpoints.sort((a, b) => a.seq - b.seq);
7095
+ } catch {
7096
+ }
7097
+ }
6375
7098
  async redriveSettledChildren(runId, fan) {
6376
7099
  if (!this.store.hasSignalWaiter) return;
6377
7100
  for (const wait of fan.waits) {
@@ -6470,13 +7193,63 @@ var WorkflowEngine = class {
6470
7193
  * knowing about the child feature.
6471
7194
  */
6472
7195
  notifyParent(runId, completion) {
6473
- void this.signal(`child:${runId}`, completion).catch(() => void 0);
7196
+ this.inBackground(this.deliverToParent(`child:${runId}`, completion));
6474
7197
  const at = runId.lastIndexOf("~retry~");
6475
7198
  if (at !== -1 && completion.ok) {
6476
- void this.signal(`child:${runId.slice(0, at)}`, completion).catch(() => void 0);
7199
+ this.inBackground(this.deliverToParent(`child:${runId.slice(0, at)}`, completion));
6477
7200
  }
6478
7201
  }
6479
7202
  /**
7203
+ * {@link signal} a child's completion to its parent, retrying a store error a few times. The child
7204
+ * is already persisted terminal, so a failure here would leave the parent waiting on a child that
7205
+ * finished; the parent's own turns reconcile that ({@link deliverFinishedChildren}), but only on
7206
+ * its next wake. A waiter already taken is delivered to directly on a retry, so the completion is
7207
+ * not buffered for a waiter that no longer exists.
7208
+ */
7209
+ async deliverToParent(token, completion) {
7210
+ let waiter;
7211
+ const cancelled = !completion.ok && completion.code === CHILD_CANCELLED;
7212
+ for (let attempt = 1; ; attempt += 1) {
7213
+ try {
7214
+ if (waiter === void 0) waiter = await this.store.takeSignalWaiter(token);
7215
+ if (waiter) {
7216
+ const parent = cancelled ? await this.store.getRun(waiter.runId) : void 0;
7217
+ if (parent && TERMINAL_RUN_STATUSES.includes(parent.status)) return;
7218
+ await this.deliverSignal(waiter, completion);
7219
+ return;
7220
+ }
7221
+ if (cancelled || await this.parentHoldsCompletion(token, completion)) return;
7222
+ await this.signal(token, completion);
7223
+ return;
7224
+ } catch (err) {
7225
+ if (err instanceof NamespaceMismatch) return;
7226
+ if (attempt >= PARENT_NOTIFY_ATTEMPTS || this.draining) return;
7227
+ await new Promise((resolve) => {
7228
+ const timer = setTimeout(resolve, PARENT_NOTIFY_BACKOFF_MS * 4 ** (attempt - 1));
7229
+ timer.unref?.();
7230
+ });
7231
+ }
7232
+ }
7233
+ }
7234
+ /**
7235
+ * Whether the parent of the child behind `token` (`child:<id>`) already holds `completion` as that
7236
+ * child's settled `signal:child:<id>` checkpoint: its own turn settled the child from the child's
7237
+ * row, so this notification is a duplicate. A different completion (a fix-and-replay run's success
7238
+ * for a child the parent saw fail) is not, and is buffered for the parent's retry. Only for a child
7239
+ * whose id the engine minted (its parent is derivable from it).
7240
+ */
7241
+ async parentHoldsCompletion(token, completion) {
7242
+ const childId = token.slice("child:".length);
7243
+ const parentId = parentRunIdOf(childId);
7244
+ if (!parentId) return false;
7245
+ const name = `${CHILD_SIGNAL_PREFIX}${childId}`;
7246
+ const checkpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parentId, [
7247
+ name
7248
+ ]) : await this.store.listCheckpoints(parentId);
7249
+ const expected = JSON.stringify(completion);
7250
+ return checkpoints.some((cp) => cp.name === name && cp.status === "completed" && JSON.stringify(cp.output) === expected);
7251
+ }
7252
+ /**
6480
7253
  * Deferred child start shared by the in-process ctx host and the remote `startChild` command.
6481
7254
  * Deferred (microtask) so a fast child can't reentrantly resume a still-suspending parent. A start
6482
7255
  * that THROWS (unregistered/unroutable workflow, input validation, singleton back-pressure, store
@@ -6489,13 +7262,16 @@ var WorkflowEngine = class {
6489
7262
  * correctly observes the failed start.
6490
7263
  */
6491
7264
  startChildDeferred(workflow, input, childId, opts) {
6492
- queueMicrotask(() => void this.start(workflow, input, childId, opts).catch((err) => {
6493
- const message = err instanceof Error ? err.message : String(err);
6494
- this.notifyParent(childId, {
6495
- ok: false,
6496
- error: `child workflow "${workflow}" failed to start: ${message}`
7265
+ this.localWork.submit(`start:${childId}`, async () => {
7266
+ if (this.draining) return;
7267
+ await this.start(workflow, input, childId, opts).catch((err) => {
7268
+ const message = err instanceof Error ? err.message : String(err);
7269
+ this.notifyParent(childId, {
7270
+ ok: false,
7271
+ error: `child workflow "${workflow}" failed to start: ${message}`
7272
+ });
6497
7273
  });
6498
- }));
7274
+ });
6499
7275
  }
6500
7276
  /**
6501
7277
  * Cancel a run (e.g. from the dashboard). Returns null if the run does not exist. Pass
@@ -6559,6 +7335,7 @@ var WorkflowEngine = class {
6559
7335
  error: error2
6560
7336
  });
6561
7337
  await this.cancelChildren(runId, opts);
7338
+ this.notifyParent(runId, cancelledCompletion(error2));
6562
7339
  this.notifyCancelled(runId);
6563
7340
  if (this.controlPlane) {
6564
7341
  void this.controlPlane.publishControl({
@@ -7073,6 +7850,7 @@ var WorkflowEngine = class {
7073
7850
  const lockNow = this.clock();
7074
7851
  if (run.lockedBy !== this.instanceId || (run.lockedUntil ?? 0) <= lockNow) {
7075
7852
  if (!await this.store.tryLockRun(run.id, this.instanceId, lockNow + this.leaseMs, lockNow)) {
7853
+ this.recheckAfterLease(run.id, run.lockedUntil);
7076
7854
  return {
7077
7855
  runId: run.id,
7078
7856
  status: run.status
@@ -7091,6 +7869,51 @@ var WorkflowEngine = class {
7091
7869
  }
7092
7870
  }
7093
7871
  /**
7872
+ * A wake that found the run leased by another instance is dropped, and normally that is fine: the
7873
+ * holder's turn either sees what the wake wrote, or re-checks once it releases the lease
7874
+ * ({@link redriveSettledSteps}, {@link redriveSettledChildren}). Not if the holder goes down first
7875
+ * (a scale-down mid-turn, an OOM): its lease lapses with the run `suspended` on a step that has
7876
+ * finished, and nothing wakes it before its timer.
7877
+ *
7878
+ * So the dropped wake is kept here: once the lease is free (looked at every
7879
+ * {@link LEASE_RECHECK_MAX_MS} at most, for a bounded number of times), a run still `suspended` is
7880
+ * leased and resumed. A holder that already moved the run on makes this one spare replay that
7881
+ * re-parks. Best effort; the run's timer remains the fallback if this instance goes down too.
7882
+ */
7883
+ recheckAfterLease(runId, lockedUntil, attempt = 1) {
7884
+ if (this.draining || this.leaseRetries.has(runId)) return;
7885
+ const remaining = Math.max(0, (lockedUntil ?? this.clock()) - this.clock());
7886
+ const timer = setTimeout(() => {
7887
+ this.leaseRetries.delete(runId);
7888
+ this.inBackground(this.retryDroppedWake(runId, attempt));
7889
+ }, Math.min(remaining, LEASE_RECHECK_MAX_MS) + LEASE_RECHECK_SLACK_MS);
7890
+ timer.unref?.();
7891
+ this.leaseRetries.set(runId, timer);
7892
+ }
7893
+ async retryDroppedWake(runId, attempt) {
7894
+ if (this.draining) return;
7895
+ const run = await this.store.getRun(runId);
7896
+ if (run?.status !== "suspended") return;
7897
+ const nowMs = this.clock();
7898
+ if (run.lockedUntil !== void 0 && run.lockedUntil > nowMs) {
7899
+ if (attempt < LEASE_RECHECK_ATTEMPTS) this.recheckAfterLease(runId, run.lockedUntil, attempt + 1);
7900
+ return;
7901
+ }
7902
+ if (this.queuesRunsOverTransport) {
7903
+ await this.wake(runId);
7904
+ return;
7905
+ }
7906
+ if (!await this.store.tryLockRun(runId, this.instanceId, nowMs + this.leaseMs, nowMs)) return;
7907
+ const fresh = await this.store.getRun(runId);
7908
+ if (fresh?.status !== "suspended") {
7909
+ await this.store.releaseRunLock(runId);
7910
+ return;
7911
+ }
7912
+ await this.resumeImplicit(runId).catch((err) => {
7913
+ if (!(err instanceof NamespaceMismatch)) throw err;
7914
+ });
7915
+ }
7916
+ /**
7094
7917
  * Renew `runId`'s lease for as long as this execution holds it, and register it in
7095
7918
  * {@link executionLeases} so settles can check the zombie fence and `drain()` can release it.
7096
7919
  *
@@ -7454,6 +8277,7 @@ ${run.workflowVersion}`;
7454
8277
  namespace: run.namespace,
7455
8278
  error: error2
7456
8279
  });
8280
+ this.notifyParent(run.id, cancelledCompletion(error2));
7457
8281
  this.notifyCancelled(run.id);
7458
8282
  if (this.controlPlane) {
7459
8283
  void this.controlPlane.publishControl({
@@ -7485,6 +8309,8 @@ ${run.workflowVersion}`;
7485
8309
  const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
7486
8310
  if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
7487
8311
  const checkpoints = await this.store.listCheckpoints(run.id);
8312
+ await this.deliverFinishedChildren(run.id, checkpoints);
8313
+ await this.deliverFinishedChildWaiters(run.id, checkpoints);
7488
8314
  const handoff = liveHandoff(run.id, checkpoints);
7489
8315
  if (handoff) {
7490
8316
  const result = await this.settleRun(run, await this.followHandoff(run, handoff));
@@ -8059,6 +8885,7 @@ ${run.workflowVersion}`;
8059
8885
  namespace: run.namespace,
8060
8886
  error: error2
8061
8887
  });
8888
+ this.notifyParent(run.id, cancelledCompletion(error2));
8062
8889
  return {
8063
8890
  runId: run.id,
8064
8891
  status: "cancelled",
@@ -8105,6 +8932,7 @@ ${run.workflowVersion}`;
8105
8932
  }
8106
8933
  const compensations = [];
8107
8934
  const snapshot = await this.store.listCheckpoints(run.id);
8935
+ await this.deliverFinishedChildren(run.id, snapshot);
8108
8936
  const replay = /* @__PURE__ */ new Map();
8109
8937
  for (const cp of snapshot) replay.set(cp.seq, cp);
8110
8938
  let suspendedOnFan;
@@ -8201,13 +9029,13 @@ ${run.workflowVersion}`;
8201
9029
  if (this.awaitedStepFans.get(run.id) === awaitedFans) this.awaitedStepFans.delete(run.id);
8202
9030
  await this.releaseLeaseBestEffort(run.id);
8203
9031
  if (suspendedOnFan) {
8204
- void this.redriveSettledChildren(run.id, suspendedOnFan).catch(() => void 0);
9032
+ this.inBackground(this.redriveSettledChildren(run.id, suspendedOnFan));
8205
9033
  }
8206
9034
  if (suspendedOnSteps && awaitedSteps.size > 0) {
8207
- void this.redriveSettledSteps(run.id, awaitedSteps).catch(() => void 0);
9035
+ this.inBackground(this.redriveSettledSteps(run.id, awaitedSteps));
8208
9036
  }
8209
9037
  if (suspendedOnSteps && awaitedFans.length > 0) {
8210
- void this.redriveSettledStepFans(run.id, awaitedFans).catch(() => void 0);
9038
+ this.inBackground(this.redriveSettledStepFans(run.id, awaitedFans));
8211
9039
  }
8212
9040
  }
8213
9041
  }
@@ -8647,9 +9475,11 @@ ${run.workflowVersion}`;
8647
9475
  })) {
8648
9476
  return this.callRemote(runId, seq, step, input, queue, transport, void 0, admission, routing);
8649
9477
  }
8650
- throw new WorkflowSuspended(wakeAt);
9478
+ throw new WorkflowSuspended(this.stepLeaseWakeAt(wakeAt));
9479
+ }
9480
+ if (this.clock() < existing.wakeAt) {
9481
+ throw new WorkflowSuspended(this.stepLeaseWakeAt(existing.wakeAt));
8651
9482
  }
8652
- if (this.clock() < existing.wakeAt) throw new WorkflowSuspended(existing.wakeAt);
8653
9483
  if (existing.attempts >= this.remoteRedispatchMax) {
8654
9484
  throw new RemoteStepError({
8655
9485
  message: `remote step "${step.name}" lost \u2014 no result after ${existing.attempts} re-dispatch(es)`,
@@ -8695,7 +9525,7 @@ ${run.workflowVersion}`;
8695
9525
  kind: "remote",
8696
9526
  redispatched: true
8697
9527
  });
8698
- throw new WorkflowSuspended(nextDeadline);
9528
+ throw new WorkflowSuspended(this.stepLeaseWakeAt(nextDeadline));
8699
9529
  }
8700
9530
  let attempt = 1;
8701
9531
  if (existing?.status === "failed") {
@@ -9058,6 +9888,7 @@ ${run.workflowVersion}`;
9058
9888
  });
9059
9889
  const reads = await Promise.allSettled(live.map((i) => this.store.getCheckpoint(runId, results[i].seq)));
9060
9890
  const pending = [];
9891
+ const resettled = [];
9061
9892
  reads.forEach((read, k) => {
9062
9893
  const index = live[k];
9063
9894
  if (read.status === "rejected") outcomes[index] = {
@@ -9067,8 +9898,17 @@ ${run.workflowVersion}`;
9067
9898
  index,
9068
9899
  cp: read.value
9069
9900
  });
9901
+ else if (read.value?.status === "completed" || read.value?.status === "failed") {
9902
+ resettled.push({
9903
+ index,
9904
+ cp: read.value
9905
+ });
9906
+ }
9070
9907
  });
9071
- if (pending.length === 0) return outcomes;
9908
+ if (pending.length === 0) {
9909
+ if (resettled.length === 0) return outcomes;
9910
+ return this.resumeForRedelivered(runId, resettled, outcomes);
9911
+ }
9072
9912
  for (const { cp } of pending) void this.releaseQueueSlot(cp.stepId).catch(() => void 0);
9073
9913
  const run = await this.store.getRun(runId);
9074
9914
  if (run && (run.status === "cancelled" || run.status === "completed")) return outcomes;
@@ -9120,6 +9960,29 @@ ${run.workflowVersion}`;
9120
9960
  return outcomes;
9121
9961
  }
9122
9962
  /**
9963
+ * The outcomes for a batch of redelivered results (their steps already settled): one of them
9964
+ * resumes the run if it is `suspended` and, for a step of a fan, the fan's barrier is open. A
9965
+ * resume of a run that had in fact moved on replays to its current wait and re-parks.
9966
+ */
9967
+ async resumeForRedelivered(runId, resettled, outcomes) {
9968
+ const run = await this.store.getRun(runId);
9969
+ if (run?.status !== "suspended") return outcomes;
9970
+ const barriers = /* @__PURE__ */ new Map();
9971
+ for (const { index, cp } of resettled) {
9972
+ const asResult = {
9973
+ runId,
9974
+ seq: cp.seq,
9975
+ stepId: cp.stepId ?? stepId(runId, cp.seq),
9976
+ status: cp.status === "failed" ? "failed" : "completed"
9977
+ };
9978
+ if (!await this.stepFanStillRunning(cp, asResult, barriers)) {
9979
+ outcomes[index] = true;
9980
+ break;
9981
+ }
9982
+ }
9983
+ return outcomes;
9984
+ }
9985
+ /**
9123
9986
  * The join barrier of a step fan: a result for one of its steps resumes the parent only once no
9124
9987
  * step of the fan is still `pending` (a settled wave), or on a failure (failFast surfaces it, and a
9125
9988
  * retry needs the parent to schedule it). Without it every result would replay the whole parent,
@@ -9243,7 +10106,7 @@ ${run.workflowVersion}`;
9243
10106
  ...waiters
9244
10107
  ];
9245
10108
  waiters.clear();
9246
- for (const runId of runIds) void this.resumeImplicit(runId).catch(() => void 0);
10109
+ for (const runId of runIds) void this.wake(runId).catch(() => void 0);
9247
10110
  }
9248
10111
  /** Release the flow-control slot a dispatched step held (if any), by its stepId. */
9249
10112
  async releaseQueueSlot(id) {
@@ -9985,6 +10848,8 @@ var CodecStateStore = class {
9985
10848
  registerChildWaits;
9986
10849
  /** The folded checkpoint carries the children's outputs, so it is encoded. */
9987
10850
  compactCheckpoints;
10851
+ /** Decodes each run like {@link getRun}. Bound only when the inner store has it. */
10852
+ getRuns;
9988
10853
  /** Forwarded verbatim — a waiter carries no payload. */
9989
10854
  listSignalWaitersByRun;
9990
10855
  /** Forwarded verbatim — a waiter carries no payload. */
@@ -10028,6 +10893,8 @@ var CodecStateStore = class {
10028
10893
  })));
10029
10894
  const compact = inner.compactCheckpoints;
10030
10895
  if (compact) this.compactCheckpoints = (runId, fromSeq, toSeq, into) => compact.call(inner, runId, fromSeq, toSeq, this.encCp(into));
10896
+ const getRuns = inner.getRuns;
10897
+ if (getRuns) this.getRuns = async (runIds) => (await getRuns.call(inner, runIds)).map((r) => this.decRun(r));
10031
10898
  const byRun = inner.listSignalWaitersByRun;
10032
10899
  if (byRun) this.listSignalWaitersByRun = (runIds) => byRun.call(inner, runIds);
10033
10900
  const hasWaiter = inner.hasSignalWaiter;
@@ -10580,6 +11447,16 @@ var InMemoryStateStore = class {
10580
11447
  ...run
10581
11448
  } : null;
10582
11449
  }
11450
+ async getRuns(runIds) {
11451
+ const out = [];
11452
+ for (const id of new Set(runIds)) {
11453
+ const run = this.runs.get(id);
11454
+ if (run) out.push({
11455
+ ...run
11456
+ });
11457
+ }
11458
+ return out;
11459
+ }
10583
11460
  async deleteRun(runId) {
10584
11461
  this.runs.delete(runId);
10585
11462
  for (const [key, cp] of this.checkpoints) {
@@ -11031,6 +11908,99 @@ var InMemoryTransport = class {
11031
11908
  this.controlHandlers.add(handler);
11032
11909
  }
11033
11910
  };
11911
+
11912
+ // src/testing/in-memory-run-queue.ts
11913
+ var InMemoryRunQueue = class {
11914
+ static {
11915
+ __name(this, "InMemoryRunQueue");
11916
+ }
11917
+ lanes = /* @__PURE__ */ new Map();
11918
+ /** A transport on this queue. */
11919
+ transport() {
11920
+ return {
11921
+ dispatch: /* @__PURE__ */ __name(async (_task) => {
11922
+ throw new Error("InMemoryRunQueue carries no remote steps");
11923
+ }, "dispatch"),
11924
+ onResult: /* @__PURE__ */ __name((_handler) => {
11925
+ }, "onResult"),
11926
+ onHeartbeat: /* @__PURE__ */ __name((_handler) => {
11927
+ }, "onHeartbeat"),
11928
+ dispatchRuns: /* @__PURE__ */ __name(async (runs, opts) => this.add(runs, opts), "dispatchRuns"),
11929
+ onDispatchedRun: /* @__PURE__ */ __name((handler, opts) => this.consume(handler, opts), "onDispatchedRun")
11930
+ };
11931
+ }
11932
+ /** Runs waiting in the queue of `namespace`. */
11933
+ waiting(namespace) {
11934
+ return (this.lanes.get(namespace ?? "")?.waiting ?? []).map((r) => r.runId);
11935
+ }
11936
+ lane(namespace) {
11937
+ const key = namespace ?? "";
11938
+ let lane = this.lanes.get(key);
11939
+ if (!lane) {
11940
+ lane = {
11941
+ waiting: [],
11942
+ queued: /* @__PURE__ */ new Map(),
11943
+ consumers: [],
11944
+ next: 0
11945
+ };
11946
+ this.lanes.set(key, lane);
11947
+ }
11948
+ return lane;
11949
+ }
11950
+ add(runs, opts) {
11951
+ const lane = this.lane(opts?.namespace);
11952
+ for (const run of runs) {
11953
+ const waiting = lane.queued.get(run.runId);
11954
+ if (waiting) {
11955
+ if (run.reason === "wake") waiting.reason = "wake";
11956
+ continue;
11957
+ }
11958
+ const entry = {
11959
+ ...run
11960
+ };
11961
+ lane.queued.set(run.runId, entry);
11962
+ lane.waiting.push(entry);
11963
+ }
11964
+ lane.waiting.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
11965
+ setImmediate(() => this.pump(lane));
11966
+ }
11967
+ consume(handler, opts) {
11968
+ const lane = this.lane(opts.namespace);
11969
+ const consumer = {
11970
+ handler,
11971
+ concurrency: Math.max(1, opts.concurrency),
11972
+ active: 0,
11973
+ stopped: false,
11974
+ idle: []
11975
+ };
11976
+ lane.consumers.push(consumer);
11977
+ setImmediate(() => this.pump(lane));
11978
+ return () => {
11979
+ consumer.stopped = true;
11980
+ lane.consumers = lane.consumers.filter((c) => c !== consumer);
11981
+ if (consumer.active === 0) return Promise.resolve();
11982
+ return new Promise((resolve) => consumer.idle.push(resolve));
11983
+ };
11984
+ }
11985
+ pump(lane) {
11986
+ while (lane.waiting.length > 0) {
11987
+ const free = lane.consumers.filter((c) => !c.stopped && c.active < c.concurrency);
11988
+ if (free.length === 0) return;
11989
+ const consumer = free[lane.next % free.length];
11990
+ lane.next += 1;
11991
+ const run = lane.waiting.shift();
11992
+ lane.queued.delete(run.runId);
11993
+ consumer.active += 1;
11994
+ void consumer.handler(run).catch(() => void 0).then(() => {
11995
+ consumer.active -= 1;
11996
+ if (consumer.active === 0 && consumer.stopped) {
11997
+ for (const resolve of consumer.idle.splice(0)) resolve();
11998
+ }
11999
+ this.pump(lane);
12000
+ });
12001
+ }
12002
+ }
12003
+ };
11034
12004
  // Annotate the CommonJS export names for ESM import in node:
11035
12005
  0 && (module.exports = {
11036
12006
  CHILD_EDGE_PREFIXES,
@@ -11058,6 +12028,7 @@ var InMemoryTransport = class {
11058
12028
  GatherError,
11059
12029
  HANDOFF_SEQ_BASE,
11060
12030
  InMemoryAdmissionBackend,
12031
+ InMemoryRunQueue,
11061
12032
  InMemoryStateStore,
11062
12033
  InMemoryTransport,
11063
12034
  LEGACY_V1_CAPABILITIES,
@@ -11142,6 +12113,7 @@ var InMemoryTransport = class {
11142
12113
  fanPageEtag,
11143
12114
  findStalledRuns,
11144
12115
  foldedCount,
12116
+ getRunsByIds,
11145
12117
  handoffHandlerId,
11146
12118
  handoffName,
11147
12119
  hasDurableExecutionWrappers,
@@ -11179,6 +12151,7 @@ var InMemoryTransport = class {
11179
12151
  poolNamespace,
11180
12152
  prevCronFireMs,
11181
12153
  queryCheckpointsOf,
12154
+ readRunsByIds,
11182
12155
  readSearchAttributes,
11183
12156
  requiredCapabilities,
11184
12157
  resolveConcurrency,