refactor(ui-permission-presets): permission row derives from the describe mirror
This commit is contained in:
1 parent
232e4beeae
commit
e3f484f62f
5 files changed
+167
-175
No files matched your search
@@ -33,9 +33,7 @@ import {
|
||||
import {
|
||||
displayPermissionPreset, FULL_ACCESS_PRESET,
|
||||
} from './presentation.ts'
|
||||
import {
|
||||
PERMISSION_SETTINGS_NS, PermissionPresetSettingsController, refreshPermissionIfLoaded,
|
||||
} from './settings-store.ts'
|
||||
import { PermissionPresetSettingsController } from './settings-store.ts'
|
||||
|
||||
export type { PermissionRowInjected, PermissionRowProps } from './PermissionRow.tsx'
|
||||
export type {
|
||||
@@ -43,7 +41,7 @@ export type {
|
||||
} from './settings-store.ts'
|
||||
|
||||
/** Required services (cordis fiber inject). */
|
||||
export const inject = ['commandUi', 'sessions', 'slots', 'locale', 'connection', 'remote']
|
||||
export const inject = ['commandUi', 'sessions', 'slots', 'locale', 'connection', 'remote', 'settingsScope']
|
||||
|
||||
const ACCESS_NS = 'permission.access'
|
||||
|
||||
@@ -113,7 +111,10 @@ export function apply(ctx: ClientContext): void {
|
||||
ctx.effect(() => ctx.locale.register('settings.permission', { zh, en }), 'ui-permission: settings row dictionaries')
|
||||
|
||||
const connection = ctx.get('connection') as ConnectionHandle
|
||||
const controller = new PermissionPresetSettingsController(connection.api)
|
||||
// The row follows the shared describe mirror, whose owning plugin already
|
||||
// refreshes it on document commits and reconnects.
|
||||
const controller = new PermissionPresetSettingsController(
|
||||
ctx.settingsScope.describe(), connection.api)
|
||||
const load = (): Promise<void> => controller.load()
|
||||
const select = (preset: string): Promise<void> => controller.select(preset)
|
||||
const injected = (): PermissionRowInjected => ({
|
||||
@@ -122,20 +123,7 @@ export function apply(ctx: ClientContext): void {
|
||||
select,
|
||||
})
|
||||
|
||||
ctx.effect(() => {
|
||||
const refresh = (): void => { refreshPermissionIfLoaded(controller) }
|
||||
const disposers = [
|
||||
ctx.remote.$on('settings/document-updated', (ns) => {
|
||||
if (ns !== PERMISSION_SETTINGS_NS) return
|
||||
refresh()
|
||||
}),
|
||||
ctx.on('connection/reset', () => { refresh() }),
|
||||
]
|
||||
return () => {
|
||||
controller.dispose()
|
||||
for (const dispose of disposers) dispose()
|
||||
}
|
||||
}, 'ui-permission: settings invalidations')
|
||||
ctx.effect(() => () => { controller.dispose() }, 'ui-permission: settings row directory')
|
||||
|
||||
ctx.slots.inject('settings.general.item', () => ctx.slots.register({
|
||||
name: 'settings.general.item',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* Permission default-settings controller. The host descriptor supplies the
|
||||
* current value and the dynamic preset enum; writes target only
|
||||
* `defaultPreset` and carry the descriptor revision.
|
||||
* Permission default-settings controller. The permission descriptor comes
|
||||
* from the shared describe mirror (the dynamic preset enum lives in the
|
||||
* namespace schema, which per-namespace scopes do not carry); writes target
|
||||
* only `defaultPreset`, carry the descriptor revision, and fold their answer
|
||||
* back into the mirror.
|
||||
*/
|
||||
|
||||
import type {
|
||||
@@ -10,6 +12,7 @@ import type {
|
||||
import {
|
||||
createSnapshotStore, type SnapshotStore,
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import {
|
||||
nodeAtPath, rehydrateSchema, type SchemaNode,
|
||||
} from '@deepseek-ai/dsh-client-schema-form'
|
||||
@@ -75,7 +78,7 @@ export function permissionDefaultOf(view: SettingsNamespaceView): {
|
||||
return { currentValue: value, options }
|
||||
}
|
||||
|
||||
/** Controller joining Settings reads, writes, and pushed invalidations. */
|
||||
/** Controller deriving the row from the shared mirror and writing the default through it. */
|
||||
export class PermissionPresetSettingsController {
|
||||
/** Row snapshot consumed through a bound selector hook. */
|
||||
readonly store: SnapshotStore<PermissionSettingsState> = createSnapshotStore({
|
||||
@@ -87,42 +90,32 @@ export class PermissionPresetSettingsController {
|
||||
revision: 0,
|
||||
})
|
||||
|
||||
private generation = 0
|
||||
private view: SettingsNamespaceView | undefined
|
||||
|
||||
/** @param api - Settings wire face. */
|
||||
constructor(private readonly api: Pick<IApiClient, 'settings'>) {}
|
||||
private following: (() => void) | undefined
|
||||
private saving = false
|
||||
private disposed = false
|
||||
|
||||
/**
|
||||
* Refresh the permission descriptor. Latest request wins.
|
||||
* @returns nothing; {@link store} carries success or failure.
|
||||
* @param describeFace - the shared mirror's read-only face (descriptor and schema source).
|
||||
* @param api - settings wire face for the `defaultPreset` write.
|
||||
*/
|
||||
constructor(
|
||||
private readonly describeFace: SettingsDescribeFace,
|
||||
private readonly api: Pick<IApiClient, 'settings'>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Begin following the mirror (idempotent) and reflect its current answer.
|
||||
* @returns settlement once the snapshot reflects the mirror.
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
const generation = ++this.generation
|
||||
if (this.disposed) return
|
||||
this.following ??= this.describeFace.subscribe(() => { this.derive() })
|
||||
this.store.update((state) => {
|
||||
state.status = 'loading'
|
||||
state.error = null
|
||||
})
|
||||
try {
|
||||
const response = await this.api.settings.describe({})
|
||||
if (!response.result.ok) throw new Error(response.result.error.message)
|
||||
if (generation !== this.generation) return
|
||||
const view = response.result.value.namespaces.find(entry => entry.ns === PERMISSION_SETTINGS_NS)
|
||||
if (view === undefined) {
|
||||
this.view = undefined
|
||||
this.store.update((state) => {
|
||||
state.status = 'unavailable'
|
||||
state.writable = false
|
||||
state.currentValue = ''
|
||||
state.options = []
|
||||
})
|
||||
return
|
||||
}
|
||||
this.accept(view, response.result.value.writable)
|
||||
} catch (error) {
|
||||
if (generation !== this.generation) return
|
||||
this.fail(error)
|
||||
}
|
||||
await this.describeFace.ensure()
|
||||
this.derive()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,10 +124,11 @@ export class PermissionPresetSettingsController {
|
||||
* @returns nothing; {@link store} carries success or failure.
|
||||
*/
|
||||
async select(preset: string): Promise<void> {
|
||||
const view = this.view
|
||||
const state = this.store.getSnapshot()
|
||||
if (view === undefined || !state.writable) return
|
||||
const generation = ++this.generation
|
||||
const view = this.describeFace.getSnapshot().view?.namespaces
|
||||
.find(entry => entry.ns === PERMISSION_SETTINGS_NS)
|
||||
if (view === undefined || !state.writable || this.saving) return
|
||||
this.saving = true
|
||||
this.store.update((draft) => {
|
||||
draft.status = 'saving'
|
||||
draft.error = null
|
||||
@@ -145,32 +139,59 @@ export class PermissionPresetSettingsController {
|
||||
ops: [{ op: 'set', path: ['defaultPreset'], value: preset }],
|
||||
expectedRevision: view.revision,
|
||||
})
|
||||
if (generation !== this.generation) return
|
||||
if (!response.result.ok) throw new Error(response.result.error.message)
|
||||
this.accept(response.result.value, true)
|
||||
this.saving = false
|
||||
if (this.disposed) return
|
||||
// The mirror publish reaches this row's own subscription, so the fold
|
||||
// is also what republishes the accepted value here.
|
||||
this.describeFace.acceptView(response.result.value)
|
||||
} catch (error) {
|
||||
if (generation !== this.generation) return
|
||||
this.saving = false
|
||||
if (this.disposed) return
|
||||
this.fail(error)
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop in-flight responses from publishing after plugin disposal. */
|
||||
/** Stop following the mirror; later publishes leave the snapshot alone. */
|
||||
dispose(): void {
|
||||
this.generation += 1
|
||||
this.view = undefined
|
||||
this.disposed = true
|
||||
this.following?.()
|
||||
this.following = undefined
|
||||
}
|
||||
|
||||
private accept(view: SettingsNamespaceView, writable: boolean): void {
|
||||
const resolved = permissionDefaultOf(view)
|
||||
this.view = view
|
||||
this.store.update((state) => {
|
||||
state.status = 'ready'
|
||||
state.error = null
|
||||
state.writable = writable
|
||||
state.currentValue = resolved.currentValue
|
||||
state.options = resolved.options
|
||||
state.revision = view.revision
|
||||
})
|
||||
private derive(): void {
|
||||
if (this.disposed || this.saving) return
|
||||
const mirrored = this.describeFace.getSnapshot()
|
||||
if (mirrored.view === undefined) {
|
||||
// A held failure with no answer is a failed row; without one the read
|
||||
// is still in flight and the row keeps its loading state.
|
||||
if (mirrored.error !== null) this.fail(new Error(mirrored.error))
|
||||
return
|
||||
}
|
||||
const view = mirrored.view.namespaces.find(entry => entry.ns === PERMISSION_SETTINGS_NS)
|
||||
if (view === undefined) {
|
||||
this.store.update((state) => {
|
||||
state.status = 'unavailable'
|
||||
state.writable = false
|
||||
state.currentValue = ''
|
||||
state.options = []
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
const resolved = permissionDefaultOf(view)
|
||||
const { writable } = mirrored.view
|
||||
this.store.update((state) => {
|
||||
state.status = 'ready'
|
||||
state.error = null
|
||||
state.writable = writable
|
||||
state.currentValue = resolved.currentValue
|
||||
state.options = resolved.options
|
||||
state.revision = view.revision
|
||||
})
|
||||
} catch (error) {
|
||||
this.fail(error)
|
||||
}
|
||||
}
|
||||
|
||||
private fail(error: unknown): void {
|
||||
@@ -180,12 +201,3 @@ export class PermissionPresetSettingsController {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refetch only after the row has opened once.
|
||||
* @param controller - permission settings controller.
|
||||
*/
|
||||
export function refreshPermissionIfLoaded(controller: PermissionPresetSettingsController): void {
|
||||
if (controller.store.getSnapshot().status === 'idle') return
|
||||
void controller.load()
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { SlotRegistry, type SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
|
||||
import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply as settingsApply, inject as settingsInject } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { CommandDecoration } from '@deepseek-ai/dsh-client-ui-commands/client'
|
||||
import type { PermissionSelect } from '@deepseek-ai/dsh-permission-presets/client'
|
||||
import {
|
||||
@@ -58,6 +59,7 @@ async function bench() {
|
||||
},
|
||||
},
|
||||
} as never)
|
||||
await ctx.plugin({ inject: [...settingsInject], apply: settingsApply }).await()
|
||||
let decoration: CommandDecoration | undefined
|
||||
ctx.provide('commandUi', {
|
||||
decorate(c: CommandDecoration) {
|
||||
|
||||
@@ -5,8 +5,15 @@ import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { PermissionRow, type PermissionRowProps } from '../src/client/PermissionRow.tsx'
|
||||
import { en } from '../src/client/locales.ts'
|
||||
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import { PermissionPresetSettingsController } from '../src/client/settings-store.ts'
|
||||
|
||||
/** Controller over a real mirror derived from the same fake wire. */
|
||||
function derivedController(api: { settings: object }) {
|
||||
const wire = api as never
|
||||
return new PermissionPresetSettingsController(new SettingsDescribeMirror(wire), wire)
|
||||
}
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
const SCHEMA = {
|
||||
@@ -58,7 +65,7 @@ function mount(controller: PermissionPresetSettingsController) {
|
||||
describe('PermissionRow', () => {
|
||||
it('loads the descriptor, opens the menu, and selects a new default', async () => {
|
||||
const mutate = vi.fn(() => Promise.resolve(ok(view('workspace-write', 1))))
|
||||
const controller = new PermissionPresetSettingsController({
|
||||
const controller = derivedController({
|
||||
settings: {
|
||||
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
||||
mutate,
|
||||
@@ -85,7 +92,7 @@ describe('PermissionRow', () => {
|
||||
|
||||
it('requires explicit acknowledgement before saving Full access', async () => {
|
||||
const mutate = vi.fn(() => Promise.resolve(ok(view('danger-full-access', 1))))
|
||||
const controller = new PermissionPresetSettingsController({
|
||||
const controller = derivedController({
|
||||
settings: {
|
||||
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
||||
mutate,
|
||||
@@ -109,7 +116,7 @@ describe('PermissionRow', () => {
|
||||
})
|
||||
|
||||
it('hides an unavailable namespace and disables a read-only provider', async () => {
|
||||
const absent = new PermissionPresetSettingsController({
|
||||
const absent = derivedController({
|
||||
settings: {
|
||||
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] })),
|
||||
mutate: vi.fn(),
|
||||
@@ -119,7 +126,7 @@ describe('PermissionRow', () => {
|
||||
await waitFor(() => { expect(rendered.container.textContent).toBe('') })
|
||||
rendered.unmount()
|
||||
|
||||
const readonly = new PermissionPresetSettingsController({
|
||||
const readonly = derivedController({
|
||||
settings: {
|
||||
describe: () => Promise.resolve(ok({ writable: false, hasDocument: false, namespaces: [view('read-only')] })),
|
||||
mutate: vi.fn(),
|
||||
@@ -134,7 +141,7 @@ describe('PermissionRow', () => {
|
||||
writable: boolean
|
||||
namespaces: SettingsNamespaceView[]
|
||||
}>>>()
|
||||
const controller = new PermissionPresetSettingsController({
|
||||
const controller = derivedController({
|
||||
settings: {
|
||||
describe: () => describe.promise,
|
||||
mutate: () => Promise.resolve({
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
||||
import { SettingsDescribeMirror } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import {
|
||||
PermissionPresetSettingsController, permissionDefaultOf, refreshPermissionIfLoaded,
|
||||
PermissionPresetSettingsController, permissionDefaultOf,
|
||||
} from '../src/client/settings-store.ts'
|
||||
|
||||
const SCHEMA = {
|
||||
@@ -30,6 +31,13 @@ function ok<T>(value: T) {
|
||||
return { rpcId: 'test', result: { ok: true as const, value } }
|
||||
}
|
||||
|
||||
/** The permission controller over a real mirror and one fake wire. */
|
||||
function permissionController(api: object) {
|
||||
const wire = { settings: api } as never
|
||||
const mirror = new SettingsDescribeMirror(wire)
|
||||
return { mirror, controller: new PermissionPresetSettingsController(mirror, wire) }
|
||||
}
|
||||
|
||||
describe('permission settings store', () => {
|
||||
it('derives dynamic options and host labels from the descriptor schema', () => {
|
||||
expect(permissionDefaultOf(view('read-only'))).toEqual({
|
||||
@@ -92,9 +100,7 @@ describe('permission settings store', () => {
|
||||
namespaces: [view('read-only', 4)],
|
||||
})))
|
||||
const mutate = vi.fn(() => Promise.resolve(ok(view('workspace-write', 5))))
|
||||
const controller = new PermissionPresetSettingsController({
|
||||
settings: { describe, mutate } as never,
|
||||
})
|
||||
const { controller } = permissionController({ describe, mutate })
|
||||
await controller.load()
|
||||
expect(controller.store.getSnapshot()).toMatchObject({
|
||||
status: 'ready',
|
||||
@@ -113,126 +119,105 @@ describe('permission settings store', () => {
|
||||
currentValue: 'workspace-write',
|
||||
revision: 5,
|
||||
})
|
||||
// The write answer folded into the mirror; no re-read followed.
|
||||
expect(describe).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('hides the row when the namespace is absent and contains write failures', async () => {
|
||||
const describe = vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] })))
|
||||
const controller = new PermissionPresetSettingsController({
|
||||
settings: { describe, mutate: vi.fn() } as never,
|
||||
})
|
||||
const { controller } = permissionController({ describe, mutate: vi.fn() })
|
||||
await controller.load()
|
||||
expect(controller.store.getSnapshot().status).toBe('unavailable')
|
||||
|
||||
const failing = new PermissionPresetSettingsController({
|
||||
settings: {
|
||||
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
||||
mutate: () => Promise.resolve({
|
||||
rpcId: 'test',
|
||||
result: {
|
||||
ok: false as const,
|
||||
error: { code: 'settings-conflict', message: 'stale', details: {} },
|
||||
},
|
||||
}),
|
||||
} as never,
|
||||
})
|
||||
const failing = permissionController({
|
||||
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
||||
mutate: () => Promise.resolve({
|
||||
rpcId: 'test',
|
||||
result: {
|
||||
ok: false as const,
|
||||
error: { code: 'settings-conflict', message: 'stale', details: {} },
|
||||
},
|
||||
}),
|
||||
}).controller
|
||||
await failing.load()
|
||||
await failing.select('workspace-write')
|
||||
expect(failing.store.getSnapshot()).toMatchObject({ status: 'error', error: 'stale' })
|
||||
})
|
||||
|
||||
it('contains read failures, no-ops without a writable view, and ignores stale responses', async () => {
|
||||
const first = Promise.withResolvers<ReturnType<typeof ok<{
|
||||
writable: boolean
|
||||
namespaces: SettingsNamespaceView[]
|
||||
}>>>()
|
||||
const describe = vi.fn()
|
||||
.mockImplementationOnce(() => first.promise)
|
||||
.mockResolvedValueOnce(ok({ writable: false, hasDocument: false, namespaces: [view('read-only', 2)] }))
|
||||
it('contains read failures and no-ops without a writable view', async () => {
|
||||
const mutate = vi.fn()
|
||||
const controller = new PermissionPresetSettingsController({
|
||||
settings: { describe, mutate } as never,
|
||||
})
|
||||
const stale = controller.load()
|
||||
await controller.load()
|
||||
first.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('workspace-write', 1)] }))
|
||||
await stale
|
||||
expect(controller.store.getSnapshot()).toMatchObject({
|
||||
const readOnly = permissionController({
|
||||
describe: () => Promise.resolve(ok({
|
||||
writable: false, hasDocument: false, namespaces: [view('read-only', 2)],
|
||||
})),
|
||||
mutate,
|
||||
}).controller
|
||||
await readOnly.load()
|
||||
expect(readOnly.store.getSnapshot()).toMatchObject({
|
||||
currentValue: 'read-only',
|
||||
writable: false,
|
||||
revision: 2,
|
||||
})
|
||||
await controller.select('workspace-write')
|
||||
await readOnly.select('workspace-write')
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
|
||||
const rejected = new PermissionPresetSettingsController({
|
||||
settings: {
|
||||
describe: () => Promise.resolve({
|
||||
rpcId: 'test',
|
||||
result: { ok: false as const, error: { code: 'internal', message: 'offline', details: {} } },
|
||||
}),
|
||||
mutate,
|
||||
} as never,
|
||||
})
|
||||
const rejected = permissionController({
|
||||
describe: () => Promise.resolve({
|
||||
rpcId: 'test',
|
||||
result: { ok: false as const, error: { code: 'internal', message: 'offline', details: {} } },
|
||||
}),
|
||||
mutate,
|
||||
}).controller
|
||||
await rejected.select('workspace-write')
|
||||
await rejected.load()
|
||||
expect(rejected.store.getSnapshot()).toMatchObject({ status: 'error', error: 'offline' })
|
||||
expect(mutate).not.toHaveBeenCalled()
|
||||
|
||||
const thrown = new PermissionPresetSettingsController({
|
||||
settings: {
|
||||
// Promise consumers must contain unknown rejection values from a
|
||||
// transport implementation, including non-Error legacy clients.
|
||||
// oxlint-disable-next-line typescript/prefer-promise-reject-errors
|
||||
describe: () => Promise.reject('disconnected'),
|
||||
mutate,
|
||||
} as never,
|
||||
})
|
||||
const thrown = permissionController({
|
||||
// Promise consumers must contain unknown rejection values from a
|
||||
// transport implementation, including non-Error legacy clients.
|
||||
describe: () => Promise.reject('disconnected' as never),
|
||||
mutate,
|
||||
}).controller
|
||||
await thrown.load()
|
||||
expect(thrown.store.getSnapshot()).toMatchObject({ status: 'error', error: 'disconnected' })
|
||||
})
|
||||
|
||||
it('disposal suppresses in-flight reads and writes, and loaded invalidations refetch', async () => {
|
||||
it('follows a mirror refresh without an own read once loaded', async () => {
|
||||
const describe = vi.fn()
|
||||
.mockResolvedValueOnce(ok({ writable: true, hasDocument: false, namespaces: [view('read-only', 1)] }))
|
||||
.mockResolvedValueOnce(ok({ writable: true, hasDocument: false, namespaces: [view('workspace-write', 2)] }))
|
||||
const { mirror, controller } = permissionController({ describe, mutate: vi.fn() })
|
||||
await controller.load()
|
||||
expect(controller.store.getSnapshot()).toMatchObject({ currentValue: 'read-only' })
|
||||
|
||||
await mirror.load()
|
||||
|
||||
expect(controller.store.getSnapshot()).toMatchObject({ currentValue: 'workspace-write', revision: 2 })
|
||||
})
|
||||
|
||||
it('disposal stops deriving and suppresses in-flight writes', async () => {
|
||||
const read = Promise.withResolvers<ReturnType<typeof ok<{
|
||||
writable: boolean
|
||||
namespaces: SettingsNamespaceView[]
|
||||
}>>>()
|
||||
const describe = vi.fn(() => read.promise)
|
||||
const idle = new PermissionPresetSettingsController({ settings: { describe, mutate: vi.fn() } as never })
|
||||
refreshPermissionIfLoaded(idle)
|
||||
expect(describe).not.toHaveBeenCalled()
|
||||
const { mirror, controller: idle } = permissionController({ describe: () => read.promise, mutate: vi.fn() })
|
||||
const loading = idle.load()
|
||||
idle.dispose()
|
||||
read.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] }))
|
||||
await loading
|
||||
await Promise.all([loading, mirror.load()])
|
||||
expect(idle.store.getSnapshot().status).toBe('loading')
|
||||
|
||||
const rejectedRead = Promise.withResolvers<ReturnType<typeof ok<{
|
||||
writable: boolean
|
||||
namespaces: SettingsNamespaceView[]
|
||||
}>>>()
|
||||
const disposedRead = new PermissionPresetSettingsController({
|
||||
settings: { describe: () => rejectedRead.promise, mutate: vi.fn() } as never,
|
||||
})
|
||||
const reading = disposedRead.load()
|
||||
disposedRead.dispose()
|
||||
rejectedRead.reject(new Error('late read'))
|
||||
await reading
|
||||
expect(disposedRead.store.getSnapshot().status).toBe('loading')
|
||||
|
||||
const mutation = Promise.withResolvers<ReturnType<typeof ok<SettingsNamespaceView>>>()
|
||||
const activeDescribe = vi.fn(() => Promise.resolve(ok({
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [view('read-only')],
|
||||
})))
|
||||
const active = new PermissionPresetSettingsController({
|
||||
settings: {
|
||||
describe: activeDescribe,
|
||||
mutate: () => mutation.promise,
|
||||
} as never,
|
||||
const { controller: active } = permissionController({
|
||||
describe: () => Promise.resolve(ok({
|
||||
writable: true,
|
||||
hasDocument: false,
|
||||
namespaces: [view('read-only')],
|
||||
})),
|
||||
mutate: () => mutation.promise,
|
||||
})
|
||||
await active.load()
|
||||
refreshPermissionIfLoaded(active)
|
||||
await vi.waitFor(() => { expect(activeDescribe).toHaveBeenCalledTimes(2) })
|
||||
const saving = active.select('workspace-write')
|
||||
active.dispose()
|
||||
mutation.resolve(ok(view('workspace-write', 1)))
|
||||
@@ -240,11 +225,9 @@ describe('permission settings store', () => {
|
||||
expect(active.store.getSnapshot().status).toBe('saving')
|
||||
|
||||
const rejectedMutation = Promise.withResolvers<ReturnType<typeof ok<SettingsNamespaceView>>>()
|
||||
const disposedWrite = new PermissionPresetSettingsController({
|
||||
settings: {
|
||||
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
||||
mutate: () => rejectedMutation.promise,
|
||||
} as never,
|
||||
const { controller: disposedWrite } = permissionController({
|
||||
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
||||
mutate: () => rejectedMutation.promise,
|
||||
})
|
||||
await disposedWrite.load()
|
||||
const writing = disposedWrite.select('workspace-write')
|
||||
|
||||
Reference in new issue
Block a user