@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.cjs CHANGED
@@ -99,6 +99,22 @@ var PrismaStateStore = class {
99
99
  });
100
100
  if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
101
101
  }
102
+ async updateRunIf(runId, expectedStatuses, inputPatch) {
103
+ if (expectedStatuses.length === 0) return false;
104
+ const patch = inputPatch.status && import_nestjs_durable_core.TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? await (0, import_nestjs_durable_core.clearTerminalSingletonAdmission)(runId, inputPatch, (id) => this.getRun(id)) : inputPatch;
105
+ const { count } = await this.db.durableWorkflowRun.updateMany({
106
+ where: {
107
+ id: runId,
108
+ status: {
109
+ in: expectedStatuses
110
+ }
111
+ },
112
+ data: toRunPatch(patch)
113
+ });
114
+ if (count === 0) return false;
115
+ if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
116
+ return true;
117
+ }
102
118
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
103
119
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
104
120
  async reindexAttributes(runId, attributes) {
@@ -271,6 +287,25 @@ var PrismaStateStore = class {
271
287
  });
272
288
  return count > 0;
273
289
  }
290
+ async saveCheckpointsIfPending(checkpoints) {
291
+ if (checkpoints.length === 0) return [];
292
+ return this.db.$transaction(async (tx) => {
293
+ const wrote = [];
294
+ for (const checkpoint of checkpoints) {
295
+ const { runId, seq, ...data } = toCheckpointData(checkpoint);
296
+ const { count } = await tx.durableStepCheckpoint.updateMany({
297
+ where: {
298
+ runId,
299
+ seq,
300
+ status: "pending"
301
+ },
302
+ data
303
+ });
304
+ wrote.push(count > 0);
305
+ }
306
+ return wrote;
307
+ });
308
+ }
274
309
  async transaction(work) {
275
310
  return this.db.$transaction(async (tx) => work({
276
311
  raw: tx,
@@ -303,6 +338,35 @@ var PrismaStateStore = class {
303
338
  });
304
339
  return rows.map(fromRunRow);
305
340
  }
341
+ async listOrphanedRuns(nowMs, limit, namespace) {
342
+ if (limit <= 0) return [];
343
+ const rows = await this.db.durableWorkflowRun.findMany({
344
+ where: {
345
+ status: {
346
+ in: [
347
+ "running",
348
+ "cancelling"
349
+ ]
350
+ },
351
+ OR: [
352
+ {
353
+ lockedUntil: null
354
+ },
355
+ {
356
+ lockedUntil: {
357
+ lte: new Date(nowMs)
358
+ }
359
+ }
360
+ ],
361
+ ...namespaceWhere(namespace)
362
+ },
363
+ orderBy: {
364
+ createdAt: "asc"
365
+ },
366
+ take: limit
367
+ });
368
+ return rows.map(fromRunRow);
369
+ }
306
370
  async listPendingRuns(limit, namespace) {
307
371
  const rows = await this.db.durableWorkflowRun.findMany({
308
372
  where: {
@@ -635,6 +699,137 @@ var PrismaStateStore = class {
635
699
  seq: "asc"
636
700
  }
637
701
  });
702
+ return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointRow);
703
+ }
704
+ /**
705
+ * One `groupBy(parallelGroup, status)` per {@link CheckpointNameKind}, each narrowed to its kind by
706
+ * a name-prefix predicate, run concurrently — Prisma cannot group by a `CASE`, so the kind is the
707
+ * `where` instead. `minEnqueuedAt` reads a NULL `enqueuedAt` as `startedAt` (as
708
+ * {@link fromCheckpointRow} does): a bucket whose non-null `enqueuedAt` count falls short of its
709
+ * row count has such rows, and only for those kinds is the NULL subset's `min(startedAt)` asked for.
710
+ *
711
+ * On SQLite `startsWith` is a case-insensitive `LIKE`, as in {@link listCheckpointsByNamePrefix};
712
+ * the kind prefixes are lower-case and wildcard-free, so only a step whose name starts with one of
713
+ * them in another case would be classified differently there.
714
+ */
715
+ async checkpointStats(runId, opts) {
716
+ const scope = opts?.group === void 0 ? {
717
+ runId
718
+ } : {
719
+ runId,
720
+ parallelGroup: opts.group
721
+ };
722
+ const grouped = await Promise.all(NAME_KINDS.map(async ({ kind, where }) => ({
723
+ kind,
724
+ where,
725
+ rows: await this.db.durableStepCheckpoint.groupBy({
726
+ by: [
727
+ "parallelGroup",
728
+ "status"
729
+ ],
730
+ where: {
731
+ ...scope,
732
+ ...where
733
+ },
734
+ _count: {
735
+ _all: true,
736
+ enqueuedAt: true
737
+ },
738
+ _min: {
739
+ seq: true,
740
+ name: true,
741
+ enqueuedAt: true,
742
+ startedAt: true
743
+ },
744
+ _max: {
745
+ seq: true,
746
+ name: true,
747
+ startedAt: true,
748
+ finishedAt: true
749
+ },
750
+ _sum: {
751
+ attempts: true
752
+ }
753
+ })
754
+ })));
755
+ const stats = await Promise.all(grouped.map(async ({ kind, where, rows }) => {
756
+ const unenqueued = rows.some((r) => r._count.enqueuedAt < r._count._all) ? await this.unenqueuedStartedAt({
757
+ ...scope,
758
+ ...where
759
+ }) : /* @__PURE__ */ new Map();
760
+ return rows.map((r) => toCheckpointStat(kind, r, unenqueued));
761
+ }));
762
+ return stats.flat();
763
+ }
764
+ /** `min(startedAt)` per `(parallelGroup, status)` over the rows whose `enqueuedAt` is NULL. */
765
+ async unenqueuedStartedAt(where) {
766
+ const rows = await this.db.durableStepCheckpoint.groupBy({
767
+ by: [
768
+ "parallelGroup",
769
+ "status"
770
+ ],
771
+ where: {
772
+ ...where,
773
+ enqueuedAt: null
774
+ },
775
+ _min: {
776
+ startedAt: true
777
+ }
778
+ });
779
+ return new Map(rows.map((r) => [
780
+ bucketKey(r.parallelGroup, r.status),
781
+ r._min.startedAt
782
+ ]));
783
+ }
784
+ /** {@link CheckpointQuery} as one `findMany`, in `seq` order, paged by `skip`/`take`. Ungrouped rows
785
+ * are kept past `excludeGroups` explicitly: `NOT IN` alone drops a NULL `parallelGroup`. */
786
+ async queryCheckpoints(runId, query) {
787
+ if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
788
+ const and = [];
789
+ if (query.excludeGroups?.length) {
790
+ and.push({
791
+ OR: [
792
+ {
793
+ parallelGroup: null
794
+ },
795
+ {
796
+ parallelGroup: {
797
+ notIn: query.excludeGroups
798
+ }
799
+ }
800
+ ]
801
+ });
802
+ }
803
+ if (query.namePrefixes) {
804
+ and.push({
805
+ OR: query.namePrefixes.map((p) => ({
806
+ name: {
807
+ startsWith: p
808
+ }
809
+ }))
810
+ });
811
+ }
812
+ const rows = await this.db.durableStepCheckpoint.findMany({
813
+ where: {
814
+ runId,
815
+ ...query.group === void 0 ? {} : {
816
+ parallelGroup: query.group
817
+ },
818
+ ...query.statuses ? {
819
+ status: {
820
+ in: query.statuses
821
+ }
822
+ } : {},
823
+ ...and.length ? {
824
+ AND: and
825
+ } : {}
826
+ },
827
+ orderBy: {
828
+ seq: "asc"
829
+ },
830
+ skip: query.offset,
831
+ take: query.limit
832
+ });
638
833
  return rows.map(fromCheckpointRow);
639
834
  }
640
835
  async putSignalWaiter(waiter) {
@@ -682,7 +877,7 @@ var PrismaStateStore = class {
682
877
  }
683
878
  }
684
879
  });
685
- return rows.map((r) => ({
880
+ return rows.filter((r) => r.token.startsWith(prefix)).map((r) => ({
686
881
  token: r.token,
687
882
  runId: r.runId,
688
883
  seq: r.seq,
@@ -972,6 +1167,69 @@ function fromRunRow(row) {
972
1167
  };
973
1168
  }
974
1169
  __name(fromRunRow, "fromRunRow");
1170
+ var KIND_PREFIXES = [
1171
+ [
1172
+ "child",
1173
+ "signal:child:"
1174
+ ],
1175
+ [
1176
+ "folded",
1177
+ "fan:"
1178
+ ],
1179
+ [
1180
+ "spawn",
1181
+ "spawn:"
1182
+ ]
1183
+ ];
1184
+ var NAME_KINDS = [
1185
+ ...KIND_PREFIXES.map(([kind, prefix]) => ({
1186
+ kind,
1187
+ where: {
1188
+ name: {
1189
+ startsWith: prefix
1190
+ }
1191
+ }
1192
+ })),
1193
+ {
1194
+ kind: "step",
1195
+ where: {
1196
+ NOT: KIND_PREFIXES.map(([, prefix]) => ({
1197
+ name: {
1198
+ startsWith: prefix
1199
+ }
1200
+ }))
1201
+ }
1202
+ }
1203
+ ];
1204
+ function bucketKey(group, status) {
1205
+ return JSON.stringify([
1206
+ group,
1207
+ status
1208
+ ]);
1209
+ }
1210
+ __name(bucketKey, "bucketKey");
1211
+ function toCheckpointStat(nameKind, r, unenqueued) {
1212
+ const fallback = unenqueued.get(bucketKey(r.parallelGroup, r.status));
1213
+ const enqueued = r._min.enqueuedAt;
1214
+ const minEnqueuedAt = enqueued && fallback ? enqueued < fallback ? enqueued : fallback : enqueued ?? fallback;
1215
+ return {
1216
+ group: r.parallelGroup,
1217
+ status: r.status,
1218
+ nameKind,
1219
+ count: r._count._all,
1220
+ minSeq: r._min.seq,
1221
+ maxSeq: r._max.seq,
1222
+ minName: r._min.name,
1223
+ maxName: r._max.name,
1224
+ // Every row has `enqueuedAt` or is in the NULL subset, so one of the two is set.
1225
+ minEnqueuedAt,
1226
+ minStartedAt: r._min.startedAt,
1227
+ maxStartedAt: r._max.startedAt,
1228
+ maxFinishedAt: r._max.finishedAt,
1229
+ sumAttempts: Number(r._sum.attempts ?? 0)
1230
+ };
1231
+ }
1232
+ __name(toCheckpointStat, "toCheckpointStat");
975
1233
  function toCheckpointData(cp) {
976
1234
  return {
977
1235
  runId: cp.runId,