@dudousxd/nestjs-durable-core 0.28.0 → 0.28.1

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
@@ -277,6 +277,28 @@ interface StateStore {
277
277
  transaction?<T>(work: (tx: StoreTransaction) => Promise<T>): Promise<T>;
278
278
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
279
279
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
280
+ /**
281
+ * The LATEST checkpoint for `runId` whose `name` equals `name` exactly (highest `seq` wins), or
282
+ * `undefined` if none. A targeted read that avoids fetching + deserializing every checkpoint just to
283
+ * keep one match — the store does the filter (`WHERE name = … ORDER BY seq DESC LIMIT 1`). Preserves
284
+ * the "last in seq order wins" semantics the engine relies on for `getEvent` (a re-published key
285
+ * overwrites the prior value at a higher seq, so the highest-seq match is the current value).
286
+ *
287
+ * Optional: a store that omits it still works — the engine falls back to {@link listCheckpoints}
288
+ * plus an in-JS filter that produces the identical result.
289
+ */
290
+ getLatestCheckpointByName?(runId: string, name: string): Promise<StepCheckpoint | undefined>;
291
+ /**
292
+ * All checkpoints for `runId` whose `name` starts with ANY of `prefixes`, ordered by `seq` ascending
293
+ * (same order as {@link listCheckpoints}). A targeted read that avoids scanning every checkpoint just
294
+ * to keep the prefix matches — the store does the filter (`WHERE name LIKE 'prefix%' …`). Used by the
295
+ * run-tree to find a parent's child edges (`signal:child:` / `spawn:` checkpoints) without loading the
296
+ * whole history. An empty `prefixes` array matches nothing.
297
+ *
298
+ * Optional: a store that omits it still works — the engine falls back to {@link listCheckpoints}
299
+ * plus an in-JS prefix scan that produces the identical result.
300
+ */
301
+ listCheckpointsByNamePrefix?(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
280
302
  }
281
303
  /** Typed, queryable per-run data — exact values for `eq`/`ne`, numbers/strings for range ops. */
282
304
  type SearchAttributes = Record<string, string | number | boolean>;
@@ -1792,6 +1814,8 @@ declare class CodecStateStore implements StateStore {
1792
1814
  } | null>;
1793
1815
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
1794
1816
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
1817
+ getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
1818
+ listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
1795
1819
  }
1796
1820
 
1797
1821
  /** Token prefix for every waiter on `name` — used by `listSignalWaiters` to fan an event out. */
@@ -1982,6 +2006,13 @@ declare class InMemoryStateStore implements StateStore {
1982
2006
  private readonly attributeIndex;
1983
2007
  /** Test/observability hook: how many candidate runs the last attribute query's index produced. */
1984
2008
  lastAttributeCandidates: number;
2009
+ /**
2010
+ * Test/observability hook: how many checkpoint rows the LAST checkpoint read materialized (the rows
2011
+ * actually returned to the caller). `listCheckpoints` returns all rows for the run; the targeted
2012
+ * {@link getLatestCheckpointByName} / {@link listCheckpointsByNamePrefix} return only the matches —
2013
+ * so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
2014
+ */
2015
+ lastCheckpointRowsRead: number;
1985
2016
  private key;
1986
2017
  /** Drop then re-add a run's normalized attribute rows so the index tracks its current attributes. */
1987
2018
  private reindexAttributes;
@@ -2016,6 +2047,8 @@ declare class InMemoryStateStore implements StateStore {
2016
2047
  private candidatesForFilter;
2017
2048
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
2018
2049
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
2050
+ getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
2051
+ listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
2019
2052
  }
2020
2053
 
2021
2054
  /**
package/dist/index.d.ts CHANGED
@@ -277,6 +277,28 @@ interface StateStore {
277
277
  transaction?<T>(work: (tx: StoreTransaction) => Promise<T>): Promise<T>;
278
278
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
279
279
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
280
+ /**
281
+ * The LATEST checkpoint for `runId` whose `name` equals `name` exactly (highest `seq` wins), or
282
+ * `undefined` if none. A targeted read that avoids fetching + deserializing every checkpoint just to
283
+ * keep one match — the store does the filter (`WHERE name = … ORDER BY seq DESC LIMIT 1`). Preserves
284
+ * the "last in seq order wins" semantics the engine relies on for `getEvent` (a re-published key
285
+ * overwrites the prior value at a higher seq, so the highest-seq match is the current value).
286
+ *
287
+ * Optional: a store that omits it still works — the engine falls back to {@link listCheckpoints}
288
+ * plus an in-JS filter that produces the identical result.
289
+ */
290
+ getLatestCheckpointByName?(runId: string, name: string): Promise<StepCheckpoint | undefined>;
291
+ /**
292
+ * All checkpoints for `runId` whose `name` starts with ANY of `prefixes`, ordered by `seq` ascending
293
+ * (same order as {@link listCheckpoints}). A targeted read that avoids scanning every checkpoint just
294
+ * to keep the prefix matches — the store does the filter (`WHERE name LIKE 'prefix%' …`). Used by the
295
+ * run-tree to find a parent's child edges (`signal:child:` / `spawn:` checkpoints) without loading the
296
+ * whole history. An empty `prefixes` array matches nothing.
297
+ *
298
+ * Optional: a store that omits it still works — the engine falls back to {@link listCheckpoints}
299
+ * plus an in-JS prefix scan that produces the identical result.
300
+ */
301
+ listCheckpointsByNamePrefix?(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
280
302
  }
281
303
  /** Typed, queryable per-run data — exact values for `eq`/`ne`, numbers/strings for range ops. */
282
304
  type SearchAttributes = Record<string, string | number | boolean>;
@@ -1792,6 +1814,8 @@ declare class CodecStateStore implements StateStore {
1792
1814
  } | null>;
1793
1815
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
1794
1816
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
1817
+ getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
1818
+ listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
1795
1819
  }
1796
1820
 
1797
1821
  /** Token prefix for every waiter on `name` — used by `listSignalWaiters` to fan an event out. */
@@ -1982,6 +2006,13 @@ declare class InMemoryStateStore implements StateStore {
1982
2006
  private readonly attributeIndex;
1983
2007
  /** Test/observability hook: how many candidate runs the last attribute query's index produced. */
1984
2008
  lastAttributeCandidates: number;
2009
+ /**
2010
+ * Test/observability hook: how many checkpoint rows the LAST checkpoint read materialized (the rows
2011
+ * actually returned to the caller). `listCheckpoints` returns all rows for the run; the targeted
2012
+ * {@link getLatestCheckpointByName} / {@link listCheckpointsByNamePrefix} return only the matches —
2013
+ * so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
2014
+ */
2015
+ lastCheckpointRowsRead: number;
1985
2016
  private key;
1986
2017
  /** Drop then re-add a run's normalized attribute rows so the index tracks its current attributes. */
1987
2018
  private reindexAttributes;
@@ -2016,6 +2047,8 @@ declare class InMemoryStateStore implements StateStore {
2016
2047
  private candidatesForFilter;
2017
2048
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
2018
2049
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
2050
+ getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
2051
+ listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
2019
2052
  }
2020
2053
 
2021
2054
  /**
package/dist/index.js CHANGED
@@ -2039,7 +2039,12 @@ var WorkflowEngine = class {
2039
2039
  for (const w of await this.store.listSignalWaiters("child:")) {
2040
2040
  if (w.runId === parentRunId) childIds.add(w.token.slice("child:".length));
2041
2041
  }
2042
- for (const cp of await this.store.listCheckpoints(parentRunId)) {
2042
+ const prefixes = [
2043
+ "signal:child:",
2044
+ "spawn:"
2045
+ ];
2046
+ const childCheckpoints = this.store.listCheckpointsByNamePrefix ? await this.store.listCheckpointsByNamePrefix(parentRunId, prefixes) : (await this.store.listCheckpoints(parentRunId)).filter((cp) => prefixes.some((p) => cp.name.startsWith(p)));
2047
+ for (const cp of childCheckpoints) {
2043
2048
  if (cp.name.startsWith("signal:child:")) childIds.add(cp.name.slice("signal:child:".length));
2044
2049
  if (cp.name.startsWith("spawn:") && typeof cp.output === "string") childIds.add(cp.output);
2045
2050
  }
@@ -2830,9 +2835,12 @@ var WorkflowEngine = class {
2830
2835
  */
2831
2836
  async getEvent(runId, key) {
2832
2837
  const name = `event:${key}`;
2833
- const checkpoints = await this.store.listCheckpoints(runId);
2838
+ if (this.store.getLatestCheckpointByName) {
2839
+ const latest2 = await this.store.getLatestCheckpointByName(runId, name);
2840
+ return latest2?.output;
2841
+ }
2834
2842
  let latest;
2835
- for (const cp of checkpoints) if (cp.name === name) latest = cp.output;
2843
+ for (const cp of await this.store.listCheckpoints(runId)) if (cp.name === name) latest = cp.output;
2836
2844
  return latest;
2837
2845
  }
2838
2846
  /**
@@ -3285,6 +3293,19 @@ var CodecStateStore = class {
3285
3293
  async listCheckpoints(runId) {
3286
3294
  return (await this.inner.listCheckpoints(runId)).map((c) => this.decCp(c));
3287
3295
  }
3296
+ async getLatestCheckpointByName(runId, name) {
3297
+ if (this.inner.getLatestCheckpointByName) {
3298
+ const cp = await this.inner.getLatestCheckpointByName(runId, name);
3299
+ return cp ? this.decCp(cp) : void 0;
3300
+ }
3301
+ let latest;
3302
+ for (const cp of await this.inner.listCheckpoints(runId)) if (cp.name === name) latest = cp;
3303
+ return latest ? this.decCp(latest) : void 0;
3304
+ }
3305
+ async listCheckpointsByNamePrefix(runId, prefixes) {
3306
+ const matches = this.inner.listCheckpointsByNamePrefix ? await this.inner.listCheckpointsByNamePrefix(runId, prefixes) : (await this.inner.listCheckpoints(runId)).filter((cp) => prefixes.some((p) => cp.name.startsWith(p)));
3307
+ return matches.map((c) => this.decCp(c));
3308
+ }
3288
3309
  };
3289
3310
 
3290
3311
  // src/metrics.ts
@@ -3309,11 +3330,16 @@ function collectMetrics(engine) {
3309
3330
  };
3310
3331
  const wf = /* @__PURE__ */ __name((name) => {
3311
3332
  const k = name ?? "unknown";
3312
- return m.byWorkflow[k] ??= {
3313
- started: 0,
3314
- completed: 0,
3315
- failed: 0
3316
- };
3333
+ let entry = m.byWorkflow[k];
3334
+ if (!entry) {
3335
+ entry = {
3336
+ started: 0,
3337
+ completed: 0,
3338
+ failed: 0
3339
+ };
3340
+ m.byWorkflow[k] = entry;
3341
+ }
3342
+ return entry;
3317
3343
  }, "wf");
3318
3344
  const stop = engine.subscribe((e) => {
3319
3345
  switch (e.type) {
@@ -3560,6 +3586,13 @@ var InMemoryStateStore = class {
3560
3586
  attributeIndex = /* @__PURE__ */ new Map();
3561
3587
  /** Test/observability hook: how many candidate runs the last attribute query's index produced. */
3562
3588
  lastAttributeCandidates = 0;
3589
+ /**
3590
+ * Test/observability hook: how many checkpoint rows the LAST checkpoint read materialized (the rows
3591
+ * actually returned to the caller). `listCheckpoints` returns all rows for the run; the targeted
3592
+ * {@link getLatestCheckpointByName} / {@link listCheckpointsByNamePrefix} return only the matches —
3593
+ * so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
3594
+ */
3595
+ lastCheckpointRowsRead = 0;
3563
3596
  key(runId, seq) {
3564
3597
  return `${runId}:${seq}`;
3565
3598
  }
@@ -3766,11 +3799,33 @@ var InMemoryStateStore = class {
3766
3799
  }));
3767
3800
  }
3768
3801
  async listCheckpoints(runId) {
3769
- return [
3802
+ const rows = [
3770
3803
  ...this.checkpoints.values()
3771
3804
  ].filter((cp) => cp.runId === runId).sort((a, b) => a.seq - b.seq).map((cp) => ({
3772
3805
  ...cp
3773
3806
  }));
3807
+ this.lastCheckpointRowsRead = rows.length;
3808
+ return rows;
3809
+ }
3810
+ async getLatestCheckpointByName(runId, name) {
3811
+ let latest;
3812
+ for (const cp of this.checkpoints.values()) {
3813
+ if (cp.runId !== runId || cp.name !== name) continue;
3814
+ if (!latest || cp.seq > latest.seq) latest = cp;
3815
+ }
3816
+ this.lastCheckpointRowsRead = latest ? 1 : 0;
3817
+ return latest ? {
3818
+ ...latest
3819
+ } : void 0;
3820
+ }
3821
+ async listCheckpointsByNamePrefix(runId, prefixes) {
3822
+ const rows = [
3823
+ ...this.checkpoints.values()
3824
+ ].filter((cp) => cp.runId === runId && prefixes.some((p) => cp.name.startsWith(p))).sort((a, b) => a.seq - b.seq).map((cp) => ({
3825
+ ...cp
3826
+ }));
3827
+ this.lastCheckpointRowsRead = rows.length;
3828
+ return rows;
3774
3829
  }
3775
3830
  };
3776
3831