ソースを参照

refactor(ui-settings): derive settings scopes from the describe mirror

Yichen Jiang 3 週間 前
コミット
fd61fa889b

+ 41 - 12
packages/client/ui-settings/src/client/index.ts

@@ -1,35 +1,64 @@
 /**
  * Settings domain base plugin, browser half. Provides `ctx.settingsScope`, the
- * settings-namespace Host transport every preference row binds its durable
- * section through, and owns the canonical slot-type contract for the settings
- * surface. It depends on no `ui-*` presentation package, so any feature that
- * owns a preference can reach it: the settings SHELL — the `sidebar.settings`
- * occupant, its navigation, and the chrome — lives in ui-settings-general,
- * because a shell dependency on ui-sidebar would close a reference cycle
- * through ui-layout and ui-theme. Export discipline: packages/client/AGENTS.md.
+ * settings-namespace scope service every preference row binds its durable
+ * section through, and owns the one `settings.describe` reader in the browser:
+ * the describe mirror, whose invalidation subscriptions
+ * (`settings/document-updated`, `connection/reset`) live here so every derived
+ * surface refreshes from a single wire read. It depends on no `ui-*`
+ * presentation package, so any feature that owns a preference can reach it:
+ * the settings SHELL — the `sidebar.settings` occupant, its navigation, and
+ * the chrome — lives in ui-settings-general, because a shell dependency on
+ * ui-sidebar would close a reference cycle through ui-layout and ui-theme.
+ * Export discipline: packages/client/AGENTS.md.
  */
 import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
+import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'
+// Type-only pair supplying `$on` and its key face without dragging a build
+// artifact into the Host graph (rationale beside the same pair in
+// settings-scope.ts).
+import type {} from '@deepseek-ai/dsh-api-remotes/types'
+import type {} from '@deepseek-ai/dsh-settings/types'
 import { SettingsScopeBinder } from './settings-scope.ts'
+import { SettingsDescribeMirror } from './settings-mirror.ts'
 
 export type {
   SettingsGeneralItemOwnerProps, SettingsHeaderOwnerProps, SettingsOnboardingOwnerProps,
   SettingsPluginsTabOwnerProps, SettingsSectionOwnerProps, SettingsTriggerOwnerProps,
 } from './contract/slots.ts'
 export { SettingsScopeController, SettingsScopeBinder } from './settings-scope.ts'
+export { SettingsDescribeMirror } from './settings-mirror.ts'
+export type { SettingsDescribeView, SettingsMirrorSnapshot } from './settings-mirror.ts'
 
 /**
- * Required services: none. The transport is resolved per caller through
- * `this.ctx` at `bind` time, so this plugin waits for nothing.
+ * Required services: the wire handle for the mirror's reads and the forwarded
+ * settings invalidation the mirror refreshes on.
  */
-export const inject = []
+export const inject = ['connection', 'remote']
 
 /**
- * Provide the settings-namespace scope service.
+ * Provide the settings-namespace scope service over one shared describe
+ * mirror, and keep that mirror fresh on the two signals that can move the
+ * settings document: a document commit and a (re)connect.
  *
  * Constructing the service in this plugin's fiber keeps its traced methods
  * bound to each consuming plugin's context.
  * @param ctx - client root context.
  */
 export function apply(ctx: ClientContext): void {
-  new SettingsScopeBinder(ctx)
+  const connection = ctx.get('connection') as ConnectionHandle
+  const mirror = new SettingsDescribeMirror(
+    connection.api,
+    connection.isLoopback ? 'host' : 'memory',
+  )
+  ctx.effect(() => {
+    const disposers = [
+      (ctx.get('remote') as ClientContext['remote']).$on('settings/document-updated', () => { void mirror.load() }),
+      ctx.on('connection/reset', () => { void mirror.load() }),
+    ]
+    // The first connection also emits connection/reset; the in-flight fold
+    // makes this eager read and that reset converge to one wire call.
+    void mirror.ensure()
+    return () => { for (const dispose of disposers) dispose() }
+  }, 'ui-settings: describe mirror invalidations')
+  new SettingsScopeBinder(ctx, { mirror })
 }

+ 72 - 66
packages/client/ui-settings/src/client/settings-scope.ts

@@ -1,8 +1,11 @@
 /**
  * Host transport for the settings-namespace scope contract. The contract types
  * live in `dsh-client-runtime` (the common dependency of every feature that
- * owns a preference); this file owns the wire behavior and the invalidation
- * subscription, both of which are Settings-surface concerns.
+ * owns a preference); this file owns the per-namespace derivation over the
+ * shared {@link SettingsDescribeMirror} and the serialized write path, both of
+ * which are Settings-surface concerns. Reads never touch the wire here: the
+ * mirror is the one `settings.describe` reader, and every scope is a selector
+ * over its snapshot.
  */
 
 import { Service } from '@deepseek-ai/cordis'
@@ -22,8 +25,8 @@ import {
 // Client half declares `ctx.remote` with no generated import, and the
 // allowlist's `types` subpath is a pure-type source file, so the pair supplies
 // `$on` and its key face without dragging a build artifact in. The runtime
-// `remote` injection belongs to whoever calls bindSettingsScope: the
-// subscription is registered on the caller's own context.
+// `remote` injection belongs to the providing plugin's apply, which registers
+// the mirror's invalidation subscriptions.
 import type {} from '@deepseek-ai/dsh-api-remotes/client'
 import type {} from '@deepseek-ai/dsh-api-remotes/types'
 // The forwarded event's own declaration: `$on`'s key face is
@@ -31,29 +34,39 @@ import type {} from '@deepseek-ai/dsh-api-remotes/types'
 // never — the owning package's client-safe, type-only subpath supplies the
 // cordis `Events` entry (and with it the branded `SettingsNamespace`).
 import type {} from '@deepseek-ai/dsh-settings/types'
+import { SettingsDescribeMirror } from './settings-mirror.ts'
+
 type SettingsFace = Pick<IApiClient, 'settings'>
 
 /**
- * Serializes one namespace's Host reads and writes behind a snapshot store.
- * Reads never block plugin activation; writes carry the latest known
- * namespace revision and teardown waits for the operation already crossing
- * the wire.
+ * One namespace's derived view over the shared describe mirror, plus that
+ * namespace's serialized Host writes. Writes carry the latest known namespace
+ * revision, fold their answers back into the mirror, and teardown waits for
+ * the operation already crossing the wire.
  */
 export class SettingsScopeController<T> implements SettingsScope<T> {
   private readonly store: SnapshotStore<SettingsScopeSnapshot<T>>
   private tail: Promise<void> = Promise.resolve()
-  private readGeneration = 0
   private writeGeneration = 0
   private disposed = false
+  private readonly unsubscribe: (() => void) | undefined
+  /**
+   * Revision answered by a superseded write still ahead of the mirror: the
+   * mirror only folds the LATEST settlement in, so a queued successor takes
+   * its fence from here first.
+   */
+  private pendingRevision: number | undefined
 
   /**
-   * @param api - settings wire face.
+   * @param api - settings wire face (writes only; reads ride the mirror).
    * @param spec - namespace identity and optional narrowing decoder.
+   * @param mirror - the shared describe mirror this scope derives from.
    * @param persistence - remote browsers remain process-local because settings RPCs are loopback-only.
    */
   constructor(
     private readonly api: SettingsFace,
     private readonly spec: SettingsScopeSpec<T>,
+    private readonly mirror: SettingsDescribeMirror,
     private readonly persistence: 'host' | 'memory' = 'host',
   ) {
     this.store = createSnapshotStore<SettingsScopeSnapshot<T>>({
@@ -65,6 +78,10 @@ export class SettingsScopeController<T> implements SettingsScope<T> {
       writable: false,
       mode: persistence,
     })
+    if (persistence === 'host') {
+      this.unsubscribe = mirror.subscribe(() => { this.derive() })
+      this.derive()
+    }
   }
 
   /** @returns the current sync snapshot (stable reference until the next change). */
@@ -81,15 +98,6 @@ export class SettingsScopeController<T> implements SettingsScope<T> {
     return this.store.subscribe(listener)
   }
 
-  /**
-   * Queue a Host refresh; a newer read or user write suppresses stale publication.
-   * @returns settlement after the queued read completes or is skipped.
-   */
-  load(): Promise<void> {
-    const generation = ++this.readGeneration
-    return this.enqueue(() => this.read(generation))
-  }
-
   /**
    * Queue one field write; see {@link SettingsScope.set} for the ordering,
    * revision, and recovery contract.
@@ -112,10 +120,9 @@ export class SettingsScopeController<T> implements SettingsScope<T> {
   }
 
   private write(op: SettingsPathOpView): Promise<void> {
-    this.readGeneration += 1
     const generation = ++this.writeGeneration
     return this.enqueue(async () => {
-      const revision = this.getSnapshot().revision
+      const revision = this.pendingRevision ?? this.getSnapshot().revision
       let response: Awaited<ReturnType<SettingsFace['settings']['mutate']>>
       try {
         response = await this.api.settings.mutate({
@@ -124,25 +131,39 @@ export class SettingsScopeController<T> implements SettingsScope<T> {
           ...(revision === undefined ? {} : { expectedRevision: revision }),
         })
       } catch (_settingsWriteFailure) {
-        if (!this.disposed && generation === this.writeGeneration) await this.read(++this.readGeneration)
+        await this.recover(generation)
         return
       }
       if (!response.result.ok) {
-        if (!this.disposed && generation === this.writeGeneration) await this.read(++this.readGeneration)
+        await this.recover(generation)
         return
       }
-      this.accept(response.result.value, generation === this.writeGeneration)
+      if (this.disposed) return
+      if (generation === this.writeGeneration) {
+        this.pendingRevision = undefined
+        this.mirror.acceptView(response.result.value)
+      } else {
+        this.pendingRevision = response.result.value.revision
+      }
     })
   }
 
+  /** Reload Host state for the latest failed write; superseded failures leave recovery to it. */
+  private async recover(generation: number): Promise<void> {
+    if (this.disposed || generation !== this.writeGeneration) return
+    this.pendingRevision = undefined
+    await this.mirror.load()
+  }
+
   /**
-   * Stop queued operations and wait for the current wire call to settle.
+   * Stop queued operations, stop deriving, and wait for the current wire call
+   * to settle.
    * @returns settlement after the controller reaches quiescence.
    */
   async dispose(): Promise<void> {
     this.disposed = true
-    this.readGeneration += 1
     this.writeGeneration += 1
+    this.unsubscribe?.()
     await this.tail
   }
 
@@ -158,36 +179,25 @@ export class SettingsScopeController<T> implements SettingsScope<T> {
     return task
   }
 
-  private async read(generation: number): Promise<void> {
-    let response: Awaited<ReturnType<SettingsFace['settings']['describe']>>
-    try {
-      response = await this.api.settings.describe({})
-    } catch (_settingsReadFailure) {
-      return
-    }
-    if (!response.result.ok || this.disposed) return
-    const { namespaces, writable } = response.result.value
-    const view = namespaces.find(candidate => candidate.ns === this.spec.namespace)
-    const publish = generation === this.readGeneration
+  private derive(): void {
+    if (this.disposed) return
+    const mirrored = this.mirror.getSnapshot()
+    if (mirrored.view === undefined) return
+    const { writable } = mirrored.view
+    const view = mirrored.view.namespaces.find(candidate => candidate.ns === this.spec.namespace)
     if (view === undefined) {
-      if (publish) {
-        this.store.update((draft) => {
-          draft.status = 'unavailable'
-          draft.writable = writable
-        })
-      }
+      this.store.update((draft) => {
+        draft.status = 'unavailable'
+        draft.writable = writable
+      })
       return
     }
-    this.accept(view, publish, writable)
-  }
-
-  private accept(view: SettingsNamespaceView, publish: boolean, writable?: boolean): void {
-    const decoded = publish ? this.decode(view) : undefined
+    const decoded = this.decode(view)
     this.store.update((draft) => {
       draft.revision = view.revision
       draft.base = view.base
       draft.user = view.user
-      if (writable !== undefined) draft.writable = writable
+      draft.writable = writable
       if (decoded === undefined) return
       draft.status = 'ready'
       draft.value = decoded
@@ -225,20 +235,24 @@ declare module '@deepseek-ai/cordis' {
  * (`packages/client/tsdown.client.ts`).
  */
 export class SettingsScopeBinder extends Service {
+  private readonly mirror: SettingsDescribeMirror
+
   /**
    * @param ctx - the providing plugin's context.
+   * @param config - the shared describe mirror every bound scope derives from.
    */
-  constructor(ctx: Context) {
+  constructor(ctx: Context, config: { mirror: SettingsDescribeMirror }) {
     super(ctx, 'settingsScope')
+    this.mirror = config.mirror
   }
 
   /**
-   * Bind one namespace scope to settings and connection invalidations on the
-   * CALLER's plugin lifecycle — the service proxy binds `this.ctx` to the
-   * caller at call time, so the scope's disposer belongs to the calling fiber.
-   * Listeners exist before the initial background read starts, so activation
-   * never blocks on the settings transport. The caller injects `connection`
-   * for the transport and `remote` for the forwarded settings invalidation.
+   * Bind one namespace scope on the CALLER's plugin lifecycle — the service
+   * proxy binds `this.ctx` to the caller at call time, so the scope's disposer
+   * belongs to the calling fiber. The scope derives from the shared mirror
+   * (whose invalidation subscriptions live with the providing plugin), so
+   * binding adds no wire read of its own and activation never blocks on the
+   * settings transport.
    * @param spec - domain-owned namespace contract.
    * @returns the bound scope consumed by the domain's services and rows.
    */
@@ -248,20 +262,12 @@ export class SettingsScopeBinder extends Service {
     const controller = new SettingsScopeController<T>(
       connection.api,
       spec,
+      this.mirror,
       connection.isLoopback ? 'host' : 'memory',
     )
     ctx.effect(() => {
-      const refresh = (namespace?: string): void => {
-        if (namespace !== undefined && namespace !== spec.namespace) return
-        void controller.load()
-      }
-      const disposers = [
-        (ctx.get('remote') as Context['remote']).$on('settings/document-updated', refresh),
-        ctx.on('connection/reset', () => { refresh() }),
-      ]
-      void controller.load()
+      void this.mirror.ensure()
       return async () => {
-        for (const dispose of disposers) dispose()
         await controller.dispose()
       }
     }, `ui-settings: ${spec.namespace} settings scope`)

+ 36 - 9
packages/client/ui-settings/tests/plugin.client.spec.ts

@@ -1,29 +1,56 @@
 /**
  * The settings domain base plugin's own mounting behavior: it stands up
- * `ctx.settingsScope` for every feature that owns a preference row, and the
- * service retires with its fiber.
+ * `ctx.settingsScope` over one shared describe mirror, keeps that mirror
+ * fresh on settings-document and connection-reset invalidations, and retires
+ * both the service and the subscriptions with its fiber.
  */
 import { Context } from '@deepseek-ai/cordis'
-import { describe, expect, it } from 'vitest'
+import { describe, expect, it, vi } from 'vitest'
+import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
 import { apply, inject, SettingsScopeBinder } from '../src/client/index.ts'
 
-/** Boot the browser half over a bare root context; it injects nothing. */
+/** Boot the browser half over a fake loopback connection and test remote. */
 function bench() {
+  const describeCall = vi.fn().mockResolvedValue({
+    rpcId: 'plugin-bench' as never,
+    result: { ok: true, value: { writable: true, hasDocument: true, namespaces: [] } },
+  })
   const ctx = new Context()
-  return { ctx, fiber: ctx.plugin({ inject: [...inject], apply }) }
+  ctx.provide('connection', {
+    api: { settings: { describe: describeCall } },
+    isLoopback: true,
+  } as never)
+  new TestRemote(ctx)
+  return { ctx, describeCall, fiber: ctx.plugin({ inject: [...inject], apply }) }
 }
 
 describe('settings domain base plugin', () => {
-  it('mounts the scope service under settingsScope', async () => {
-    const { ctx, fiber } = bench()
+  it('mounts the scope service under settingsScope and reads once eagerly', async () => {
+    const { ctx, describeCall, fiber } = bench()
     await fiber.await()
     expect(ctx.get('settingsScope')).toBeInstanceOf(SettingsScopeBinder)
+    await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(1) })
+  })
+
+  it('refreshes the mirror on document commits and connection resets, once each', async () => {
+    const { ctx, describeCall, fiber } = bench()
+    await fiber.await()
+    await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(1) })
+    ctx.remote.$dispatch('settings/document-updated', ['ui-test', 0])
+    await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(2) })
+    ctx.emit('connection/reset')
+    await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(3) })
   })
 
-  it('fiber disposal retires the service', async () => {
-    const { ctx, fiber } = bench()
+  it('fiber disposal retires the service and its invalidation subscriptions', async () => {
+    const { ctx, describeCall, fiber } = bench()
     await fiber.await()
+    await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(1) })
     await fiber.dispose()
     expect(ctx.get('settingsScope')).toBeUndefined()
+    ctx.remote.$dispatch('settings/document-updated', ['ui-test', 0])
+    ctx.emit('connection/reset')
+    await Promise.resolve()
+    expect(describeCall).toHaveBeenCalledTimes(1)
   })
 })

+ 101 - 131
packages/client/ui-settings/tests/settings-scope.client.spec.ts

@@ -5,6 +5,7 @@ import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-api-re
 import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
 import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client'
 import { SettingsScopeController, SettingsScopeBinder } from '../src/client/settings-scope.ts'
+import { SettingsDescribeMirror } from '../src/client/settings-mirror.ts'
 
 interface UiTestSettings {
   preference: 'light' | 'dark' | 'system'
@@ -52,6 +53,17 @@ function deferred<T>() {
   return { promise, resolve, reject }
 }
 
+/** A host-mode mirror plus a controller derived from it, over one fake wire. */
+function derivedScope(
+  api: { describe?: ReturnType<typeof vi.fn>; mutate?: ReturnType<typeof vi.fn> },
+  spec: { namespace: string; decode?: (section: unknown) => UiTestSettings | undefined } = { namespace: 'ui-test' },
+) {
+  const wire = { settings: api } as never
+  const mirror = new SettingsDescribeMirror(wire)
+  const scope = new SettingsScopeController<UiTestSettings>(wire, spec, mirror)
+  return { mirror, scope }
+}
+
 /** Record each distinct published section, starting from the current one. */
 function trackValues(scope: SettingsScope<UiTestSettings>): Array<UiTestSettings | undefined> {
   const seen: Array<UiTestSettings | undefined> = [scope.getSnapshot().value]
@@ -63,16 +75,13 @@ function trackValues(scope: SettingsScope<UiTestSettings>): Array<UiTestSettings
 }
 
 describe('SettingsScopeController', () => {
-  it('starts loading and publishes a schema-valid section with revision and writability', async () => {
+  it('starts loading and derives a schema-valid section with revision and writability', async () => {
     const describeCall = vi.fn().mockResolvedValueOnce(described({ preference: 'dark' }, 3))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { mirror, scope } = derivedScope({ describe: describeCall })
     expect(scope.getSnapshot()).toEqual({
       status: 'loading', value: undefined, revision: undefined, writable: false, mode: 'host',
     })
-    await scope.load()
+    await mirror.load()
     expect(scope.getSnapshot()).toEqual({
       status: 'ready', value: { preference: 'dark' }, revision: 3, writable: true, mode: 'host',
     })
@@ -87,12 +96,9 @@ describe('SettingsScopeController', () => {
       .mockResolvedValueOnce(described(['queue'], 7))
       .mockResolvedValueOnce(rejected())
       .mockRejectedValueOnce(new Error('offline'))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { mirror, scope } = derivedScope({ describe: describeCall })
     const good = trackValues(scope)
-    for (let i = 0; i < 7; i++) await scope.load()
+    for (let i = 0; i < 7; i++) await mirror.load()
     expect(scope.getSnapshot()).toMatchObject({
       status: 'ready', value: { preference: 'dark' }, revision: 7,
     })
@@ -103,45 +109,22 @@ describe('SettingsScopeController', () => {
     const broken = { ...view({ preference: 'dark' }, 2), schema: null }
     const describeCall = vi.fn()
       .mockResolvedValueOnce(ok({ writable: true, hasDocument: true, namespaces: [broken] }))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      { namespace: 'ui-test' },
-    )
-    await scope.load()
+    const { mirror, scope } = derivedScope({ describe: describeCall })
+    await mirror.load()
     expect(scope.getSnapshot()).toMatchObject({ status: 'loading', value: undefined, revision: 2 })
   })
 
-  it('suppresses a superseded read of an unexposed namespace', async () => {
-    const describeCall = vi.fn()
-      .mockResolvedValueOnce(ok({ writable: true, hasDocument: true, namespaces: [] }))
-      .mockResolvedValueOnce(described({ preference: 'dark' }, 1))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      { namespace: 'ui-test' },
-    )
-    const statuses: string[] = []
-    scope.subscribe(() => { statuses.push(scope.getSnapshot().status) })
-    const stale = scope.load()
-    const fresh = scope.load()
-    await Promise.all([stale, fresh])
-    expect(statuses).not.toContain('unavailable')
-    expect(scope.getSnapshot()).toMatchObject({ status: 'ready', value: { preference: 'dark' } })
-  })
-
   it('reports an unexposed namespace as unavailable and recovers when it reappears', async () => {
     const describeCall = vi.fn()
       .mockResolvedValueOnce(described({ preference: 'light' }, 1))
       .mockResolvedValueOnce(ok({ writable: true, hasDocument: true, namespaces: [] }))
       .mockResolvedValueOnce(described({ preference: 'system' }, 2))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      { namespace: 'ui-test' },
-    )
-    await scope.load()
+    const { mirror, scope } = derivedScope({ describe: describeCall })
+    await mirror.load()
     expect(scope.getSnapshot().status).toBe('ready')
-    await scope.load()
+    await mirror.load()
     expect(scope.getSnapshot()).toMatchObject({ status: 'unavailable', value: { preference: 'light' } })
-    await scope.load()
+    await mirror.load()
     expect(scope.getSnapshot()).toMatchObject({ status: 'ready', value: { preference: 'system' }, revision: 2 })
   })
 
@@ -149,18 +132,15 @@ describe('SettingsScopeController', () => {
     const describeCall = vi.fn()
       .mockResolvedValueOnce(described({ preference: 'light' }, 1))
       .mockResolvedValueOnce(described({ preference: 'dark' }, 2))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      {
-        namespace: 'ui-test',
-        decode: section => (section as UiTestSettings).preference === 'dark'
-          ? section as UiTestSettings
-          : undefined,
-      },
-    )
-    await scope.load()
+    const { mirror, scope } = derivedScope({ describe: describeCall }, {
+      namespace: 'ui-test',
+      decode: section => (section as UiTestSettings).preference === 'dark'
+        ? section as UiTestSettings
+        : undefined,
+    })
+    await mirror.load()
     expect(scope.getSnapshot()).toMatchObject({ status: 'loading', value: undefined, revision: 1 })
-    await scope.load()
+    await mirror.load()
     expect(scope.getSnapshot()).toMatchObject({ status: 'ready', value: { preference: 'dark' }, revision: 2 })
   })
 
@@ -170,12 +150,9 @@ describe('SettingsScopeController', () => {
     const mutate = vi.fn()
       .mockReturnValueOnce(first.promise)
       .mockResolvedValueOnce(ok(view({ preference: 'light' }, 6)))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall, mutate } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { mirror, scope } = derivedScope({ describe: describeCall, mutate })
     const published = trackValues(scope)
-    await scope.load()
+    await mirror.load()
     const dark = scope.set('preference', 'dark')
     const light = scope.set('preference', 'light')
     await vi.waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
@@ -195,6 +172,19 @@ describe('SettingsScopeController', () => {
     })
   })
 
+  it('folds the latest write answer into the mirror so a sibling scope sees it', async () => {
+    const describeCall = vi.fn().mockResolvedValueOnce(described({ preference: 'system' }, 4))
+    const mutate = vi.fn().mockResolvedValueOnce(ok(view({ preference: 'dark' }, 5)))
+    const wire = { settings: { describe: describeCall, mutate } } as never
+    const mirror = new SettingsDescribeMirror(wire)
+    const writer = new SettingsScopeController<UiTestSettings>(wire, { namespace: 'ui-test' }, mirror)
+    const sibling = new SettingsScopeController<UiTestSettings>(wire, { namespace: 'ui-test' }, mirror)
+    await mirror.load()
+    await writer.set('preference', 'dark')
+    expect(describeCall).toHaveBeenCalledTimes(1)
+    expect(sibling.getSnapshot()).toMatchObject({ value: { preference: 'dark' }, revision: 5 })
+  })
+
   it('recovers the latest rejected or thrown write from Host state', async () => {
     const describeCall = vi.fn()
       .mockResolvedValueOnce(described({ preference: 'system' }, 2))
@@ -202,52 +192,45 @@ describe('SettingsScopeController', () => {
     const mutate = vi.fn()
       .mockResolvedValueOnce(rejected())
       .mockRejectedValueOnce(new Error('offline'))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall, mutate } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { mirror, scope } = derivedScope({ describe: describeCall, mutate })
     const published = trackValues(scope)
+    await mirror.load()
     await scope.set('preference', 'dark')
     await scope.set('preference', 'system')
     expect(published.map(section => section?.preference)).toEqual([undefined, 'system', 'light'])
   })
 
   it('does not recover superseded rejected or thrown writes', async () => {
-    const describeCall = vi.fn()
+    const describeCall = vi.fn().mockResolvedValueOnce(described({ preference: 'system' }, 2))
     const mutate = vi.fn()
       .mockResolvedValueOnce(rejected())
       .mockRejectedValueOnce(new Error('offline'))
       .mockResolvedValueOnce(ok(view({ preference: 'light' }, 3)))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall, mutate } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { mirror, scope } = derivedScope({ describe: describeCall, mutate })
     const published = trackValues(scope)
+    await mirror.load()
     await Promise.all([
       scope.set('preference', 'dark'),
       scope.set('preference', 'system'),
       scope.set('preference', 'light'),
     ])
-    expect(describeCall).not.toHaveBeenCalled()
-    expect(published.map(section => section?.preference)).toEqual([undefined, 'light'])
+    expect(describeCall).toHaveBeenCalledTimes(1)
+    expect(published.map(section => section?.preference)).toEqual([undefined, 'system', 'light'])
   })
 
   it('keeps the write queue usable when a subscriber throws', async () => {
     const describeCall = vi.fn()
       .mockResolvedValueOnce(described({ preference: 'dark' }, 1))
       .mockResolvedValueOnce(described({ preference: 'light' }, 2))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { mirror, scope } = derivedScope({ describe: describeCall })
     let thrown = false
     scope.subscribe(() => {
       if (thrown) return
       thrown = true
       throw new Error('subscriber failed')
     })
-    await expect(scope.load()).rejects.toThrow('subscriber failed')
-    await expect(scope.load()).resolves.toBeUndefined()
+    await expect(mirror.load()).rejects.toThrow('subscriber failed')
+    await expect(mirror.load()).resolves.toBeUndefined()
     expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'light' }, revision: 2 })
   })
 
@@ -255,10 +238,7 @@ describe('SettingsScopeController', () => {
     const first = deferred<RpcResponse<SettingsNamespaceView>>()
     const mutate = vi.fn().mockReturnValue(first.promise)
     const describeCall = vi.fn()
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall, mutate } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { scope } = derivedScope({ describe: describeCall, mutate })
     const published = trackValues(scope)
     const dark = scope.set('preference', 'dark')
     await vi.waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
@@ -270,24 +250,34 @@ describe('SettingsScopeController', () => {
     first.resolve(ok(view({ preference: 'dark' }, 1)))
     await Promise.all([dark, light, stop])
     await scope.set('preference', 'system')
-    await scope.load()
     expect(mutate).toHaveBeenCalledOnce()
     expect(describeCall).not.toHaveBeenCalled()
     expect(published).toEqual([undefined])
   })
 
+  it('stops deriving from the mirror after dispose', async () => {
+    const describeCall = vi.fn()
+      .mockResolvedValueOnce(described({ preference: 'dark' }, 1))
+      .mockResolvedValueOnce(described({ preference: 'light' }, 2))
+    const { mirror, scope } = derivedScope({ describe: describeCall })
+    await mirror.load()
+    expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'dark' } })
+    await scope.dispose()
+    await mirror.load()
+    expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'dark' }, revision: 1 })
+  })
+
   it('keeps a remote browser in memory mode without Host calls', async () => {
     const describeCall = vi.fn()
     const mutate = vi.fn()
+    const wire = { settings: { describe: describeCall, mutate } } as never
+    const mirror = new SettingsDescribeMirror(wire, 'memory')
     const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall, mutate } } as never,
-      { namespace: 'ui-test' },
-      'memory',
-    )
+      wire, { namespace: 'ui-test' }, mirror, 'memory')
     expect(scope.getSnapshot()).toEqual({
       status: 'unavailable', value: undefined, revision: undefined, writable: false, mode: 'memory',
     })
-    await scope.load()
+    await mirror.load()
     await scope.set('preference', 'dark')
     await scope.dispose()
     expect(describeCall).not.toHaveBeenCalled()
@@ -302,12 +292,9 @@ describe('SettingsScopeController', () => {
     }
     const describeCall = vi.fn()
       .mockResolvedValueOnce(ok({ writable: true, hasDocument: true, namespaces: [layered] }))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { mirror, scope } = derivedScope({ describe: describeCall })
 
-    await scope.load()
+    await mirror.load()
 
     expect(scope.getSnapshot()).toMatchObject({
       value: { preference: 'dark' },
@@ -320,12 +307,9 @@ describe('SettingsScopeController', () => {
     const inherited: SettingsNamespaceView = { ...view({ preference: 'system' }, 1), base: { preference: 'system' } }
     const describeCall = vi.fn()
       .mockResolvedValueOnce(ok({ writable: true, hasDocument: true, namespaces: [inherited] }))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall } } as never,
-      { namespace: 'ui-test' },
-    )
+    const { mirror, scope } = derivedScope({ describe: describeCall })
 
-    await scope.load()
+    await mirror.load()
 
     expect(scope.getSnapshot().user).toBeUndefined()
   })
@@ -333,11 +317,8 @@ describe('SettingsScopeController', () => {
   it('clears one field through an unset op fenced by the held revision', async () => {
     const mutate = vi.fn().mockResolvedValueOnce(ok(view({ preference: 'system' }, 4)))
     const describeCall = vi.fn().mockResolvedValueOnce(described({ preference: 'dark' }, 3))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall, mutate } } as never,
-      { namespace: 'ui-test' },
-    )
-    await scope.load()
+    const { mirror, scope } = derivedScope({ describe: describeCall, mutate })
+    await mirror.load()
 
     await scope.unset('preference')
 
@@ -354,64 +335,53 @@ describe('SettingsScopeController', () => {
     const describeCall = vi.fn()
       .mockResolvedValueOnce(described({ preference: 'dark' }, 3))
       .mockResolvedValueOnce(described({ preference: 'light' }, 5))
-    const scope = new SettingsScopeController<UiTestSettings>(
-      { settings: { describe: describeCall, mutate } } as never,
-      { namespace: 'ui-test' },
-    )
-    await scope.load()
+    const { mirror, scope } = derivedScope({ describe: describeCall, mutate })
+    await mirror.load()
 
     await scope.unset('preference')
 
     expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'light' }, revision: 5 })
   })
 })
+
 describe('SettingsScopeBinder.bind', () => {
-  it('subscribes before the initial read and converges to the latest queued invalidation', async () => {
-    const initial = deferred<ReturnType<typeof described>>()
-    const describeCall = vi.fn()
-      .mockReturnValueOnce(initial.promise)
-      .mockResolvedValueOnce(described({ preference: 'light' }, 2))
-      .mockResolvedValueOnce(described({ preference: 'system' }, 3))
+  it('shares one mirror read across bound scopes and disposes each with its fiber', async () => {
+    const describeCall = vi.fn().mockResolvedValue(described({ preference: 'dark' }, 1))
+    const wire = { settings: { describe: describeCall } }
+    const mirror = new SettingsDescribeMirror(wire as never)
     const ctx = new Context()
-    ctx.provide('connection', {
-      api: { settings: { describe: describeCall } },
-      isLoopback: true,
-    } as never)
-    let scope!: SettingsScope<UiTestSettings>
+    ctx.provide('connection', { api: wire, isLoopback: true } as never)
+    let theme!: SettingsScope<UiTestSettings>
+    let locale!: SettingsScope<UiTestSettings>
     new TestRemote(ctx)
-    await ctx.plugin(SettingsScopeBinder).await()
+    await ctx.plugin(SettingsScopeBinder, { mirror }).await()
     const fiber = ctx.plugin({
       inject: ['connection', 'remote', 'settingsScope'],
       apply: (plugin: Context) => {
-        scope = plugin.settingsScope.bind<UiTestSettings>({ namespace: 'ui-test' })
+        theme = plugin.settingsScope.bind<UiTestSettings>({ namespace: 'ui-test' })
+        locale = plugin.settingsScope.bind<UiTestSettings>({ namespace: 'ui-test' })
       },
     })
     await fiber.await()
-    await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledOnce() })
-    ctx.remote.$dispatch('settings/document-updated', ['unrelated', 0])
-    ctx.remote.$dispatch('settings/document-updated', ['ui-test', 0])
-    ctx.emit('connection/reset')
-    initial.resolve(described({ preference: 'dark' }, 1))
-    await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(3) })
     await vi.waitFor(() => {
-      expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'system' }, revision: 3 })
+      expect(theme.getSnapshot()).toMatchObject({ status: 'ready', value: { preference: 'dark' } })
+      expect(locale.getSnapshot()).toMatchObject({ status: 'ready', value: { preference: 'dark' } })
     })
+    expect(describeCall).toHaveBeenCalledTimes(1)
     await fiber.dispose()
-    ctx.remote.$dispatch('settings/document-updated', ['ui-test', 0])
-    await Promise.resolve()
-    expect(describeCall).toHaveBeenCalledTimes(3)
+    await mirror.load()
+    expect(theme.getSnapshot()).toMatchObject({ revision: 1 })
   })
 
   it('binds a remote browser in memory mode without starting a settings read', async () => {
     const describeCall = vi.fn()
+    const wire = { settings: { describe: describeCall } }
+    const mirror = new SettingsDescribeMirror(wire as never, 'memory')
     const ctx = new Context()
-    ctx.provide('connection', {
-      api: { settings: { describe: describeCall } },
-      isLoopback: false,
-    } as never)
+    ctx.provide('connection', { api: wire, isLoopback: false } as never)
     let scope!: SettingsScope<UiTestSettings>
     new TestRemote(ctx)
-    await ctx.plugin(SettingsScopeBinder).await()
+    await ctx.plugin(SettingsScopeBinder, { mirror }).await()
     const fiber = ctx.plugin({
       inject: ['connection', 'remote', 'settingsScope'],
       apply: (plugin: Context) => {