@dudousxd/nestjs-durable-store-mikro-orm 0.29.0 → 0.31.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.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _mikro_orm_core from '@mikro-orm/core';
2
2
  import { EntityRepository, EntityRepositoryType, EntitySchema, MikroORM } from '@mikro-orm/core';
3
- import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
4
4
 
5
5
  /**
6
6
  * Custom repositories for the durable entities, so a host app can `@InjectRepository`/inject a
@@ -188,6 +188,7 @@ declare class MikroOrmStateStore implements StateStore {
188
188
  tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
189
189
  createRun(run: WorkflowRun): Promise<void>;
190
190
  updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
191
+ updateRunIf(runId: string, expectedStatuses: RunStatus[], inputPatch: Partial<WorkflowRun>): Promise<boolean>;
191
192
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
192
193
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
193
194
  private reindexAttributes;
@@ -198,11 +199,13 @@ declare class MikroOrmStateStore implements StateStore {
198
199
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
199
200
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
200
201
  saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
202
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
201
203
  transaction<T>(work: (tx: {
202
204
  raw: unknown;
203
205
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
204
206
  }) => Promise<T>): Promise<T>;
205
207
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
208
+ listOrphanedRuns(nowMs: number, limit: number, namespace?: string): Promise<WorkflowRun[]>;
206
209
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
207
210
  listDueTimers(nowMs: number, namespace?: string, limit?: number): Promise<WorkflowRun[]>;
208
211
  tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
@@ -290,6 +293,19 @@ declare class MikroOrmStateStore implements StateStore {
290
293
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
291
294
  countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
292
295
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
296
+ /** One `GROUP BY parallel_group, status, <name kind>` over the run's checkpoints — the buckets a
297
+ * run detail summarizes a fan from, without loading its rows. The name-kind `CASE` is the SQL of
298
+ * `checkpointNameKind`, and `min(coalesce(enqueued_at, started_at))` reads `enqueuedAt` the way
299
+ * {@link listCheckpoints} does. Raw SQL on metadata-resolved names, so a custom column naming
300
+ * strategy is honoured. */
301
+ checkpointStats(runId: string, opts?: {
302
+ group?: string | undefined;
303
+ }): Promise<CheckpointStat[]>;
304
+ /** The run's checkpoints matching `query`, in `seq` order and paged in the database — a fan's
305
+ * failures or one page of its members without the rest of the run. An excluded group keeps the
306
+ * ungrouped rows (`parallel_group is null or not in (...)`); an empty `statuses`/`namePrefixes`
307
+ * matches nothing, so it returns before querying. */
308
+ queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
293
309
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
294
310
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
295
311
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _mikro_orm_core from '@mikro-orm/core';
2
2
  import { EntityRepository, EntityRepositoryType, EntitySchema, MikroORM } from '@mikro-orm/core';
3
- import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
3
+ import { StepKind, RunStatus, StateStore, WorkflowRun, RetentionPolicy, StepCheckpoint, RunQuery, RunFacetQuery, RunFacetRow, RunValueAxis, RunValueFacetOptions, RunValueFacetRow, ScheduleRecord, ScheduleQuery, CheckpointStat, CheckpointQuery, SignalWaiter, ChildWait } from '@dudousxd/nestjs-durable-core';
4
4
 
5
5
  /**
6
6
  * Custom repositories for the durable entities, so a host app can `@InjectRepository`/inject a
@@ -188,6 +188,7 @@ declare class MikroOrmStateStore implements StateStore {
188
188
  tryAdmitSingleton(runId: string, tag: string, workflow: string, limit: number, retryWakeAt?: number): Promise<boolean>;
189
189
  createRun(run: WorkflowRun): Promise<void>;
190
190
  updateRun(runId: string, inputPatch: Partial<WorkflowRun>): Promise<void>;
191
+ updateRunIf(runId: string, expectedStatuses: RunStatus[], inputPatch: Partial<WorkflowRun>): Promise<boolean>;
191
192
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
192
193
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
193
194
  private reindexAttributes;
@@ -198,11 +199,13 @@ declare class MikroOrmStateStore implements StateStore {
198
199
  getCheckpoint(runId: string, seq: number): Promise<StepCheckpoint | null>;
199
200
  saveCheckpoint(checkpoint: StepCheckpoint): Promise<void>;
200
201
  saveCheckpointIfPending(checkpoint: StepCheckpoint): Promise<boolean>;
202
+ saveCheckpointsIfPending(checkpoints: StepCheckpoint[]): Promise<boolean[]>;
201
203
  transaction<T>(work: (tx: {
202
204
  raw: unknown;
203
205
  saveCheckpoint: (cp: StepCheckpoint) => Promise<void>;
204
206
  }) => Promise<T>): Promise<T>;
205
207
  listIncompleteRuns(namespace?: string): Promise<WorkflowRun[]>;
208
+ listOrphanedRuns(nowMs: number, limit: number, namespace?: string): Promise<WorkflowRun[]>;
206
209
  listPendingRuns(limit: number, namespace?: string): Promise<WorkflowRun[]>;
207
210
  listDueTimers(nowMs: number, namespace?: string, limit?: number): Promise<WorkflowRun[]>;
208
211
  tryLockRun(runId: string, owner: string, leaseUntilMs: number, nowMs: number): Promise<boolean>;
@@ -290,6 +293,19 @@ declare class MikroOrmStateStore implements StateStore {
290
293
  getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
291
294
  countCheckpoints(runId: string, fromSeq: number, toSeq: number, statuses: StepCheckpoint['status'][]): Promise<number>;
292
295
  listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
296
+ /** One `GROUP BY parallel_group, status, <name kind>` over the run's checkpoints — the buckets a
297
+ * run detail summarizes a fan from, without loading its rows. The name-kind `CASE` is the SQL of
298
+ * `checkpointNameKind`, and `min(coalesce(enqueued_at, started_at))` reads `enqueuedAt` the way
299
+ * {@link listCheckpoints} does. Raw SQL on metadata-resolved names, so a custom column naming
300
+ * strategy is honoured. */
301
+ checkpointStats(runId: string, opts?: {
302
+ group?: string | undefined;
303
+ }): Promise<CheckpointStat[]>;
304
+ /** The run's checkpoints matching `query`, in `seq` order and paged in the database — a fan's
305
+ * failures or one page of its members without the rest of the run. An excluded group keeps the
306
+ * ungrouped rows (`parallel_group is null or not in (...)`); an empty `statuses`/`namePrefixes`
307
+ * matches nothing, so it returns before querying. */
308
+ queryCheckpoints(runId: string, query: CheckpointQuery): Promise<StepCheckpoint[]>;
293
309
  putSignalWaiter(waiter: SignalWaiter): Promise<void>;
294
310
  takeSignalWaiter(token: string): Promise<SignalWaiter | null>;
295
311
  listSignalWaiters(prefix: string): Promise<SignalWaiter[]>;
package/dist/index.js CHANGED
@@ -337,6 +337,19 @@ function durableEntities(options = {}) {
337
337
  class: StepCheckpointEntity,
338
338
  tableName: "durable_step_checkpoints",
339
339
  repository: /* @__PURE__ */ __name(() => StepCheckpointRepository, "repository"),
340
+ // `(runId, parallelGroup, seq)` serves the run detail's per-group reads — `checkpointStats` with a
341
+ // group and `queryCheckpoints` paging one fan in `seq` order — without scanning the whole run.
342
+ // Mirrors the Prisma adapter's index of the same name.
343
+ indexes: [
344
+ {
345
+ name: "durable_step_checkpoints_group_idx",
346
+ properties: [
347
+ "runId",
348
+ "parallelGroup",
349
+ "seq"
350
+ ]
351
+ }
352
+ ],
340
353
  properties: {
341
354
  runId: {
342
355
  type: "string",
@@ -956,6 +969,30 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
956
969
  await em.flush();
957
970
  if ("searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
958
971
  }
972
+ async updateRunIf(runId, expectedStatuses, inputPatch) {
973
+ if (expectedStatuses.length === 0) return false;
974
+ const patch = inputPatch.status && TERMINAL_RUN_STATUSES.includes(inputPatch.status) ? await clearTerminalSingletonAdmission(runId, inputPatch, (id) => this.withScope({}).getRun(id)) : inputPatch;
975
+ const data = runUpdateData(patch);
976
+ const em = this.fork();
977
+ let applied;
978
+ if (Object.keys(data).length) {
979
+ applied = await em.nativeUpdate(WorkflowRunEntity, {
980
+ id: runId,
981
+ status: {
982
+ $in: expectedStatuses
983
+ }
984
+ }, data) > 0;
985
+ } else {
986
+ applied = await em.count(WorkflowRunEntity, {
987
+ id: runId,
988
+ status: {
989
+ $in: expectedStatuses
990
+ }
991
+ }) > 0;
992
+ }
993
+ if (applied && "searchAttributes" in patch) await this.reindexAttributes(runId, patch.searchAttributes);
994
+ return applied;
995
+ }
959
996
  /** Rewrite a run's normalized attribute rows: delete the old set, insert the current one. Mirrors
960
997
  * the in-memory store's reindex so the side-table always reflects the run's live searchAttributes. */
961
998
  async reindexAttributes(runId, attributes, forked) {
@@ -1078,6 +1115,25 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1078
1115
  });
1079
1116
  return affected > 0;
1080
1117
  }
1118
+ async saveCheckpointsIfPending(checkpoints) {
1119
+ if (checkpoints.length === 0) return [];
1120
+ return this.fork().transactional(async (em) => {
1121
+ const wrote = [];
1122
+ for (const checkpoint of checkpoints) {
1123
+ const { runId: _runId, seq: _seq, ...data } = toCheckpointEntity(checkpoint);
1124
+ const affected = await em.nativeUpdate(StepCheckpointEntity, {
1125
+ runId: checkpoint.runId,
1126
+ seq: checkpoint.seq,
1127
+ status: "pending"
1128
+ }, {
1129
+ ...data,
1130
+ wakeAt: checkpoint.wakeAt != null ? new Date(checkpoint.wakeAt) : null
1131
+ });
1132
+ wrote.push(affected > 0);
1133
+ }
1134
+ return wrote;
1135
+ });
1136
+ }
1081
1137
  async transaction(work) {
1082
1138
  return this.fork().transactional(async (em) => work({
1083
1139
  raw: em,
@@ -1101,6 +1157,37 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1101
1157
  });
1102
1158
  return rows.map(fromRunEntity);
1103
1159
  }
1160
+ async listOrphanedRuns(nowMs, limit, namespace) {
1161
+ if (limit <= 0) return [];
1162
+ const em = this.fork();
1163
+ const rows = await em.find(WorkflowRunEntity, {
1164
+ status: {
1165
+ $in: [
1166
+ "running",
1167
+ "cancelling"
1168
+ ]
1169
+ },
1170
+ $or: [
1171
+ {
1172
+ lockedUntil: null
1173
+ },
1174
+ {
1175
+ lockedUntil: {
1176
+ $lte: new Date(nowMs)
1177
+ }
1178
+ }
1179
+ ],
1180
+ ...namespace !== void 0 ? {
1181
+ namespace
1182
+ } : {}
1183
+ }, {
1184
+ orderBy: {
1185
+ createdAt: "ASC"
1186
+ },
1187
+ limit
1188
+ });
1189
+ return rows.map(fromRunEntity);
1190
+ }
1104
1191
  async listPendingRuns(limit, namespace) {
1105
1192
  const em = this.fork();
1106
1193
  const rows = await em.find(WorkflowRunEntity, {
@@ -1598,6 +1685,103 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1598
1685
  seq: "asc"
1599
1686
  }
1600
1687
  });
1688
+ return rows.filter((r) => prefixes.some((p) => r.name.startsWith(p))).map(fromCheckpointEntity);
1689
+ }
1690
+ /** One `GROUP BY parallel_group, status, <name kind>` over the run's checkpoints — the buckets a
1691
+ * run detail summarizes a fan from, without loading its rows. The name-kind `CASE` is the SQL of
1692
+ * `checkpointNameKind`, and `min(coalesce(enqueued_at, started_at))` reads `enqueuedAt` the way
1693
+ * {@link listCheckpoints} does. Raw SQL on metadata-resolved names, so a custom column naming
1694
+ * strategy is honoured. */
1695
+ async checkpointStats(runId, opts) {
1696
+ const em = this.fork();
1697
+ const quote = this.idQuote(em);
1698
+ const meta = em.getMetadata().get(StepCheckpointEntity);
1699
+ const c = /* @__PURE__ */ __name((prop) => quote(meta.properties[prop]?.fieldNames?.[0] ?? prop), "c");
1700
+ 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`;
1701
+ const params = [
1702
+ runId
1703
+ ];
1704
+ let where = `${c("runId")} = ?`;
1705
+ if (opts?.group !== void 0) {
1706
+ where += ` and ${c("parallelGroup")} = ?`;
1707
+ params.push(opts.group);
1708
+ }
1709
+ 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}`;
1710
+ const rows = await em.getConnection().execute(sql, params);
1711
+ const date = aggregateDateReader(em);
1712
+ return rows.map((row) => ({
1713
+ group: row.grp ?? null,
1714
+ status: row.status,
1715
+ nameKind: row.name_kind,
1716
+ count: Number(row.n),
1717
+ minSeq: Number(row.min_seq),
1718
+ maxSeq: Number(row.max_seq),
1719
+ minName: String(row.min_name),
1720
+ maxName: String(row.max_name),
1721
+ minEnqueuedAt: date(row.min_enqueued_at),
1722
+ minStartedAt: date(row.min_started_at),
1723
+ maxStartedAt: date(row.max_started_at),
1724
+ maxFinishedAt: date(row.max_finished_at),
1725
+ sumAttempts: Number(row.sum_attempts)
1726
+ }));
1727
+ }
1728
+ /** The run's checkpoints matching `query`, in `seq` order and paged in the database — a fan's
1729
+ * failures or one page of its members without the rest of the run. An excluded group keeps the
1730
+ * ungrouped rows (`parallel_group is null or not in (...)`); an empty `statuses`/`namePrefixes`
1731
+ * matches nothing, so it returns before querying. */
1732
+ async queryCheckpoints(runId, query) {
1733
+ if (query.statuses?.length === 0 || query.namePrefixes?.length === 0) return [];
1734
+ const and = [
1735
+ {
1736
+ runId
1737
+ }
1738
+ ];
1739
+ if (query.group !== void 0) and.push({
1740
+ parallelGroup: query.group
1741
+ });
1742
+ if (query.excludeGroups?.length) {
1743
+ and.push({
1744
+ $or: [
1745
+ {
1746
+ parallelGroup: null
1747
+ },
1748
+ {
1749
+ parallelGroup: {
1750
+ $nin: query.excludeGroups
1751
+ }
1752
+ }
1753
+ ]
1754
+ });
1755
+ }
1756
+ if (query.statuses) and.push({
1757
+ status: {
1758
+ $in: query.statuses
1759
+ }
1760
+ });
1761
+ if (query.namePrefixes) {
1762
+ and.push({
1763
+ $or: query.namePrefixes.map((p) => ({
1764
+ name: {
1765
+ $like: `${p}%`
1766
+ }
1767
+ }))
1768
+ });
1769
+ }
1770
+ const rows = await this.fork().find(StepCheckpointEntity, {
1771
+ $and: and
1772
+ }, {
1773
+ orderBy: {
1774
+ seq: "asc"
1775
+ },
1776
+ // An offset alone is not valid SQL on SQLite/MySQL, so an unbounded page gets a limit no
1777
+ // run reaches.
1778
+ ...query.limit !== void 0 || query.offset ? {
1779
+ limit: query.limit ?? Number.MAX_SAFE_INTEGER
1780
+ } : {},
1781
+ ...query.offset ? {
1782
+ offset: query.offset
1783
+ } : {}
1784
+ });
1601
1785
  return rows.map(fromCheckpointEntity);
1602
1786
  }
1603
1787
  async putSignalWaiter(waiter) {
@@ -1632,7 +1816,7 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1632
1816
  $like: `${prefix}%`
1633
1817
  }
1634
1818
  });
1635
- return rows.map((e) => ({
1819
+ return rows.filter((e) => e.token.startsWith(prefix)).map((e) => ({
1636
1820
  token: e.token,
1637
1821
  runId: e.runId,
1638
1822
  seq: e.seq,
@@ -1765,6 +1949,29 @@ var MikroOrmStateStore = class _MikroOrmStateStore {
1765
1949
  return affected > 0;
1766
1950
  }
1767
1951
  };
1952
+ function runUpdateData(patch) {
1953
+ const data = {};
1954
+ if ("workflow" in patch) data.workflow = patch.workflow;
1955
+ if ("workflowVersion" in patch) data.workflowVersion = patch.workflowVersion;
1956
+ if ("status" in patch) data.status = patch.status;
1957
+ if ("input" in patch) data.input = patch.input ?? null;
1958
+ if ("output" in patch) data.output = patch.output ?? null;
1959
+ if ("error" in patch) data.error = patch.error ?? null;
1960
+ if ("wakeAt" in patch) data.wakeAt = patch.wakeAt == null ? null : new Date(patch.wakeAt);
1961
+ if ("lockedBy" in patch) data.lockedBy = patch.lockedBy ?? null;
1962
+ if ("lockedUntil" in patch) data.lockedUntil = patch.lockedUntil == null ? null : new Date(patch.lockedUntil);
1963
+ if ("awaitingDecisionTaskId" in patch) data.awaitingDecisionTaskId = patch.awaitingDecisionTaskId ?? null;
1964
+ if ("recoveryAttempts" in patch) data.recoveryAttempts = patch.recoveryAttempts ?? null;
1965
+ if ("tags" in patch) data.tags = patch.tags ?? null;
1966
+ if ("searchAttributes" in patch) data.searchAttributes = patch.searchAttributes ?? null;
1967
+ if ("priority" in patch) data.priority = patch.priority ?? null;
1968
+ if ("namespace" in patch) data.namespace = patch.namespace ?? "default";
1969
+ if ("origin" in patch) data.origin = patch.origin ?? null;
1970
+ if (patch.createdAt != null) data.createdAt = patch.createdAt;
1971
+ if (patch.updatedAt != null) data.updatedAt = patch.updatedAt;
1972
+ return data;
1973
+ }
1974
+ __name(runUpdateData, "runUpdateData");
1768
1975
  function toRunEntity(run) {
1769
1976
  const e = new WorkflowRunEntity();
1770
1977
  e.id = run.id;
@@ -1814,6 +2021,21 @@ function fromRunEntity(e) {
1814
2021
  };
1815
2022
  }
1816
2023
  __name(fromRunEntity, "fromRunEntity");
2024
+ function aggregateDateReader(em) {
2025
+ const platform = em.getPlatform();
2026
+ const tz = platform.getTimezone();
2027
+ return (value) => {
2028
+ if (value instanceof Date) return value;
2029
+ if (typeof value === "bigint") return platform.parseDate(Number(value));
2030
+ if (typeof value === "number" || !tz || tz === "local") {
2031
+ return platform.parseDate(value);
2032
+ }
2033
+ const text = String(value);
2034
+ const zoned = text.includes("+") || text.lastIndexOf("-") > 10 || text.endsWith("Z");
2035
+ return platform.parseDate(zoned ? text : `${text}${tz}`);
2036
+ };
2037
+ }
2038
+ __name(aggregateDateReader, "aggregateDateReader");
1817
2039
  function toCheckpointEntity(cp) {
1818
2040
  const e = new StepCheckpointEntity();
1819
2041
  e.runId = cp.runId;