@dudousxd/nestjs-durable-store-prisma 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.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, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
2
2
 
3
3
  interface RunRow {
4
4
  id: string;
@@ -83,8 +83,8 @@ interface Delegate<Row> {
83
83
  count: number;
84
84
  }>;
85
85
  count(args?: Args): Promise<number>;
86
- /** Aggregate form — used only by `runFacets`, whose rows are `(status, origin, count)` cells, not
87
- * model rows, so the result type is stated at the call site rather than derived from `Row`. */
86
+ /** Aggregate form — used by the run facets and `checkpointStats`, whose rows are aggregate cells,
87
+ * not model rows, so the result type is stated at the call site rather than derived from `Row`. */
88
88
  groupBy(args: Args): Promise<Record<string, unknown>[]>;
89
89
  }
90
90
  interface ScheduleRow {
@@ -138,6 +138,8 @@ declare class PrismaStateStore implements StateStore {
138
138
  pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
139
139
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
140
140
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
141
+ saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
142
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
141
143
  transaction<T>(work: (tx: {
142
144
  raw: unknown;
143
145
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
@@ -170,7 +172,27 @@ declare class PrismaStateStore implements StateStore {
170
172
  listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
171
173
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
172
174
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
175
+ countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
173
176
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
177
+ /**
178
+ * One `groupBy(parallelGroup, status)` per {@link CheckpointNameKind}, each narrowed to its kind by
179
+ * a name-prefix predicate, run concurrently — Prisma cannot group by a `CASE`, so the kind is the
180
+ * `where` instead. `minEnqueuedAt` reads a NULL `enqueuedAt` as `startedAt` (as
181
+ * {@link fromCheckpointRow} does): a bucket whose non-null `enqueuedAt` count falls short of its
182
+ * row count has such rows, and only for those kinds is the NULL subset's `min(startedAt)` asked for.
183
+ *
184
+ * On SQLite `startsWith` is a case-insensitive `LIKE`, as in {@link listCheckpointsByNamePrefix};
185
+ * the kind prefixes are lower-case and wildcard-free, so only a step whose name starts with one of
186
+ * them in another case would be classified differently there.
187
+ */
188
+ checkpointStats(runId: string, opts?: {
189
+ group?: string | undefined;
190
+ }): Promise<CheckpointStat[]>;
191
+ /** `min(startedAt)` per `(parallelGroup, status)` over the rows whose `enqueuedAt` is NULL. */
192
+ private unenqueuedStartedAt;
193
+ /** {@link CheckpointQuery} as one `findMany`, in `seq` order, paged by `skip`/`take`. Ungrouped rows
194
+ * are kept past `excludeGroups` explicitly: `NOT IN` alone drops a NULL `parallelGroup`. */
195
+ queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
174
196
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
175
197
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
176
198
  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, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
2
2
 
3
3
  interface RunRow {
4
4
  id: string;
@@ -83,8 +83,8 @@ interface Delegate<Row> {
83
83
  count: number;
84
84
  }>;
85
85
  count(args?: Args): Promise<number>;
86
- /** Aggregate form — used only by `runFacets`, whose rows are `(status, origin, count)` cells, not
87
- * model rows, so the result type is stated at the call site rather than derived from `Row`. */
86
+ /** Aggregate form — used by the run facets and `checkpointStats`, whose rows are aggregate cells,
87
+ * not model rows, so the result type is stated at the call site rather than derived from `Row`. */
88
88
  groupBy(args: Args): Promise<Record<string, unknown>[]>;
89
89
  }
90
90
  interface ScheduleRow {
@@ -138,6 +138,8 @@ declare class PrismaStateStore implements StateStore {
138
138
  pruneTerminalRuns(policy: RetentionPolicy, nowMs: number, limit: number): Promise<number>;
139
139
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
140
140
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
141
+ saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
142
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
141
143
  transaction<T>(work: (tx: {
142
144
  raw: unknown;
143
145
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
@@ -170,7 +172,27 @@ declare class PrismaStateStore implements StateStore {
170
172
  listSchedules(query: ScheduleQuery): Promise<ScheduleRecord[]>;
171
173
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
172
174
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
175
+ countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
173
176
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
177
+ /**
178
+ * One `groupBy(parallelGroup, status)` per {@link CheckpointNameKind}, each narrowed to its kind by
179
+ * a name-prefix predicate, run concurrently — Prisma cannot group by a `CASE`, so the kind is the
180
+ * `where` instead. `minEnqueuedAt` reads a NULL `enqueuedAt` as `startedAt` (as
181
+ * {@link fromCheckpointRow} does): a bucket whose non-null `enqueuedAt` count falls short of its
182
+ * row count has such rows, and only for those kinds is the NULL subset's `min(startedAt)` asked for.
183
+ *
184
+ * On SQLite `startsWith` is a case-insensitive `LIKE`, as in {@link listCheckpointsByNamePrefix};
185
+ * the kind prefixes are lower-case and wildcard-free, so only a step whose name starts with one of
186
+ * them in another case would be classified differently there.
187
+ */
188
+ checkpointStats(runId: string, opts?: {
189
+ group?: string | undefined;
190
+ }): Promise<CheckpointStat[]>;
191
+ /** `min(startedAt)` per `(parallelGroup, status)` over the rows whose `enqueuedAt` is NULL. */
192
+ private unenqueuedStartedAt;
193
+ /** {@link CheckpointQuery} as one `findMany`, in `seq` order, paged by `skip`/`take`. Ungrouped rows
194
+ * are kept past `excludeGroups` explicitly: `NOT IN` alone drops a NULL `parallelGroup`. */
195
+ queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
174
196
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
175
197
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
176
198
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
package/dist/index.js CHANGED
@@ -234,6 +234,37 @@ var PrismaStateStore = class {
234
234
  update: data
235
235
  });
236
236
  }
237
+ async saveCheckpointIfPending(checkpoint) {
238
+ const { runId, seq, ...data } = toCheckpointData(checkpoint);
239
+ const { count } = await this.db.durableStepCheckpoint.updateMany({
240
+ where: {
241
+ runId,
242
+ seq,
243
+ status: "pending"
244
+ },
245
+ data
246
+ });
247
+ return count > 0;
248
+ }
249
+ async saveCheckpointsIfPending(checkpoints) {
250
+ if (checkpoints.length === 0) return [];
251
+ return this.db.$transaction(async (tx) => {
252
+ const wrote = [];
253
+ for (const checkpoint of checkpoints) {
254
+ const { runId, seq, ...data } = toCheckpointData(checkpoint);
255
+ const { count } = await tx.durableStepCheckpoint.updateMany({
256
+ where: {
257
+ runId,
258
+ seq,
259
+ status: "pending"
260
+ },
261
+ data
262
+ });
263
+ wrote.push(count > 0);
264
+ }
265
+ return wrote;
266
+ });
267
+ }
237
268
  async transaction(work) {
238
269
  return this.db.$transaction(async (tx) => work({
239
270
  raw: tx,
@@ -568,6 +599,21 @@ var PrismaStateStore = class {
568
599
  });
569
600
  return row ? fromCheckpointRow(row) : void 0;
570
601
  }
602
+ async countCheckpoints(runId, fromSeq, toSeq, statuses) {
603
+ if (statuses.length === 0) return 0;
604
+ return this.db.durableStepCheckpoint.count({
605
+ where: {
606
+ runId,
607
+ seq: {
608
+ gte: fromSeq,
609
+ lte: toSeq
610
+ },
611
+ status: {
612
+ in: statuses
613
+ }
614
+ }
615
+ });
616
+ }
571
617
  async listCheckpointsByNamePrefix(runId, prefixes) {
572
618
  if (prefixes.length === 0) return [];
573
619
  const rows = await this.db.durableStepCheckpoint.findMany({
@@ -583,6 +629,137 @@ var PrismaStateStore = class {
583
629
  seq: "asc"
584
630
  }
585
631
  });
632
+ return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointRow);
633
+ }
634
+ /**
635
+ * One `groupBy(parallelGroup, status)` per {@link CheckpointNameKind}, each narrowed to its kind by
636
+ * a name-prefix predicate, run concurrently — Prisma cannot group by a `CASE`, so the kind is the
637
+ * `where` instead. `minEnqueuedAt` reads a NULL `enqueuedAt` as `startedAt` (as
638
+ * {@link fromCheckpointRow} does): a bucket whose non-null `enqueuedAt` count falls short of its
639
+ * row count has such rows, and only for those kinds is the NULL subset's `min(startedAt)` asked for.
640
+ *
641
+ * On SQLite `startsWith` is a case-insensitive `LIKE`, as in {@link listCheckpointsByNamePrefix};
642
+ * the kind prefixes are lower-case and wildcard-free, so only a step whose name starts with one of
643
+ * them in another case would be classified differently there.
644
+ */
645
+ async checkpointStats(runId, opts) {
646
+ const scope = opts?.group === void 0 ? {
647
+ runId
648
+ } : {
649
+ runId,
650
+ parallelGroup: opts.group
651
+ };
652
+ const grouped = await Promise.all(NAME_KINDS.map(async ({ kind, where }) => ({
653
+ kind,
654
+ where,
655
+ rows: await this.db.durableStepCheckpoint.groupBy({
656
+ by: [
657
+ "parallelGroup",
658
+ "status"
659
+ ],
660
+ where: {
661
+ ...scope,
662
+ ...where
663
+ },
664
+ _count: {
665
+ _all: true,
666
+ enqueuedAt: true
667
+ },
668
+ _min: {
669
+ seq: true,
670
+ name: true,
671
+ enqueuedAt: true,
672
+ startedAt: true
673
+ },
674
+ _max: {
675
+ seq: true,
676
+ name: true,
677
+ startedAt: true,
678
+ finishedAt: true
679
+ },
680
+ _sum: {
681
+ attempts: true
682
+ }
683
+ })
684
+ })));
685
+ const stats = await Promise.all(grouped.map(async ({ kind, where, rows }) => {
686
+ const unenqueued = rows.some((r) => r._count.enqueuedAt < r._count._all) ? await this.unenqueuedStartedAt({
687
+ ...scope,
688
+ ...where
689
+ }) : /* @__PURE__ */ new Map();
690
+ return rows.map((r) => toCheckpointStat(kind, r, unenqueued));
691
+ }));
692
+ return stats.flat();
693
+ }
694
+ /** `min(startedAt)` per `(parallelGroup, status)` over the rows whose `enqueuedAt` is NULL. */
695
+ async unenqueuedStartedAt(where) {
696
+ const rows = await this.db.durableStepCheckpoint.groupBy({
697
+ by: [
698
+ "parallelGroup",
699
+ "status"
700
+ ],
701
+ where: {
702
+ ...where,
703
+ enqueuedAt: null
704
+ },
705
+ _min: {
706
+ startedAt: true
707
+ }
708
+ });
709
+ return new Map(rows.map((r) => [
710
+ bucketKey(r.parallelGroup, r.status),
711
+ r._min.startedAt
712
+ ]));
713
+ }
714
+ /** {@link CheckpointQuery} as one `findMany`, in `seq` order, paged by `skip`/`take`. Ungrouped rows
715
+ * are kept past `excludeGroups` explicitly: `NOT IN` alone drops a NULL `parallelGroup`. */
716
+ async queryCheckpoints(runId, query) {
717
+ if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
718
+ const and = [];
719
+ if (query.excludeGroups?.length) {
720
+ and.push({
721
+ OR: [
722
+ {
723
+ parallelGroup: null
724
+ },
725
+ {
726
+ parallelGroup: {
727
+ notIn: query.excludeGroups
728
+ }
729
+ }
730
+ ]
731
+ });
732
+ }
733
+ if (query.namePrefixes) {
734
+ and.push({
735
+ OR: query.namePrefixes.map((p) => ({
736
+ name: {
737
+ startsWith: p
738
+ }
739
+ }))
740
+ });
741
+ }
742
+ const rows = await this.db.durableStepCheckpoint.findMany({
743
+ where: {
744
+ runId,
745
+ ...query.group === void 0 ? {} : {
746
+ parallelGroup: query.group
747
+ },
748
+ ...query.statuses ? {
749
+ status: {
750
+ in: query.statuses
751
+ }
752
+ } : {},
753
+ ...and.length ? {
754
+ AND: and
755
+ } : {}
756
+ },
757
+ orderBy: {
758
+ seq: "asc"
759
+ },
760
+ skip: query.offset,
761
+ take: query.limit
762
+ });
586
763
  return rows.map(fromCheckpointRow);
587
764
  }
588
765
  async putSignalWaiter(waiter) {
@@ -630,7 +807,7 @@ var PrismaStateStore = class {
630
807
  }
631
808
  }
632
809
  });
633
- return rows.map((r) => ({
810
+ return rows.filter((r) => r.token.startsWith(prefix)).map((r) => ({
634
811
  token: r.token,
635
812
  runId: r.runId,
636
813
  seq: r.seq,
@@ -920,6 +1097,69 @@ function fromRunRow(row) {
920
1097
  };
921
1098
  }
922
1099
  __name(fromRunRow, "fromRunRow");
1100
+ var KIND_PREFIXES = [
1101
+ [
1102
+ "child",
1103
+ "signal:child:"
1104
+ ],
1105
+ [
1106
+ "folded",
1107
+ "fan:"
1108
+ ],
1109
+ [
1110
+ "spawn",
1111
+ "spawn:"
1112
+ ]
1113
+ ];
1114
+ var NAME_KINDS = [
1115
+ ...KIND_PREFIXES.map(([kind, prefix]) => ({
1116
+ kind,
1117
+ where: {
1118
+ name: {
1119
+ startsWith: prefix
1120
+ }
1121
+ }
1122
+ })),
1123
+ {
1124
+ kind: "step",
1125
+ where: {
1126
+ NOT: KIND_PREFIXES.map(([, prefix]) => ({
1127
+ name: {
1128
+ startsWith: prefix
1129
+ }
1130
+ }))
1131
+ }
1132
+ }
1133
+ ];
1134
+ function bucketKey(group, status) {
1135
+ return JSON.stringify([
1136
+ group,
1137
+ status
1138
+ ]);
1139
+ }
1140
+ __name(bucketKey, "bucketKey");
1141
+ function toCheckpointStat(nameKind, r, unenqueued) {
1142
+ const fallback = unenqueued.get(bucketKey(r.parallelGroup, r.status));
1143
+ const enqueued = r._min.enqueuedAt;
1144
+ const minEnqueuedAt = enqueued && fallback ? enqueued < fallback ? enqueued : fallback : enqueued ?? fallback;
1145
+ return {
1146
+ group: r.parallelGroup,
1147
+ status: r.status,
1148
+ nameKind,
1149
+ count: r._count._all,
1150
+ minSeq: r._min.seq,
1151
+ maxSeq: r._max.seq,
1152
+ minName: r._min.name,
1153
+ maxName: r._max.name,
1154
+ // Every row has `enqueuedAt` or is in the NULL subset, so one of the two is set.
1155
+ minEnqueuedAt,
1156
+ minStartedAt: r._min.startedAt,
1157
+ maxStartedAt: r._max.startedAt,
1158
+ maxFinishedAt: r._max.finishedAt,
1159
+ sumAttempts: Number(r._sum.attempts ?? 0)
1160
+ };
1161
+ }
1162
+ __name(toCheckpointStat, "toCheckpointStat");
923
1163
  function toCheckpointData(cp) {
924
1164
  return {
925
1165
  runId: cp.runId,