Skip to content

Commit 047f66c

Browse files
committed
fix(restore): serialize missing workspace mounts
Restore-created workspaces previously mounted in parallel. Solid could invoke Virtua refs before document adoption, causing ResizeObserver initialization to throw after the workspace entered the UI but before its restore binding committed. The affected tabs then lost saved ordering, session selection, idle markers, scroll snapshots, and generation recovery. Create missing workspaces sequentially while preserving parallel hydration for existing workspaces and SideCars. Budget the startup deadline per serialized workspace and bound failed-creation cleanup so one stalled cancellation cannot block later tabs. Validated with a packaged Tauri release against a three-workspace snapshot, confirming exact tab order, active tab, per-workspace active sessions, idle markers, scroll snapshot counts, and generation recovery. Added focused regression coverage and ran UI typecheck and restore tests.
1 parent 5265926 commit 047f66c

2 files changed

Lines changed: 27 additions & 13 deletions

File tree

packages/ui/src/lib/hooks/use-app-session-capture.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@ describe("app session capture listener readiness", () => {
1515
assert.ok(restore.indexOf("await capture.ready") < restore.indexOf("capture.start("))
1616
})
1717

18+
it("serializes missing workspace mounts during session restore", () => {
19+
const restore = source("./use-app-session-restore.ts")
20+
assert.match(restore, /for \(const group of groups\.values\(\)\) for \(const match of group\) await restoreWorkspace\(match\)/)
21+
assert.doesNotMatch(restore, /Array\.from\(groups\.values\(\), async/)
22+
})
23+
1824
})

packages/ui/src/lib/hooks/use-app-session-restore.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,23 @@ const NO_SESSION_DRAFT_SESSION_ID = "__no_session_draft__"
3434
const INITIAL_LOAD_TIMEOUT_MS = 15_000
3535
const OPERATION_TIMEOUT_MS = 30_000
3636
const CREATE_TIMEOUT_MS = OPERATION_TIMEOUT_MS * 2
37+
const CLEANUP_TIMEOUT_MS = 5_000
3738
const MINIMUM_STARTUP_TIMEOUT_MS = 60_000
3839
function startupTimeout(snapshot: RestorableSessionState): number {
39-
const counts = new Map<string, number>()
40-
for (const tab of snapshot.tabs) if (tab.kind === "workspace") {
41-
const path = normalizeWorkspacePath(tab.folder)
42-
counts.set(path, (counts.get(path) ?? 0) + 1)
43-
}
40+
const workspaceCount = snapshot.tabs.filter((tab) => tab.kind === "workspace").length
4441
return Math.max(MINIMUM_STARTUP_TIMEOUT_MS,
45-
INITIAL_LOAD_TIMEOUT_MS + Math.max(1, ...counts.values()) * CREATE_TIMEOUT_MS + 5_000)
42+
INITIAL_LOAD_TIMEOUT_MS + Math.max(1, workspaceCount) * (CREATE_TIMEOUT_MS + CLEANUP_TIMEOUT_MS) + 5_000)
43+
}
44+
async function disposeFailedRestoreWorkspace(instanceId: string): Promise<void> {
45+
const cleanup = disposeRestoreCreatedInstance(instanceId)
46+
try {
47+
await runAbortable(() => cleanup, {
48+
timeoutMs: CLEANUP_TIMEOUT_MS,
49+
message: `Timed out cleaning up restored workspace ${instanceId}`,
50+
})
51+
} catch (error) {
52+
log.warn("Restore workspace cleanup continues in the background", { instanceId, error })
53+
}
4654
}
4755
async function restoreWorkspaceState(
4856
instanceId: string,
@@ -171,8 +179,8 @@ async function restoreTabs(context: RestoreContext): Promise<void> {
171179
} catch (error) {
172180
if (!existingId && creation?.requestId) {
173181
capture.settleRestoredTab(match.tabIndex, getInstanceAppTabId(id), null)
174-
await disposeRestoreCreatedInstance(id)
175182
if (created) createdId = null
183+
await disposeFailedRestoreWorkspace(id)
176184
}
177185
throw error
178186
}
@@ -187,7 +195,7 @@ async function restoreTabs(context: RestoreContext): Promise<void> {
187195
} catch (error) {
188196
if (createdId) {
189197
capture.settleRestoredTab(match.tabIndex, getInstanceAppTabId(createdId), null)
190-
await disposeRestoreCreatedInstance(createdId)
198+
await disposeFailedRestoreWorkspace(createdId)
191199
}
192200
if (!signal.aborted) log.warn("Skipped workspace while restoring app session", { folder: tab.folder, error })
193201
}
@@ -208,11 +216,11 @@ async function restoreTabs(context: RestoreContext): Promise<void> {
208216
if (!signal.aborted) log.warn("Skipped SideCar while restoring app session", { sidecarId: tab.sidecarId, error })
209217
}
210218
}
211-
await Promise.all([
212-
...existing.map(restoreWorkspace),
213-
...Array.from(groups.values(), async (group) => { for (const match of group) await restoreWorkspace(match) }),
214-
...sidecars,
215-
])
219+
const restoreMissing = async () => {
220+
// ponytail: serialize DOM-heavy workspace mounts; parallel mounts can initialize Virtua before document adoption.
221+
for (const group of groups.values()) for (const match of group) await restoreWorkspace(match)
222+
}
223+
await Promise.all([...existing.map(restoreWorkspace), restoreMissing(), ...sidecars])
216224
}
217225
export function useAppSessionRestore(): void {
218226
const capture = useAppSessionCapture()

0 commit comments

Comments
 (0)