@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.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,