fix: 回应 Codex 审查的 4 条意见(2×P1 + 2×P2)
- P1 state.ts reduceFor:恢复共享 uid 计数器到调用前水平(目标 session 的 max 可能低于激活 session 已用 id,否则下次 mint 撞 id 损坏布局); 回归测试实证:无修复时 split 产生重复 pane id - P1 service.ts 生命周期分类:dedupe 聚焦判定改为基于输入态的存在性 (dedupeKey 匹配或 id 匹配都算聚焦)——按 path 去重 + 新 id 打开时 不再对从未插入的幽灵 tab 触发 onOpen(资源泄漏源) - P2 SideCardSection:乐观 prefs ref(optimisticRef)驱动嵌套 map 合并 (tabsEnabled/viewersEnabled/pluginSettings),同 tick 多次写入不再 互相丢弃;mergePluginSetting 抽纯函数 + 单测 - P2 state.ts schedulePersist:每 session 独立防抖定时器(persistTimers Map),定向打开不再取消其他 session 的待写;单测验证双写都落盘
This commit is contained in:
+21
-3
@@ -938,7 +938,9 @@ export class SidebarStore {
|
||||
prefs: { ...SIDEBAR_PREFS_DEFAULTS },
|
||||
}
|
||||
private readonly listeners = new Set<() => void>()
|
||||
private persistTimer: number | undefined
|
||||
/** Per-session persist debounce timers (v0.12.0+: one per session, so a
|
||||
* targeted open never cancels another session's pending write). */
|
||||
private readonly persistTimers = new Map<string, number>()
|
||||
/** User-facing side card prefs seeding brand-new session states (defaults until the settings RPC resolves). */
|
||||
private prefs: SidebarPrefs = { ...SIDEBAR_PREFS_DEFAULTS }
|
||||
|
||||
@@ -1037,6 +1039,13 @@ export class SidebarStore {
|
||||
* session's layout and is visible whenever the user switches to it.
|
||||
*/
|
||||
reduceFor(sessionId: string, reducer: (state: SidebarState) => SidebarState): void {
|
||||
// The uid counter is SHARED across sessions, and the ACTIVE session's
|
||||
// safety requires it to never drop below the ids IT minted. Seeding it
|
||||
// from the target's max may LOWER it (a cached target older than the
|
||||
// active session): restoring the pre-call level afterwards keeps the
|
||||
// active session's next mint collision-free — ids minted for the target
|
||||
// only need to exceed the target's own max, which the seed guaranteed.
|
||||
const counterBefore = nextIdCounter
|
||||
let state = this.bySession.get(sessionId)
|
||||
if (state === undefined) {
|
||||
state = loadState(sessionId, this.prefs)
|
||||
@@ -1048,18 +1057,27 @@ export class SidebarStore {
|
||||
}
|
||||
const next = reducer(state)
|
||||
this.bySession.set(sessionId, next)
|
||||
nextIdCounter = Math.max(nextIdCounter, counterBefore)
|
||||
this.schedulePersist(sessionId, next)
|
||||
}
|
||||
|
||||
private schedulePersist(sessionId: string, state: SidebarState): void {
|
||||
window.clearTimeout(this.persistTimer)
|
||||
this.persistTimer = window.setTimeout(() => {
|
||||
// Per-session debounce timers: one session's pending write must never
|
||||
// cancel another's (targeted opens schedule writes for INACTIVE
|
||||
// sessions while the active session may already have one pending —
|
||||
// a shared timer would drop the earlier write and the reload would
|
||||
// lose that session's layout).
|
||||
const existing = this.persistTimers.get(sessionId)
|
||||
if (existing !== undefined) window.clearTimeout(existing)
|
||||
const timer = window.setTimeout(() => {
|
||||
this.persistTimers.delete(sessionId)
|
||||
try {
|
||||
localStorage.setItem(`${STORAGE_PREFIX}:${sessionId}`, JSON.stringify(state))
|
||||
} catch {
|
||||
// Storage full or unavailable: layout memory is best-effort.
|
||||
}
|
||||
}, 200)
|
||||
this.persistTimers.set(sessionId, timer)
|
||||
}
|
||||
|
||||
private notify(): void {
|
||||
|
||||
Reference in New Issue
Block a user