@dudousxd/nestjs-durable-core 0.28.0 → 0.28.2

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. */
@@ -1960,6 +1984,16 @@ declare function attributeOperand(filter: AttributeFilter): string | number;
1960
1984
  /**
1961
1985
  * Dependency-injection tokens. Adapters bind their implementations to these so `core` and
1962
1986
  * `@dudousxd/nestjs-durable` depend only on the interfaces in `./interfaces`.
1987
+ *
1988
+ * These use `Symbol.for(key)` (the global symbol registry), NOT `Symbol(key)`, so the token is the
1989
+ * SAME instance no matter how many times `core` is evaluated in a process. A consumer can hold more
1990
+ * than one physical copy of `core` at runtime — pnpm peer-dependency multiplexing installs a
1991
+ * separate virtual copy per distinct peer set, and the dual ESM/CJS build can be loaded once as
1992
+ * `import` (`index.js`) and once as `require` (`index.cjs`). A plain `Symbol()` would mint a
1993
+ * DISTINCT token per copy, so `DurableModule` (provider) and `DashboardService` / a store adapter
1994
+ * (injector) could resolve different symbols → Nest can't satisfy the dependency
1995
+ * (`STATE_STORE is not available in the DurableApiModule context`). A registered symbol collapses
1996
+ * them to one. Mirrors the `CONTEXT_ACCESSOR` token in `@dudousxd/nestjs-durable`'s `tokens.ts`.
1963
1997
  */
1964
1998
  declare const STATE_STORE: unique symbol;
1965
1999
  declare const TRANSPORT: unique symbol;
@@ -1982,6 +2016,13 @@ declare class InMemoryStateStore implements StateStore {
1982
2016
  private readonly attributeIndex;
1983
2017
  /** Test/observability hook: how many candidate runs the last attribute query's index produced. */
1984
2018
  lastAttributeCandidates: number;
2019
+ /**
2020
+ * Test/observability hook: how many checkpoint rows the LAST checkpoint read materialized (the rows
2021
+ * actually returned to the caller). `listCheckpoints` returns all rows for the run; the targeted
2022
+ * {@link getLatestCheckpointByName} / {@link listCheckpointsByNamePrefix} return only the matches —
2023
+ * so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
2024
+ */
2025
+ lastCheckpointRowsRead: number;
1985
2026
  private key;
1986
2027
  /** Drop then re-add a run's normalized attribute rows so the index tracks its current attributes. */
1987
2028
  private reindexAttributes;
@@ -2016,6 +2057,8 @@ declare class InMemoryStateStore implements StateStore {
2016
2057
  private candidatesForFilter;
2017
2058
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
2018
2059
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
2060
+ getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
2061
+ listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
2019
2062
  }
2020
2063
 
2021
2064
  /**
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. */
@@ -1960,6 +1984,16 @@ declare function attributeOperand(filter: AttributeFilter): string | number;
1960
1984
  /**
1961
1985
  * Dependency-injection tokens. Adapters bind their implementations to these so `core` and
1962
1986
  * `@dudousxd/nestjs-durable` depend only on the interfaces in `./interfaces`.
1987
+ *
1988
+ * These use `Symbol.for(key)` (the global symbol registry), NOT `Symbol(key)`, so the token is the
1989
+ * SAME instance no matter how many times `core` is evaluated in a process. A consumer can hold more
1990
+ * than one physical copy of `core` at runtime — pnpm peer-dependency multiplexing installs a
1991
+ * separate virtual copy per distinct peer set, and the dual ESM/CJS build can be loaded once as
1992
+ * `import` (`index.js`) and once as `require` (`index.cjs`). A plain `Symbol()` would mint a
1993
+ * DISTINCT token per copy, so `DurableModule` (provider) and `DashboardService` / a store adapter
1994
+ * (injector) could resolve different symbols → Nest can't satisfy the dependency
1995
+ * (`STATE_STORE is not available in the DurableApiModule context`). A registered symbol collapses
1996
+ * them to one. Mirrors the `CONTEXT_ACCESSOR` token in `@dudousxd/nestjs-durable`'s `tokens.ts`.
1963
1997
  */
1964
1998
  declare const STATE_STORE: unique symbol;
1965
1999
  declare const TRANSPORT: unique symbol;
@@ -1982,6 +2016,13 @@ declare class InMemoryStateStore implements StateStore {
1982
2016
  private readonly attributeIndex;
1983
2017
  /** Test/observability hook: how many candidate runs the last attribute query's index produced. */
1984
2018
  lastAttributeCandidates: number;
2019
+ /**
2020
+ * Test/observability hook: how many checkpoint rows the LAST checkpoint read materialized (the rows
2021
+ * actually returned to the caller). `listCheckpoints` returns all rows for the run; the targeted
2022
+ * {@link getLatestCheckpointByName} / {@link listCheckpointsByNamePrefix} return only the matches —
2023
+ * so this lets the benchmark contrast "fetch N, scan in JS" against "fetch the k that match".
2024
+ */
2025
+ lastCheckpointRowsRead: number;
1985
2026
  private key;
1986
2027
  /** Drop then re-add a run's normalized attribute rows so the index tracks its current attributes. */
1987
2028
  private reindexAttributes;
@@ -2016,6 +2057,8 @@ declare class InMemoryStateStore implements StateStore {
2016
2057
  private candidatesForFilter;
2017
2058
  listRuns(query: RunQuery): Promise<WorkflowRun[]>;
2018
2059
  listCheckpoints(runId: string): Promise<StepCheckpoint[]>;
2060
+ getLatestCheckpointByName(runId: string, name: string): Promise<StepCheckpoint | undefined>;
2061
+ listCheckpointsByNamePrefix(runId: string, prefixes: string[]): Promise<StepCheckpoint[]>;
2019
2062
  }
2020
2063
 
2021
2064
  /**
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) {
@@ -3539,9 +3565,9 @@ function attributeOperand(filter) {
3539
3565
  __name(attributeOperand, "attributeOperand");
3540
3566
 
3541
3567
  // src/tokens.ts
3542
- var STATE_STORE = Symbol("nestjs-durable:STATE_STORE");
3543
- var TRANSPORT = Symbol("nestjs-durable:TRANSPORT");
3544
- var DURABLE_OPTIONS = Symbol("nestjs-durable:DURABLE_OPTIONS");
3568
+ var STATE_STORE = Symbol.for("nestjs-durable:STATE_STORE");
3569
+ var TRANSPORT = Symbol.for("nestjs-durable:TRANSPORT");
3570
+ var DURABLE_OPTIONS = Symbol.for("nestjs-durable:DURABLE_OPTIONS");
3545
3571
 
3546
3572
  // src/testing/in-memory-state-store.ts
3547
3573
  var InMemoryStateStore = class {
@@ -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