vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Draw the celebration on the main thread, not in a blocked worker
6b7d6a1897dca7a8f42669a02c3f6981192ccd22 · Keenan Nemetz · 13d ago · parent 0f8054ad
Draw the celebration on the main thread, not in a blocked worker The burst never painted. canvas-confetti's default export is built with useWorker: true, and it builds that worker from a blob: URL, which this page's CSP (default-src 'self', no worker-src) blocks. The library does not fall back: it transfers its canvas to the worker with transferControlToOffscreen and then posts to it, so the canvas is created, is full-size, is permanently empty, and reports no error. reload, shared link and bookmark behaviour, and the address-bar strip, all worked — which is why the flag looked handled and the burst looked absent. Creating our own instance with useWorker: false draws on the main thread, where 120 particles for ~2s cost nothing, and needs no CSP change. Widening the policy with worker-src blob: to keep the offscreen path would trade a real security boundary for a party trick. Verified by clicking Merge in a real Firefox against a scratch instance: the canvas has 4148 non-transparent pixels and no transferred context. The same probe against the old build reports getContext failing with InvalidStateError.
server/web/static/app.js +11−5
| @@ -342,9 +342,15 @@ const copyText = (value, done) => { | |||
| 342 | 342 | // gets one burst of confetti. The library is a separate 25 KB asset and is fetched | |
| 343 | 343 | // only on the redirect that carries ?merged=1: no other page pays for it, and a | |
| 344 | 344 | // reload, bookmark or shared link is an ordinary visit because the flag is dropped | |
| 345 | // from the address as soon as it has been read. The library honours | ||
| 346 | // prefers-reduced-motion itself (disableForReducedMotion), and reset() first means a | ||
| 347 | // page restored from the back/forward cache cannot stack a second canvas. | ||
| 345 | // from the address as soon as it has been read. | ||
| 346 | // | ||
| 347 | // The burst is drawn by an instance created with useWorker:false, not by the | ||
| 348 | // library's default export. The default export opts into a Web Worker built from | ||
| 349 | // a blob: URL, and this page's CSP (default-src 'self', no worker-src) blocks that | ||
| 350 | // worker — but the library hands its canvas to the worker anyway, transferring it | ||
| 351 | // with transferControlToOffscreen, and then draws nothing: the canvas is present | ||
| 352 | // and permanently empty, with no error to see. An instance that draws on the main | ||
| 353 | // thread costs 120 particles for ~2s, which is nothing, and needs no CSP change. | ||
| 348 | 354 | (() => { | |
| 349 | 355 | const params = new URLSearchParams(window.location.search); | |
| 350 | 356 | if (!params.has('merged')) return; | |
| @@ -357,8 +363,8 @@ const copyText = (value, done) => { | |||
| 357 | 363 | script.src = '/static/confetti.browser.js'; | |
| 358 | 364 | script.onload = () => { | |
| 359 | 365 | if (typeof window.confetti !== 'function') return; | |
| 360 | window.confetti.reset(); | ||
| 361 | window.confetti({ | ||
| 366 | const burst = window.confetti.create(null, { resize: true, useWorker: false }); | ||
| 367 | burst({ | ||
| 362 | 368 | particleCount: 120, | |
| 363 | 369 | spread: 70, | |
| 364 | 370 | origin: { x: 0.5, y: 0.6 }, | |