@dudousxd/nestjs-durable-store-mikro-orm 0.29.0 → 0.30.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
@@ -379,6 +379,19 @@ function durableEntities(options = {}) {
379
379
  class: StepCheckpointEntity,
380
380
  tableName: "durable_step_checkpoints",
381
381
  repository: /* @__PURE__ */ __name(() => StepCheckpointRepository, "repository"),
382
+ // `(runId, parallelGroup, seq)` serves the run detail's per-group reads — `checkpointStats` with a
383
+ // group and `queryCheckpoints` paging one fan in `seq` order — without scanning the whole run.
384
+ // Mirrors the Prisma adapter's index of the same name.
385
+ indexes: [
386
+ {
387
+ name: "durable_step_checkpoints_group_idx",
388
+ properties: [
389
+ "runId",
390
+ "parallelGroup",
391
+ "seq"
392
+ ]
393
+ }
394
+ ],
382
395
  properties: {
383
396
  runId: {
384
397
  type: "string",
@@ -1120,6 +1133,25 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1120
1133
  });
1121
1134
  return affected > 0;
1122
1135
  }
1136
+ async saveCheckpointsIfPending(checkpoints) {
1137
+ if (checkpoints.length === 0) return [];
1138
+ return this.fork().transactional(async (em) => {
1139
+ const wrote = [];
1140
+ for (const checkpoint of checkpoints) {
1141
+ const { runId: _runId, seq: _seq, ...data } = toCheckpointEntity(checkpoint);
1142
+ const affected = await em.nativeUpdate(StepCheckpointEntity, {
1143
+ runId: checkpoint.runId,
1144
+ seq: checkpoint.seq,
1145
+ status: "pending"
1146
+ }, {
1147
+ ...data,
1148
+ wakeAt: checkpoint.wakeAt != null ? new Date(checkpoint.wakeAt) : null
1149
+ });
1150
+ wrote.push(affected > 0);
1151
+ }
1152
+ return wrote;
1153
+ });
1154
+ }
1123
1155
  async transaction(work) {
1124
1156
  return this.fork().transactional(async (em) => work({
1125
1157
  raw: em,
@@ -1640,6 +1672,103 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1640
1672
  seq: "asc"
1641
1673
  }
1642
1674
  });
1675
+ return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointEntity);
1676
+ }
1677
+ /** One `GROUP BY parallel_group, status, <name kind>` over the run's checkpoints — the buckets a
1678
+ * run detail summarizes a fan from, without loading its rows. The name-kind `CASE` is the SQL of
1679
+ * `checkpointNameKind`, and `min(coalesce(enqueued_at, started_at))` reads `enqueuedAt` the way
1680
+ * {@link listCheckpoints} does. Raw SQL on metadata-resolved names, so a custom column naming
1681
+ * strategy is honoured. */
1682
+ async checkpointStats(runId, opts) {
1683
+ const em = this.fork();
1684
+ const quote = this.idQuote(em);
1685
+ const meta = em.getMetadata().get(StepCheckpointEntity);
1686
+ const c = /* @__PURE__ */ __name((prop) => quote(meta.properties[prop]?.fieldNames?.[0] ?? prop), "c");
1687
+ const nameKind = `case when ${c("name")} like 'signal:child:%' then 'child' when ${c("name")} like 'fan:%' then 'folded' when ${c("name")} like 'spawn:%' then 'spawn' else 'step' end`;
1688
+ const params = [
1689
+ runId
1690
+ ];
1691
+ let where = `${c("runId")} = ?`;
1692
+ if (opts?.group !== void 0) {
1693
+ where += ` and ${c("parallelGroup")} = ?`;
1694
+ params.push(opts.group);
1695
+ }
1696
+ const sql = `select ${c("parallelGroup")} as ${quote("grp")}, ${c("status")} as ${quote("status")}, ${nameKind} as ${quote("name_kind")}, count(*) as ${quote("n")}, min(${c("seq")}) as ${quote("min_seq")}, max(${c("seq")}) as ${quote("max_seq")}, min(${c("name")}) as ${quote("min_name")}, max(${c("name")}) as ${quote("max_name")}, min(coalesce(${c("enqueuedAt")}, ${c("startedAt")})) as ${quote("min_enqueued_at")}, min(${c("startedAt")}) as ${quote("min_started_at")}, max(${c("startedAt")}) as ${quote("max_started_at")}, max(${c("finishedAt")}) as ${quote("max_finished_at")}, sum(${c("attempts")}) as ${quote("sum_attempts")} from ${quote(meta.tableName)} where ${where} group by ${c("parallelGroup")}, ${c("status")}, ${nameKind}`;
1697
+ const rows = await em.getConnection().execute(sql, params);
1698
+ const date = aggregateDateReader(em);
1699
+ return rows.map((row) => ({
1700
+ group: row.grp ?? null,
1701
+ status: row.status,
1702
+ nameKind: row.name_kind,
1703
+ count: Number(row.n),
1704
+ minSeq: Number(row.min_seq),
1705
+ maxSeq: Number(row.max_seq),
1706
+ minName: String(row.min_name),
1707
+ maxName: String(row.max_name),
1708
+ minEnqueuedAt: date(row.min_enqueued_at),
1709
+ minStartedAt: date(row.min_started_at),
1710
+ maxStartedAt: date(row.max_started_at),
1711
+ maxFinishedAt: date(row.max_finished_at),
1712
+ sumAttempts: Number(row.sum_attempts)
1713
+ }));
1714
+ }
1715
+ /** The run's checkpoints matching `query`, in `seq` order and paged in the database — a fan's
1716
+ * failures or one page of its members without the rest of the run. An excluded group keeps the
1717
+ * ungrouped rows (`parallel_group is null or not in (...)`); an empty `statuses`/`namePrefixes`
1718
+ * matches nothing, so it returns before querying. */
1719
+ async queryCheckpoints(runId, query) {
1720
+ if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
1721
+ const and = [
1722
+ {
1723
+ runId
1724
+ }
1725
+ ];
1726
+ if (query.group !== void 0) and.push({
1727
+ parallelGroup: query.group
1728
+ });
1729
+ if (query.excludeGroups?.length) {
1730
+ and.push({
1731
+ $or: [
1732
+ {
1733
+ parallelGroup: null
1734
+ },
1735
+ {
1736
+ parallelGroup: {
1737
+ $nin: query.excludeGroups
1738
+ }
1739
+ }
1740
+ ]
1741
+ });
1742
+ }
1743
+ if (query.statuses) and.push({
1744
+ status: {
1745
+ $in: query.statuses
1746
+ }
1747
+ });
1748
+ if (query.namePrefixes) {
1749
+ and.push({
1750
+ $or: query.namePrefixes.map((p) => ({
1751
+ name: {
1752
+ $like: `${p}%`
1753
+ }
1754
+ }))
1755
+ });
1756
+ }
1757
+ const rows = await this.fork().find(StepCheckpointEntity, {
1758
+ $and: and
1759
+ }, {
1760
+ orderBy: {
1761
+ seq: "asc"
1762
+ },
1763
+ // An offset alone is not valid SQL on SQLite/MySQL, so an unbounded page gets a limit no
1764
+ // run reaches.
1765
+ ...query.limit !== void 0 || query.offset ? {
1766
+ limit: query.limit ?? Number.MAX_SAFE_INTEGER
1767
+ } : {},
1768
+ ...query.offset ? {
1769
+ offset: query.offset
1770
+ } : {}
1771
+ });
1643
1772
  return rows.map(fromCheckpointEntity);
1644
1773
  }
1645
1774
  async putSignalWaiter(waiter) {
@@ -1674,7 +1803,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1674
1803
  $like: `${prefix}%`
1675
1804
  }
1676
1805
  });
1677
- return rows.map((e) => ({
1806
+ return rows.filter((e) => e.token.startsWith(prefix)).map((e) => ({
1678
1807
  token: e.token,
1679
1808
  runId: e.runId,
1680
1809
  seq: e.seq,
@@ -1856,6 +1985,21 @@ function fromRunEntity(e) {
1856
1985
  };
1857
1986
  }
1858
1987
  __name(fromRunEntity, "fromRunEntity");
1988
+ function aggregateDateReader(em) {
1989
+ const platform = em.getPlatform();
1990
+ const tz = platform.getTimezone();
1991
+ return (value) => {
1992
+ if (value instanceof Date) return value;
1993
+ if (typeof value === "bigint") return platform.parseDate(Number(value));
1994
+ if (typeof value === "number" || !tz || tz === "local") {
1995
+ return platform.parseDate(value);
1996
+ }
1997
+ const text = String(value);
1998
+ const zoned = text.includes("+") || text.lastIndexOf("-") > 10 || text.endsWith("Z");
1999
+ return platform.parseDate(zoned ? text : `${text}${tz}`);
2000
+ };
2001
+ }
2002
+ __name(aggregateDateReader, "aggregateDateReader");
1859
2003
  function toCheckpointEntity(cp) {
1860
2004
  const e = new StepCheckpointEntity();
1861
2005
  e.runId = cp.runId;