@deepseek-ai/dsh-settings 0.1.1-rc.2 → 0.1.2-alpha.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/README.i18n.yaml +2 -2
- package/README.md +139 -23
- package/README.zh.md +140 -24
- package/lib/index.js +53 -81
- package/lib/invariant.js +1 -146
- package/lib/types/index.d.ts +28 -35
- package/lib/types/index.js +57 -99
- package/lib/types/invariant.js +1 -1
- package/lib/types/types.d.ts +60 -2
- package/lib/types/types.js +3 -2
- package/package.json +14 -9
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/settings/settings/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 5c7313ac5015f64cca6b3d91ee75d44f27ab356c
|
|
6
|
+
README.zh.md: 337effed483a1bf28d42b76e95c69bc01f805667
|
package/README.md
CHANGED
|
@@ -1,38 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "The user-settings service for plugin authors and maintainers registering configurable namespaces, reading resolved values, or wiring configuration surfaces."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-settings
|
|
2
7
|
|
|
3
8
|
English | [中文](README.zh.md)
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-settings` lets plugins expose configuration that users can change at runtime: a plugin registers a namespace with a schema, and the resolved value honors schema defaults, the deployment's own composition `base`, and the user-edited document section — with user overrides winning. Consumers read a snapshot of the resolved value and are notified of every committed change; configuration surfaces get one descriptor per namespace — schema, current value, which layer each field came from, effect timing — without touching storage directly. Writes change only the user overrides, run one at a time per namespace, and can carry an expected revision so a stale writer is refused instead of silently overwriting a newer one. A provider must be mounted to store the document; without one, nothing changes and configuration stays exactly as composed.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Plugins and configuration surfaces use `ctx.settings` to read and change configuration at runtime. The common path: mount a provider, register a namespace with a schema, read and watch the resolved value, and write through the owner scope.
|
|
29
|
+
|
|
30
|
+
### When to choose it
|
|
31
|
+
|
|
32
|
+
Choose settings when a plugin's configuration should be changeable at runtime — by the user editing a document or by a configuration UI — without restarting or re-reading `cordis.yml`. It fits when several plugins each own one configuration namespace, and when a configuration surface must render schemas, mark user-overridden fields, and persist edits. It is unnecessary when configuration is fixed at load time: without a provider mounted, nothing changes and configuration stays exactly as composed.
|
|
33
|
+
|
|
34
|
+
### Mounting a provider
|
|
35
|
+
|
|
36
|
+
The service stores nothing by itself; mount a provider such as the shipped file-backed one:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@deepseek-ai/dsh-settings-file'
|
|
40
|
+
config:
|
|
41
|
+
path: /absolute/path/to/settings.yaml
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`ctx.settings` appears once the provider is live. The provider README owns the full configuration surface; the generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-settings-file) lists every accepted field.
|
|
45
|
+
|
|
46
|
+
### Registering a namespace
|
|
47
|
+
|
|
48
|
+
A plugin registers its own namespace with a schemastery schema, optionally supplying the composition entry as the `base` layer so the resolved value starts from what the deployment already configured:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
const scope = ctx.settings.register('ui-theme', ThemeSchema, {
|
|
52
|
+
base: config, // composition entry config; the user layer resolves above it
|
|
53
|
+
})
|
|
54
|
+
const theme = scope.get() // deep-frozen resolved snapshot
|
|
55
|
+
scope.update({ density: 'compact' }) // merges into the user section and persists
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Literal namespace arguments are checked by TypeScript against the lowercase letter, digit, and hyphen grammar; dynamically supplied strings receive the same validation at runtime. `ctx.settings.installSection(owner, ns, schema, entry, hooks)` packages the optional-service wiring for a consumer plugin: while a settings service exists it registers the namespace with the plugin's composition entry as `base`; when the service goes away the plugin falls back to its entry config and keeps working exactly as composed.
|
|
59
|
+
|
|
60
|
+
### Reading and observing values
|
|
61
|
+
|
|
62
|
+
`get(ns)` returns the resolved value as a deep-frozen snapshot, `undefined` while the namespace is unregistered. `watch(callback)` invokes the callback after each committed change with `(next, prev)`: invocations of one callback run one at a time in commit order, and failures are contained and logged, so a slow or throwing observer never blocks or breaks other observers.
|
|
63
|
+
|
|
64
|
+
### Writing values
|
|
65
|
+
|
|
66
|
+
`update(ns, patch)` deep-merges a plain-object patch into the user section only — never into `base` — validates the resolved candidate, persists through the provider, then commits. `replace(ns, section)` sets the user section wholesale, which is the removal/reset path: `replace({})` re-inherits `base` and schema defaults. `mutate(ns, ops)` applies ordered `{ op: 'set' | 'unset', path }` edits to the section as it stands when the write reaches the front of the queue — the removal path for a caller holding an incomplete (for example redacted) view, because rebuilding a section from what a wire surface returned and replacing it wholesale would delete every field the wire never sent back.
|
|
67
|
+
|
|
68
|
+
Every write rejects non-JSON-compatible data (a `Date`, `Map`, `BigInt`, non-finite number, or circular reference fails with its `$`-rooted path before anything persists), rejects on a read-only provider, and accepts an optional `expectedRevision`: pass back the `revision` from a descriptor, and a namespace that moved past it refuses the write with `SettingsConflictError` instead of overwriting the writer that landed first.
|
|
69
|
+
|
|
70
|
+
### Configuration surfaces
|
|
6
71
|
|
|
7
|
-
|
|
72
|
+
`describe()` returns one descriptor per registered namespace: the serialized schema, the resolved value, the detached `base` and `user` layers (a field's presence in `user` marks it user-overridden), the effect timing, and the namespace's revision. Pass `redactSecrets: true` on every wire surface: it strips `role('secret')` fields from every layer and enumerates them as `{ path, set }` slots so a page can render write-only inputs without ever receiving a secret. `documentPath` and `prepareDocument()` expose the provider's user-editable file to a native editor when one exists.
|
|
8
73
|
|
|
9
|
-
|
|
10
|
-
- `prepareDocument()` — return that path after making the document ready for a native editor. The base implementation returns `documentPath`; a file provider may materialize an absent document first.
|
|
11
|
-
- `register(ns, schema, { base?, applies? })` — returns the owner `SettingsScope` (`get`/`watch`/`update`). The registration is an effect on the calling plugin's fiber: disposing that fiber removes the namespace and its observers. A stored section the schema rejects fails the registration itself; a duplicate namespace fails loud.
|
|
12
|
-
- `describe(options?)` — one descriptor per namespace (`schema.toJSON()` envelope, resolved value, detached `base`/`user` layers, `applies`) for configuration surfaces; a field's presence in `user` is what marks it user-overridden. `describe({ redactSecrets: true })` strips `role('secret')` fields from every layer and adds the `secrets` slot list (`{ path, set }`); every wire surface MUST pass it, and the pure `redactSecrets(schema, value)` walker is exported for other wires.
|
|
13
|
-
- `get(ns)` — resolved value, `undefined` while unregistered.
|
|
14
|
-
- `update(ns, patch)` — deep-merges the plain-object patch into the user section only (never the `base`), validates the resolved candidate, persists through the provider, then commits. Patches may contain only JSON-compatible data: a Date, Map, BigInt, non-finite number, or circular reference rejects with its `$`-rooted path before anything persists (YAML/JSON storage would silently change such values on reload). Validation failure rejects before anything is persisted; a read-only provider (`writable: false`) rejects every write. Writes to one namespace are serialized in call order.
|
|
15
|
-
- `replace(ns, section)` — sets the user section wholesale: the deliberate reset (`replace({})` re-inherits `base` and schema defaults).
|
|
16
|
-
- `mutate(ns, ops)` — applies ordered `{ op: 'set' | 'unset', path }` edits to the section as it stands when the write reaches the front of the queue. This is the removal path for any caller holding an INCOMPLETE view: a configuration UI reads the redacted descriptor, so rebuilding a section from it and replacing wholesale deletes every secret the wire never returned, while an op names the one field it means.
|
|
17
|
-
- Every write takes an optional `expectedRevision`. Each descriptor carries the namespace's `revision`, a monotonic counter over its RAW section; a write whose expectation no longer matches rejects with `SettingsConflictError` (`code: 'SETTINGS_CONFLICT'`, both revisions attached) instead of overwriting the writer that landed first. The write queue orders writes but cannot by itself tell a fresh writer from one holding a stale snapshot.
|
|
18
|
-
- Resolved values are deep-frozen snapshots. Watchers receive `(next, prev)` after each commit: invocations of one callback run asynchronously, one at a time, in commit order (a slow stale invocation can never apply after a newer one), and failures — sync throws and async rejections alike — are contained. After a watch disposer returns, no further invocation starts (one already queued is skipped); an invocation already started still settles. The `settings/updated` event fans out one listener at a time, so one throwing listener cannot starve the rest; an async listener's rejection is contained and logged, which is why `INVARIANT`-coded failures rethrow only from synchronous listeners.
|
|
19
|
-
- Service teardown refuses new writes and watcher starts, then drains every queued write and every started watcher invocation before disposal completes; a write whose registrant fiber was disposed mid-flight still reaches storage but commits and notifies nobody.
|
|
74
|
+
### Events and failures
|
|
20
75
|
|
|
21
|
-
|
|
76
|
+
`settings/updated (ns, next, prev, source)` fires after each committed change — an in-process write (`source: 'update'`) or an externally observed edit (`source: 'provider'`) — and never when the resolved value is deep-equal. `settings/document-updated (ns, revision)` fires whenever the raw user section changed, even when the resolved value did not, which is what an open editor needs to learn that a field went from inherited to overridden. A stored section the schema rejects keeps the namespace's last good value and warns on reload; at registration the same failure rejects the registration itself.
|
|
22
77
|
|
|
23
|
-
|
|
78
|
+
-----
|
|
24
79
|
|
|
25
|
-
|
|
80
|
+
<a id="understand-the-implementation"></a>
|
|
81
|
+
## Understand the implementation
|
|
26
82
|
|
|
27
|
-
|
|
83
|
+
<details>
|
|
84
|
+
<summary>Implementation internals — click to expand</summary>
|
|
28
85
|
|
|
29
|
-
|
|
86
|
+
This section explains the design decisions behind the service and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
|
|
30
87
|
|
|
31
|
-
|
|
88
|
+
### Design philosophy
|
|
32
89
|
|
|
90
|
+
- **Layered resolution, single user layer.** A namespace's value is schema defaults, then the registrant's composition `base`, then the user document section; writes touch only the user layer, so `replace({})` is a true reset.
|
|
91
|
+
- **Commits are deep-equal gated.** `settings/updated` fires only when the resolved value moved; the raw-section event is separate because configuration surfaces must also learn "inherited became overridden".
|
|
92
|
+
- **Writes are queued and revision-checked.** Per-namespace write queues serialize in call order, and `expectedRevision` is judged at the front of the queue, where the service can tell a fresh writer from one holding a stale snapshot.
|
|
93
|
+
- **Observer and listener failures are contained.** Watcher invocations and event fan-out isolate sync throws and async rejections so one broken observer cannot wedge commits or a provider's reload loop; `INVARIANT`-coded failures rethrow after every listener ran.
|
|
94
|
+
- **Registrations are fiber effects.** Registering a namespace is an effect on the calling plugin's fiber: disposing that fiber removes the namespace and its observers.
|
|
95
|
+
|
|
96
|
+
### Source map
|
|
97
|
+
|
|
98
|
+
| File | Role |
|
|
99
|
+
|---|---|
|
|
100
|
+
| [`src/index.ts`](src/index.ts) | Service Definition: namespace validation, registration, resolution, write queue, describe/redaction, events, `installSection` |
|
|
101
|
+
| [`src/redact.ts`](src/redact.ts) | `redactSecrets` walker: strip `role('secret')` fields and enumerate their slots |
|
|
102
|
+
| [`src/types.ts`](src/types.ts) | Client-safe type surface: event declarations, `SettingsNamespace`, `SettingsUpdateSource` |
|
|
103
|
+
| [`src/invariant.ts`](src/invariant.ts) | Invariant companion: `settings/updated` fires only for a registered namespace, only on a resolved-value change, with the authoritative value |
|
|
104
|
+
|
|
105
|
+
### Resolution and write paths
|
|
106
|
+
|
|
107
|
+
Each write snapshots its input at call time (detaching and validating JSON-shaped data), then queues on the namespace's serialized chain. At the front of the queue the service re-reads the section as it stands, checks `expectedRevision`, merges/replaces/mutates, resolves and validates the candidate through the schema plus the owner's optional `validate`, persists through the provider, and only then commits and emits. A write whose registrant fiber was disposed mid-flight still reaches storage but commits and notifies nobody; teardown refuses new writes and drains queued writes and started watcher invocations before disposal completes.
|
|
108
|
+
|
|
109
|
+
### Change detection and events
|
|
110
|
+
|
|
111
|
+
`commit` compares resolved values with the seam's `deepEqualJson` predicate and fans `settings/updated` out one listener at a time. `bumpRevision` compares raw sections and emits `settings/document-updated` with the new revision; it runs independently of the resolved-value check. Both fan-outs contain listener failures the same way.
|
|
112
|
+
|
|
113
|
+
### Client-safe types
|
|
114
|
+
|
|
115
|
+
The `./types` subpath export holds the event declarations together with the `SettingsNamespace` and `SettingsUpdateSource` types their signatures name, and the package root re-exports those types. A consumer outside the Host compilation face reads the exact signature the Host emits instead of restating it.
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
-----
|
|
120
|
+
|
|
121
|
+
<a id="further-exploration"></a>
|
|
122
|
+
## Further Exploration
|
|
123
|
+
|
|
124
|
+
Read these pages when the service-level contract is not enough. They move from the shared subsystem vocabulary to the shipped provider and the capability architecture.
|
|
125
|
+
|
|
126
|
+
- [Settings subsystem reference](../../../docs/subsystems/settings.md) — namespaces, registration, owner scope, descriptors, change commits, and the generated cordis surface.
|
|
127
|
+
- [File-backed settings provider](../settings-file/README.md) — the shipped YAML/JSON provider: configuration, hot reload, comment-preserving writes.
|
|
128
|
+
- [Settings package map](../README.md) — the two packages of the user-settings capability and their roles.
|
|
129
|
+
- [Capability seams](../../../docs/capability-seams.md) — the Service Definition / Service Provider / Consumer split this service follows.
|
|
130
|
+
|
|
131
|
+
-----
|
|
132
|
+
|
|
133
|
+
<a id="model-experience"></a>
|
|
33
134
|
## Model Experience
|
|
34
135
|
|
|
35
|
-
Indirectly, through consumer plugins
|
|
136
|
+
Indirectly, through consumer plugins, which own any model-facing content fed by a settings value; the service only stores and resolves user settings and registers nothing model-facing itself.
|
|
36
137
|
|
|
37
138
|
#### KV Cache effect
|
|
38
139
|
|
|
@@ -40,6 +141,21 @@ No direct invalidation; a consumer that folds a settings value into the request
|
|
|
40
141
|
|
|
41
142
|
## Known Limitations and Deferred Work
|
|
42
143
|
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
144
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
These limits define when the service is a poor fit or needs special care. They are current package constraints, not a task backlog.
|
|
148
|
+
|
|
149
|
+
- **Single user layer** — resolution knows schema defaults, one composition `base`, and one user document; it does not record which layer supplied each resolved value.
|
|
150
|
+
- **`redactSecrets` is not a proven wire boundary** — the walker follows `object`/`dict`/`array` containers, so a `role('secret')` field reachable only through a union, intersection, or transform is returned verbatim with an empty `secrets` list, and the serialized schema carries a secret field's default to every client. Neither case is rejected; a schema whose secrets are not reachable through the walked containers must not be registered on a wire-exposed namespace. A fail-closed `describeForWire()` — one that refuses a schema it cannot prove safe and sanitizes the serialized envelope and error text — is the deferred answer.
|
|
151
|
+
- **Cross-process concurrency is provider-defined** — the service serializes writes per namespace in-process only; concurrent processes converge by provider behavior (the file provider read-modify-writes under a writer lock, so namespaces survive concurrent writers and same-namespace conflicts resolve last-write-wins).
|
|
152
|
+
|
|
153
|
+
<a id="dev-note"></a>
|
|
154
|
+
### Dev Note
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
158
|
+
|
|
159
|
+
This Dev Note is working context for maintainers: open design directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above and the package code. Open directions, tracked in code TODOs: rename the public `ns` parameter to `namespace` across the API, provider contract, implementations, tests, and consumers; deactivate watchers and await their tails on registration disposal so callbacks cannot outlive the registrant fiber; re-resolve a replacement registration from its persisted section so an in-flight old write cannot leave it stale; and use property-safe object construction so valid JSON keys such as `__proto__` remain own data. The fail-closed `describeForWire()` sanitizer is the deferred answer to the redaction limitation above.
|
|
160
|
+
|
|
161
|
+
</details>
|
package/README.zh.md
CHANGED
|
@@ -1,45 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向插件作者与维护者的用户设置服务:注册可配置 namespace、读取解析值或接入配置界面。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# @deepseek-ai/dsh-settings
|
|
2
7
|
|
|
3
8
|
[English](README.md) | 中文
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-settings` 让插件把配置开放给用户运行时修改:插件用一个 schema 注册 namespace,解析值依次尊重 schema 默认值、部署自身的组合 `base` 与用户编辑的文档分节——用户覆盖优先。消费方读取解析值快照并在每次已提交变更后收到通知;配置界面每个 namespace 得到一条 descriptor——schema、当前值、每个字段来自哪一层、生效时机——而无需直接触碰存储。写入只改动用户覆盖、按 namespace 逐个执行,并可携带期望 revision,让持有陈旧快照的写入方被拒绝,而不是悄悄覆盖较新的写入。文档必须由挂载的提供方存储;没有提供方时一切照旧,配置保持组合原样。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
插件与配置界面通过 `ctx.settings` 在运行时读取并修改配置。常用路径:挂载提供方、用 schema 注册 namespace、读取并观察解析值,并通过 owner scope 写入。
|
|
29
|
+
|
|
30
|
+
### 何时选择
|
|
31
|
+
|
|
32
|
+
当插件的配置需要在运行时可变——用户编辑文档或配置界面修改——且无需重启或重读 `cordis.yml` 时,选择设置服务。它适合多个插件各拥有一个配置 namespace、以及配置界面需要渲染 schema、标记用户覆盖字段并持久化编辑的场景。当配置在加载时固定则没有必要:没有挂载提供方时一切照旧,配置保持组合原样。
|
|
33
|
+
|
|
34
|
+
### 挂载提供方
|
|
35
|
+
|
|
36
|
+
服务本身不存储任何内容;请挂载一个提供方,例如随附的文件型提供方:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
- name: '@deepseek-ai/dsh-settings-file'
|
|
40
|
+
config:
|
|
41
|
+
path: /absolute/path/to/settings.yaml
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
提供方上线后 `ctx.settings` 即出现。完整配置面由提供方 README 负责;生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-settings-file)列出每个受支持字段。
|
|
45
|
+
|
|
46
|
+
### 注册 namespace
|
|
47
|
+
|
|
48
|
+
插件用 schemastery schema 注册自己的 namespace,并可选地把组合配置作为 `base` 层传入,让解析值从部署已配置的内容起步:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
const scope = ctx.settings.register('ui-theme', ThemeSchema, {
|
|
52
|
+
base: config, // composition entry config; the user layer resolves above it
|
|
53
|
+
})
|
|
54
|
+
const theme = scope.get() // deep-frozen resolved snapshot
|
|
55
|
+
scope.update({ density: 'compact' }) // merges into the user section and persists
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
TypeScript 会按小写字母、数字与连字符文法检查字面量 namespace 参数;运行时动态传入的字符串接受相同校验。`ctx.settings.installSection(owner, ns, schema, entry, hooks)` 为消费方插件封装可选服务接线:只要设置服务存在,它就用插件的组合配置作为 `base` 注册 namespace;服务消失时插件回退到组合配置,行为与原先完全一致。
|
|
59
|
+
|
|
60
|
+
### 读取与观察值
|
|
61
|
+
|
|
62
|
+
`get(ns)` 以深冻结快照返回解析值,namespace 未注册时为 `undefined`。`watch(callback)` 在每次已提交变更后以 `(next, prev)` 调用回调:同一回调的调用按提交顺序逐个执行,异常被隔离并记入日志,因此慢或抛错的观察者绝不会阻塞或破坏其他观察者。
|
|
63
|
+
|
|
64
|
+
### 写入值
|
|
65
|
+
|
|
66
|
+
`update(ns, patch)` 把普通对象 patch 深合并进用户分节——绝不进 `base`——校验解析候选值、经提供方持久化后提交。`replace(ns, section)` 整体替换用户分节,是删除/重置路径:`replace({})` 重新继承 `base` 与 schema 默认值。`mutate(ns, ops)` 在写入排到队首那一刻的分节上按序施加 `{ op: 'set' | 'unset', path }` 编辑——这是持有不完整(例如脱敏后)视图的调用方的删除路径,因为按协议接口返回的内容重建分节再整体替换,会删掉协议从未回传的每个字段。
|
|
67
|
+
|
|
68
|
+
每次写入都会拒绝与 JSON 不兼容的数据(`Date`、`Map`、`BigInt`、非有限数或循环引用会在任何内容持久化前以 `$` 为根的路径报错)、拒绝只读提供方上的写入,并可接受可选的 `expectedRevision`:把 descriptor 中的 `revision` 传回,namespace 已越过该值时写入会被 `SettingsConflictError` 拒绝,而不是覆盖先完成写入的一方。
|
|
69
|
+
|
|
70
|
+
### 配置界面
|
|
6
71
|
|
|
7
|
-
|
|
72
|
+
`describe()` 为每个已注册 namespace 返回一条 descriptor:序列化 schema、解析值、分离的 `base` 与 `user` 层(字段出现在 `user` 中即标记为用户覆盖)、生效时机与 namespace 的 revision。每个协议接口都必须传入 `redactSecrets: true`:它从每一层剥离 `role('secret')` 字段,并把它们枚举为 `{ path, set }` slot,让页面可以渲染只写输入而不接触任何机密。`documentPath` 与 `prepareDocument()` 在提供方拥有用户可编辑文件时把它暴露给原生编辑器。
|
|
8
73
|
|
|
9
|
-
|
|
10
|
-
- `prepareDocument()` — 让文档做好供原生编辑器打开的准备后返回该路径。基类实现返回 `documentPath`;文件提供方可先创建缺失的文档。
|
|
11
|
-
- `register(ns, schema, { base?, applies? })` — 返回 owner 的 `SettingsScope`(`get`/`watch`/`update`)。注册是调用方插件 fiber 上的 effect:dispose(资源释放)该 fiber 即移除 namespace 及其观察者。schema 拒绝的存量分节会使注册本身失败;重复 namespace 立即报错。
|
|
12
|
-
- `describe(options?)` — 每个 namespace 一条描述(`schema.toJSON()` 封装、解析值、分离出的 `base`/`user` 层、`applies`),供配置界面使用;字段出现在 `user` 中即标记其被用户覆盖。`describe({ redactSecrets: true })` 从每一层剥离 `role('secret')` 字段,并附加 `secrets` slot 列表(`{ path, set }`);每个协议接口都必须传入它,纯遍历器 `redactSecrets(schema, value)` 已导出,供其他 wire 使用。
|
|
13
|
-
- `get(ns)` — 解析值;未注册时为 `undefined`。
|
|
14
|
-
- `update(ns, patch)` — 把普通对象 patch 深合并进用户分节(绝不合并进 `base`),校验解析候选值,经提供方持久化后提交。patch 只能包含与 JSON 兼容的数据:Date、Map、BigInt、非有限数或循环引用会在任何内容持久化前被拒绝,并给出以 `$` 为根的路径(YAML/JSON 存储在重载时会静默改变这类值)。校验失败在持久化前拒绝;只读提供方(`writable: false`)拒绝一切写入。同一 namespace 的写入按调用顺序串行。
|
|
15
|
-
- `replace(ns, section)` — 整体替换用户分节:这是刻意的重置(`replace({})` 重新继承 `base` 与 schema 默认值)。
|
|
16
|
-
- `mutate(ns, ops)` — 在写入排到队首那一刻的分节上,按序施加 `{ op: 'set' | 'unset', path }` 编辑。这是任何持有**不完整**视图的调用方的删除路径:配置 UI 读到的是脱敏后的 descriptor,据此重建分节再整体替换,会把 wire 从未回传的每个机密都删掉,而一条 op 只点名它真正要改的那个字段。
|
|
17
|
-
- 每次写入都可携带可选的 `expectedRevision`。每个 descriptor 都带有该 namespace 的 `revision`——一个针对其**原始**分节的单调计数器;期望值不再匹配的写入会以 `SettingsConflictError`(`code: 'SETTINGS_CONFLICT'`,并附上两个 revision)被拒绝,而不是覆盖先完成写入的写入方。写队列只保证写入的先后次序,它本身分辨不出持有新鲜快照的写入方与持有陈旧快照的写入方。
|
|
18
|
-
- 解析值是深冻结快照。每次提交后观察者收到 `(next, prev)`:同一回调的调用异步、逐次、按提交顺序执行(慢的旧调用绝不会晚于较新的调用生效),异常——同步抛出与异步拒绝——均被隔离。watch 的 disposer 返回后不再启动新的调用(已排队的那一次会被跳过);已启动的调用仍会结算。`settings/updated` 事件逐监听器扇出,一个抛错的 listener 不会饿死其余 listener;异步 listener 的拒绝会被隔离并记入日志,这正是 `INVARIANT` 编码的失败只从同步 listener 重新抛出的原因。
|
|
19
|
-
- 服务卸载先拒绝新写入与观察者调用的启动,再排干全部排队写入与已启动的观察者调用后才完成;registrant fiber 在写入途中被 dispose 时,该写入仍到达存储,但不会提交,也不会通知任何人。
|
|
74
|
+
### 事件与失败
|
|
20
75
|
|
|
21
|
-
|
|
76
|
+
`settings/updated (ns, next, prev, source)` 在每次已提交变更后触发——进程内写入(`source: 'update'`)或外部观察到的编辑(`source: 'provider'`)——解析值深相等时绝不触发。`settings/document-updated (ns, revision)` 在原始用户分节发生变化时触发,即使解析值没有变——已打开的编辑器正需要它来得知字段从继承变为覆盖。schema 拒绝的存量分节在重载时保留该 namespace 的最后可用值并告警;注册时同样的失败会直接拒绝注册。
|
|
22
77
|
|
|
23
|
-
|
|
78
|
+
-----
|
|
24
79
|
|
|
25
|
-
|
|
80
|
+
<a id="understand-the-implementation"></a>
|
|
81
|
+
## 理解实现
|
|
26
82
|
|
|
27
|
-
|
|
83
|
+
<details>
|
|
84
|
+
<summary>实现细节——点击展开</summary>
|
|
28
85
|
|
|
29
|
-
|
|
86
|
+
本节解释服务背后的设计决策并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
|
|
30
87
|
|
|
31
|
-
|
|
88
|
+
### 设计理念
|
|
32
89
|
|
|
90
|
+
- **分层解析,单一用户层。** namespace 的值依次为 schema 默认值、注册方的组合 `base`、用户文档分节;写入只触碰用户层,因此 `replace({})` 是真正的重置。
|
|
91
|
+
- **提交以深相等为门槛。** 只有解析值变化时 `settings/updated` 才触发;原始分节事件独立存在,因为配置界面还必须得知「继承变成了覆盖」。
|
|
92
|
+
- **写入排队并做 revision 检查。** 每个 namespace 的写队列按调用顺序串行,`expectedRevision` 在队首判断——那里服务才能分辨持有新鲜快照的写入方与持有陈旧快照的写入方。
|
|
93
|
+
- **观察者与监听器异常被隔离。** watcher 调用与事件扇出隔离同步抛出与异步拒绝,一个坏掉的观察者不会卡死提交或提供方的重载循环;`INVARIANT` 编码的失败在所有监听器执行完后重新抛出。
|
|
94
|
+
- **注册是 fiber 上的 effect。** 注册 namespace 是调用方插件 fiber 上的 effect:dispose(资源释放)该 fiber 即移除 namespace 及其观察者。
|
|
95
|
+
|
|
96
|
+
### 源码地图
|
|
97
|
+
|
|
98
|
+
| 文件 | 职责 |
|
|
99
|
+
|---|---|
|
|
100
|
+
| [`src/index.ts`](src/index.ts) | Service Definition:namespace 校验、注册、解析、写队列、describe/脱敏、事件、`installSection` |
|
|
101
|
+
| [`src/redact.ts`](src/redact.ts) | `redactSecrets` 遍历器:剥离 `role('secret')` 字段并枚举其 slot |
|
|
102
|
+
| [`src/types.ts`](src/types.ts) | 客户端安全类型面:事件声明、`SettingsNamespace`、`SettingsUpdateSource` |
|
|
103
|
+
| [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件:`settings/updated` 只对已注册 namespace、只在解析值变化时、且携带权威值触发 |
|
|
104
|
+
|
|
105
|
+
### 解析与写入路径
|
|
106
|
+
|
|
107
|
+
每次写入都在调用时对输入做快照(分离并校验 JSON 形状的数据),然后排上该 namespace 的串行链。在队首,服务按当前状态重读分节、检查 `expectedRevision`、合并/替换/编辑、经 schema 与 owner 的可选 `validate` 解析并校验候选值、经提供方持久化,然后才提交并发出事件。registrant fiber 在写入途中被 dispose 的写入仍到达存储,但不会提交、也不会通知任何人;卸载先拒绝新写入,并排干排队写入与已启动的 watcher 调用后才完成。
|
|
108
|
+
|
|
109
|
+
### 变更检测与事件
|
|
110
|
+
|
|
111
|
+
`commit` 用 seam 的 `deepEqualJson` 谓词比较解析值,并逐监听器扇出 `settings/updated`。`bumpRevision` 比较原始分节并携带新 revision 发出 `settings/document-updated`;它与解析值检查相互独立。两个扇出以相同方式隔离监听器异常。
|
|
112
|
+
|
|
113
|
+
### 客户端安全类型
|
|
114
|
+
|
|
115
|
+
`./types` 子路径出口持有事件声明及其签名点名的 `SettingsNamespace`、`SettingsUpdateSource` 类型,包根继续 re-export 这些类型。于是 Host 编译面之外的消费方读到的正是 Host 发射的那一份签名,而不必再写一遍。
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
-----
|
|
120
|
+
|
|
121
|
+
<a id="further-exploration"></a>
|
|
122
|
+
## 进一步探索
|
|
123
|
+
|
|
124
|
+
当服务级约定不够用时阅读以下页面。它们从共享子系统词汇逐步进入随附提供方与能力架构。
|
|
125
|
+
|
|
126
|
+
- [设置子系统参考](../../../docs/subsystems/settings.zh.md)——namespace、注册、owner scope、descriptor、变更提交与生成的 cordis 接口面。
|
|
127
|
+
- [文件型设置提供方](../settings-file/README.zh.md)——随附的 YAML/JSON 提供方:配置、热重载、保留注释的写入。
|
|
128
|
+
- [设置包映射](../README.zh.md)——用户设置能力的两个包及其角色。
|
|
129
|
+
- [能力 seam](../../../docs/capability-seams.zh.md)——本服务遵循的 Service Definition / Service Provider / Consumer 拆分。
|
|
130
|
+
|
|
131
|
+
-----
|
|
132
|
+
|
|
133
|
+
<a id="model-experience"></a>
|
|
33
134
|
## 模型体验
|
|
34
135
|
|
|
35
|
-
|
|
136
|
+
间接生效:消费方插件拥有任何由设置值喂给的模型面内容;本服务只存储并解析用户设置,自身不注册任何模型面内容。
|
|
36
137
|
|
|
37
138
|
#### KV Cache 影响
|
|
38
139
|
|
|
39
140
|
无直接失效;把设置值纳入请求前缀的消费方负责该变更。
|
|
40
141
|
|
|
41
|
-
##
|
|
142
|
+
## 已知限制与延期工作
|
|
143
|
+
|
|
144
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
这些限制说明本服务何时不合适或需要特别注意。它们是当前包约束,不是任务积压。
|
|
148
|
+
|
|
149
|
+
- **单一用户层**——解析只认识 schema 默认值、一个组合 `base` 与一个用户文档;它不记录每个解析值由哪一层提供。
|
|
150
|
+
- **`redactSecrets` 并非一条可被证明的协议边界**——遍历器只跟随 `object`/`dict`/`array` 容器,因此只能经由 union、intersection 或 transform 抵达的 `role('secret')` 字段会被原样返回,且 `secrets` 列表为空;序列化 schema 还会把 secret 字段的默认值带给每个客户端。两种情况都不会被拒绝;机密无法经由被遍历的容器抵达的 schema,绝不可注册到暴露于协议的 namespace 上。fail-closed 的 `describeForWire()`——拒绝自己无法证明安全的 schema,并对序列化封装与错误文本做净化——是暂缓的答案。
|
|
151
|
+
- **跨进程并发由提供方定义**——服务仅在进程内按 namespace 串行写入;跨进程并发按提供方行为收敛(文件提供方在写锁下读-改-写,因此并发写入者不会丢掉彼此的 namespace,同 namespace 冲突按后写胜出解决)。
|
|
152
|
+
|
|
153
|
+
<a id="dev-note"></a>
|
|
154
|
+
### 开发备注
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
158
|
+
|
|
159
|
+
本开发备注是维护者的工作上下文:尚未决定的开放设计方向。它明确非权威——已发布的行为、限制与已接受的理由见上文各节与包代码。代码 TODO 中记录的开放方向:把公开的 `ns` 参数更名为 `namespace`(API、提供方约定、实现、测试与消费方同步);注册释放时停用所有 watcher 并等待其 tail,让回调不越过 registrant fiber 存活;替换注册从持久化分节重新解析,让进行中的旧写入不会把它留成陈旧值;改用属性安全的对象构造,让 `__proto__` 这类合法 JSON 键保持为自有数据。fail-closed 的 `describeForWire()` 净化器是上文脱敏限制的暂缓答案。
|
|
42
160
|
|
|
43
|
-
|
|
44
|
-
- **`redactSecrets` 并非一条可被证明的协议边界**:walker 只跟随 `object`/`dict`/`array`,因此只能经由 union、intersection 或 transform 抵达的 `role('secret')` 会被**原样**返回,且 `secrets` 列表为空;而 `schema.toJSON()` 会把 secret 字段的 `.default(...)` 一并带给每个客户端。这两种情况都不会被拒绝;机密无法经由被遍历的容器抵达的 schema,绝不可注册到暴露于协议的 namespace 上。真正的答案是一个 fail-closed 的 `describeForWire()`——它拒绝自己无法证明安全的 schema,并对序列化封装与错误文本做净化——此项暂缓。
|
|
45
|
-
- **跨进程并发由提供方定义** — seam 仅在进程内按 namespace 串行化写入;跨进程并发按提供方行为收敛(本地文件提供方在写锁下读-改-写,因此 namespace 在并发写入者下不会丢失,同 namespace 冲突按后写胜出解决)。
|
|
161
|
+
</details>
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { deepEqualJson, deepFreeze } from "@deepseek-ai/dsh-util-values";
|
|
2
3
|
//#region lib/types/redact.js
|
|
3
4
|
/**
|
|
4
5
|
* Structural secret redaction for settings values. `role('secret')` fields are
|
|
@@ -79,37 +80,11 @@ function redactSecrets(schema, value) {
|
|
|
79
80
|
* @module @deepseek-ai/dsh-settings
|
|
80
81
|
*/
|
|
81
82
|
const NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
82
|
-
|
|
83
|
-
* Brand a raw string as a {@link SettingsNamespace}.
|
|
84
|
-
* @param value - candidate namespace; lowercase kebab-case, as in plugin short names.
|
|
85
|
-
* @returns the branded namespace.
|
|
86
|
-
*/
|
|
87
|
-
function settingsNamespace(value) {
|
|
83
|
+
function parseSettingsNamespace(value) {
|
|
88
84
|
if (!NAMESPACE_PATTERN.test(value)) throw new TypeError(`settings namespace "${value}" must match ${String(NAMESPACE_PATTERN)}`);
|
|
89
85
|
return value;
|
|
90
86
|
}
|
|
91
87
|
/**
|
|
92
|
-
* Deep equality over JSON-compatible data (objects, arrays, primitives) — the
|
|
93
|
-
* Service Definition's single change-detection predicate, exported so the invariant
|
|
94
|
-
* companion checks exactly the implementation's relation.
|
|
95
|
-
* @param a - one JSON-compatible value.
|
|
96
|
-
* @param b - the other JSON-compatible value.
|
|
97
|
-
* @returns whether the two values are structurally equal.
|
|
98
|
-
*/
|
|
99
|
-
function deepEqualJson(a, b) {
|
|
100
|
-
if (a === b) return true;
|
|
101
|
-
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
102
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
103
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
104
|
-
return a.every((entry, index) => deepEqualJson(entry, b[index]));
|
|
105
|
-
}
|
|
106
|
-
const left = a;
|
|
107
|
-
const right = b;
|
|
108
|
-
const keys = Object.keys(left);
|
|
109
|
-
if (keys.length !== Object.keys(right).length) return false;
|
|
110
|
-
return keys.every((key) => key in right && deepEqualJson(left[key], right[key]));
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
88
|
* A write refused because the namespace moved since the caller read it. The
|
|
114
89
|
* Service Definition's serialized write queue orders writes; it cannot tell a fresh writer
|
|
115
90
|
* from one holding a stale snapshot, which is what this reports.
|
|
@@ -239,12 +214,6 @@ function mergeLayers(under, over) {
|
|
|
239
214
|
for (const [key, value] of Object.entries(over)) merged[key] = key in merged ? mergeLayers(merged[key], value) : value;
|
|
240
215
|
return merged;
|
|
241
216
|
}
|
|
242
|
-
/** Recursively freeze one resolved value so handed-out snapshots stay immutable. */
|
|
243
|
-
function deepFreeze(value) {
|
|
244
|
-
if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value;
|
|
245
|
-
for (const entry of Object.values(value)) deepFreeze(entry);
|
|
246
|
-
return Object.freeze(value);
|
|
247
|
-
}
|
|
248
217
|
/**
|
|
249
218
|
* Abstract settings service. Providers implement raw-document storage
|
|
250
219
|
* (`load`/`persist`) and push external changes through {@link Settings.publish};
|
|
@@ -307,23 +276,25 @@ var SettingsProvider = class extends Service {
|
|
|
307
276
|
* @param schema - schemastery schema resolving this namespace's value.
|
|
308
277
|
* @param options - composition `base` layer and effect timing.
|
|
309
278
|
* @returns the owner scope for reads, observation, and updates.
|
|
279
|
+
* @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
|
|
310
280
|
*/
|
|
311
281
|
register(ns, schema, options) {
|
|
312
|
-
|
|
282
|
+
const parsedNs = parseSettingsNamespace(ns);
|
|
283
|
+
if (this.registrations.has(parsedNs)) throw new Error(`settings namespace "${parsedNs}" is already registered`);
|
|
313
284
|
const registration = {
|
|
314
|
-
ns,
|
|
285
|
+
ns: parsedNs,
|
|
315
286
|
schema,
|
|
316
287
|
base: options?.base,
|
|
317
288
|
applies: options?.applies ?? "live",
|
|
318
289
|
...options?.validate === void 0 ? {} : { validate: options.validate },
|
|
319
|
-
resolved: deepFreeze(this.resolve(schema, options?.base, this.section(
|
|
290
|
+
resolved: deepFreeze(this.resolve(schema, options?.base, this.section(parsedNs), options?.validate)),
|
|
320
291
|
revision: 0,
|
|
321
292
|
watchers: /* @__PURE__ */ new Set()
|
|
322
293
|
};
|
|
323
294
|
this.ctx.effect(() => {
|
|
324
|
-
this.registrations.set(
|
|
325
|
-
return () => this.registrations.delete(
|
|
326
|
-
}, `settings.register(${JSON.stringify(String(
|
|
295
|
+
this.registrations.set(parsedNs, registration);
|
|
296
|
+
return () => this.registrations.delete(parsedNs);
|
|
297
|
+
}, `settings.register(${JSON.stringify(String(parsedNs))})`);
|
|
327
298
|
return {
|
|
328
299
|
get: () => registration.resolved,
|
|
329
300
|
watch: (callback) => {
|
|
@@ -338,11 +309,39 @@ var SettingsProvider = class extends Service {
|
|
|
338
309
|
registration.watchers.delete(watcher);
|
|
339
310
|
};
|
|
340
311
|
},
|
|
341
|
-
update: (patch) => this.update(
|
|
342
|
-
replace: (section) => this.replace(
|
|
312
|
+
update: (patch) => this.update(parsedNs, patch),
|
|
313
|
+
replace: (section) => this.replace(parsedNs, section)
|
|
343
314
|
};
|
|
344
315
|
}
|
|
345
316
|
/**
|
|
317
|
+
* Attach one optional-settings consumer to this provider. The consumer
|
|
318
|
+
* registers its composition entry as the base layer while this provider is
|
|
319
|
+
* present, then falls back to that entry if the provider detaches.
|
|
320
|
+
* @param owner - consumer context whose unload suppresses fallback work.
|
|
321
|
+
* @param ns - consumer-owned settings namespace.
|
|
322
|
+
* @param schema - schema resolving the namespace.
|
|
323
|
+
* @param entry - composition entry used as the base and fallback value.
|
|
324
|
+
* @param hooks - source sink, change notification, and optional validation.
|
|
325
|
+
* @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
|
|
326
|
+
*/
|
|
327
|
+
installSection(owner, ns, schema, entry, hooks) {
|
|
328
|
+
const scope = this.register(ns, schema, {
|
|
329
|
+
base: entry,
|
|
330
|
+
...hooks.validate === void 0 ? {} : { validate: hooks.validate }
|
|
331
|
+
});
|
|
332
|
+
hooks.setSource(() => scope.get());
|
|
333
|
+
this.ctx.effect(() => () => {
|
|
334
|
+
if (isUnloading(owner)) return;
|
|
335
|
+
hooks.setSource(() => entry);
|
|
336
|
+
hooks.onChange();
|
|
337
|
+
});
|
|
338
|
+
hooks.onChange();
|
|
339
|
+
scope.watch(() => {
|
|
340
|
+
if (isUnloading(owner)) return;
|
|
341
|
+
hooks.onChange();
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
346
345
|
* Describe every registered namespace for configuration surfaces, including
|
|
347
346
|
* the composition `base` and raw user layers so a form can mark which fields
|
|
348
347
|
* the user overrode (presence in `user`) and what a reset returns to.
|
|
@@ -384,9 +383,10 @@ var SettingsProvider = class extends Service {
|
|
|
384
383
|
* Read one registered namespace's resolved value.
|
|
385
384
|
* @param ns - the namespace to read.
|
|
386
385
|
* @returns the resolved value, or `undefined` while unregistered.
|
|
386
|
+
* @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
|
|
387
387
|
*/
|
|
388
388
|
get(ns) {
|
|
389
|
-
return this.registrations.get(ns)?.resolved;
|
|
389
|
+
return this.registrations.get(parseSettingsNamespace(ns))?.resolved;
|
|
390
390
|
}
|
|
391
391
|
/**
|
|
392
392
|
* Merge a patch into one registered namespace's user layer, validate the
|
|
@@ -398,9 +398,10 @@ var SettingsProvider = class extends Service {
|
|
|
398
398
|
* @param patch - plain-object patch over the user section.
|
|
399
399
|
* @param expectedRevision - the descriptor `revision` the caller read; a
|
|
400
400
|
* namespace that moved past it rejects with {@link SettingsConflictError}.
|
|
401
|
+
* @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
|
|
401
402
|
*/
|
|
402
403
|
async update(ns, patch, expectedRevision) {
|
|
403
|
-
return this.write(ns, patch, "merge", expectedRevision);
|
|
404
|
+
return this.write(parseSettingsNamespace(ns), patch, "merge", expectedRevision);
|
|
404
405
|
}
|
|
405
406
|
/**
|
|
406
407
|
* Replace one registered namespace's user section wholesale, validate,
|
|
@@ -411,9 +412,10 @@ var SettingsProvider = class extends Service {
|
|
|
411
412
|
* @param section - the complete next user section.
|
|
412
413
|
* @param expectedRevision - the descriptor `revision` the caller read; a
|
|
413
414
|
* namespace that moved past it rejects with {@link SettingsConflictError}.
|
|
415
|
+
* @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
|
|
414
416
|
*/
|
|
415
417
|
async replace(ns, section, expectedRevision) {
|
|
416
|
-
return this.write(ns, section, "replace", expectedRevision);
|
|
418
|
+
return this.write(parseSettingsNamespace(ns), section, "replace", expectedRevision);
|
|
417
419
|
}
|
|
418
420
|
/**
|
|
419
421
|
* Apply path-addressed edits to one registered namespace's user section,
|
|
@@ -426,14 +428,16 @@ var SettingsProvider = class extends Service {
|
|
|
426
428
|
* @param ops - ordered path edits; later ops observe earlier ones.
|
|
427
429
|
* @param expectedRevision - the descriptor `revision` the caller read; a
|
|
428
430
|
* namespace that moved past it rejects with {@link SettingsConflictError}.
|
|
431
|
+
* @throws {TypeError} when `ns` is not a lowercase hyphenated identifier.
|
|
429
432
|
*/
|
|
430
433
|
async mutate(ns, ops, expectedRevision) {
|
|
431
|
-
|
|
434
|
+
const parsedNs = parseSettingsNamespace(ns);
|
|
435
|
+
if (!Array.isArray(ops)) throw new TypeError(`settings mutate for "${parsedNs}" must be an array of path ops`);
|
|
432
436
|
for (const op of ops) {
|
|
433
|
-
if (!isPlainObject(op) || op["op"] !== "set" && op["op"] !== "unset") throw new TypeError(`settings mutate for "${
|
|
434
|
-
if (!Array.isArray(op["path"]) || op["path"].some((part) => typeof part !== "string")) throw new TypeError(`settings mutate for "${
|
|
437
|
+
if (!isPlainObject(op) || op["op"] !== "set" && op["op"] !== "unset") throw new TypeError(`settings mutate for "${parsedNs}" ops must be {op:'set'|'unset', path}`);
|
|
438
|
+
if (!Array.isArray(op["path"]) || op["path"].some((part) => typeof part !== "string")) throw new TypeError(`settings mutate for "${parsedNs}" op paths must be arrays of strings`);
|
|
435
439
|
}
|
|
436
|
-
return this.write(
|
|
440
|
+
return this.write(parsedNs, ops, "mutate", expectedRevision);
|
|
437
441
|
}
|
|
438
442
|
/** Validate a write, then queue it on the namespace's serialized write chain. */
|
|
439
443
|
write(ns, input, mode, expectedRevision) {
|
|
@@ -602,37 +606,5 @@ function isUnloading(ctx) {
|
|
|
602
606
|
const state = ctx.fiber.state;
|
|
603
607
|
return state === FIBER_UNLOADING || state === FIBER_DISPOSED;
|
|
604
608
|
}
|
|
605
|
-
/**
|
|
606
|
-
* Install the canonical optional-settings consumer wiring: while a settings
|
|
607
|
-
* service exists, register `ns` with the consumer's composition entry as the
|
|
608
|
-
* `base` layer and point the source thunk at the resolved scope; when the
|
|
609
|
-
* service goes away (disposal, provider reload), fall back to the entry so
|
|
610
|
-
* the consumer keeps working exactly as composed. The registration rides the
|
|
611
|
-
* scoped fiber, so no settings service ever mounted means none of this runs.
|
|
612
|
-
* @param ctx - consumer plugin context owning the wiring.
|
|
613
|
-
* @param ns - the consumer-owned settings namespace.
|
|
614
|
-
* @param schema - schema resolving the namespace (typically the plugin Config).
|
|
615
|
-
* @param entry - the consumer's composition entry config, used as `base`.
|
|
616
|
-
* @param hooks - source sink and change notification.
|
|
617
|
-
*/
|
|
618
|
-
function installSettingsSection(ctx, ns, schema, entry, hooks) {
|
|
619
|
-
ctx.inject(["settings"], (sctx) => {
|
|
620
|
-
const scope = sctx.settings.register(ns, schema, {
|
|
621
|
-
base: entry,
|
|
622
|
-
...hooks.validate === void 0 ? {} : { validate: hooks.validate }
|
|
623
|
-
});
|
|
624
|
-
hooks.setSource(() => scope.get());
|
|
625
|
-
sctx.effect(() => () => {
|
|
626
|
-
if (isUnloading(ctx)) return;
|
|
627
|
-
hooks.setSource(() => entry);
|
|
628
|
-
hooks.onChange();
|
|
629
|
-
});
|
|
630
|
-
hooks.onChange();
|
|
631
|
-
scope.watch(() => {
|
|
632
|
-
if (isUnloading(ctx)) return;
|
|
633
|
-
hooks.onChange();
|
|
634
|
-
});
|
|
635
|
-
});
|
|
636
|
-
}
|
|
637
609
|
//#endregion
|
|
638
|
-
export { SettingsConflictError, SettingsProvider, SettingsProvider as default,
|
|
610
|
+
export { SettingsConflictError, SettingsProvider, SettingsProvider as default, redactSecrets };
|