@dudousxd/nestjs-durable-store-typeorm 0.20.1 → 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
@@ -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 {
@@ -1104,6 +1124,18 @@ var TypeOrmStateStore = class {
1104
1124
  "seq"
1105
1125
  ]);
1106
1126
  }
1127
+ async saveCheckpointIfPending(checkpoint) {
1128
+ return updateIfPending(this.checkpoints(), checkpoint);
1129
+ }
1130
+ async saveCheckpointsIfPending(checkpoints) {
1131
+ if (checkpoints.length === 0) return [];
1132
+ return this.dataSource.transaction(async (em) => {
1133
+ const repo = em.getRepository(StepCheckpointEntity);
1134
+ const wrote = [];
1135
+ for (const checkpoint of checkpoints) wrote.push(await updateIfPending(repo, checkpoint));
1136
+ return wrote;
1137
+ });
1138
+ }
1107
1139
  async transaction(work) {
1108
1140
  return this.dataSource.transaction(async (em) => work({
1109
1141
  raw: em,
@@ -1404,6 +1436,15 @@ var TypeOrmStateStore = class {
1404
1436
  });
1405
1437
  return e ? fromCheckpointEntity(e) : void 0;
1406
1438
  }
1439
+ async countCheckpoints(runId, fromSeq, toSeq, statuses) {
1440
+ if (statuses.length === 0) return 0;
1441
+ const row = await this.checkpoints().createQueryBuilder("cp").select("COUNT(*)", "count").where({
1442
+ runId,
1443
+ seq: (0, import_typeorm2.Between)(fromSeq, toSeq),
1444
+ status: (0, import_typeorm2.In)(statuses)
1445
+ }).getRawOne();
1446
+ return Number(row?.count ?? 0);
1447
+ }
1407
1448
  async listCheckpointsByNamePrefix(runId, prefixes) {
1408
1449
  if (prefixes.length === 0) return [];
1409
1450
  const rows = await this.checkpoints().find({
@@ -1415,7 +1456,71 @@ var TypeOrmStateStore = class {
1415
1456
  seq: "ASC"
1416
1457
  }
1417
1458
  });
1418
- return rows.map(fromCheckpointEntity);
1459
+ return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointEntity);
1460
+ }
1461
+ /** `GROUP BY parallelGroup, status, nameKind` over one run's checkpoints — one aggregate, so a run
1462
+ * detail draws a wide fan from a few bucket rows instead of loading every member. The `CASE` is
1463
+ * {@link checkpointNameKind} in SQL, and the date aggregates are hydrated through the driver the
1464
+ * way an entity read hydrates the column, so a bucket compares equal to the in-memory reference. */
1465
+ async checkpointStats(runId, opts) {
1466
+ 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", {
1467
+ runId
1468
+ });
1469
+ if (opts?.group !== void 0) qb.andWhere("cp.parallelGroup = :group", {
1470
+ group: opts.group
1471
+ });
1472
+ const rows = await qb.groupBy("cp.parallelGroup").addGroupBy("cp.status").addGroupBy(CHECKPOINT_NAME_KIND_SQL).getRawMany();
1473
+ const dateColumn = this.dataSource.getMetadata(StepCheckpointEntity).findColumnWithPropertyName("startedAt");
1474
+ const toDate = /* @__PURE__ */ __name((raw) => dateColumn ? this.dataSource.driver.prepareHydratedValue(raw, dateColumn) : new Date(raw), "toDate");
1475
+ return rows.map((r) => ({
1476
+ group: r.group ?? null,
1477
+ status: r.status,
1478
+ nameKind: r.nameKind,
1479
+ count: Number(r.count),
1480
+ minSeq: Number(r.minSeq),
1481
+ maxSeq: Number(r.maxSeq),
1482
+ minName: r.minName,
1483
+ maxName: r.maxName,
1484
+ minEnqueuedAt: toDate(r.minEnqueuedAt),
1485
+ minStartedAt: toDate(r.minStartedAt),
1486
+ maxStartedAt: toDate(r.maxStartedAt),
1487
+ maxFinishedAt: toDate(r.maxFinishedAt),
1488
+ sumAttempts: Number(r.sumAttempts)
1489
+ }));
1490
+ }
1491
+ /** One run's checkpoints matching `query`, in `seq` order and paged in SQL — the read the run
1492
+ * detail uses for a fan's failures, its folded checkpoints and one page of members. An empty
1493
+ * `statuses`/`namePrefixes` matches nothing; an empty `excludeGroups` excludes nothing. */
1494
+ async queryCheckpoints(runId, query) {
1495
+ if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
1496
+ const qb = this.checkpoints().createQueryBuilder("cp").where("cp.runId = :runId", {
1497
+ runId
1498
+ });
1499
+ if (query.group !== void 0) qb.andWhere("cp.parallelGroup = :group", {
1500
+ group: query.group
1501
+ });
1502
+ if (query.excludeGroups?.length) {
1503
+ qb.andWhere("(cp.parallelGroup IS NULL OR cp.parallelGroup NOT IN (:...excludeGroups))", {
1504
+ excludeGroups: query.excludeGroups
1505
+ });
1506
+ }
1507
+ if (query.statuses) qb.andWhere("cp.status IN (:...statuses)", {
1508
+ statuses: query.statuses
1509
+ });
1510
+ if (query.namePrefixes) {
1511
+ const prefixes = query.namePrefixes.map((p) => `${p.replace(/[!%_]/g, "!$&")}%`);
1512
+ const branches = prefixes.map((_, i) => `cp.name LIKE :namePrefix${i} ESCAPE '!'`);
1513
+ qb.andWhere(`(${branches.join(" OR ")})`, Object.fromEntries(prefixes.map((p, i) => [
1514
+ `namePrefix${i}`,
1515
+ p
1516
+ ])));
1517
+ }
1518
+ qb.orderBy("cp.seq", "ASC");
1519
+ if (query.offset != null) qb.offset(query.offset);
1520
+ if (query.limit != null || query.offset != null) {
1521
+ qb.limit(query.limit ?? Number.MAX_SAFE_INTEGER);
1522
+ }
1523
+ return (await qb.getMany()).map(fromCheckpointEntity);
1419
1524
  }
1420
1525
  async putSignalWaiter(waiter) {
1421
1526
  await this.waiters().save({
@@ -1431,7 +1536,7 @@ var TypeOrmStateStore = class {
1431
1536
  token: (0, import_typeorm2.Like)(`${prefix}%`)
1432
1537
  }
1433
1538
  });
1434
- return rows.map((e) => ({
1539
+ return rows.filter((e) => e.token.startsWith(prefix)).map((e) => ({
1435
1540
  token: e.token,
1436
1541
  runId: e.runId,
1437
1542
  seq: e.seq,
@@ -1722,6 +1827,23 @@ function fromScheduleEntity(e) {
1722
1827
  };
1723
1828
  }
1724
1829
  __name(fromScheduleEntity, "fromScheduleEntity");
1830
+ async function updateIfPending(repo, checkpoint) {
1831
+ const { runId, seq, ...data } = toCheckpointEntity(checkpoint);
1832
+ const set = {
1833
+ ...data,
1834
+ wakeAt: checkpoint.wakeAt != null ? new Date(checkpoint.wakeAt) : () => "NULL"
1835
+ };
1836
+ for (const key of Object.keys(set)) {
1837
+ if (set[key] === void 0) delete set[key];
1838
+ }
1839
+ const result = await repo.createQueryBuilder().update().set(set).where({
1840
+ runId,
1841
+ seq,
1842
+ status: "pending"
1843
+ }).execute();
1844
+ return (result.affected ?? 0) > 0;
1845
+ }
1846
+ __name(updateIfPending, "updateIfPending");
1725
1847
  // Annotate the CommonJS export names for ESM import in node:
1726
1848
  0 && (module.exports = {
1727
1849
  BufferedEventEntity,