@dudousxd/nestjs-durable-store-drizzle 0.19.1 → 0.21.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 +105 -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 +106 -3
- package/dist/index.js.map +1 -1
- package/dist/pg/index.cjs +221 -96
- 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 +171 -47
- package/dist/pg/index.js.map +1 -1
- package/package.json +4 -4
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, 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
|
|
|
@@ -23,6 +23,8 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
23
23
|
pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
|
|
24
24
|
getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
|
|
25
25
|
saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
|
|
26
|
+
saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
|
|
27
|
+
saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
|
|
26
28
|
transaction<T>(work: (tx: {
|
|
27
29
|
raw: unknown;
|
|
28
30
|
saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
|
|
@@ -64,7 +66,16 @@ declare class DrizzleStateStore implements StateStore {
|
|
|
64
66
|
listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
|
|
65
67
|
listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
|
|
66
68
|
getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
|
|
69
|
+
countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
|
|
67
70
|
listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
|
|
71
|
+
/** {@link StateStore.checkpointStats}: one `GROUP BY parallel_group, status, name_kind` over the
|
|
72
|
+
* run's checkpoints, so a wide fan comes back as a few bucket rows instead of every member. */
|
|
73
|
+
checkpointStats(runId: string, opts?: {
|
|
74
|
+
group?: string | undefined;
|
|
75
|
+
}): Promise<CheckpointStat[]>;
|
|
76
|
+
/** {@link StateStore.queryCheckpoints}: the run's checkpoints narrowed in SQL, in `seq` order and
|
|
77
|
+
* paged there, so reading one page of a wide fan never loads the rest of it. */
|
|
78
|
+
queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
|
|
68
79
|
putSignalWaiter(waiter: SignalWaiter): Promise<void>;
|
|
69
80
|
takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
|
|
70
81
|
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(),
|
|
@@ -335,6 +341,41 @@ var DrizzleStateStore = class {
|
|
|
335
341
|
set: row
|
|
336
342
|
});
|
|
337
343
|
}
|
|
344
|
+
async saveCheckpointIfPending(checkpoint) {
|
|
345
|
+
const { runId, seq, ...set } = toCheckpointRow(checkpoint);
|
|
346
|
+
const result = await this.db.update(stepCheckpoints).set(set).where(and(eq(stepCheckpoints.runId, runId), eq(stepCheckpoints.seq, seq), eq(stepCheckpoints.status, "pending")));
|
|
347
|
+
const { changes, rowsAffected } = result;
|
|
348
|
+
return (changes ?? rowsAffected ?? 0) > 0;
|
|
349
|
+
}
|
|
350
|
+
async saveCheckpointsIfPending(checkpoints) {
|
|
351
|
+
if (checkpoints.length === 0) return [];
|
|
352
|
+
const wrote = [];
|
|
353
|
+
await this.db.transaction((tx) => {
|
|
354
|
+
const update = /* @__PURE__ */ __name((checkpoint) => {
|
|
355
|
+
const { runId, seq, ...set } = toCheckpointRow(checkpoint);
|
|
356
|
+
return tx.update(stepCheckpoints).set(set).where(and(eq(stepCheckpoints.runId, runId), eq(stepCheckpoints.seq, seq), eq(stepCheckpoints.status, "pending"))).run();
|
|
357
|
+
}, "update");
|
|
358
|
+
const record = /* @__PURE__ */ __name((result) => {
|
|
359
|
+
const { changes, rowsAffected } = result;
|
|
360
|
+
wrote.push((changes ?? rowsAffected ?? 0) > 0);
|
|
361
|
+
}, "record");
|
|
362
|
+
const runFrom = /* @__PURE__ */ __name((from) => {
|
|
363
|
+
for (let i = from; i < checkpoints.length; i += 1) {
|
|
364
|
+
const result = update(checkpoints[i]);
|
|
365
|
+
if (result instanceof Promise) {
|
|
366
|
+
return result.then((r) => {
|
|
367
|
+
record(r);
|
|
368
|
+
return runFrom(i + 1);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
record(result);
|
|
372
|
+
}
|
|
373
|
+
return void 0;
|
|
374
|
+
}, "runFrom");
|
|
375
|
+
return runFrom(0);
|
|
376
|
+
});
|
|
377
|
+
return wrote;
|
|
378
|
+
}
|
|
338
379
|
async transaction(work) {
|
|
339
380
|
return this.db.transaction(async (tx) => work({
|
|
340
381
|
raw: tx,
|
|
@@ -527,9 +568,71 @@ var DrizzleStateStore = class {
|
|
|
527
568
|
const rows = await this.db.select().from(stepCheckpoints).where(and(eq(stepCheckpoints.runId, runId), eq(stepCheckpoints.name, name))).orderBy(desc(stepCheckpoints.seq)).limit(1);
|
|
528
569
|
return rows[0] ? fromCheckpointRow(rows[0]) : void 0;
|
|
529
570
|
}
|
|
571
|
+
async countCheckpoints(runId, fromSeq, toSeq, statuses) {
|
|
572
|
+
if (statuses.length === 0) return 0;
|
|
573
|
+
const rows = await this.db.select({
|
|
574
|
+
count: sql`count(*)`
|
|
575
|
+
}).from(stepCheckpoints).where(and(eq(stepCheckpoints.runId, runId), gte(stepCheckpoints.seq, fromSeq), lte(stepCheckpoints.seq, toSeq), inArray(stepCheckpoints.status, statuses)));
|
|
576
|
+
return Number(rows[0]?.count ?? 0);
|
|
577
|
+
}
|
|
530
578
|
async listCheckpointsByNamePrefix(runId, prefixes) {
|
|
531
579
|
if (prefixes.length === 0) return [];
|
|
532
580
|
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));
|
|
581
|
+
return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointRow);
|
|
582
|
+
}
|
|
583
|
+
/** {@link StateStore.checkpointStats}: one `GROUP BY parallel_group, status, name_kind` over the
|
|
584
|
+
* run's checkpoints, so a wide fan comes back as a few bucket rows instead of every member. */
|
|
585
|
+
async checkpointStats(runId, opts) {
|
|
586
|
+
const c = stepCheckpoints;
|
|
587
|
+
const nameKind = sql`CASE
|
|
588
|
+
WHEN substr(${c.name}, 1, 13) = 'signal:child:' THEN 'child'
|
|
589
|
+
WHEN substr(${c.name}, 1, 4) = 'fan:' THEN 'folded'
|
|
590
|
+
WHEN substr(${c.name}, 1, 6) = 'spawn:' THEN 'spawn'
|
|
591
|
+
ELSE 'step' END`.as("name_kind");
|
|
592
|
+
const rows = await this.db.select({
|
|
593
|
+
group: c.parallelGroup,
|
|
594
|
+
status: c.status,
|
|
595
|
+
nameKind,
|
|
596
|
+
count: sql`count(*)`,
|
|
597
|
+
minSeq: sql`min(${c.seq})`,
|
|
598
|
+
maxSeq: sql`max(${c.seq})`,
|
|
599
|
+
minName: sql`min(${c.name})`,
|
|
600
|
+
maxName: sql`max(${c.name})`,
|
|
601
|
+
minEnqueuedAt: sql`min(coalesce(${c.enqueuedAt}, ${c.startedAt}))`,
|
|
602
|
+
minStartedAt: sql`min(${c.startedAt})`,
|
|
603
|
+
maxStartedAt: sql`max(${c.startedAt})`,
|
|
604
|
+
maxFinishedAt: sql`max(${c.finishedAt})`,
|
|
605
|
+
sumAttempts: sql`sum(${c.attempts})`
|
|
606
|
+
}).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`);
|
|
607
|
+
return rows.map((r) => ({
|
|
608
|
+
group: r.group ?? null,
|
|
609
|
+
status: r.status,
|
|
610
|
+
nameKind: r.nameKind,
|
|
611
|
+
count: Number(r.count),
|
|
612
|
+
minSeq: Number(r.minSeq),
|
|
613
|
+
maxSeq: Number(r.maxSeq),
|
|
614
|
+
minName: r.minName,
|
|
615
|
+
maxName: r.maxName,
|
|
616
|
+
minEnqueuedAt: new Date(Number(r.minEnqueuedAt)),
|
|
617
|
+
minStartedAt: new Date(Number(r.minStartedAt)),
|
|
618
|
+
maxStartedAt: new Date(Number(r.maxStartedAt)),
|
|
619
|
+
maxFinishedAt: new Date(Number(r.maxFinishedAt)),
|
|
620
|
+
sumAttempts: Number(r.sumAttempts)
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
/** {@link StateStore.queryCheckpoints}: the run's checkpoints narrowed in SQL, in `seq` order and
|
|
624
|
+
* paged there, so reading one page of a wide fan never loads the rest of it. */
|
|
625
|
+
async queryCheckpoints(runId, query) {
|
|
626
|
+
if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
|
|
627
|
+
const c = stepCheckpoints;
|
|
628
|
+
const rows = await this.db.select().from(c).where(and(
|
|
629
|
+
eq(c.runId, runId),
|
|
630
|
+
query.group !== void 0 ? eq(c.parallelGroup, query.group) : void 0,
|
|
631
|
+
// `NOT IN` is never true for a NULL group, so ungrouped rows are kept explicitly.
|
|
632
|
+
query.excludeGroups?.length ? or(isNull(c.parallelGroup), notInArray(c.parallelGroup, query.excludeGroups)) : void 0,
|
|
633
|
+
query.statuses ? inArray(c.status, query.statuses) : void 0,
|
|
634
|
+
query.namePrefixes ? or(...query.namePrefixes.map((p) => sql`substr(${c.name}, 1, length(${p})) = ${p}`)) : void 0
|
|
635
|
+
)).orderBy(asc(c.seq)).limit(query.limit ?? Number.MAX_SAFE_INTEGER).offset(query.offset ?? 0);
|
|
533
636
|
return rows.map(fromCheckpointRow);
|
|
534
637
|
}
|
|
535
638
|
async putSignalWaiter(waiter) {
|
|
@@ -561,7 +664,7 @@ var DrizzleStateStore = class {
|
|
|
561
664
|
}
|
|
562
665
|
async listSignalWaiters(prefix) {
|
|
563
666
|
const rows = await this.db.select().from(signalWaiters).where(like(signalWaiters.token, `${prefix}%`));
|
|
564
|
-
return rows.map((r) => ({
|
|
667
|
+
return rows.filter((r) => r.token.startsWith(prefix)).map((r) => ({
|
|
565
668
|
token: r.token,
|
|
566
669
|
runId: r.runId,
|
|
567
670
|
seq: r.seq,
|