@dudousxd/nestjs-durable-store-drizzle 0.20.0 → 0.22.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 +115 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +116 -3
- package/dist/index.js.map +1 -1
- package/dist/pg/index.cjs +234 -99
- package/dist/pg/index.cjs.map +1 -1
- package/dist/pg/index.d.cts +15 -2
- package/dist/pg/index.d.ts +15 -2
- package/dist/pg/index.js +182 -48
- package/dist/pg/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
|
|
1
|
+
import { StateStore, WorkflowRun, RunStatus, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
|
|
2
2
|
import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
|
|
3
3
|
import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
|
|
4
4
|
|
|
@@ -14,6 +14,7 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
14
14
|
tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
|
|
15
15
|
createRun(run: WorkflowRun): Promise<void>;
|
|
16
16
|
updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
|
|
17
|
+
updateRunIf(runId: string, expectedStatuses: RunStatus[], inputPatch: Partial<WorkflowRun>): Promise<boolean>;
|
|
17
18
|
/** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
|
|
18
19
|
* the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
|
|
19
20
|
private reindexAttributes;
|
|
@@ -24,6 +25,7 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
24
25
|
getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
|
|
25
26
|
saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
|
|
26
27
|
saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
|
|
28
|
+
saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
|
|
27
29
|
transaction<T>(work: (tx: {
|
|
28
30
|
raw: unknown;
|
|
29
31
|
saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
|
|
@@ -31,6 +33,7 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
31
33
|
/** Recovery on boot. Namespace-scoped: a worker re-drives only its own tenant's crashed runs.
|
|
32
34
|
* `and(...)` drops an `undefined` predicate, so an unscoped caller recovers every namespace. */
|
|
33
35
|
listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
|
|
36
|
+
listOrphanedRuns(nowMs: number, limit: number, namespace?: string): Promise<WorkflowRun[]>;
|
|
34
37
|
/** Dispatch pick-up. Namespace-scoped — without it a worker serving one tenant drains another
|
|
35
38
|
* tenant's queue, and the `limit` makes it worse: foreign runs consume the FIFO budget. */
|
|
36
39
|
listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
|
|
@@ -67,6 +70,14 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
67
70
|
getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
|
|
68
71
|
countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
|
|
69
72
|
listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
|
|
73
|
+
/** {@link StateStore.checkpointStats}: one `GROUP BY parallel_group, status, name_kind` over the
|
|
74
|
+
* run's checkpoints, so a wide fan comes back as a few bucket rows instead of every member. */
|
|
75
|
+
checkpointStats(runId: string, opts?: {
|
|
76
|
+
group?: string | undefined;
|
|
77
|
+
}): Promise<CheckpointStat[]>;
|
|
78
|
+
/** {@link StateStore.queryCheckpoints}: the run's checkpoints narrowed in SQL, in `seq` order and
|
|
79
|
+
* paged there, so reading one page of a wide fan never loads the rest of it. */
|
|
80
|
+
queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
|
|
70
81
|
putSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
71
82
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
72
83
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
|
|
1
|
+
import { StateStore, WorkflowRun, RunStatus, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
|
|
2
2
|
import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
|
|
3
3
|
import { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
|
|
4
4
|
|
|
@@ -14,6 +14,7 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
14
14
|
tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
|
|
15
15
|
createRun(run: WorkflowRun): Promise<void>;
|
|
16
16
|
updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
|
|
17
|
+
updateRunIf(runId: string, expectedStatuses: RunStatus[], inputPatch: Partial<WorkflowRun>): Promise<boolean>;
|
|
17
18
|
/** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
|
|
18
19
|
* the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
|
|
19
20
|
private reindexAttributes;
|
|
@@ -24,6 +25,7 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
24
25
|
getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
|
|
25
26
|
saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
|
|
26
27
|
saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
|
|
28
|
+
saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
|
|
27
29
|
transaction<T>(work: (tx: {
|
|
28
30
|
raw: unknown;
|
|
29
31
|
saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
|
|
@@ -31,6 +33,7 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
31
33
|
/** Recovery on boot. Namespace-scoped: a worker re-drives only its own tenant's crashed runs.
|
|
32
34
|
* `and(...)` drops an `undefined` predicate, so an unscoped caller recovers every namespace. */
|
|
33
35
|
listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
|
|
36
|
+
listOrphanedRuns(nowMs: number, limit: number, namespace?: string): Promise<WorkflowRun[]>;
|
|
34
37
|
/** Dispatch pick-up. Namespace-scoped — without it a worker serving one tenant drains another
|
|
35
38
|
* tenant's queue, and the `limit` makes it worse: foreign runs consume the FIFO budget. */
|
|
36
39
|
listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
|
|
@@ -67,6 +70,14 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
67
70
|
getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
|
|
68
71
|
countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
|
|
69
72
|
listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
|
|
73
|
+
/** {@link StateStore.checkpointStats}: one `GROUP BY parallel_group, status, name_kind` over the
|
|
74
|
+
* run's checkpoints, so a wide fan comes back as a few bucket rows instead of every member. */
|
|
75
|
+
checkpointStats(runId: string, opts?: {
|
|
76
|
+
group?: string | undefined;
|
|
77
|
+
}): Promise<CheckpointStat[]>;
|
|
78
|
+
/** {@link StateStore.queryCheckpoints}: the run's checkpoints narrowed in SQL, in `seq` order and
|
|
79
|
+
* paged there, so reading one page of a wide fan never loads the rest of it. */
|
|
80
|
+
queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
|
|
70
81
|
putSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
71
82
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
72
83
|
listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
3
3
|
|
|
4
4
|
// src/drizzle-state-store.ts
|
|
5
5
|
import { RUN_VALUE_FACET_SCAN, SINGLETON_ACTIVE_STATUSES, TERMINAL_RUN_STATUSES, attributePredicateOperands, axisIsRunColumn, clearTerminalSingletonAdmission, mergeRunFacetRows, mergeRunValueFacetRows, normalizeAttributeRows, parseDuration, runValueFacetsFromRuns, singletonAdmissionDenialPatch, singletonAdmissionTags } from "@dudousxd/nestjs-durable-core";
|
|
6
|
-
import { and, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, like, lt, lte, or, sql } from "drizzle-orm";
|
|
6
|
+
import { and, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, like, lt, lte, notInArray, or, sql } from "drizzle-orm";
|
|
7
7
|
|
|
8
8
|
// src/schema.ts
|
|
9
9
|
import { getTableName } from "drizzle-orm";
|
|
@@ -119,7 +119,13 @@ var stepCheckpoints = sqliteTable("durable_step_checkpoints", {
|
|
|
119
119
|
t.runId,
|
|
120
120
|
t.seq
|
|
121
121
|
]
|
|
122
|
-
})
|
|
122
|
+
}),
|
|
123
|
+
// A run detail reads one fan at a time (`checkpointStats({ group })`, `queryCheckpoints({ group })`
|
|
124
|
+
// paged by seq). Like every index here, an already-deployed database needs it run by hand:
|
|
125
|
+
//
|
|
126
|
+
// CREATE INDEX IF NOT EXISTS durable_step_checkpoints_group_idx
|
|
127
|
+
// ON durable_step_checkpoints (run_id, parallel_group, seq);
|
|
128
|
+
index("durable_step_checkpoints_group_idx").on(t.runId, t.parallelGroup, t.seq)
|
|
123
129
|
]);
|
|
124
130
|
var runAttributes = sqliteTable("durable_run_attributes", {
|
|
125
131
|
runId: text("run_id").notNull(),
|
|
@@ -266,6 +272,21 @@ var DrizzleStateStore = class {
|
|
|
266
272
|
if (Object.keys(row).length) await this.db.update(workflowRuns).set(row).where(eq(workflowRuns.id, runId));
|
|
267
273
|
if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
|
|
268
274
|
}
|
|
275
|
+
async updateRunIf(runId, expectedStatuses, inputPatch) {
|
|
276
|
+
if (expectedStatuses.length === 0) return false;
|
|
277
|
+
const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? await clearTerminalSingletonAdmission(runId, inputPatch, (id) => this.getRun(id)) : inputPatch;
|
|
278
|
+
const row = toRunPatch(patch);
|
|
279
|
+
if (!Object.keys(row).length) {
|
|
280
|
+
const current = await this.getRun(runId);
|
|
281
|
+
if (!current || !expectedStatuses.includes(current.status)) return false;
|
|
282
|
+
} else {
|
|
283
|
+
const result = await this.db.update(workflowRuns).set(row).where(and(eq(workflowRuns.id, runId), inArray(workflowRuns.status, expectedStatuses)));
|
|
284
|
+
const { changes, rowsAffected } = result;
|
|
285
|
+
if ((changes ?? rowsAffected ?? 0) === 0) return false;
|
|
286
|
+
}
|
|
287
|
+
if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
269
290
|
/** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
|
|
270
291
|
* the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
|
|
271
292
|
async reindexAttributes(runId, attributes) {
|
|
@@ -341,6 +362,35 @@ var DrizzleStateStore = class {
|
|
|
341
362
|
const { changes, rowsAffected } = result;
|
|
342
363
|
return (changes ?? rowsAffected ?? 0) > 0;
|
|
343
364
|
}
|
|
365
|
+
async saveCheckpointsIfPending(checkpoints) {
|
|
366
|
+
if (checkpoints.length === 0) return [];
|
|
367
|
+
const wrote = [];
|
|
368
|
+
await this.db.transaction((tx) => {
|
|
369
|
+
const update = /* @__PURE__ */ __name((checkpoint) => {
|
|
370
|
+
const { runId, seq, ...set } = toCheckpointRow(checkpoint);
|
|
371
|
+
return tx.update(stepCheckpoints).set(set).where(and(eq(stepCheckpoints.runId, runId), eq(stepCheckpoints.seq, seq), eq(stepCheckpoints.status, "pending"))).run();
|
|
372
|
+
}, "update");
|
|
373
|
+
const record = /* @__PURE__ */ __name((result) => {
|
|
374
|
+
const { changes, rowsAffected } = result;
|
|
375
|
+
wrote.push((changes ?? rowsAffected ?? 0) > 0);
|
|
376
|
+
}, "record");
|
|
377
|
+
const runFrom = /* @__PURE__ */ __name((from) => {
|
|
378
|
+
for (let i = from; i < checkpoints.length; i += 1) {
|
|
379
|
+
const result = update(checkpoints[i]);
|
|
380
|
+
if (result instanceof Promise) {
|
|
381
|
+
return result.then((r) => {
|
|
382
|
+
record(r);
|
|
383
|
+
return runFrom(i + 1);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
record(result);
|
|
387
|
+
}
|
|
388
|
+
return void 0;
|
|
389
|
+
}, "runFrom");
|
|
390
|
+
return runFrom(0);
|
|
391
|
+
});
|
|
392
|
+
return wrote;
|
|
393
|
+
}
|
|
344
394
|
async transaction(work) {
|
|
345
395
|
return this.db.transaction(async (tx) => work({
|
|
346
396
|
raw: tx,
|
|
@@ -365,6 +415,14 @@ var DrizzleStateStore = class {
|
|
|
365
415
|
]), namespaceFilter(namespace)));
|
|
366
416
|
return rows.map(fromRunRow);
|
|
367
417
|
}
|
|
418
|
+
async listOrphanedRuns(nowMs, limit, namespace) {
|
|
419
|
+
if (limit <= 0) return [];
|
|
420
|
+
const rows = await this.db.select().from(workflowRuns).where(and(inArray(workflowRuns.status, [
|
|
421
|
+
"running",
|
|
422
|
+
"cancelling"
|
|
423
|
+
]), or(isNull(workflowRuns.lockedUntil), lte(workflowRuns.lockedUntil, nowMs)), namespaceFilter(namespace))).orderBy(asc(workflowRuns.createdAt)).limit(limit);
|
|
424
|
+
return rows.map(fromRunRow);
|
|
425
|
+
}
|
|
368
426
|
/** Dispatch pick-up. Namespace-scoped — without it a worker serving one tenant drains another
|
|
369
427
|
* tenant's queue, and the `limit` makes it worse: foreign runs consume the FIFO budget. */
|
|
370
428
|
async listPendingRuns(limit, namespace) {
|
|
@@ -543,6 +601,61 @@ var DrizzleStateStore = class {
|
|
|
543
601
|
async listCheckpointsByNamePrefix(runId, prefixes) {
|
|
544
602
|
if (prefixes.length === 0) return [];
|
|
545
603
|
const rows = await this.db.select().from(stepCheckpoints).where(and(eq(stepCheckpoints.runId, runId), or(...prefixes.map((p) => like(stepCheckpoints.name, `${p}%`))))).orderBy(asc(stepCheckpoints.seq));
|
|
604
|
+
return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointRow);
|
|
605
|
+
}
|
|
606
|
+
/** {@link StateStore.checkpointStats}: one `GROUP BY parallel_group, status, name_kind` over the
|
|
607
|
+
* run's checkpoints, so a wide fan comes back as a few bucket rows instead of every member. */
|
|
608
|
+
async checkpointStats(runId, opts) {
|
|
609
|
+
const c = stepCheckpoints;
|
|
610
|
+
const nameKind = sql`CASE
|
|
611
|
+
WHEN substr(${c.name}, 1, 13) = 'signal:child:' THEN 'child'
|
|
612
|
+
WHEN substr(${c.name}, 1, 4) = 'fan:' THEN 'folded'
|
|
613
|
+
WHEN substr(${c.name}, 1, 6) = 'spawn:' THEN 'spawn'
|
|
614
|
+
ELSE 'step' END`.as("name_kind");
|
|
615
|
+
const rows = await this.db.select({
|
|
616
|
+
group: c.parallelGroup,
|
|
617
|
+
status: c.status,
|
|
618
|
+
nameKind,
|
|
619
|
+
count: sql`count(*)`,
|
|
620
|
+
minSeq: sql`min(${c.seq})`,
|
|
621
|
+
maxSeq: sql`max(${c.seq})`,
|
|
622
|
+
minName: sql`min(${c.name})`,
|
|
623
|
+
maxName: sql`max(${c.name})`,
|
|
624
|
+
minEnqueuedAt: sql`min(coalesce(${c.enqueuedAt}, ${c.startedAt}))`,
|
|
625
|
+
minStartedAt: sql`min(${c.startedAt})`,
|
|
626
|
+
maxStartedAt: sql`max(${c.startedAt})`,
|
|
627
|
+
maxFinishedAt: sql`max(${c.finishedAt})`,
|
|
628
|
+
sumAttempts: sql`sum(${c.attempts})`
|
|
629
|
+
}).from(c).where(and(eq(c.runId, runId), opts?.group !== void 0 ? eq(c.parallelGroup, opts.group) : void 0)).groupBy(c.parallelGroup, c.status, sql`name_kind`);
|
|
630
|
+
return rows.map((r) => ({
|
|
631
|
+
group: r.group ?? null,
|
|
632
|
+
status: r.status,
|
|
633
|
+
nameKind: r.nameKind,
|
|
634
|
+
count: Number(r.count),
|
|
635
|
+
minSeq: Number(r.minSeq),
|
|
636
|
+
maxSeq: Number(r.maxSeq),
|
|
637
|
+
minName: r.minName,
|
|
638
|
+
maxName: r.maxName,
|
|
639
|
+
minEnqueuedAt: new Date(Number(r.minEnqueuedAt)),
|
|
640
|
+
minStartedAt: new Date(Number(r.minStartedAt)),
|
|
641
|
+
maxStartedAt: new Date(Number(r.maxStartedAt)),
|
|
642
|
+
maxFinishedAt: new Date(Number(r.maxFinishedAt)),
|
|
643
|
+
sumAttempts: Number(r.sumAttempts)
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
/** {@link StateStore.queryCheckpoints}: the run's checkpoints narrowed in SQL, in `seq` order and
|
|
647
|
+
* paged there, so reading one page of a wide fan never loads the rest of it. */
|
|
648
|
+
async queryCheckpoints(runId, query) {
|
|
649
|
+
if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
|
|
650
|
+
const c = stepCheckpoints;
|
|
651
|
+
const rows = await this.db.select().from(c).where(and(
|
|
652
|
+
eq(c.runId, runId),
|
|
653
|
+
query.group !== void 0 ? eq(c.parallelGroup, query.group) : void 0,
|
|
654
|
+
// `NOT IN` is never true for a NULL group, so ungrouped rows are kept explicitly.
|
|
655
|
+
query.excludeGroups?.length ? or(isNull(c.parallelGroup), notInArray(c.parallelGroup, query.excludeGroups)) : void 0,
|
|
656
|
+
query.statuses ? inArray(c.status, query.statuses) : void 0,
|
|
657
|
+
query.namePrefixes ? or(...query.namePrefixes.map((p) => sql`substr(${c.name}, 1, length(${p})) = ${p}`)) : void 0
|
|
658
|
+
)).orderBy(asc(c.seq)).limit(query.limit ?? Number.MAX_SAFE_INTEGER).offset(query.offset ?? 0);
|
|
546
659
|
return rows.map(fromCheckpointRow);
|
|
547
660
|
}
|
|
548
661
|
async putSignalWaiter(waiter) {
|
|
@@ -574,7 +687,7 @@ var DrizzleStateStore = class {
|
|
|
574
687
|
}
|
|
575
688
|
async listSignalWaiters(prefix) {
|
|
576
689
|
const rows = await this.db.select().from(signalWaiters).where(like(signalWaiters.token, `${prefix}%`));
|
|
577
|
-
return rows.map((r) => ({
|
|
690
|
+
return rows.filter((r) => r.token.startsWith(prefix)).map((r) => ({
|
|
578
691
|
token: r.token,
|
|
579
692
|
runId: r.runId,
|
|
580
693
|
seq: r.seq,
|