93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
/**
|
|||
|
|
* 宿主侧状态镜像(本地补丁)。
|
||
|
|
*
|
||
|
|
* 背景:桌面端宿主每次启动都带 `--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()
|
||
|
|
}
|