@goodandready/dsh-key-rotation 0.8.21 → 0.8.22

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/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to `@goodandready/dsh-key-rotation` are documented here.
4
4
  User-facing feature notes also appear in README (en is source of truth).
5
5
 
6
+ ## 0.8.22 - 2026-09-23
7
+
8
+ ### Performance
9
+ - **Cache settings and volatile configuration on hot path (#350)**:
10
+ - Cached `settings.get(PIAI_NS)` calls in `createProviderProfilesReader` (`lib/config-compat.js`) with invalidation on `settings/document-updated`. In DSH 0.1.7-alpha.2+, `settings.get(ns)` materializes the tree via `dsh-config-editor` and `structuredClone`, taking hundreds of milliseconds and blocking the Node.js event loop when called on every request.
11
+ - Cached `createConfigReader` (`lib/config-compat.js`) across request hot paths using `settings/document-updated`, `loader/volatile-update`, and `config` invalidation listeners, eliminating expensive recursive traversal of volatile config objects on every request.
12
+ - Added regression test suite verifying N calls to `buildRuntime` without settings modifications yield exactly 1 invocation of `settings.get`.
13
+
6
14
  ## 0.8.21 - 2026-09-23
7
15
 
8
16
  ### Fixed
@@ -1,7 +1,23 @@
1
1
  // Decode modern volatile Config references while retaining stable identities for
2
2
  // the request hot path. Legacy cores pass ordinary resolved configuration.
3
- export function createConfigReader(config) {
3
+ // If ctx is provided, invalidation listeners cache the unwrapped tree so the hot
4
+ // path never recursively walks volatile nodes on unchanged configuration.
5
+ export function createConfigReader(config, ctx) {
4
6
  let previous;
7
+ let dirty = true;
8
+ if (ctx && typeof ctx.on === 'function') {
9
+ const markDirty = () => { dirty = true; };
10
+ if (typeof ctx.effect === 'function') {
11
+ ctx.effect(() => ctx.on('settings/document-updated', markDirty));
12
+ ctx.effect(() => ctx.on('loader/volatile-update', markDirty));
13
+ ctx.effect(() => ctx.on('config', markDirty));
14
+ } else {
15
+ ctx.on('settings/document-updated', markDirty);
16
+ ctx.on('loader/volatile-update', markDirty);
17
+ ctx.on('config', markDirty);
18
+ }
19
+ }
20
+
5
21
  const read = (value, held) => {
6
22
  if (value && typeof value.get === 'function') value = value.get();
7
23
  if (!value || typeof value !== 'object') return value;
@@ -15,27 +31,51 @@ export function createConfigReader(config) {
15
31
  }
16
32
  return same ? held : next;
17
33
  };
18
- return () => (previous = read(config, previous));
34
+
35
+ return () => {
36
+ if (ctx && !dirty && previous !== undefined) {
37
+ return previous;
38
+ }
39
+ previous = read(config, previous);
40
+ dirty = false;
41
+ return previous;
42
+ };
19
43
  }
20
44
 
21
45
  // Modern settings owns descriptors rather than the removed get(namespace) API.
22
- // Cache by document invalidation; clone-producing describe() must not rebuild
23
- // every key pool on every request.
46
+ // Cache by document invalidation; clone-producing describe() and tree-materializing
47
+ // settings.get() on 0.1.7+ must not execute on every request.
24
48
  export function createProviderProfilesReader(ctx, namespace) {
25
49
  let dirty = true, held = null, owner;
26
- ctx.effect(() => ctx.on('settings/document-updated', () => { dirty = true; }));
50
+ const markDirty = () => { dirty = true; };
51
+ if (typeof ctx?.effect === 'function') {
52
+ ctx.effect(() => ctx.on('settings/document-updated', markDirty));
53
+ } else if (typeof ctx?.on === 'function') {
54
+ ctx.on('settings/document-updated', markDirty);
55
+ }
56
+
27
57
  return () => {
28
- const settings = ctx.get('settings');
58
+ const settings = ctx?.get?.('settings');
29
59
  if (settings !== owner) { owner = settings; dirty = true; }
30
60
  if (!settings) return null;
31
- if (typeof settings.get === 'function') {
32
- try { return settings.get(namespace)?.providers ?? null; } catch (_) { return null; }
33
- }
34
61
  if (dirty) {
35
- try {
36
- held = settings.describe({ redactSecrets: true }).find(row => row.ns === namespace)?.value?.providers ?? null;
37
- dirty = false;
38
- } catch (_) { held = null; }
62
+ if (typeof settings.get === 'function') {
63
+ try {
64
+ held = settings.get(namespace)?.providers ?? null;
65
+ dirty = false;
66
+ } catch (_) {
67
+ held = null;
68
+ dirty = false;
69
+ }
70
+ } else {
71
+ try {
72
+ held = settings.describe({ redactSecrets: true }).find(row => row.ns === namespace)?.value?.providers ?? null;
73
+ dirty = false;
74
+ } catch (_) {
75
+ held = null;
76
+ dirty = false;
77
+ }
78
+ }
39
79
  }
40
80
  return held;
41
81
  };
package/lib/index.js CHANGED
@@ -157,7 +157,7 @@ function registerConfigBridge(ctx, getCloneIds) {
157
157
 
158
158
  export function apply(ctx, config = {}) {
159
159
  const logger = getLogger(ctx);
160
- const readEntryConfig = createConfigReader(config);
160
+ const readEntryConfig = createConfigReader(config, ctx);
161
161
  getConfig = readEntryConfig;
162
162
  const readProfiles = createProviderProfilesReader(ctx, PIAI_NS);
163
163
  registerConfigBridge(ctx, () => buildRuntime().cloneIds);
@@ -553,6 +553,8 @@ export function apply(ctx, config = {}) {
553
553
  if (typeof settingsSvc?.register !== 'function') return;
554
554
  const scope = settingsSvc.register(NS, SettingsSchema, { base: readEntryConfig() });
555
555
  getConfig = () => scope.get() ?? readEntryConfig();
556
- sctx.effect(() => () => { getConfig = readEntryConfig; });
556
+ if (typeof sctx?.effect === 'function') {
557
+ sctx.effect(() => () => { getConfig = readEntryConfig; });
558
+ }
557
559
  });
558
560
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.21",
3
+ "version": "0.8.22",
4
4
  "packageManager": "pnpm@10.33.2",
5
5
  "description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
6
6
  "keywords": [