refactor(ui-settings-general): document action derives from the mirror

This commit is contained in:
Yichen Jiang committed 2026-08-17 17:33:02 +08:00
1 parent 608fb895a6
commit 8ea21a166c
6 files changed
+100 -81

No files matched your search

@@ -25,7 +25,7 @@ import { CloseLabel, HeaderContent, TriggerContent } from './chrome.tsx'
import { GeneralSection } from './GeneralSection.tsx'
import { SettingsDocumentAction } from './SettingsDocumentAction.tsx'
import type { SettingsDocumentActionInjected } from './SettingsDocumentAction.tsx'
import { refreshDocumentIfLoaded, SettingsDocumentStore } from './settings-document-store.ts'
import { SettingsDocumentStore } from './settings-document-store.ts'
import { en, zh, type SettingsKey } from './locales.ts'
export type {
@@ -54,7 +54,7 @@ const NS = 'settings'
* ui-settings' apply, whose activation order relative to this one is NOT
* constrained; registrations depend on their slots through `slots.inject()`.
*/
export const inject = ['slots', 'locale', 'connection']
export const inject = ['slots', 'locale', 'connection', 'settingsScope']
/**
* Register the `settings` dictionaries, the chrome content, and the General
@@ -69,8 +69,10 @@ export function apply(ctx: ClientContext): void {
// locale/change re-registration wiring.
const t = ctx.locale.bind(NS)
const connection = ctx.get('connection') as ConnectionHandle
// The action follows the shared describe mirror, whose owning plugin
// already refreshes it on document commits and reconnects.
const documentController = connection.isLoopback
? new SettingsDocumentStore(connection.api)
? new SettingsDocumentStore(connection.api, ctx.settingsScope.describe())
: undefined
const documentInjected = documentController === undefined
? undefined
@@ -78,9 +80,7 @@ export function apply(ctx: ClientContext): void {
const useSnapshot = bindSnapshotSelector(documentController.store)
return (): SettingsDocumentActionInjected => ({ controller: documentController, useSnapshot })
})()
ctx.effect(() => ctx.on('connection/reset', () => {
refreshDocumentIfLoaded(documentController)
}), 'ui-settings-general: metadata invalidations')
ctx.effect(() => () => { documentController?.dispose() }, 'ui-settings-general: document action directory')
// The settings shell: this package occupies the sidebar-owned hole and
// declares the settings slots. Ledger → nav-row projection as an observable
// source (uSES contract: getSnapshot returns the cached rows until the
@@ -2,6 +2,7 @@
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
import { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
/** Browser state of the Host-owned settings document. */
export interface SettingsDocumentState {
@@ -17,51 +18,37 @@ function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
/** Loads local-document availability and invokes the pathless Host-owned open operation. */
/** Derives local-document availability from the shared mirror and invokes the pathless Host-owned open operation. */
export class SettingsDocumentStore {
/** uSES-safe state source shared by the registered header action. */
readonly store: SnapshotStore<SettingsDocumentState> = createSnapshotStore({
status: 'idle', opening: false, error: null,
})
private generation = 0
private following: (() => void) | undefined
/**
* @param api - loopback settings wire face that reports and opens the provider document.
* @param api - loopback settings wire face that opens the provider document.
* @param describeFace - the shared mirror's read-only face (`hasDocument` source).
*/
constructor(private readonly api: Pick<IApiClient, 'settings'>) {}
constructor(
private readonly api: Pick<IApiClient, 'settings'>,
private readonly describeFace: SettingsDescribeFace,
) {}
/**
* Load whether the current provider owns a local document.
* @returns after the latest metadata response updates the store.
* Begin following the mirror (idempotent) and reflect whether the current
* provider owns a local document.
* @returns settlement once the snapshot reflects the mirror.
*/
async load(): Promise<void> {
const generation = ++this.generation
this.following ??= this.describeFace.subscribe(() => { this.derive() })
this.store.update((state) => {
state.status = 'loading'
state.error = null
})
try {
const { result } = await this.api.settings.describe({})
if (generation !== this.generation) return
if (!result.ok) {
this.store.update((state) => {
state.status = 'unavailable'
state.error = result.error.message
})
return
}
this.store.update((state) => {
state.status = result.value.hasDocument ? 'ready' : 'unavailable'
state.error = null
})
} catch (error) {
if (generation !== this.generation) return
this.store.update((state) => {
state.status = 'unavailable'
state.error = messageOf(error)
})
}
await this.describeFace.ensure()
this.derive()
}
/**
@@ -84,13 +71,30 @@ export class SettingsDocumentStore {
this.store.update((state) => { state.opening = false })
}
}
}
/**
* Refresh document availability after reconnect only when a surface has already requested it.
* @param controller - optional loopback document state owner.
*/
export function refreshDocumentIfLoaded(controller: SettingsDocumentStore | undefined): void {
if (controller === undefined || controller.store.getSnapshot().status === 'idle') return
void controller.load()
/** Stop following the mirror. */
dispose(): void {
this.following?.()
this.following = undefined
}
private derive(): void {
const mirrored = this.describeFace.getSnapshot()
if (mirrored.view === undefined) {
// A held failure with no answer means the document cannot be located;
// without one the read is still in flight and loading stands.
if (mirrored.error !== null) {
this.store.update((state) => {
state.status = 'unavailable'
state.error = mirrored.error
})
}
return
}
const { hasDocument } = mirrored.view
this.store.update((state) => {
state.status = hasDocument ? 'ready' : 'unavailable'
state.error = null
})
}
}
@@ -4,7 +4,8 @@ import { describe, expect, it, vi } from 'vitest'
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings-general/client'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import { GeneralSection } from '../src/client/GeneralSection.tsx'
@@ -48,6 +49,8 @@ async function bench(isLoopback = true) {
api: { settings: { describe: settingsDescribe, openDocument: settingsOpenDocument } },
isLoopback,
} as never)
new TestRemote(ctx)
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, settingsDescribe, settingsOpenDocument }
}
@@ -75,7 +78,7 @@ function generalEntry(slots: SlotRegistry) {
describe('ui-settings-general apply', () => {
it('declares the services it uses', () => {
expect(inject).toEqual(['slots', 'locale', 'connection'])
expect(inject).toEqual(['slots', 'locale', 'connection', 'settingsScope'])
})
it('fills all five seats for declarations before or after apply', async () => {
@@ -149,16 +152,17 @@ describe('ui-settings-general apply', () => {
expect(resolveSlotLabel(generalEntry(b.slots)!.options.label)).toBe('通用设置')
})
it('refreshes loaded document availability on reconnect without reading it eagerly', async () => {
it('reads availability from the shared mirror and follows its reconnect refresh', async () => {
const b = await bench()
declare(b.slots)
await b.ctx.plugin({ inject: [...inject], apply }).await()
const entry = b.slots.entries('settings.action')[0]!
const { controller } = (entry.inject as unknown as () => SettingsDocumentActionInjected)()
b.ctx.emit('connection/reset')
expect(b.settingsDescribe).not.toHaveBeenCalled()
// The mirror read once at its own boot; the action's load adds no read.
await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledOnce() })
await controller.load()
expect(b.settingsDescribe).toHaveBeenCalledOnce()
expect(controller.store.getSnapshot().status).toBe('ready')
b.ctx.emit('connection/reset')
await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(2) })
})
@@ -7,7 +7,14 @@ import { GeneralSection } from '../src/client/GeneralSection.tsx'
import { CloseLabel, HeaderContent, TriggerContent } from '../src/client/chrome.tsx'
import type { TriggerContentProps } from '../src/client/chrome.tsx'
import { SettingsDocumentAction } from '../src/client/SettingsDocumentAction.tsx'
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/client'
import { SettingsDocumentStore } from '../src/client/settings-document-store.ts'
/** Store over a real mirror derived from the same fake wire. */
function derivedDocumentStore(api: object) {
const wire = api as never
return new SettingsDocumentStore(wire, new SettingsDescribeMirror(wire))
}
import { en } from '../src/client/locales.ts'
afterEach(cleanup)
@@ -64,7 +71,7 @@ describe('SettingsDocumentAction', () => {
rpcId: 'document-open' as never,
result: { ok: true as const, value: { opened: true as const } },
}))
const controller = new SettingsDocumentStore({
const controller = derivedDocumentStore({
settings: {
describe: vi.fn(() => Promise.resolve({
rpcId: 'document-action' as never,
@@ -87,7 +94,7 @@ describe('SettingsDocumentAction', () => {
await waitFor(() => { expect(openDocument).toHaveBeenCalledWith({}) })
})
it('stays absent without a document and retries availability after remount', async () => {
it('stays absent without a document and follows a mirror refresh to available', async () => {
const describe = vi.fn()
.mockResolvedValueOnce({
rpcId: 'document-action-absent' as never,
@@ -97,12 +104,9 @@ describe('SettingsDocumentAction', () => {
rpcId: 'document-action-ready' as never,
result: { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [] } },
})
const controller = new SettingsDocumentStore({
settings: {
describe,
openDocument: vi.fn(),
},
} as never)
const wire = { settings: { describe, openDocument: vi.fn() } } as never
const mirror = new SettingsDescribeMirror(wire)
const controller = new SettingsDocumentStore(wire, mirror)
const first = render(<SettingsDocumentAction
{...kit}
t={t}
@@ -118,12 +122,17 @@ describe('SettingsDocumentAction', () => {
controller={controller}
useSnapshot={bindSnapshotSelector(controller.store)}
/>)
// A remount alone re-reads nothing; availability moves with the mirror's
// own refresh (a document commit or reconnect in production).
await waitFor(() => { expect(controller.store.getSnapshot().status).toBe('unavailable') })
expect(describe).toHaveBeenCalledTimes(1)
await mirror.load()
expect(await screen.findByRole('button', { name: 'Open configuration file' })).toBeTruthy()
expect(describe).toHaveBeenCalledTimes(2)
})
it('keeps the action available and reports a native-open failure', async () => {
const controller = new SettingsDocumentStore({
const controller = derivedDocumentStore({
settings: {
describe: vi.fn(() => Promise.resolve({
rpcId: 'document-action' as never,
@@ -1,7 +1,14 @@
import { describe, expect, it, vi } from 'vitest'
import type { RpcResponse } from '@deepseek-ai/dsh-api-remotes/client'
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/client'
import { SettingsDocumentStore } from '../src/client/settings-document-store.ts'
/** Store over a real mirror derived from the same fake wire. */
function derivedDocumentStore(api: object) {
const wire = api as never
return new SettingsDocumentStore(wire, new SettingsDescribeMirror(wire))
}
function response(hasDocument = false): RpcResponse<{
writable: boolean
hasDocument: boolean
@@ -34,7 +41,7 @@ describe('SettingsDocumentStore', () => {
it('loads provider metadata and asks the settings domain to open its document', async () => {
const describe = vi.fn(() => Promise.resolve(response(true)))
const openDocument = vi.fn(() => Promise.resolve(opened()))
const controller = new SettingsDocumentStore({ settings: { describe, openDocument } } as never)
const controller = derivedDocumentStore({ settings: { describe, openDocument } } as never)
await controller.load()
expect(controller.store.getSnapshot()).toEqual({
status: 'ready', opening: false, error: null,
@@ -45,7 +52,7 @@ describe('SettingsDocumentStore', () => {
it('marks absent or failed metadata unavailable without opening anything', async () => {
const openDocument = vi.fn(() => Promise.resolve(opened()))
const absent = new SettingsDocumentStore({
const absent = derivedDocumentStore({
settings: { describe: () => Promise.resolve(response()), openDocument },
} as never)
await absent.load()
@@ -53,13 +60,13 @@ describe('SettingsDocumentStore', () => {
expect(absent.store.getSnapshot().status).toBe('unavailable')
expect(openDocument).not.toHaveBeenCalled()
const failed = new SettingsDocumentStore({
const failed = derivedDocumentStore({
settings: { describe: () => Promise.reject(new Error('offline')), openDocument },
} as never)
await failed.load()
expect(failed.store.getSnapshot()).toMatchObject({ status: 'unavailable', error: 'offline' })
const rejected = new SettingsDocumentStore({
const rejected = derivedDocumentStore({
settings: { describe: () => Promise.resolve(describeFailed('provider failed')), openDocument },
} as never)
await rejected.load()
@@ -71,7 +78,7 @@ describe('SettingsDocumentStore', () => {
it('collapses concurrent open gestures and recovers after a failure', async () => {
let resolveOpen!: (response: RpcResponse<{ opened: true }>) => void
const openDocument = vi.fn(() => new Promise<RpcResponse<{ opened: true }>>((resolve) => { resolveOpen = resolve }))
const controller = new SettingsDocumentStore({
const controller = derivedDocumentStore({
settings: { describe: () => Promise.resolve(response(true)), openDocument },
} as never)
await controller.load()
@@ -88,23 +95,15 @@ describe('SettingsDocumentStore', () => {
})
})
it('ignores stale metadata completions and reports non-Error native failures', async () => {
let resolveFirst!: (value: ReturnType<typeof response>) => void
const first = new Promise<ReturnType<typeof response>>((resolve) => { resolveFirst = resolve })
const describe = vi.fn()
.mockReturnValueOnce(first)
.mockResolvedValueOnce(response(true))
it('reports non-Error native failures and recovers availability via a mirror refresh', async () => {
let rejectOpen!: (reason?: unknown) => void
const controller = new SettingsDocumentStore({
const controller = derivedDocumentStore({
settings: {
describe,
describe: vi.fn(() => Promise.resolve(response(true))),
openDocument: () => new Promise((_, reject) => { rejectOpen = reject }),
},
} as never)
const stale = controller.load()
await controller.load()
resolveFirst(response())
await stale
expect(controller.store.getSnapshot().status).toBe('ready')
const opening = controller.open()
rejectOpen('native unavailable')
@@ -113,20 +112,21 @@ describe('SettingsDocumentStore', () => {
status: 'ready', opening: false, error: 'native unavailable',
})
let rejectFirst!: (error: Error) => void
const rejectedFirst = new Promise<ReturnType<typeof response>>((_, reject) => { rejectFirst = reject })
const caught = new SettingsDocumentStore({
// A first read that failed leaves the action unavailable with the miss
// recorded; the mirror's next refresh (a commit or reconnect) recovers it.
const wire = {
settings: {
describe: vi.fn()
.mockReturnValueOnce(rejectedFirst)
.mockRejectedValueOnce(new Error('offline'))
.mockResolvedValueOnce(response(true)),
openDocument: vi.fn(),
},
} as never)
const staleRejection = caught.load()
} as never
const mirror = new SettingsDescribeMirror(wire)
const caught = new SettingsDocumentStore(wire, mirror)
await caught.load()
rejectFirst(new Error('stale offline'))
await staleRejection
expect(caught.store.getSnapshot()).toMatchObject({ status: 'unavailable', error: 'offline' })
await mirror.load()
expect(caught.store.getSnapshot()).toMatchObject({ status: 'ready', error: null })
})
})
@@ -2,6 +2,7 @@
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, vi } from 'vitest'
import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client'
import { apply, inject } from '../src/client/index.ts'
import type { SettingsRootInjected } from '../src/client/shell-contract.ts'
import { SettingsRoot } from '../src/client/SettingsRoot.tsx'
@@ -22,6 +23,7 @@ async function bench() {
isLoopback: false,
} as never)
ctx.provide('remote', { $on: () => () => {} } as never)
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
return { ctx, slots: ctx.get('slots') as SlotRegistry }
}
@@ -49,7 +51,7 @@ const CHILD_SPECS = {
describe('ui-settings apply', () => {
it('declares only the slot registry (a pure composition face, no locale)', () => {
expect(inject).toEqual(['slots', 'locale', 'connection'])
expect(inject).toEqual(['slots', 'locale', 'connection', 'settingsScope'])
})
it('registers the shell and declares every child slot, before or after the declaration', async () => {