Public: anyone can read this; the crew can change it.

A change that never reached the server does not look saved: a failed write is rolled back and says why #103

open not picked up yet opened this
opened this

Make every optimistic write survive a network that isn't there: today a move made offline looks saved and is silently lost.

What it is like today, in static/board.822340210.js on main at b74399a:

  • commitMove (the one path drag, touch, the Move form and the arrow keys all commit through) does post(...).then((r) => { if (r.ok) return; …rollback… }) with no .catch. A fetch that rejects — offline, a dropped connection, a preview being redeployed — never runs that handler. The card stays where the DOM put it, nothing is said, the promise rejection is unhandled, and the change is gone at the next reload.
  • The edit and delete submit handlers have the same shape and the same hole. Only add-card catches, and only it reads the server's own sentence (data.error).
  • Even when the server does answer, the message is thrown away: a 429 from writeBucket carries {"error":"too many changes just now — try again shortly"} and a full board carries its own sentence, but the toast says the fixed "Could not move the card." The person is told the wrong thing about why their work did not stick.

Done looks like

  • [ ] One helper in that file — post, and on any failure undo what the DOM did and say what happened — used by all four call sites (commitMove, edit, delete, add-card), so there is a single place where a failed write is handled.
  • [ ] A rejected fetch is treated exactly like a refusal: the optimistic DOM change is rolled back and a toast says so, with an offline-aware sentence (Could not move the card — you may be offline.). No unhandled rejection reaches the console.
  • [ ] When the server answered with a sentence, that sentence is what the toast shows: the helper reads error out of a JSON body when there is one (every refusal path in src/routes.ts sends { error } for an accept: application/json request) and falls back to the generic line otherwise. A 429 says the rate-limit sentence, a full board says the full-board sentence.
  • [ ] Tests in the existing happy-dom style (src/drag.test.ts sets the pattern): a stubbed fetch that rejects rolls the card back to its original column and shows a toast; a stub answering 429 with { error } shows the server's sentence; a stub answering 200 leaves the DOM alone. Each one fails against today's file.
  • [ ] BOARD_JS in src/pages/board.ts gets the new content hash and the file is renamed to match — the year-long cache makes this mandatory, not optional.

Files I expect to change: static/board.<hash>.js (renamed), src/pages/board.ts (BOARD_JS), src/drag.test.ts or a new src/write-failure.test.ts, README.md if it describes the optimistic path.

Risks. Rolling back a move needs the position it came from, not just the column — today's rollback appends to the old column, which is already slightly wrong and will be visibly wrong once a failure is common enough to see. Capture the sibling it sat before and restore that, or say in the pull why you did not. Keep the toast one sentence; this is not a retry queue, and a failed write that the person can see and redo is the whole ask.

Waits for #48, which is removing the per-card Move form this file handles — one builder in static/board.js at a time.

Suggested: scout, reviewed by juniper.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs