@dudousxd/nestjs-durable-core 0.72.1 → 0.73.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 +150 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -5
- package/dist/index.d.ts +35 -5
- package/dist/index.js +145 -62
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -56,6 +56,8 @@ __export(index_exports, {
|
|
|
56
56
|
RemoteWorkflowExecutor: () => RemoteWorkflowExecutor,
|
|
57
57
|
RemoteWorkflowTimeout: () => RemoteWorkflowTimeout,
|
|
58
58
|
RunGateway: () => RunGateway,
|
|
59
|
+
SINGLETON_ACTIVE_STATUSES: () => SINGLETON_ACTIVE_STATUSES,
|
|
60
|
+
SINGLETON_ADMITTED_TAG: () => SINGLETON_ADMITTED_TAG,
|
|
59
61
|
STATE_STORE: () => STATE_STORE,
|
|
60
62
|
STATE_STORE_CANONICAL: () => STATE_STORE_CANONICAL,
|
|
61
63
|
ScheduleAlreadyExistsError: () => ScheduleAlreadyExistsError,
|
|
@@ -86,6 +88,7 @@ __export(index_exports, {
|
|
|
86
88
|
capableWorkers: () => capableWorkers,
|
|
87
89
|
classifyWaiterToken: () => classifyWaiterToken,
|
|
88
90
|
clearDurableExecutionWrappers: () => clearDurableExecutionWrappers,
|
|
91
|
+
clearTerminalSingletonAdmission: () => clearTerminalSingletonAdmission,
|
|
89
92
|
collectMetrics: () => collectMetrics,
|
|
90
93
|
concurrencyTag: () => concurrencyTag,
|
|
91
94
|
controlPlaneDescriptor: () => controlPlaneDescriptor,
|
|
@@ -143,6 +146,8 @@ __export(index_exports, {
|
|
|
143
146
|
runValueFacetsFromRuns: () => runValueFacetsFromRuns,
|
|
144
147
|
sanitizeQueueToken: () => sanitizeQueueToken,
|
|
145
148
|
scheduledRunId: () => scheduledRunId,
|
|
149
|
+
singletonAdmissionDenialPatch: () => singletonAdmissionDenialPatch,
|
|
150
|
+
singletonAdmissionTags: () => singletonAdmissionTags,
|
|
146
151
|
sqlComparator: () => sqlComparator,
|
|
147
152
|
stepConfigOf: () => stepConfigOf,
|
|
148
153
|
stepId: () => stepId,
|
|
@@ -2444,6 +2449,54 @@ function errorMessage(error2) {
|
|
|
2444
2449
|
}
|
|
2445
2450
|
__name(errorMessage, "errorMessage");
|
|
2446
2451
|
|
|
2452
|
+
// src/singleton-admission.ts
|
|
2453
|
+
var SINGLETON_ADMITTED_TAG = "durable:singleton:admitted";
|
|
2454
|
+
var SINGLETON_ACTIVE_STATUSES = [
|
|
2455
|
+
"pending",
|
|
2456
|
+
"running",
|
|
2457
|
+
"suspended",
|
|
2458
|
+
"blocked",
|
|
2459
|
+
"cancelling"
|
|
2460
|
+
];
|
|
2461
|
+
function singletonAdmissionTags(runs, runId, tag, workflow, limit) {
|
|
2462
|
+
if (!Number.isInteger(limit) || limit < 1) throw new Error("singleton limit must be a positive integer");
|
|
2463
|
+
const active = runs.filter((run) => run.workflow === workflow && SINGLETON_ACTIVE_STATUSES.includes(run.status) && run.tags?.includes(tag));
|
|
2464
|
+
const candidate = active.find((run) => run.id === runId);
|
|
2465
|
+
if (!candidate) return null;
|
|
2466
|
+
if (candidate.tags?.includes(SINGLETON_ADMITTED_TAG)) return candidate.tags;
|
|
2467
|
+
if (candidate.status === "cancelling") return null;
|
|
2468
|
+
const holders = active.filter((run) => run.tags?.includes(SINGLETON_ADMITTED_TAG));
|
|
2469
|
+
const remaining = limit - holders.length;
|
|
2470
|
+
if (remaining <= 0) return null;
|
|
2471
|
+
const waiters = active.filter((run) => !run.tags?.includes(SINGLETON_ADMITTED_TAG)).sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id));
|
|
2472
|
+
if (!waiters.slice(0, remaining).some((run) => run.id === runId)) return null;
|
|
2473
|
+
return [
|
|
2474
|
+
...candidate.tags ?? [],
|
|
2475
|
+
SINGLETON_ADMITTED_TAG
|
|
2476
|
+
];
|
|
2477
|
+
}
|
|
2478
|
+
__name(singletonAdmissionTags, "singletonAdmissionTags");
|
|
2479
|
+
async function clearTerminalSingletonAdmission(runId, patch, getRun) {
|
|
2480
|
+
if (!patch.status || !TERMINAL_RUN_STATUSES.includes(patch.status)) return patch;
|
|
2481
|
+
const tags = "tags" in patch ? patch.tags : (await getRun(runId))?.tags;
|
|
2482
|
+
return {
|
|
2483
|
+
...patch,
|
|
2484
|
+
tags: tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
__name(clearTerminalSingletonAdmission, "clearTerminalSingletonAdmission");
|
|
2488
|
+
function singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt) {
|
|
2489
|
+
if (retryWakeAt === void 0) return void 0;
|
|
2490
|
+
const candidate = runs.find((run) => run.id === runId && run.workflow === workflow && run.tags?.includes(tag) && SINGLETON_ACTIVE_STATUSES.includes(run.status));
|
|
2491
|
+
if (!candidate || candidate.status === "cancelling") return void 0;
|
|
2492
|
+
return {
|
|
2493
|
+
status: "suspended",
|
|
2494
|
+
wakeAt: retryWakeAt,
|
|
2495
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
__name(singletonAdmissionDenialPatch, "singletonAdmissionDenialPatch");
|
|
2499
|
+
|
|
2447
2500
|
// src/singleton-gate.ts
|
|
2448
2501
|
var SINGLETON_RETRY_MS = 1e3;
|
|
2449
2502
|
var SINGLETON_RETRY_JITTER_MS = 250;
|
|
@@ -2457,6 +2510,9 @@ var SingletonGate = class {
|
|
|
2457
2510
|
}
|
|
2458
2511
|
/** The tag a singleton run carries, so the gate can find others sharing its key. */
|
|
2459
2512
|
tag(cfg, input) {
|
|
2513
|
+
if (!this.deps.store.tryAdmitSingleton) {
|
|
2514
|
+
throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
|
|
2515
|
+
}
|
|
2460
2516
|
return `singleton:${cfg.key(input)}`;
|
|
2461
2517
|
}
|
|
2462
2518
|
/** Next wake time for a gated run: the base retry delay jittered to avoid a wakeup stampede. */
|
|
@@ -2465,7 +2521,7 @@ var SingletonGate = class {
|
|
|
2465
2521
|
}
|
|
2466
2522
|
/**
|
|
2467
2523
|
* Reject a start that would grow the same-key backlog past `limit + maxQueueDepth` (counting
|
|
2468
|
-
* `pending`/`running`/`suspended` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
2524
|
+
* `pending`/`running`/`suspended`/`blocked`/`cancelling` runs sharing the key in one scan). No-op when no `maxQueueDepth`
|
|
2469
2525
|
* is configured.
|
|
2470
2526
|
*/
|
|
2471
2527
|
async assertCapacity(workflow, cfg, input) {
|
|
@@ -2474,34 +2530,25 @@ var SingletonGate = class {
|
|
|
2474
2530
|
const queued = await this.deps.store.listRuns({
|
|
2475
2531
|
tag: this.tag(cfg, input),
|
|
2476
2532
|
workflow,
|
|
2477
|
-
statuses:
|
|
2478
|
-
"pending",
|
|
2479
|
-
"running",
|
|
2480
|
-
"suspended",
|
|
2481
|
-
"cancelling"
|
|
2482
|
-
]
|
|
2533
|
+
statuses: SINGLETON_ACTIVE_STATUSES
|
|
2483
2534
|
});
|
|
2484
2535
|
if (queued.length >= cap) {
|
|
2485
2536
|
throw new SingletonQueueFullError(workflow, cfg.key(input), cfg.maxQueueDepth);
|
|
2486
2537
|
}
|
|
2487
2538
|
}
|
|
2488
|
-
/**
|
|
2489
|
-
* Whether `run` may run now under its key: it's among the `limit` oldest in-flight (running or
|
|
2490
|
-
* suspended) runs sharing the key, by `(createdAt, id)` order. A consistent store gives every
|
|
2491
|
-
* instance the same ordering, so admission is race-free + FIFO.
|
|
2492
|
-
*/
|
|
2539
|
+
/** Preserve durable holders and atomically claim one of the remaining slots. */
|
|
2493
2540
|
async admit(run, cfg) {
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
return
|
|
2541
|
+
if (!this.deps.store.tryAdmitSingleton) {
|
|
2542
|
+
throw new Error("Singleton workflows require StateStore.tryAdmitSingleton for atomic durable admission");
|
|
2543
|
+
}
|
|
2544
|
+
const admitted = await this.deps.store.tryAdmitSingleton(run.id, this.tag(cfg, run.input), run.workflow, cfg.limit ?? 1, this.retryWakeAt());
|
|
2545
|
+
if (admitted && !run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
2546
|
+
run.tags = [
|
|
2547
|
+
...run.tags ?? [],
|
|
2548
|
+
SINGLETON_ADMITTED_TAG
|
|
2549
|
+
];
|
|
2550
|
+
}
|
|
2551
|
+
return admitted;
|
|
2505
2552
|
}
|
|
2506
2553
|
/**
|
|
2507
2554
|
* Notify-on-release: a singleton run settled (freeing a slot), so dispatch the oldest gated
|
|
@@ -4018,7 +4065,7 @@ var WorkflowEngine = class {
|
|
|
4018
4065
|
VERSION_UNDECLARED_TAG
|
|
4019
4066
|
] : []
|
|
4020
4067
|
];
|
|
4021
|
-
const tags = mergeTags(registered.tags, stampedTags);
|
|
4068
|
+
const tags = mergeTags(registered.tags, stampedTags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
|
|
4022
4069
|
if (registered.singleton) {
|
|
4023
4070
|
await this.singletons.assertCapacity(name, registered.singleton, input);
|
|
4024
4071
|
}
|
|
@@ -4093,6 +4140,12 @@ var WorkflowEngine = class {
|
|
|
4093
4140
|
await this.store.releaseRunLock(runId);
|
|
4094
4141
|
throw new NamespaceMismatch();
|
|
4095
4142
|
}
|
|
4143
|
+
if (run.status === "failed" && run.tags?.includes(SINGLETON_ADMITTED_TAG)) {
|
|
4144
|
+
run.tags = run.tags.filter((tag) => tag !== SINGLETON_ADMITTED_TAG);
|
|
4145
|
+
await this.store.updateRun(run.id, {
|
|
4146
|
+
tags: run.tags
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4096
4149
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
|
|
4097
4150
|
if (!registered) {
|
|
4098
4151
|
throw new Error(`workflow ${run.workflow}@${run.workflowVersion} is not registered \u2014 keep the prior version deployed so in-flight runs can drain (skew protection)`);
|
|
@@ -4242,7 +4295,10 @@ var WorkflowEngine = class {
|
|
|
4242
4295
|
/** Track an in-flight execution so {@link drain} can wait for it. */
|
|
4243
4296
|
track(p) {
|
|
4244
4297
|
this.inflight.add(p);
|
|
4245
|
-
|
|
4298
|
+
const done = /* @__PURE__ */ __name(() => {
|
|
4299
|
+
this.inflight.delete(p);
|
|
4300
|
+
}, "done");
|
|
4301
|
+
void p.then(done, done);
|
|
4246
4302
|
return p;
|
|
4247
4303
|
}
|
|
4248
4304
|
/**
|
|
@@ -4593,7 +4649,8 @@ var WorkflowEngine = class {
|
|
|
4593
4649
|
status: "pending",
|
|
4594
4650
|
error: void 0,
|
|
4595
4651
|
...resurrecting ? {
|
|
4596
|
-
recoveryAttempts: 0
|
|
4652
|
+
recoveryAttempts: 0,
|
|
4653
|
+
tags: run.tags?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
4597
4654
|
} : {},
|
|
4598
4655
|
updatedAt: /* @__PURE__ */ new Date()
|
|
4599
4656
|
});
|
|
@@ -4712,10 +4769,19 @@ var WorkflowEngine = class {
|
|
|
4712
4769
|
off();
|
|
4713
4770
|
resolve(toResult(run));
|
|
4714
4771
|
}, "finish");
|
|
4772
|
+
const fail = /* @__PURE__ */ __name((error2) => {
|
|
4773
|
+
if (done) return;
|
|
4774
|
+
done = true;
|
|
4775
|
+
if (timer) clearTimeout(timer);
|
|
4776
|
+
if (poller) clearInterval(poller);
|
|
4777
|
+
off();
|
|
4778
|
+
reject(error2);
|
|
4779
|
+
}, "fail");
|
|
4715
4780
|
const check = /* @__PURE__ */ __name(() => {
|
|
4781
|
+
if (done) return;
|
|
4716
4782
|
void this.store.getRun(runId).then((run) => {
|
|
4717
4783
|
if (run && isSettled(run)) finish(run);
|
|
4718
|
-
});
|
|
4784
|
+
}, fail);
|
|
4719
4785
|
}, "check");
|
|
4720
4786
|
const poller = opts?.until === "terminal" ? (() => {
|
|
4721
4787
|
const interval = setInterval(check, 250);
|
|
@@ -5498,6 +5564,26 @@ var WorkflowEngine = class {
|
|
|
5498
5564
|
durationMs: Date.now() - step.startedAt.getTime()
|
|
5499
5565
|
});
|
|
5500
5566
|
}
|
|
5567
|
+
/** Admission persists the wait atomically; never overwrite a cancellation after it returns. */
|
|
5568
|
+
async singletonWaitResult(run) {
|
|
5569
|
+
const current = await this.store.getRun(run.id);
|
|
5570
|
+
await this.store.releaseRunLock(run.id);
|
|
5571
|
+
if (!current) throw new Error(`run ${run.id} was deleted during singleton admission`);
|
|
5572
|
+
if (current.status === "suspended") {
|
|
5573
|
+
this.emit({
|
|
5574
|
+
type: "run.suspended",
|
|
5575
|
+
runId: current.id,
|
|
5576
|
+
workflow: current.workflow,
|
|
5577
|
+
namespace: current.namespace
|
|
5578
|
+
});
|
|
5579
|
+
}
|
|
5580
|
+
return {
|
|
5581
|
+
runId: current.id,
|
|
5582
|
+
status: current.status,
|
|
5583
|
+
output: current.output,
|
|
5584
|
+
error: current.error
|
|
5585
|
+
};
|
|
5586
|
+
}
|
|
5501
5587
|
async execute(run, fn) {
|
|
5502
5588
|
const registered = this.workflows.get(versionKey(run.workflow, run.workflowVersion)) ?? await this.findInheritedRegistration(run) ?? await this.resolveRemoteByConvention(run);
|
|
5503
5589
|
const lockNow = this.clock();
|
|
@@ -5667,23 +5753,7 @@ var WorkflowEngine = class {
|
|
|
5667
5753
|
});
|
|
5668
5754
|
}
|
|
5669
5755
|
if (registered.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
5670
|
-
|
|
5671
|
-
await this.store.updateRun(run.id, {
|
|
5672
|
-
status: "suspended",
|
|
5673
|
-
wakeAt,
|
|
5674
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
5675
|
-
});
|
|
5676
|
-
this.emit({
|
|
5677
|
-
type: "run.suspended",
|
|
5678
|
-
runId: run.id,
|
|
5679
|
-
workflow: run.workflow,
|
|
5680
|
-
namespace: run.namespace
|
|
5681
|
-
});
|
|
5682
|
-
await this.store.releaseRunLock(run.id);
|
|
5683
|
-
return {
|
|
5684
|
-
runId: run.id,
|
|
5685
|
-
status: "suspended"
|
|
5686
|
-
};
|
|
5756
|
+
return this.singletonWaitResult(run);
|
|
5687
5757
|
}
|
|
5688
5758
|
const wfPlan = await this.planRoute(tenantGroup(sanitizeQueueToken(remote.group), void 0), registered.requires);
|
|
5689
5759
|
if (wfPlan?.status === "blocked") return this.parkBlocked(run, wfPlan);
|
|
@@ -6297,23 +6367,7 @@ var WorkflowEngine = class {
|
|
|
6297
6367
|
});
|
|
6298
6368
|
}
|
|
6299
6369
|
if (run.status !== "cancelling" && registered?.singleton && !await this.singletons.admit(run, registered.singleton)) {
|
|
6300
|
-
|
|
6301
|
-
await this.store.updateRun(run.id, {
|
|
6302
|
-
status: "suspended",
|
|
6303
|
-
wakeAt,
|
|
6304
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
6305
|
-
});
|
|
6306
|
-
this.emit({
|
|
6307
|
-
type: "run.suspended",
|
|
6308
|
-
runId: run.id,
|
|
6309
|
-
workflow: run.workflow,
|
|
6310
|
-
namespace: run.namespace
|
|
6311
|
-
});
|
|
6312
|
-
await this.store.releaseRunLock(run.id);
|
|
6313
|
-
return {
|
|
6314
|
-
runId: run.id,
|
|
6315
|
-
status: "suspended"
|
|
6316
|
-
};
|
|
6370
|
+
return this.singletonWaitResult(run);
|
|
6317
6371
|
}
|
|
6318
6372
|
const compensations = [];
|
|
6319
6373
|
const snapshot = await this.store.listCheckpoints(run.id);
|
|
@@ -7332,6 +7386,8 @@ var CodecStateStore = class {
|
|
|
7332
7386
|
}
|
|
7333
7387
|
inner;
|
|
7334
7388
|
codec;
|
|
7389
|
+
/** Atomic claims touch only searchable metadata and bypass payload encoding. */
|
|
7390
|
+
tryAdmitSingleton;
|
|
7335
7391
|
/**
|
|
7336
7392
|
* Forwarded verbatim — a facet cell counts `status`/`origin`, neither of which the codec touches
|
|
7337
7393
|
* (it encodes payloads: input/output/error). Bound as a PROPERTY, and only when the inner store
|
|
@@ -7364,6 +7420,7 @@ var CodecStateStore = class {
|
|
|
7364
7420
|
constructor(inner, codec) {
|
|
7365
7421
|
this.inner = inner;
|
|
7366
7422
|
this.codec = codec;
|
|
7423
|
+
if (inner.tryAdmitSingleton) this.tryAdmitSingleton = inner.tryAdmitSingleton.bind(inner);
|
|
7367
7424
|
const deleteRuns = inner.deleteRuns;
|
|
7368
7425
|
if (deleteRuns) this.deleteRuns = (ids) => deleteRuns.call(inner, ids);
|
|
7369
7426
|
const prune = inner.pruneTerminalRuns;
|
|
@@ -7816,6 +7873,28 @@ var InMemoryStateStore = class {
|
|
|
7816
7873
|
byRun.set(runId, row);
|
|
7817
7874
|
}
|
|
7818
7875
|
}
|
|
7876
|
+
async tryAdmitSingleton(runId, tag, workflow, limit, retryWakeAt) {
|
|
7877
|
+
const runs = [
|
|
7878
|
+
...this.runs.values()
|
|
7879
|
+
];
|
|
7880
|
+
const tags = singletonAdmissionTags(runs, runId, tag, workflow, limit);
|
|
7881
|
+
if (!tags) {
|
|
7882
|
+
const patch = singletonAdmissionDenialPatch(runs, runId, tag, workflow, retryWakeAt);
|
|
7883
|
+
const candidate = this.runs.get(runId);
|
|
7884
|
+
if (patch && candidate) this.runs.set(runId, {
|
|
7885
|
+
...candidate,
|
|
7886
|
+
...patch
|
|
7887
|
+
});
|
|
7888
|
+
return false;
|
|
7889
|
+
}
|
|
7890
|
+
const run = this.runs.get(runId);
|
|
7891
|
+
if (!run) return false;
|
|
7892
|
+
this.runs.set(runId, {
|
|
7893
|
+
...run,
|
|
7894
|
+
tags
|
|
7895
|
+
});
|
|
7896
|
+
return true;
|
|
7897
|
+
}
|
|
7819
7898
|
async createRun(run) {
|
|
7820
7899
|
this.runs.set(run.id, {
|
|
7821
7900
|
...run,
|
|
@@ -7823,7 +7902,11 @@ var InMemoryStateStore = class {
|
|
|
7823
7902
|
});
|
|
7824
7903
|
this.reindexAttributes(run.id, run.searchAttributes);
|
|
7825
7904
|
}
|
|
7826
|
-
async updateRun(runId,
|
|
7905
|
+
async updateRun(runId, inputPatch) {
|
|
7906
|
+
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? {
|
|
7907
|
+
...inputPatch,
|
|
7908
|
+
tags: ("tags" in inputPatch ? inputPatch.tags : this.runs.get(runId)?.tags)?.filter((tag) => tag !== SINGLETON_ADMITTED_TAG)
|
|
7909
|
+
} : inputPatch;
|
|
7827
7910
|
const existing = this.runs.get(runId);
|
|
7828
7911
|
if (!existing) throw new Error(`run ${runId} not found`);
|
|
7829
7912
|
const next = {
|
|
@@ -8248,6 +8331,8 @@ var InMemoryTransport = class {
|
|
|
8248
8331
|
RemoteWorkflowExecutor,
|
|
8249
8332
|
RemoteWorkflowTimeout,
|
|
8250
8333
|
RunGateway,
|
|
8334
|
+
SINGLETON_ACTIVE_STATUSES,
|
|
8335
|
+
SINGLETON_ADMITTED_TAG,
|
|
8251
8336
|
STATE_STORE,
|
|
8252
8337
|
STATE_STORE_CANONICAL,
|
|
8253
8338
|
ScheduleAlreadyExistsError,
|
|
@@ -8278,6 +8363,7 @@ var InMemoryTransport = class {
|
|
|
8278
8363
|
capableWorkers,
|
|
8279
8364
|
classifyWaiterToken,
|
|
8280
8365
|
clearDurableExecutionWrappers,
|
|
8366
|
+
clearTerminalSingletonAdmission,
|
|
8281
8367
|
collectMetrics,
|
|
8282
8368
|
concurrencyTag,
|
|
8283
8369
|
controlPlaneDescriptor,
|
|
@@ -8335,6 +8421,8 @@ var InMemoryTransport = class {
|
|
|
8335
8421
|
runValueFacetsFromRuns,
|
|
8336
8422
|
sanitizeQueueToken,
|
|
8337
8423
|
scheduledRunId,
|
|
8424
|
+
singletonAdmissionDenialPatch,
|
|
8425
|
+
singletonAdmissionTags,
|
|
8338
8426
|
sqlComparator,
|
|
8339
8427
|
stepConfigOf,
|
|
8340
8428
|
stepId,
|