@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 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
- const { runId, seq, ...data } = toCheckpointEntity(checkpoint);
1109
- const set = {
1110
- ...data,
1111
- wakeAt: checkpoint.wakeAt != null ? new Date(checkpoint.wakeAt) : () => "NULL"
1112
- };
1113
- for (const key of Object.keys(set)) {
1114
- if (set[key] === void 0) delete set[key];
1115
- }
1116
- const result = await this.checkpoints().createQueryBuilder().update().set(set).where({
1117
- runId,
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,