@dudousxd/nestjs-durable-store-typeorm 0.21.0 → 0.23.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 +181 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +181 -35
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -298,6 +298,18 @@ function durableEntities(options = {}) {
|
|
|
298
298
|
name: "StepCheckpointEntity",
|
|
299
299
|
target: StepCheckpointEntity,
|
|
300
300
|
tableName: "durable_step_checkpoints",
|
|
301
|
+
// A run's fan, in member order: `checkpointStats`/`queryCheckpoints` narrow by group and page by
|
|
302
|
+
// seq. The DDL that actually creates it lives in `ensureTypeOrmDurableSchema`.
|
|
303
|
+
indices: [
|
|
304
|
+
{
|
|
305
|
+
name: "durable_step_checkpoints_group_idx",
|
|
306
|
+
columns: [
|
|
307
|
+
"runId",
|
|
308
|
+
"parallelGroup",
|
|
309
|
+
"seq"
|
|
310
|
+
]
|
|
311
|
+
}
|
|
312
|
+
],
|
|
301
313
|
columns: {
|
|
302
314
|
runId: {
|
|
303
315
|
type: "text",
|
|
@@ -840,6 +852,13 @@ async function ensureTypeOrmDurableSchema(dataSource) {
|
|
|
840
852
|
"durable_runs_namespace_status_idx",
|
|
841
853
|
`${runs} (${runsCol("namespace")}, ${runsCol("status")}, ${runsCol("createdAt")})`
|
|
842
854
|
],
|
|
855
|
+
// A run's checkpoints by fan, in seq order: the run detail's per-group aggregates and fan
|
|
856
|
+
// pages. `parallel_group` is `varchar(191)` in this DDL (also when added by ALTER above), so
|
|
857
|
+
// MySQL can key it without a prefix length.
|
|
858
|
+
[
|
|
859
|
+
"durable_step_checkpoints_group_idx",
|
|
860
|
+
`${checkpoints} (${cpCol("runId")}, ${cpCol("parallelGroup")}, ${cpCol("seq")})`
|
|
861
|
+
],
|
|
843
862
|
// A run's waiters: the `ctx.all` join barrier probes them once per settled child.
|
|
844
863
|
[
|
|
845
864
|
"durable_signal_waiters_run_id_idx",
|
|
@@ -899,6 +918,7 @@ __name(existingColumns, "existingColumns");
|
|
|
899
918
|
// src/typeorm-state-store.ts
|
|
900
919
|
var import_nestjs_durable_core = require("@dudousxd/nestjs-durable-core");
|
|
901
920
|
var import_typeorm2 = require("typeorm");
|
|
921
|
+
var CHECKPOINT_NAME_KIND_SQL = "CASE WHEN cp.name LIKE 'signal:child:%' THEN 'child' WHEN cp.name LIKE 'fan:%' THEN 'folded' WHEN cp.name LIKE 'spawn:%' THEN 'spawn' ELSE 'step' END";
|
|
902
922
|
var sqliteSingletonClaims = /* @__PURE__ */ new WeakMap();
|
|
903
923
|
var TypeOrmStateStore = class {
|
|
904
924
|
static {
|
|
@@ -999,31 +1019,24 @@ var TypeOrmStateStore = class {
|
|
|
999
1019
|
}
|
|
1000
1020
|
async updateRun(runId, inputPatch) {
|
|
1001
1021
|
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;
|
|
1002
|
-
const update =
|
|
1003
|
-
if ("workflow" in patch) update.workflow = patch.workflow;
|
|
1004
|
-
if ("workflowVersion" in patch) update.workflowVersion = patch.workflowVersion;
|
|
1005
|
-
if ("status" in patch) update.status = patch.status;
|
|
1006
|
-
if ("input" in patch) update.input = patch.input ?? null;
|
|
1007
|
-
if ("output" in patch) update.output = patch.output ?? null;
|
|
1008
|
-
if ("error" in patch) update.error = patch.error ?? null;
|
|
1009
|
-
if ("wakeAt" in patch) update.wakeAt = patch.wakeAt == null ? null : new Date(patch.wakeAt);
|
|
1010
|
-
if ("lockedBy" in patch) update.lockedBy = patch.lockedBy ?? null;
|
|
1011
|
-
if ("lockedUntil" in patch) update.lockedUntil = patch.lockedUntil == null ? null : new Date(patch.lockedUntil);
|
|
1012
|
-
if ("awaitingDecisionTaskId" in patch) update.awaitingDecisionTaskId = patch.awaitingDecisionTaskId ?? null;
|
|
1013
|
-
if ("recoveryAttempts" in patch) update.recoveryAttempts = patch.recoveryAttempts;
|
|
1014
|
-
if ("tags" in patch) update.tags = patch.tags ?? null;
|
|
1015
|
-
if ("searchAttributes" in patch) update.searchAttributes = patch.searchAttributes ?? null;
|
|
1016
|
-
if ("priority" in patch) update.priority = patch.priority ?? null;
|
|
1017
|
-
if ("namespace" in patch) update.namespace = patch.namespace ?? "default";
|
|
1018
|
-
if ("origin" in patch) update.origin = patch.origin ?? null;
|
|
1019
|
-
if ("createdAt" in patch) update.createdAt = patch.createdAt;
|
|
1020
|
-
if ("updatedAt" in patch) update.updatedAt = patch.updatedAt;
|
|
1022
|
+
const update = runUpdateSet(patch);
|
|
1021
1023
|
const result = await this.runs().createQueryBuilder().update().set(update).where({
|
|
1022
1024
|
id: runId
|
|
1023
1025
|
}).execute();
|
|
1024
1026
|
if (!result.affected) throw new Error(`run ${runId} not found`);
|
|
1025
1027
|
if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
|
|
1026
1028
|
}
|
|
1029
|
+
async updateRunIf(runId, expectedStatuses, inputPatch) {
|
|
1030
|
+
if (expectedStatuses.length === 0) return false;
|
|
1031
|
+
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;
|
|
1032
|
+
const result = await this.runs().createQueryBuilder().update().set(runUpdateSet(patch)).where({
|
|
1033
|
+
id: runId,
|
|
1034
|
+
status: (0, import_typeorm2.In)(expectedStatuses)
|
|
1035
|
+
}).execute();
|
|
1036
|
+
if (!result.affected) return false;
|
|
1037
|
+
if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1027
1040
|
async getRun(runId) {
|
|
1028
1041
|
const e = await this.runs().findOneBy({
|
|
1029
1042
|
id: runId
|
|
@@ -1105,20 +1118,16 @@ var TypeOrmStateStore = class {
|
|
|
1105
1118
|
]);
|
|
1106
1119
|
}
|
|
1107
1120
|
async saveCheckpointIfPending(checkpoint) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
seq,
|
|
1119
|
-
status: "pending"
|
|
1120
|
-
}).execute();
|
|
1121
|
-
return (result.affected ?? 0) > 0;
|
|
1121
|
+
return updateIfPending(this.checkpoints(), checkpoint);
|
|
1122
|
+
}
|
|
1123
|
+
async saveCheckpointsIfPending(checkpoints) {
|
|
1124
|
+
if (checkpoints.length === 0) return [];
|
|
1125
|
+
return this.dataSource.transaction(async (em) => {
|
|
1126
|
+
const repo = em.getRepository(StepCheckpointEntity);
|
|
1127
|
+
const wrote = [];
|
|
1128
|
+
for (const checkpoint of checkpoints) wrote.push(await updateIfPending(repo, checkpoint));
|
|
1129
|
+
return wrote;
|
|
1130
|
+
});
|
|
1122
1131
|
}
|
|
1123
1132
|
async transaction(work) {
|
|
1124
1133
|
return this.dataSource.transaction(async (em) => work({
|
|
@@ -1147,6 +1156,39 @@ var TypeOrmStateStore = class {
|
|
|
1147
1156
|
});
|
|
1148
1157
|
return rows.map(fromRunEntity);
|
|
1149
1158
|
}
|
|
1159
|
+
async listOrphanedRuns(nowMs, limit, namespace) {
|
|
1160
|
+
if (limit <= 0) return [];
|
|
1161
|
+
const now = new Date(nowMs);
|
|
1162
|
+
const rows = await this.runs().find({
|
|
1163
|
+
where: [
|
|
1164
|
+
{
|
|
1165
|
+
status: (0, import_typeorm2.In)([
|
|
1166
|
+
"running",
|
|
1167
|
+
"cancelling"
|
|
1168
|
+
]),
|
|
1169
|
+
lockedUntil: (0, import_typeorm2.IsNull)(),
|
|
1170
|
+
...namespace !== void 0 ? {
|
|
1171
|
+
namespace
|
|
1172
|
+
} : {}
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
status: (0, import_typeorm2.In)([
|
|
1176
|
+
"running",
|
|
1177
|
+
"cancelling"
|
|
1178
|
+
]),
|
|
1179
|
+
lockedUntil: (0, import_typeorm2.LessThanOrEqual)(now),
|
|
1180
|
+
...namespace !== void 0 ? {
|
|
1181
|
+
namespace
|
|
1182
|
+
} : {}
|
|
1183
|
+
}
|
|
1184
|
+
],
|
|
1185
|
+
order: {
|
|
1186
|
+
createdAt: "ASC"
|
|
1187
|
+
},
|
|
1188
|
+
take: limit
|
|
1189
|
+
});
|
|
1190
|
+
return rows.map(fromRunEntity);
|
|
1191
|
+
}
|
|
1150
1192
|
async listPendingRuns(limit, namespace) {
|
|
1151
1193
|
const rows = await this.runs().find({
|
|
1152
1194
|
where: {
|
|
@@ -1440,7 +1482,71 @@ var TypeOrmStateStore = class {
|
|
|
1440
1482
|
seq: "ASC"
|
|
1441
1483
|
}
|
|
1442
1484
|
});
|
|
1443
|
-
return rows.map(fromCheckpointEntity);
|
|
1485
|
+
return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointEntity);
|
|
1486
|
+
}
|
|
1487
|
+
/** `GROUP BY parallelGroup, status, nameKind` over one run's checkpoints — one aggregate, so a run
|
|
1488
|
+
* detail draws a wide fan from a few bucket rows instead of loading every member. The `CASE` is
|
|
1489
|
+
* {@link checkpointNameKind} in SQL, and the date aggregates are hydrated through the driver the
|
|
1490
|
+
* way an entity read hydrates the column, so a bucket compares equal to the in-memory reference. */
|
|
1491
|
+
async checkpointStats(runId, opts) {
|
|
1492
|
+
const qb = this.checkpoints().createQueryBuilder("cp").select("cp.parallelGroup", "group").addSelect("cp.status", "status").addSelect(CHECKPOINT_NAME_KIND_SQL, "nameKind").addSelect("COUNT(*)", "count").addSelect("MIN(cp.seq)", "minSeq").addSelect("MAX(cp.seq)", "maxSeq").addSelect("MIN(cp.name)", "minName").addSelect("MAX(cp.name)", "maxName").addSelect("MIN(COALESCE(cp.enqueuedAt, cp.startedAt))", "minEnqueuedAt").addSelect("MIN(cp.startedAt)", "minStartedAt").addSelect("MAX(cp.startedAt)", "maxStartedAt").addSelect("MAX(cp.finishedAt)", "maxFinishedAt").addSelect("SUM(cp.attempts)", "sumAttempts").where("cp.runId = :runId", {
|
|
1493
|
+
runId
|
|
1494
|
+
});
|
|
1495
|
+
if (opts?.group !== void 0) qb.andWhere("cp.parallelGroup = :group", {
|
|
1496
|
+
group: opts.group
|
|
1497
|
+
});
|
|
1498
|
+
const rows = await qb.groupBy("cp.parallelGroup").addGroupBy("cp.status").addGroupBy(CHECKPOINT_NAME_KIND_SQL).getRawMany();
|
|
1499
|
+
const dateColumn = this.dataSource.getMetadata(StepCheckpointEntity).findColumnWithPropertyName("startedAt");
|
|
1500
|
+
const toDate = /* @__PURE__ */ __name((raw) => dateColumn ? this.dataSource.driver.prepareHydratedValue(raw, dateColumn) : new Date(raw), "toDate");
|
|
1501
|
+
return rows.map((r) => ({
|
|
1502
|
+
group: r.group ?? null,
|
|
1503
|
+
status: r.status,
|
|
1504
|
+
nameKind: r.nameKind,
|
|
1505
|
+
count: Number(r.count),
|
|
1506
|
+
minSeq: Number(r.minSeq),
|
|
1507
|
+
maxSeq: Number(r.maxSeq),
|
|
1508
|
+
minName: r.minName,
|
|
1509
|
+
maxName: r.maxName,
|
|
1510
|
+
minEnqueuedAt: toDate(r.minEnqueuedAt),
|
|
1511
|
+
minStartedAt: toDate(r.minStartedAt),
|
|
1512
|
+
maxStartedAt: toDate(r.maxStartedAt),
|
|
1513
|
+
maxFinishedAt: toDate(r.maxFinishedAt),
|
|
1514
|
+
sumAttempts: Number(r.sumAttempts)
|
|
1515
|
+
}));
|
|
1516
|
+
}
|
|
1517
|
+
/** One run's checkpoints matching `query`, in `seq` order and paged in SQL — the read the run
|
|
1518
|
+
* detail uses for a fan's failures, its folded checkpoints and one page of members. An empty
|
|
1519
|
+
* `statuses`/`namePrefixes` matches nothing; an empty `excludeGroups` excludes nothing. */
|
|
1520
|
+
async queryCheckpoints(runId, query) {
|
|
1521
|
+
if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
|
|
1522
|
+
const qb = this.checkpoints().createQueryBuilder("cp").where("cp.runId = :runId", {
|
|
1523
|
+
runId
|
|
1524
|
+
});
|
|
1525
|
+
if (query.group !== void 0) qb.andWhere("cp.parallelGroup = :group", {
|
|
1526
|
+
group: query.group
|
|
1527
|
+
});
|
|
1528
|
+
if (query.excludeGroups?.length) {
|
|
1529
|
+
qb.andWhere("(cp.parallelGroup IS NULL OR cp.parallelGroup NOT IN (:...excludeGroups))", {
|
|
1530
|
+
excludeGroups: query.excludeGroups
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
if (query.statuses) qb.andWhere("cp.status IN (:...statuses)", {
|
|
1534
|
+
statuses: query.statuses
|
|
1535
|
+
});
|
|
1536
|
+
if (query.namePrefixes) {
|
|
1537
|
+
const prefixes = query.namePrefixes.map((p) => `${p.replace(/[!%_]/g, "!$&")}%`);
|
|
1538
|
+
const branches = prefixes.map((_, i) => `cp.name LIKE :namePrefix${i} ESCAPE '!'`);
|
|
1539
|
+
qb.andWhere(`(${branches.join(" OR ")})`, Object.fromEntries(prefixes.map((p, i) => [
|
|
1540
|
+
`namePrefix${i}`,
|
|
1541
|
+
p
|
|
1542
|
+
])));
|
|
1543
|
+
}
|
|
1544
|
+
qb.orderBy("cp.seq", "ASC");
|
|
1545
|
+
if (query.offset != null) qb.offset(query.offset);
|
|
1546
|
+
if (query.limit != null || query.offset != null) {
|
|
1547
|
+
qb.limit(query.limit ?? Number.MAX_SAFE_INTEGER);
|
|
1548
|
+
}
|
|
1549
|
+
return (await qb.getMany()).map(fromCheckpointEntity);
|
|
1444
1550
|
}
|
|
1445
1551
|
async putSignalWaiter(waiter) {
|
|
1446
1552
|
await this.waiters().save({
|
|
@@ -1456,7 +1562,7 @@ var TypeOrmStateStore = class {
|
|
|
1456
1562
|
token: (0, import_typeorm2.Like)(`${prefix}%`)
|
|
1457
1563
|
}
|
|
1458
1564
|
});
|
|
1459
|
-
return rows.map((e) => ({
|
|
1565
|
+
return rows.filter((e) => e.token.startsWith(prefix)).map((e) => ({
|
|
1460
1566
|
token: e.token,
|
|
1461
1567
|
runId: e.runId,
|
|
1462
1568
|
seq: e.seq,
|
|
@@ -1747,6 +1853,46 @@ function fromScheduleEntity(e) {
|
|
|
1747
1853
|
};
|
|
1748
1854
|
}
|
|
1749
1855
|
__name(fromScheduleEntity, "fromScheduleEntity");
|
|
1856
|
+
function runUpdateSet(patch) {
|
|
1857
|
+
const update = {};
|
|
1858
|
+
if ("workflow" in patch) update.workflow = patch.workflow;
|
|
1859
|
+
if ("workflowVersion" in patch) update.workflowVersion = patch.workflowVersion;
|
|
1860
|
+
if ("status" in patch) update.status = patch.status;
|
|
1861
|
+
if ("input" in patch) update.input = patch.input ?? null;
|
|
1862
|
+
if ("output" in patch) update.output = patch.output ?? null;
|
|
1863
|
+
if ("error" in patch) update.error = patch.error ?? null;
|
|
1864
|
+
if ("wakeAt" in patch) update.wakeAt = patch.wakeAt == null ? null : new Date(patch.wakeAt);
|
|
1865
|
+
if ("lockedBy" in patch) update.lockedBy = patch.lockedBy ?? null;
|
|
1866
|
+
if ("lockedUntil" in patch) update.lockedUntil = patch.lockedUntil == null ? null : new Date(patch.lockedUntil);
|
|
1867
|
+
if ("awaitingDecisionTaskId" in patch) update.awaitingDecisionTaskId = patch.awaitingDecisionTaskId ?? null;
|
|
1868
|
+
if ("recoveryAttempts" in patch) update.recoveryAttempts = patch.recoveryAttempts;
|
|
1869
|
+
if ("tags" in patch) update.tags = patch.tags ?? null;
|
|
1870
|
+
if ("searchAttributes" in patch) update.searchAttributes = patch.searchAttributes ?? null;
|
|
1871
|
+
if ("priority" in patch) update.priority = patch.priority ?? null;
|
|
1872
|
+
if ("namespace" in patch) update.namespace = patch.namespace ?? "default";
|
|
1873
|
+
if ("origin" in patch) update.origin = patch.origin ?? null;
|
|
1874
|
+
if ("createdAt" in patch) update.createdAt = patch.createdAt;
|
|
1875
|
+
if ("updatedAt" in patch) update.updatedAt = patch.updatedAt;
|
|
1876
|
+
return update;
|
|
1877
|
+
}
|
|
1878
|
+
__name(runUpdateSet, "runUpdateSet");
|
|
1879
|
+
async function updateIfPending(repo, checkpoint) {
|
|
1880
|
+
const { runId, seq, ...data } = toCheckpointEntity(checkpoint);
|
|
1881
|
+
const set = {
|
|
1882
|
+
...data,
|
|
1883
|
+
wakeAt: checkpoint.wakeAt != null ? new Date(checkpoint.wakeAt) : () => "NULL"
|
|
1884
|
+
};
|
|
1885
|
+
for (const key of Object.keys(set)) {
|
|
1886
|
+
if (set[key] === void 0) delete set[key];
|
|
1887
|
+
}
|
|
1888
|
+
const result = await repo.createQueryBuilder().update().set(set).where({
|
|
1889
|
+
runId,
|
|
1890
|
+
seq,
|
|
1891
|
+
status: "pending"
|
|
1892
|
+
}).execute();
|
|
1893
|
+
return (result.affected ?? 0) > 0;
|
|
1894
|
+
}
|
|
1895
|
+
__name(updateIfPending, "updateIfPending");
|
|
1750
1896
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1751
1897
|
0 && (module.exports = {
|
|
1752
1898
|
BufferedEventEntity,
|