@dudousxd/nestjs-durable-store-prisma 0.20.0 → 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 {
@@ -139,6 +139,7 @@ declare class PrismaStateStore implements StateStore {
139
139
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
140
140
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
141
141
  saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
142
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
142
143
  transaction<T>(work: (tx: {
143
144
  raw: unknown;
144
145
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
@@ -173,6 +174,25 @@ declare class PrismaStateStore implements StateStore {
173
174
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
174
175
  countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
175
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[]>;
176
196
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
177
197
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
178
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 {
@@ -139,6 +139,7 @@ declare class PrismaStateStore implements StateStore {
139
139
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
140
140
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
141
141
  saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
142
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
142
143
  transaction<T>(work: (tx: {
143
144
  raw: unknown;
144
145
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
@@ -173,6 +174,25 @@ declare class PrismaStateStore implements StateStore {
173
174
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
174
175
  countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
175
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[]>;
176
196
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
177
197
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
178
198
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
package/dist/index.js CHANGED
@@ -246,6 +246,25 @@ var PrismaStateStore = class {
246
246
  });
247
247
  return count > 0;
248
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
+ }
249
268
  async transaction(work) {
250
269
  return this.db.$transaction(async (tx) => work({
251
270
  raw: tx,
@@ -610,6 +629,137 @@ var PrismaStateStore = class {
610
629
  seq: "asc"
611
630
  }
612
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
+ });
613
763
  return rows.map(fromCheckpointRow);
614
764
  }
615
765
  async putSignalWaiter(waiter) {
@@ -657,7 +807,7 @@ var PrismaStateStore = class {
657
807
  }
658
808
  }
659
809
  });
660
- return rows.map((r) => ({
810
+ return rows.filter((r) => r.token.startsWith(prefix)).map((r) => ({
661
811
  token: r.token,
662
812
  runId: r.runId,
663
813
  seq: r.seq,
@@ -947,6 +1097,69 @@ function fromRunRow(row) {
947
1097
  };
948
1098
  }
949
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");
950
1163
  function toCheckpointData(cp) {
951
1164
  return {
952
1165
  runId: cp.runId,