feat(canvas): one lease per canvas and a cap on mounted canvases
Merged
GN
2d ago+2659+2659+0123456789012345678901234567890123456789-198-198-012345678901234567890123456789
Summary
- A desktop window and a browser must not run two editors on one document, and memory must not grow with every project.
- Part 6 of 10 in a stacked series. Review and merge bottom-up; each PR targets the one below it. Canvas mode v3 (figcraft engine). Spec:
spec-openpencil-canvas-mode; architecture spine 2026-10-16 (AD-17 to AD-31).
Related Issue
No issue. Split from #1369 so each part stays under 50 files.
Type of Change
- feat: new feature
What Changed
- One lease per document held by one renderer; a newer opener takes it after the holder saves, the holder shows "open in another window" with Take back, and a reload keeps it within a grace period.
- At most
canvasMaxMountedcanvases stay mounted; the least active one is saved and unloaded, and a failed save blocks the eviction.
Scope
- E2-4, E2-3.
How It Was Tested
-
bun run typecheckon this branch - Each part is a checkpoint that was built and tested when it was made (
cargo testfor canvas and host_mcp, vitest, real runs in headless Edge). The bottom and top of the stack were rebuilt and re-tested on currentdev.
Checklist
- PR title follows the conventional commit format
- Explained why no issue exists
- Added or updated tests
- Read AGENTS.md
- A human reviewed the complete diff before submission
🤖 Generated with Claude Code
Summary by cubic
Prevents a desktop window and a browser tab from editing the same canvas at once, and caps how many canvases stay mounted so memory does not grow with every opened project.
- Only the renderer holding the canvas lease (AD-30) runs a mount; a newer opener takes the lease only after the holder saves and unmounts, and the displaced holder sees "open in another window" with a Take back action.
- An open whose takeover save fails is refused with
CANVAS_OPEN_ELSEWHEREand the holder keeps its mount. - A reload or reconnect of the same window keeps its lease within the grace period; after it expires, the mount is dropped.
- At most
canvas.maxMountedcanvases stay mounted (AD-26); a new open unloads the least recently active one after saving it, and a victim that cannot be saved blocks the open withEVICTION_BLOCKED.