@dudousxd/nestjs-durable-store-prisma 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.js CHANGED
@@ -74,6 +74,22 @@ var PrismaStateStore = class {
74
74
  });
75
75
  if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
76
76
  }
77
+ async updateRunIf(runId, expectedStatuses, inputPatch) {
78
+ if (expectedStatuses.length === 0) return false;
79
+ const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? await clearTerminalSingletonAdmission(runId, inputPatch, (id) => this.getRun(id)) : inputPatch;
80
+ const { count } = await this.db.durableWorkflowRun.updateMany({
81
+ where: {
82
+ id: runId,
83
+ status: {
84
+ in: expectedStatuses
85
+ }
86
+ },
87
+ data: toRunPatch(patch)
88
+ });
89
+ if (count === 0) return false;
90
+ if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
91
+ return true;
92
+ }
77
93
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
78
94
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
79
95
  async reindexAttributes(runId, attributes) {
@@ -246,6 +262,25 @@ var PrismaStateStore = class {
246
262
  });
247
263
  return count > 0;
248
264
  }
265
+ async saveCheckpointsIfPending(checkpoints) {
266
+ if (checkpoints.length === 0) return [];
267
+ return this.db.$transaction(async (tx) => {
268
+ const wrote = [];
269
+ for (const checkpoint of checkpoints) {
270
+ const { runId, seq, ...data } = toCheckpointData(checkpoint);
271
+ const { count } = await tx.durableStepCheckpoint.updateMany({
272
+ where: {
273
+ runId,
274
+ seq,
275
+ status: "pending"
276
+ },
277
+ data
278
+ });
279
+ wrote.push(count > 0);
280
+ }
281
+ return wrote;
282
+ });
283
+ }
249
284
  async transaction(work) {
250
285
  return this.db.$transaction(async (tx) => work({
251
286
  raw: tx,
@@ -278,6 +313,35 @@ var PrismaStateStore = class {
278
313
  });
279
314
  return rows.map(fromRunRow);
280
315
  }
316
+ async listOrphanedRuns(nowMs, limit, namespace) {
317
+ if (limit <= 0) return [];
318
+ const rows = await this.db.durableWorkflowRun.findMany({
319
+ where: {
320
+ status: {
321
+ in: [
322
+ "running",
323
+ "cancelling"
324
+ ]
325
+ },
326
+ OR: [
327
+ {
328
+ lockedUntil: null
329
+ },
330
+ {
331
+ lockedUntil: {
332
+ lte: new Date(nowMs)
333
+ }
334
+ }
335
+ ],
336
+ ...namespaceWhere(namespace)
337
+ },
338
+ orderBy: {
339
+ createdAt: "asc"
340
+ },
341
+ take: limit
342
+ });
343
+ return rows.map(fromRunRow);
344
+ }
281
345
  async listPendingRuns(limit, namespace) {
282
346
  const rows = await this.db.durableWorkflowRun.findMany({
283
347
  where: {
@@ -610,6 +674,137 @@ var PrismaStateStore = class {
610
674
  seq: "asc"
611
675
  }
612
676
  });
677
+ return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointRow);
678
+ }
679
+ /**
680
+ * One `groupBy(parallelGroup, status)` per {@link CheckpointNameKind}, each narrowed to its kind by
681
+ * a name-prefix predicate, run concurrently — Prisma cannot group by a `CASE`, so the kind is the
682
+ * `where` instead. `minEnqueuedAt` reads a NULL `enqueuedAt` as `startedAt` (as
683
+ * {@link fromCheckpointRow} does): a bucket whose non-null `enqueuedAt` count falls short of its
684
+ * row count has such rows, and only for those kinds is the NULL subset's `min(startedAt)` asked for.
685
+ *
686
+ * On SQLite `startsWith` is a case-insensitive `LIKE`, as in {@link listCheckpointsByNamePrefix};
687
+ * the kind prefixes are lower-case and wildcard-free, so only a step whose name starts with one of
688
+ * them in another case would be classified differently there.
689
+ */
690
+ async checkpointStats(runId, opts) {
691
+ const scope = opts?.group === void 0 ? {
692
+ runId
693
+ } : {
694
+ runId,
695
+ parallelGroup: opts.group
696
+ };
697
+ const grouped = await Promise.all(NAME_KINDS.map(async ({ kind, where }) => ({
698
+ kind,
699
+ where,
700
+ rows: await this.db.durableStepCheckpoint.groupBy({
701
+ by: [
702
+ "parallelGroup",
703
+ "status"
704
+ ],
705
+ where: {
706
+ ...scope,
707
+ ...where
708
+ },
709
+ _count: {
710
+ _all: true,
711
+ enqueuedAt: true
712
+ },
713
+ _min: {
714
+ seq: true,
715
+ name: true,
716
+ enqueuedAt: true,
717
+ startedAt: true
718
+ },
719
+ _max: {
720
+ seq: true,
721
+ name: true,
722
+ startedAt: true,
723
+ finishedAt: true
724
+ },
725
+ _sum: {
726
+ attempts: true
727
+ }
728
+ })
729
+ })));
730
+ const stats = await Promise.all(grouped.map(async ({ kind, where, rows }) => {
731
+ const unenqueued = rows.some((r) => r._count.enqueuedAt < r._count._all) ? await this.unenqueuedStartedAt({
732
+ ...scope,
733
+ ...where
734
+ }) : /* @__PURE__ */ new Map();
735
+ return rows.map((r) => toCheckpointStat(kind, r, unenqueued));
736
+ }));
737
+ return stats.flat();
738
+ }
739
+ /** `min(startedAt)` per `(parallelGroup, status)` over the rows whose `enqueuedAt` is NULL. */
740
+ async unenqueuedStartedAt(where) {
741
+ const rows = await this.db.durableStepCheckpoint.groupBy({
742
+ by: [
743
+ "parallelGroup",
744
+ "status"
745
+ ],
746
+ where: {
747
+ ...where,
748
+ enqueuedAt: null
749
+ },
750
+ _min: {
751
+ startedAt: true
752
+ }
753
+ });
754
+ return new Map(rows.map((r) => [
755
+ bucketKey(r.parallelGroup, r.status),
756
+ r._min.startedAt
757
+ ]));
758
+ }
759
+ /** {@link CheckpointQuery} as one `findMany`, in `seq` order, paged by `skip`/`take`. Ungrouped rows
760
+ * are kept past `excludeGroups` explicitly: `NOT IN` alone drops a NULL `parallelGroup`. */
761
+ async queryCheckpoints(runId, query) {
762
+ if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
763
+ const and = [];
764
+ if (query.excludeGroups?.length) {
765
+ and.push({
766
+ OR: [
767
+ {
768
+ parallelGroup: null
769
+ },
770
+ {
771
+ parallelGroup: {
772
+ notIn: query.excludeGroups
773
+ }
774
+ }
775
+ ]
776
+ });
777
+ }
778
+ if (query.namePrefixes) {
779
+ and.push({
780
+ OR: query.namePrefixes.map((p) => ({
781
+ name: {
782
+ startsWith: p
783
+ }
784
+ }))
785
+ });
786
+ }
787
+ const rows = await this.db.durableStepCheckpoint.findMany({
788
+ where: {
789
+ runId,
790
+ ...query.group === void 0 ? {} : {
791
+ parallelGroup: query.group
792
+ },
793
+ ...query.statuses ? {
794
+ status: {
795
+ in: query.statuses
796
+ }
797
+ } : {},
798
+ ...and.length ? {
799
+ AND: and
800
+ } : {}
801
+ },
802
+ orderBy: {
803
+ seq: "asc"
804
+ },
805
+ skip: query.offset,
806
+ take: query.limit
807
+ });
613
808
  return rows.map(fromCheckpointRow);
614
809
  }
615
810
  async putSignalWaiter(waiter) {
@@ -657,7 +852,7 @@ var PrismaStateStore = class {
657
852
  }
658
853
  }
659
854
  });
660
- return rows.map((r) => ({
855
+ return rows.filter((r) => r.token.startsWith(prefix)).map((r) => ({
661
856
  token: r.token,
662
857
  runId: r.runId,
663
858
  seq: r.seq,
@@ -947,6 +1142,69 @@ function fromRunRow(row) {
947
1142
  };
948
1143
  }
949
1144
  __name(fromRunRow, "fromRunRow");
1145
+ var KIND_PREFIXES = [
1146
+ [
1147
+ "child",
1148
+ "signal:child:"
1149
+ ],
1150
+ [
1151
+ "folded",
1152
+ "fan:"
1153
+ ],
1154
+ [
1155
+ "spawn",
1156
+ "spawn:"
1157
+ ]
1158
+ ];
1159
+ var NAME_KINDS = [
1160
+ ...KIND_PREFIXES.map(([kind, prefix]) => ({
1161
+ kind,
1162
+ where: {
1163
+ name: {
1164
+ startsWith: prefix
1165
+ }
1166
+ }
1167
+ })),
1168
+ {
1169
+ kind: "step",
1170
+ where: {
1171
+ NOT: KIND_PREFIXES.map(([, prefix]) => ({
1172
+ name: {
1173
+ startsWith: prefix
1174
+ }
1175
+ }))
1176
+ }
1177
+ }
1178
+ ];
1179
+ function bucketKey(group, status) {
1180
+ return JSON.stringify([
1181
+ group,
1182
+ status
1183
+ ]);
1184
+ }
1185
+ __name(bucketKey, "bucketKey");
1186
+ function toCheckpointStat(nameKind, r, unenqueued) {
1187
+ const fallback = unenqueued.get(bucketKey(r.parallelGroup, r.status));
1188
+ const enqueued = r._min.enqueuedAt;
1189
+ const minEnqueuedAt = enqueued && fallback ? enqueued < fallback ? enqueued : fallback : enqueued ?? fallback;
1190
+ return {
1191
+ group: r.parallelGroup,
1192
+ status: r.status,
1193
+ nameKind,
1194
+ count: r._count._all,
1195
+ minSeq: r._min.seq,
1196
+ maxSeq: r._max.seq,
1197
+ minName: r._min.name,
1198
+ maxName: r._max.name,
1199
+ // Every row has `enqueuedAt` or is in the NULL subset, so one of the two is set.
1200
+ minEnqueuedAt,
1201
+ minStartedAt: r._min.startedAt,
1202
+ maxStartedAt: r._max.startedAt,
1203
+ maxFinishedAt: r._max.finishedAt,
1204
+ sumAttempts: Number(r._sum.attempts ?? 0)
1205
+ };
1206
+ }
1207
+ __name(toCheckpointStat, "toCheckpointStat");
950
1208
  function toCheckpointData(cp) {
951
1209
  return {
952
1210
  runId: cp.runId,