fix(desktop): 状态镜像到宿主侧,修复随机端口导致重启丢数据
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 宿主侧状态镜像(本地补丁)。
|
||||
*
|
||||
* 背景:桌面端宿主每次启动都带 `--port 0`(随机端口),页面来源随之变化
|
||||
* (如 127.0.0.1:2080 → 127.0.0.1:8494)。浏览器按来源隔离 localStorage /
|
||||
* IndexedDB,换端口就等于换一个空库,项目/布局/外观重启后全丢。
|
||||
*
|
||||
* 做法:把「项目 + 视图」这份 JSON 镜像到宿主侧文件(/api/worktable/state),
|
||||
* 启动时若宿主侧比本地新,就用宿主侧恢复。localStorage 仍是第一落点(同步、快),
|
||||
* 宿主文件是跨重启的兜底。
|
||||
*/
|
||||
|
||||
const ENDPOINT = '/api/worktable/state'
|
||||
|
||||
export type HostStateParts = { projects?: unknown; view?: unknown }
|
||||
|
||||
/** 宿主镜像里可能带的写入时间戳(毫秒) */
|
||||
export type HostState = HostStateParts & { updatedAt?: number }
|
||||
|
||||
/** 拉取宿主侧镜像;没有/不可用都返回 null(不抛错,不影响工作台本身) */
|
||||
export async function loadHostState(): Promise<HostState | null> {
|
||||
try {
|
||||
const r = await fetch(ENDPOINT, { cache: 'no-store', headers: { accept: 'application/json' } })
|
||||
if (!r.ok) return null
|
||||
const j: any = await r.json()
|
||||
return j && typeof j === 'object' && j.state && typeof j.state === 'object' ? (j.state as HostState) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 累积中的镜像内容:POST 只带最新值,避免每次编辑都发整包 */
|
||||
let parts: HostStateParts = {}
|
||||
let timer: any = null
|
||||
let inflight = false
|
||||
let dirty = false
|
||||
/** 恢复完成前不写宿主,否则首屏的默认值会把宿主里的旧数据覆盖掉 */
|
||||
let hydrated = false
|
||||
|
||||
function schedule() {
|
||||
if (timer) return
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
void flush()
|
||||
}, 400)
|
||||
}
|
||||
|
||||
async function flush(): Promise<void> {
|
||||
if (!hydrated || inflight) {
|
||||
if (inflight) dirty = true
|
||||
return
|
||||
}
|
||||
if (!parts.projects && !parts.view) return
|
||||
inflight = true
|
||||
try {
|
||||
await fetch(ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ state: { ...parts, updatedAt: Date.now() } }),
|
||||
keepalive: true,
|
||||
})
|
||||
} catch {
|
||||
// 宿主不可用时静默:localStorage 仍是本次会话的真值
|
||||
} finally {
|
||||
inflight = false
|
||||
if (dirty) {
|
||||
dirty = false
|
||||
schedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 记下要镜像的部分;400ms 内多次调用合并成一次 POST */
|
||||
export function queueHostSave(patch: HostStateParts): void {
|
||||
parts = { ...parts, ...patch }
|
||||
if (!hydrated) return
|
||||
schedule()
|
||||
}
|
||||
|
||||
/** 首次恢复判定结束:之后再放行宿主写入 */
|
||||
export function markHydrated(): void {
|
||||
hydrated = true
|
||||
}
|
||||
|
||||
/** 立即冲刷(页面隐藏/关闭前调用,避免丢掉最后一个 400ms 内的改动) */
|
||||
export function flushHostSave(): void {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
void flush()
|
||||
}
|
||||
Reference in New Issue
Block a user