feat(service,host): a task's board follows over Neon Realtime
https://entire.io/gh/ReviewStage/luke/trails/32
Asked for: one small PR that makes a task's board update live over Neon Realtime, standing on the plumbing stack (#4071 → #4073 → #4072) and nothing else from the Realtime tree. Only the board part of #3757 is taken: no pages or calls, no txid writes, no web-tab extras, no same-user check.
What changes
Service
boardLiveStatement(userId, taskId)in the database board store: one row a board overtask_board, withdrawingNumber,appliedDrawing, andimaged(image is not null). It is a mark, so it carries no elements or image bytes. It is inner-joined throughmemberon the task's workspace, as Neon's guide authorizes a live read, so an account that leaves the workspace stops seeing the row.live.taskBoardon the oRPC live contract (GET /api/live/task-board?id=), built the way #3232 buildslive.taskInbox. A non-UUID id, or a task the account does not reach (read throughreadTask), answersnot-found. A deployment without Realtime, or a seal that is refused, answersunavailable.- Wire:
taskBoardLiveAnswerSchemaandtaskBoardLiveRowsSchema(goldens recorded),HOSTED_SERVICE_PATH.LIVE_TASK_BOARD, andHostedTaskClient.boardLive.
Host
board-follow.ts:boardMovesfollows the sealed statement throughliveRows, decodes the rows at the boundary, and emits the rows on the first delivery and on every delivery that moved the mark.compose-tasks.ts: the board's first read on open, start, and refresh stays asmainhas it. Only the refresh changes. The call's activity (a draw settling, a look starting, the brain stopping) no longer reads the board. Instead the open task's board follow queues the samereadBoardeach time the mark moves, and the board reaches the window through thetasks.changedview it already uses.- If the service does not answer a seal, or a follow stops, the board is sealed again after 2 s. That wait doubles up to 1 min and goes back to 2 s once a delivery lands, so a Realtime outage gets backed-off retries rather than a hammering.
not-foundends the follow. Closing the task, deleting it, leaving it, or signing out lets the subscription go.
Tests
live-statements.test.ts: the board statement passesmutableFunctionsOfand thedrizzleAdapter()seal check.board-store.test.ts(new) runs the statement on PGlite: a teammate sees the row move with a drawing and with a save that carries the image, and an outsider reads nothing.live-procedures.test.ts: the board is sealed for the bearer and the task. Another account and a non-UUID id getnot-found, and no Realtime getsunavailable.board-follow.test.ts: the first delivery and each moved mark are emitted, a repeated mark emits nothing, and a row that doesn't decode fails.compose-tasks.test.ts: the whole flow, from the open read, to a read on first delivery, no read on an unmoved mark, a read on a moved mark, a reseal after the follow stops, and unsubscribing on close.
Checks I ran one at a time (check:affected runs out of memory here): biome and oxlint on the changed files, repository-checks.sh, knip, discarded-effect, and typecheck for database, features, service, host, desktop and web. I also ran the host and features suites, the service live and board tests, and the web vite build. The bundle budget passes: app is 978617 of 980869 gzipped bytes, so the budget is not raised.
Risk
The board no longer refreshes on the call's activity. If Realtime is down, a drawing Luke makes mid-call appears only after the task is opened again or refreshed, and look_at_board may wait out its deadline. This PR is meant as the low-traffic production trial of exactly that.
Made with Luke