@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 +259 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -3
- package/dist/index.d.ts +25 -3
- package/dist/index.js +259 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/prisma/schema.prisma +2 -0
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,
|