Repository navigation
feat(service): a board save answers the transaction that moved its live row - #4432
Conversation
…ve row The scene's write returns `pg_current_xact_id()::text` from its own RETURNING, and the save answers it as `txid` where the write moved the board's live row (a new board, or the drawing the scene holds or its image changed), as Neon's guide to optimistic mutations has it. A save that leaves that row as it stood, such as one moving only the scene's elements or one changing nothing, answers none, since no follower of the board would ever see its transaction. The row's prior mark is read under the task lock the write already takes. Ported from the txid half of #3757 (11af552), which merged without it. Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| const moved = mark.appliedDrawing !== row.appliedDrawing || mark.imaged !== row.imaged; | ||
| return moved ? { board, txid: row.txid } : { board }; |
There was a problem hiding this comment.
🟡 Concurrent board saves return phantom confirmations
When writeScene waits behind another save, mark can reflect the board before that save commits. An unchanged live row then returns a txid, leaving its follower waiting for a change it cannot see.
Learn more
lockTask joins task_board in the same SELECT FOR UPDATE OF task that waits for the task lock. Under PostgreSQL READ COMMITTED, the waiting statement can retain an earlier snapshot for the unlocked joined board row, even after it obtains the task lock. The upsert runs afterward and observes the committed board, so comparing its result with the earlier mark can incorrectly claim that its transaction changed the live query.
Example: Save A inserts the first board row with appliedDrawing: 0; save B starts its lock query while A holds the task lock. B sees a null joined board mark, then A commits. B writes the same appliedDrawing: 0 with no image and returns a txid, although the live row stays unchanged.
Recommended fix: Acquire the task lock in one statement, then read the board mark in a separate statement under that lock before upsertScene. This gives the comparison a snapshot taken after any preceding save commits. Add a PostgreSQL concurrency test that overlaps two saves; PGlite serializes access to its connection and cannot reproduce this ordering.
Was this helpful? React with 👍 or 👎 to provide feedback.
Service half of the board-save area of the txid rollout: saving a task's whiteboard returns the txid of the write that changed its row, and a save that changes nothing returns none. The host half (waiting on it through the board follow with
confirmWrite, #4423) follows in its own PR.Ported from the txid half of #3757 (
11af552b, from that PR's earlier history; its branchluke/neon-realtime-task-recordis gone). #3757 merged without it.What changes
writeScene(packages/database/src/models/board-store.ts) returnspg_current_xact_id()::textin the upsert's ownRETURNING, as Neon's mutations guide says. It opens no extra transaction: the write already runs under the task row's lock, and that lock read now also reads where the board's live row stood before.txidonly where the write moved the board's live row (boardLiveStatement: a new board, a changedappliedDrawing, or a change in whether it holds an image).md5of the elements. Today's row is a mark without the elements, so a save that moves only elements, or changes nothing, never shows on the follow.awaitTxIdwould then sit out its whole 10 s timeout, so these saves answer no txid.boardSaveAnswerSchema={ board, txid? }(a decimal string) is thesaveBoardoutput.HostedTaskClient.saveBoardanswers it whole. The host only adapts to the new shape here (saved.board) and doesn't wait yet.Tests
board-store.test.ts: the first save answers a committed txid. An elements-only save and an unchanged save answer none. A save that takes the drawing and its image answers a newer committed txid.tasks-app.test.ts(web, over HTTP): the firstPUT /boardanswers a txid, and the same scene again answers none.Checks run: typecheck for database, features, service, host and web; vitest on the files above plus
compose-tasks.test.ts,board-look.test.tsand the luke-mcp app test; biome;./scripts/oxlint.sh.Made with Luke