@goodandready/dsh-key-rotation 0.8.21 → 0.8.23
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 +16 -0
- package/lib/config-compat.js +61 -12
- package/lib/index.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.23 - 2026-09-23
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- **Fix settings cache invalidation on Cordis traceable Proxies (#350)**:
|
|
10
|
+
- Removed `settings !== owner` reference equality check in `createProviderProfilesReader` (`lib/config-compat.js`). In Cordis, `ctx.get('settings')` creates and returns a fresh traceable `Proxy` wrapper on every single invocation, which was constantly triggering `dirty = true` and invalidating the cache per-request.
|
|
11
|
+
- Subscribed to `internal/service` event (for service name `'settings'`) alongside `settings/document-updated` to safely invalidate cache upon service replacement without comparing proxy identities.
|
|
12
|
+
- Added regression test suite verifying that when `ctx.get('settings')` returns a new `Proxy` instance on every call, 100 subsequent invocations of `buildRuntime()` or `readProfiles()` execute strictly 1 call to `settings.get()`.
|
|
13
|
+
|
|
14
|
+
## 0.8.22 - 2026-09-23
|
|
15
|
+
|
|
16
|
+
### Performance
|
|
17
|
+
- **Cache settings and volatile configuration on hot path (#350)**:
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
- Added regression test suite verifying N calls to `buildRuntime` without settings modifications yield exactly 1 invocation of `settings.get`.
|
|
21
|
+
|
|
6
22
|
## 0.8.21 - 2026-09-23
|
|
7
23
|
|
|
8
24
|
### Fixed
|
package/lib/config-compat.js
CHANGED
|
@@ -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
|
-
|
|
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,60 @@ export function createConfigReader(config) {
|
|
|
15
31
|
}
|
|
16
32
|
return same ? held : next;
|
|
17
33
|
};
|
|
18
|
-
|
|
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()
|
|
23
|
-
//
|
|
46
|
+
// Cache by document invalidation; clone-producing describe() and tree-materializing
|
|
47
|
+
// settings.get() on 0.1.7+ must not execute on every request.
|
|
48
|
+
// Invalidation occurs strictly on settings/document-updated or internal/service ('settings').
|
|
49
|
+
// Never compare ctx.get('settings') by reference: Cordis wraps services in fresh
|
|
50
|
+
// traceable Proxies on every get() call, which would invalidate the cache on every request.
|
|
24
51
|
export function createProviderProfilesReader(ctx, namespace) {
|
|
25
|
-
let dirty = true, held = null
|
|
26
|
-
|
|
52
|
+
let dirty = true, held = null;
|
|
53
|
+
const markDirty = () => { dirty = true; };
|
|
54
|
+
if (typeof ctx?.effect === 'function') {
|
|
55
|
+
ctx.effect(() => ctx.on('settings/document-updated', markDirty));
|
|
56
|
+
ctx.effect(() => ctx.on('internal/service', (name) => {
|
|
57
|
+
if (!name || name === 'settings') markDirty();
|
|
58
|
+
}));
|
|
59
|
+
} else if (typeof ctx?.on === 'function') {
|
|
60
|
+
ctx.on('settings/document-updated', markDirty);
|
|
61
|
+
ctx.on('internal/service', (name) => {
|
|
62
|
+
if (!name || name === 'settings') markDirty();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
27
66
|
return () => {
|
|
28
|
-
|
|
29
|
-
|
|
67
|
+
if (!dirty) {
|
|
68
|
+
return held;
|
|
69
|
+
}
|
|
70
|
+
const settings = ctx?.get?.('settings');
|
|
30
71
|
if (!settings) return null;
|
|
31
72
|
if (typeof settings.get === 'function') {
|
|
32
|
-
try {
|
|
33
|
-
|
|
34
|
-
|
|
73
|
+
try {
|
|
74
|
+
held = settings.get(namespace)?.providers ?? null;
|
|
75
|
+
dirty = false;
|
|
76
|
+
} catch (_) {
|
|
77
|
+
held = null;
|
|
78
|
+
dirty = false;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
35
81
|
try {
|
|
36
82
|
held = settings.describe({ redactSecrets: true }).find(row => row.ns === namespace)?.value?.providers ?? null;
|
|
37
83
|
dirty = false;
|
|
38
|
-
} catch (_) {
|
|
84
|
+
} catch (_) {
|
|
85
|
+
held = null;
|
|
86
|
+
dirty = false;
|
|
87
|
+
}
|
|
39
88
|
}
|
|
40
89
|
return held;
|
|
41
90
|
};
|
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
|
|
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.
|
|
3
|
+
"version": "0.8.23",
|
|
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": [
|