feat(lists): collaborative freshness poll + presence — and a fix for blank grid cells (#53) - #123
Merged
Merged
Conversation
Wires POST /api/lists/{id}/data/versions into the list detail screen as a
combined freshness poll and presence heartbeat.
- The heartbeat is tied to the composition: DisposableEffect starts it on
entry and stops it on dispose, and the ViewModel refuses to beat once the
screen is gone (so a late edit callback cannot revive it).
- Each beat quotes the versions of the rows on screen plus the focused row,
and the answer is applied surgically: `changed` repaints exactly those
rows, `deleted` drops them. The table is never refetched.
- Presence is rendered as an avatar cluster in the top bar, mirroring the
documents editor's indicator — a separate copy, since features do not
depend on each other.
- Polling is frugal: 10s while rows are moving, 60s when a beat brings no
news or fails, stop after 10 quiet minutes (interaction revives it), and
stop entirely when the server reports `collaborative: false`. At most 500
row versions per request, the server's documented ceiling.
Rows now also carry their `version`, and RowDto accepts the `rowData` key
the live API actually sends (`data` still works), without which the poll has
nothing to compare against.
Closes #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #53. Part of epic #49.
RowDtoread the cell map from adatakey, but the API sendsrowData. So every list row'scells decoded as empty — the grid has been rendering "—" for every cell against real data.
I verified this independently of the agent that found it:
rowDataappears 12 times in theOpenAPI spec and 4 times in the help-centre lists reference;
dataappears nowhere as the rowfield name.
RowDtonow readsrowData ?: dataand carriesversion;ListRowgainedversion: Int? = null. Existingdata-shaped tests still pass unchanged.This is adjacent to #53 rather than inside it, but the poll could not have worked without it — there
would have been no versions to quote.
The heartbeat
DisposableEffect(Unit) { start(); onDispose { stop() } }, plusan
isOnScreenflag so a late callback (a row save completing after the user navigated away)cannot revive the loop, plus
onCleared(). Presence is cleared on stop, so a returning screennever shows who was here.
rowVersionsfor the rows in state plusfocusedRowId;changedreplaces exactly those rows anddeletedremoves them.getListDetail/getRowsis never called from the poll path — asserted with a call counter.+Noverflow, nothing when empty) as a deliberate copy — no dependency on
:feature:documents.minutes (any interaction revives it), and stop outright on
collaborative: false— and staystopped in that case. This mirrors the first-party grid's own policy documented in
helpapi/lists.txt, which warns the backing database bills for being awake and does not autosuspendwhile anything polls. Requests are capped at the documented 500-row ceiling.
Live probe: what it established, and the cleanup
Created a list
tmp-53-freshness-probe, one row, and onecollaboratorshare link; then revokedthe link, deleted the row, and deleted the list. (I independently re-verified: the account is back
to its single pre-existing list,
"New list", which was never touched.)Previously unobservable, now confirmed:
changed[]elements are whole rows in the same shapeGET …/datareturns —{ id, rowData, version, createdAt, updatedAt, createdByUser{id,username,displayName,avatar}, lastEditedByUser{…} }. NoterowData, and user objects keyedid, notuserId.deletedis a bare array of id strings (an unresolvable id comes back verbatim).collaborativeis false — so the flag is advice aboutpolling, not an access gate.
collaborative— it stayedfalse. The flag tracksactual watchers/collaborators, not link existence.
Still unconfirmed: the
users[]element shape. Establishing it needs a second real person on thelist, and the only route to that would have been emailing a stranger an invite. It is modelled from
the help centre (
userId, name, username, color, focusedRowId) and decodes tolerantly forid/displayName; a nameless entry falls back to username then id, and an entry with no resolvableid is dropped rather than drawn as a blank avatar.
Verification
./gradlew :app:assembleDebug testDebugUnitTest→ BUILD SUCCESSFUL, 1440 tests, 0 failures(
:feature:listsis 268). 14 heartbeat cases, 6 repository, 3 presence, plus mapper additions.Compose presence tests compile, not executed (no emulator).
Follow-ups
up. A changed row we were not holding is appended defensively, but insertions are not in
changed. A manual reload is still needed — worth its own issue if it matters.{ data }form;If-Match+{ changes, expect }per-fielddeltas and
409 version_conflicthandling are untouched.versionis now plumbed through, whichis the prerequisite.
version, the poll degrades to presence-only rather thanspamming the whole table. Deliberate.