feat(canvas): one lease per canvas and a cap on mounted canvases

Merged
GN
2d ago
+2659-198

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 canvasMaxMounted canvases 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 typecheck on this branch
  • Each part is a checkpoint that was built and tested when it was made (cargo test for canvas and host_mcp, vitest, real runs in headless Edge). The bottom and top of the stack were rebuilt and re-tested on current dev.

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_ELSEWHERE and 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.maxMounted canvases 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 with EVICTION_BLOCKED.