The old export protocol used a single state slot per session: the MCP tool
set exportFormat/exportXml, the browser bridge picked it up on its 2s poll,
and the rendered image came back as one exportData field that whichever
tool call polled first consumed and cleared. Under concurrency this meant:
requests overwrote each other, at most one of N parallel exports succeeded,
a late render could satisfy the next export with the WRONG image, the
browser's leaked 10s fallback timer could silently swallow a successor's
request, and a closed preview tab made every export hang to timeout.
Replace it with a per-session export job queue:
- each export_diagram call enqueues a job (unique id, format, optional
single-page projection) and awaits its own job's promise
- GET /api/state exposes only the head of the queue; the browser renders
jobs strictly one at a time and reports each result/failure BY JOB ID,
which resolves exactly the waiting call; stale ids are ignored
- browser-side fallback timer (30s) is tracked and cleared per job and
only ever fails its own job, so failures advance the queue
- server-side per-job backstop is 120s (DRAWIO_EXPORT_TIMEOUT_MS) since a
queued job also waits for its predecessors
- browser heartbeat (20s staleness) fails enqueues/waiters fast when the
preview tab is gone, instead of hanging to the timeout
- expired-session cleanup now fails waiting jobs and drops queue state
Add tests/export-queue.test.ts covering serialization, per-job routing,
stale-result rejection, fail/timeout queue advance, and the untouched
autosave paths.