refactor(client): replace the per-field settings preference controller with a namespace settings scope
bindSettingsScope mirrors the Host-side settings owner seam in the browser: one scope per namespace publishes a snapshot store (status, section value, revision, writability, host/memory mode), validates sections against the namespace's serialized wire schema via dsh-client-schema-form, and keeps the controller's listener-before-read, revisioned serialized writes, latest-wins publication, conflict recovery, and disposal quiescence. Theme, locale, and busy-Enter services now take the scope as a constructor collaborator, which removes the bindPersistence/syncPreference two-phase callback pair and the defaulted no-op persist writers; hand-written wire guards fall away in favor of the registered schema. test-runtime gains a stubSettingsScope double.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md
|
||||
2026-08-06-host-backed-web-preferences.md: ee1c0aea360eb1a4b34eadc86c5c3091abc6663a
|
||||
2026-08-06-host-backed-web-preferences.zh.md: 376e670f9af39f43783a1447498ca2d4c65a49cd
|
||||
2026-08-06-host-backed-web-preferences.md: d56a8d2e330b214a1922997e3cc7165fd0fb31e4
|
||||
2026-08-06-host-backed-web-preferences.zh.md: 593646fe0845c20fb09cb7d115e6fa558226506e
|
||||
|
||||
@@ -14,9 +14,9 @@ The first theme implementation moved only Appearance to Host settings but awaite
|
||||
|
||||
The owning Host halves register three schemas: optional `locale.preference` (`zh` or `en`, where absence delegates to the browser), `ui-theme.preference` (`light`, `dark`, or `system`, default `system`), and `ui-conversation.busyEnter` (`queue` or `steer`, default `queue`). The local settings provider stores explicit choices in `$DSH_HOME/settings.yaml`, which resolves to `~/.dsh/settings.yaml` under the default home. The API proxy explicitly exposes all three namespaces beside the other Web settings; registration alone never crosses that configuration boundary.
|
||||
|
||||
The client runtime provides one `bindSettingsPreference` lifecycle for scalar preferences. It installs `settings/changed` and `connection/reset` listeners before starting a background initial read, so no settings transport can block plugin activation and an invalidation cannot fall into a read-before-subscribe gap. Domain services publish their provisional defaults immediately—browser-derived locale, system theme, and Queue—then accept a validated Host value without writing it back.
|
||||
The client runtime provides one `bindSettingsScope` lifecycle per namespace — the browser mirror of the Host-side settings owner seam. It installs `settings/changed` and `connection/reset` listeners before starting a background initial read, so no settings transport can block plugin activation and an invalidation cannot fall into a read-before-subscribe gap, and it publishes a snapshot store (status, section value, revision, writability, host/memory mode) the domain service subscribes to. The default decoder validates each incoming section against the namespace's own serialized wire schema, rehydrated through dsh-client-schema-form, so domains carry no hand-written wire guards. Domain services take the scope as an ordinary constructor collaborator, publish their provisional defaults immediately—browser-derived locale, system theme, and Queue—then adopt an accepted Host section without writing it back; a service constructed without a scope (standalone dictionary or policy fixtures) simply stays process-local.
|
||||
|
||||
User changes update the live service synchronously and queue a `settings.mutate` path operation. The controller serializes gestures, sends the latest known namespace revision as `expectedRevision`, records every successful revision, and lets only the latest write settlement republish live state. A rejected or failed latest write reloads Host state. Disposal rejects new work, skips queued operations, suppresses publication by the in-flight operation, and waits for that operation to settle before the plugin reaches quiescence.
|
||||
User changes update the live service synchronously and queue a `settings.mutate` path operation through `scope.set`. The scope serializes gestures, sends the latest known namespace revision as `expectedRevision`, records every successful revision, and lets only the latest write settlement republish live state. A rejected or failed latest write reloads Host state. Disposal rejects new work, skips queued operations, suppresses publication by the in-flight operation, and waits for that operation to settle before the plugin reaches quiescence.
|
||||
|
||||
Remote browsers cannot call the loopback-only configuration API, so their preferences remain process-local. Dynamic third-party theme ids remain in-process extensions outside the built-in Host schema; removing one resets the live registry without replacing the last durable built-in preference.
|
||||
|
||||
@@ -28,7 +28,9 @@ Remote browsers cannot call the loopback-only configuration API, so their prefer
|
||||
|
||||
**Await the initial read to avoid a provisional render.** Configuration availability is not a prerequisite for drawing the page. A background read may cause one live convergence, but it keeps failure isolated and preserves the existing browser/system/default fallbacks.
|
||||
|
||||
**Give every domain its own settings controller.** The concurrency, revision, failure, invalidation, and disposal rules are identical; copying them already produced lifecycle drift in the theme implementation. Domain-owned schemas and decoders keep product policy out of the shared runtime.
|
||||
**Give every domain its own settings controller.** The concurrency, revision, failure, invalidation, and disposal rules are identical; copying them already produced lifecycle drift in the theme implementation. Domain-owned schemas keep product policy out of the shared runtime.
|
||||
|
||||
**A per-field preference controller with paired sync/persist callbacks.** The first shared lifecycle synchronized one scalar field through a domain `sync` callback while the service wrote back through an injected `persist` callback. The mutual callbacks forced two-phase construction — a defaulted no-op writer later replaced via `bindPersistence` — every additional field of a namespace would have carried its own controller and whole-document read, and each domain re-declared a hand-written guard the registered wire schema already expresses. The namespace scope publishes a snapshot the service subscribes to and accepts writes directly, so the callback pair and the second construction phase do not exist.
|
||||
|
||||
**Move every `localStorage` entry into settings.** Current session, drafts, panel disclosure, trajectory display state, and similar entries are browser-instance state rather than user configuration. Promoting them would synchronize transient navigation state across tabs and ports without a product contract.
|
||||
|
||||
@@ -38,4 +40,4 @@ Appearance, Language, and busy-Enter choices follow the DSH user home across rel
|
||||
|
||||
Boot may briefly show the domain default before the background read settles. A transient read failure keeps that default or the last good in-process value; reconnect retries. A write rejection can visibly restore the durable preference after the immediate local change.
|
||||
|
||||
Focused unit coverage pins schema registration, listener-before-read ordering, nonblocking activation, revisioned ordered writes, stale-response containment, failure recovery, disposal quiescence, and remote memory mode. The keyless Web settings scenario writes all three preferences through the UI, verifies the YAML document and empty legacy storage, reloads, and boots another Host on a distinct port against the same DSH home.
|
||||
Focused unit coverage pins schema registration, listener-before-read ordering, nonblocking activation, schema-validated section acceptance, revisioned ordered writes, stale-response containment, failure recovery, disposal quiescence, and remote memory mode. The namespace-granular scope also carries multi-field sections, so later configuration surfaces can ride the same lifecycle instead of hand-rolling describe/mutate synchronization. The keyless Web settings scenario writes all three preferences through the UI, verifies the YAML document and empty legacy storage, reloads, and boots another Host on a distinct port against the same DSH home.
|
||||
|
||||
@@ -14,9 +14,9 @@ Web 的 Appearance、Language 和繁忙态 Enter 偏好原本存在浏览器 `lo
|
||||
|
||||
各领域所属的 Host half 注册三份 schema:可选的 `locale.preference`(`zh` 或 `en`,缺失时交由浏览器决定)、`ui-theme.preference`(`light`、`dark` 或 `system`,默认为 `system`),以及 `ui-conversation.busyEnter`(`queue` 或 `steer`,默认为 `queue`)。本地 settings 提供方将显式选择存入 `$DSH_HOME/settings.yaml`,在使用默认 home 时,该路径解析为 `~/.dsh/settings.yaml`。API 代理会显式暴露这三个 namespace,与其他 Web settings 并列;仅注册它们,绝不会跨越该配置边界。
|
||||
|
||||
客户端运行时为标量偏好提供一份 `bindSettingsPreference` 生命周期。它在开始后台初始读取之前安装 `settings/changed` 和 `connection/reset` 监听器,因此任何 settings 传输都不会阻塞插件激活,失效通知也不会掉入先读取、后订阅的空档。领域服务会立即发布各自的暂定默认值:由浏览器派生的 locale、系统主题和 Queue;随后接纳已校验的 Host 值,但不将其写回。
|
||||
客户端运行时为每个 namespace 提供一份 `bindSettingsScope` 生命周期——即 Host 侧 settings owner seam 的浏览器镜像。它在开始后台初始读取之前安装 `settings/changed` 和 `connection/reset` 监听器,因此任何 settings 传输都不会阻塞插件激活,失效通知也不会掉入先读取、后订阅的空档;它还会发布一个供领域服务订阅的快照 store(状态、分节值、revision、可写性、host/内存模式)。默认解码器会对照该 namespace 自身的序列化 wire schema(经 dsh-client-schema-form 还原)校验每个传入分节,因此各领域无需携带手写的 wire 校验器。领域服务把 scope 当作普通的构造函数协作者接收,立即发布各自的暂定默认值:由浏览器派生的 locale、系统主题和 Queue;随后采纳已获接受的 Host 分节,但不将其写回;不带 scope 构造的服务——独立词典或政策 fixture(测试前置数据)——则仅停留在进程本地。
|
||||
|
||||
用户变更会同步更新实时服务,并将一项 `settings.mutate` 路径操作排入队列。控制器会串行处理手势,以最新已知 namespace revision 作为 `expectedRevision` 发送,记录每次成功写入的 revision,并且只允许最新写入的结算结果重新发布实时状态。最新写入被拒或失败时,控制器会重新加载 Host 状态。插件释放会拒绝新工作、跳过已排队操作、抑制运行中操作发布状态,并等待该操作结算后才让插件达到完全停稳。
|
||||
用户变更会同步更新实时服务,并经 `scope.set` 将一项 `settings.mutate` 路径操作排入队列。scope 会串行处理手势,以最新已知 namespace revision 作为 `expectedRevision` 发送,记录每次成功写入的 revision,并且只允许最新写入的结算结果重新发布实时状态。最新写入被拒或失败时,scope 会重新加载 Host 状态。插件释放会拒绝新工作、跳过已排队操作、抑制运行中操作发布状态,并等待该操作结算后才让插件达到完全停稳。
|
||||
|
||||
远程浏览器无法调用仅限回环请求的配置 API,因此其偏好仅保留在进程内。动态第三方主题 id 仍是内置 Host schema 之外的进程内扩展;移除其中一个会重置实时注册表,但不会替换上一个持久化的内置偏好。
|
||||
|
||||
@@ -28,7 +28,9 @@ Web 的 Appearance、Language 和繁忙态 Enter 偏好原本存在浏览器 `lo
|
||||
|
||||
**等待初始读取,以避免暂定渲染。** 绘制页面不以配置可用为前置条件。后台读取可能引发一次实时收敛,但它会隔离失败,并保留既有的浏览器/系统/默认回落路径。
|
||||
|
||||
**让每个领域拥有自己的 settings 控制器。** 并发、revision、失败、失效与释放规则完全一致;此前的主题实现已因复制这些规则产生生命周期漂移。由领域持有 schema 和解码器,可以避免把产品政策放入共享运行时。
|
||||
**让每个领域拥有自己的 settings 控制器。** 并发、revision、失败、失效与释放规则完全一致;此前的主题实现已因复制这些规则产生生命周期漂移。由领域持有 schema,可以避免把产品政策放入共享运行时。
|
||||
|
||||
**带成对 sync/persist 回调的逐字段偏好控制器。** 第一版共享生命周期经领域提供的 `sync` 回调同步单个标量字段,服务则经注入的 `persist` 回调写回。这对相互依赖的回调迫使构造分两阶段完成——写入器先默认为无操作,稍后经 `bindPersistence` 替换——namespace 每新增一个字段,本都得再携带一个自己的控制器和一次全文档读取,且每个领域都重新声明了一个已注册 wire schema 本已表达的手写校验器。namespace scope 发布一份供服务订阅的快照并直接接受写入,因此这对回调与第二个构造阶段都不存在。
|
||||
|
||||
**把每个 `localStorage` 条目都移入 settings。** 当前会话、草稿、面板展开状态、trajectory 显示状态和类似条目属于浏览器实例状态,而非用户配置。将它们提升为设置,会在没有产品契约的情况下,跨标签页和端口同步短暂导航状态。
|
||||
|
||||
@@ -38,4 +40,4 @@ Appearance、Language 和繁忙态 Enter 选择会跟随 DSH 用户 home,跨
|
||||
|
||||
启动时可能会在后台读取结算前短暂显示领域默认值。短暂的读取失败会保留该默认值或上一个正确的进程内值;重连时会重试。写入被拒时,界面可能会在本地值立即变化后明显恢复为持久化偏好。
|
||||
|
||||
聚焦的单元测试覆盖 schema 注册、先监听后读取的顺序、非阻塞激活、携带 revision 的有序写入、陈旧响应隔离、故障恢复、释放时完全停稳,以及远程端仅内存模式。无密钥 Web settings 场景通过 UI 写入全部三项偏好,校验 YAML 文档并确认旧 `localStorage` 为空,重新加载,再使用同一个 DSH home 在不同端口上启动另一个 Host。
|
||||
聚焦的单元测试覆盖 schema 注册、先监听后读取的顺序、非阻塞激活、经 schema 校验的分节接受、携带 revision 的有序写入、陈旧响应隔离、故障恢复、释放时完全停稳,以及远程端仅内存模式。以 namespace 为粒度的 scope 也承载多字段分节,因此后续的配置表面可以沿用同一份生命周期,而不必手搭 describe/mutate 同步。无密钥 Web settings 场景通过 UI 写入全部三项偏好,校验 YAML 文档并确认旧 `localStorage` 为空,重新加载,再使用同一个 DSH home 在不同端口上启动另一个 Host。
|
||||
|
||||
Reference in New Issue
Block a user