Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

#41

Draw the celebration on the main thread, not in a blocked worker

merged · by keenan 13d ago · merged into main at 6b7d6a18 by keenan 13d ago

keenan opened this 13d ago

The burst from #01M5XZDTV4R3EE44RA8P9BGRG4 never drew. The redirect carried ?merged=1, app.js fired, the address bar was stripped, the bundle was fetched 200 — and the page was empty, with nothing in the console.

Cause

canvas-confetti’s default export is built as confettiCannon(null, { useWorker: true, resize: true }), so every window.confetti(...) call tries a Web Worker created from a blob: URL. This page’s CSP is default-src 'self' with no worker-src, so the worker is blocked — and the library does not fall back. It transfers the canvas with transferControlToOffscreen before posting to that worker, so the canvas is created at full size, handed away, and left permanently empty. On Firefox the worker constructor does not even throw, so there is nothing to see.

Fix

Draw on the main thread, with our own instance:

const burst = window.confetti.create(null, { resize: true, useWorker: false });

120 particles for ~2s is nothing, and the CSP stays exactly as it is. Adding worker-src 'self' blob: to keep the offscreen path would trade a security boundary for a celebration, on a surface whose markdown input is untrusted.

Verification, in a real browser

A headless Firefox on a scratch instance, after a real click on the Merge button, reading the canvas back:

build canvas pixels transferred
before 1360×682 null, InvalidStateError true
after 1360×682 4148 painted false

Screenshot of the same run shows the particles over the request page. This is the check the earlier change lacked: counting <canvas> elements said “works”, because the element exists either way. Only painted pixels distinguish the two.

server/ui_test.go gains the guard it can hold without a browser — that app.js creates a useWorker: false instance — with the reason written down, since the symptom is invisible from the server side.

docs/ui-plan.md records why the default export is unusable here.

Checks

gofmt, go vet ./..., go test ./..., vole workflow check, and vole workflow run verify (7 steps, -race) all pass.

Closes #01M5Z00VJ6215NFZ8F3KJ48FXA

Discussion (2)

keenan 13d ago

Added a second commit to this branch: the burst was working but looked wrong — 120 particles at scalar: 1 from one point read as a small puff on a full-width page.

It is now two of the library’s own examples:

  • the realistic look preset for the opening burst (particleCount: 500, spread: 180, ticks: 400, gravity: 0.5, decay: 0.92, startVelocity: 30, scalar: 1.2);
  • the fireworks volley from both edges, tapering over 1.2s instead of the example’s 15, so the page you land on is not raining confetti for a quarter of a minute.

Measured in a headless Firefox at 1360x682 after a real Merge click: 181 frames over 3s, median 17ms, p95 20ms, worst 23ms — 60fps — and the canvas is gone 6s later, because the volley’s timer ends with the burst instead of lingering.

Both settings honour prefers-reduced-motion through disableForReducedMotion, and the CSP is still untouched.

keenan 13d ago

The look change could not ride on this branch after it merged, so it now has its own request: #01M5Z0K6ZE1XVPS5KKSN8Y0SD3 (branch 01M5Z0R6T6-confetti-look, based on this merged head). Nothing in this request changed.