Files
deepseek-harness/packages/api/remote-access/src/client/when-allowed.ts
T
imccyu 09899f3a0f feat(remote-access): 插件注册跟随访问状态(whenAccess),index 按 Peer 注入访问快照
- remote-access 新增 whenAccess / whenResourceAccess,ui-session、ui-workspace 各自提供 whenAccess
- schedule、plugin-manager、settings-general、settings-account、directory-picker-native 的入口包进 global whenAccess;消息反馈按钮按会话 useAccess 渲染
- webserver/index-inject 带 Peer,renderIndex / collectIndexInjections 需要 Peer,remote-access 注入 snapshotFor(peer.principal)
- client-test-runtime 新增 StaticRemoteAccess、staticAccessPlugin 与 TestClientOptions.access
- implemented Agent Note 2026-10-21-when-access 与升级指南 webserver-index-peer

Refs #2940
2026-10-21 17:57:51 +08:00

86 lines
2.8 KiB
TypeScript

/**
* The effect controller behind `whenAccess`: install a callback's effects
* while one access answer is true and dispose them while it is false.
* @module @deepseek-ai/dsh-api-remote-access/src/client/when-allowed
*/
import type { Context } from '@deepseek-ai/cordis'
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'
/** One synchronous effect installed while access holds: a disposer, or disposers installed as one transaction. */
export type AccessEffect = (() => void) | Iterable<() => void, void, void>
/**
* Install `callback`'s effects while `answer` is true. The callback runs
* synchronously when the answer is already true; a later true runs it again
* after a false disposed the previous installation in reverse order. The
* controller belongs to `ctx`'s fiber.
* @param ctx - the caller's Context; it owns the controller and every installation.
* @param answer - the access answer to follow.
* @param callback - creates one disposer or an iterable of disposers.
* @param label - effect label for diagnostics.
* @returns idempotent disposer for the controller and any installed effects.
* @throws callback setup failures synchronously when the answer is already true.
*/
export function whenAllowed(
ctx: Context,
answer: HostObservable<boolean>,
callback: () => AccessEffect,
label: string,
): () => void {
const disposeController = ctx.effect(() => {
let installed: (() => void) | undefined
let stopped = false
const uninstall = (): void => {
const dispose = installed
installed = undefined
dispose?.()
}
const reconcile = (): void => {
if (stopped) return
const allowed = answer.getSnapshot()
if (allowed === (installed !== undefined)) return
if (!allowed) {
uninstall()
return
}
// Each installation is a nested Cordis effect, so an iterable callback
// installs transactionally and disposes in reverse order.
const disposeEffect = ctx.effect(callback, `${label}: allowed`)
installed = () => { void disposeEffect() }
}
const unsubscribe = answer.subscribe(() => { changed() })
const stop = (): void => {
if (stopped) return
// A failed installation retires the controller: a later grant never retries it.
stopped = true
unsubscribe()
uninstall()
}
const changed = (): void => {
try {
reconcile()
} catch (error) {
stop()
if ((error as { code?: unknown } | null)?.code === 'INACTIVE_EFFECT') return
const failure = error instanceof Error ? error : new Error(String(error))
queueMicrotask(() => { throw failure })
}
}
try {
reconcile()
} catch (error) {
stop()
throw error
}
return stop
}, label)
return () => { void disposeController() }
}