feat(queue): add grouped writes that flush as one change #206

Merged
nalum merged 1 commit from feat/queue-grouped-writes into main 2026-08-26 18:33:26 +00:00
Owner

Closes #182.

The offline queue stored one RPC per entry, drained FIFO. That is enough
for every action shipped today and not enough for an action that is only
correct as a set.

The first one that needs it: making a job a step of another. The server
refuses SetParent while the job carries a repeat rule or a deadline
rather than stripping them silently, so the client must clear both first.
Sent one at a time, a refusal at the last call leaves a job stripped of its
date and its rule for an action that never happened.

A group is one queue row holding several steps, each with an optional
undo built from values captured at enqueue. Steps run in declaration
order and cannot interleave with anything else. A refusal at step N
compensates 1..N-1 in reverse; a compensation that stalls finishes on
reconnect; a compensation that is itself refused stops the unwinding and
keeps the row as failed with both reasons, so the sync sheet can say what
could not be put back. Confirmed steps are persisted, so a group cut off
mid-flush resumes where it reached instead of replaying from the top.

The sync sheet shows a group as one change, labelled with the action a
person took rather than the RPCs it decomposes into.

Single-op enqueue keeps its exact shape and semantics; two regression tests
pin that. convertToStep is wired as the one proof, and the convert
affordance now correctly appears for jobs that repeat or have a deadline,
because the group clears them and puts them back on failure.

🤖 Generated with Claude Code

Closes #182. The offline queue stored one RPC per entry, drained FIFO. That is enough for every action shipped today and not enough for an action that is only correct as a set. The first one that needs it: making a job a step of another. The server refuses `SetParent` while the job carries a repeat rule or a deadline rather than stripping them silently, so the client must clear both first. Sent one at a time, a refusal at the last call leaves a job stripped of its date and its rule for an action that never happened. A group is one queue row holding several steps, each with an optional `undo` built from values captured at enqueue. Steps run in declaration order and cannot interleave with anything else. A refusal at step N compensates 1..N-1 in reverse; a compensation that stalls finishes on reconnect; a compensation that is itself refused stops the unwinding and keeps the row as failed with both reasons, so the sync sheet can say what could not be put back. Confirmed steps are persisted, so a group cut off mid-flush resumes where it reached instead of replaying from the top. The sync sheet shows a group as **one** change, labelled with the action a person took rather than the RPCs it decomposes into. Single-op enqueue keeps its exact shape and semantics; two regression tests pin that. `convertToStep` is wired as the one proof, and the convert affordance now correctly appears for jobs that repeat or have a deadline, because the group clears them and puts them back on failure. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(queue): add grouped writes that flush as one change
All checks were successful
check / commits (pull_request) Successful in 7s
check / go (pull_request) Successful in 2m36s
android / build (pull_request) Successful in 6m48s
check / web (pull_request) Successful in 4m20s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 5s
c8d9eb0599
The offline queue describes one RPC per entry, which is right for every
write that stands on its own and wrong for an action that is only
correct as a set. Making a job part of another job is the first: the
server refuses SetParent while the job carries a repeat rule or a
deadline (ADR-0025 — a step converts as-is or not at all), so the client
must clear both first. Sent as three independent entries, a refusal at
the last one leaves the household's job stripped of its date and its
repeat for an action that never happened — and offline, hours after
anyone could connect the two. The jobs and calendar redesigns bring more
of these.

The server has no cross-RPC transaction and is not growing one for this,
so a group's atomicity is compensation: each step carries the write that
undoes it, built from the values read at enqueue, and a refusal at step N
undoes 1..N-1 in reverse. A refused compensation stops the unwinding and
the group is recorded as partly put back, naming both verdicts — the one
thing the sync sheet must never leave unsaid. The whole group is one
pending_op row, which is what buys the rest: it cannot interleave with
other entries, it reads as one line in the sheet under the name of the
action a person took, and its progress rides with it, so a group cut off
by a lost network resumes at the step it reached instead of replaying
from the top.

Rejected: a server-side batch RPC (a new endpoint per combination, and
the mutation trail would stop describing what a person did); undoing by
re-reading the entity at failure time (the values are gone by then, and
offline there is nothing to read); and treating a half-sent group as
success with a warning, which is the outcome this exists to prevent.

Test report

Suite Tests Result Skipped
Unit 1410 ✅ pass 1
Integration 113 ✅ pass —

Coverage: 26.9%

Updated by the check workflow · commit 5ab42d7595

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1410 | ✅ pass | 1 | | Integration | 113 | ✅ pass | — | **Coverage:** 26.9% <sub>Updated by the check workflow · commit 5ab42d7595c18f7e87051c38e3957fd19ac3721e</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) 43 ✅ pass 0

Coverage: 3.0% of lines

Updated by the android workflow · commit 5ab42d7595

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 43 | ✅ pass | 0 | **Coverage:** 3.0% of lines <sub>Updated by the android workflow · commit 5ab42d7595c18f7e87051c38e3957fd19ac3721e</sub>
nalum force-pushed feat/queue-grouped-writes from c8d9eb0599
All checks were successful
check / commits (pull_request) Successful in 7s
check / go (pull_request) Successful in 2m36s
android / build (pull_request) Successful in 6m48s
check / web (pull_request) Successful in 4m20s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 5s
to 5ab42d7595
Some checks failed
check / commits (pull_request) Successful in 16s
check / go (pull_request) Successful in 2m40s
android / build (pull_request) Successful in 6m32s
check / web (pull_request) Successful in 4m10s
check / report (pull_request) Successful in 4s
android / report (pull_request) Successful in 4s
android / report (push) Has been cancelled
check / commits (push) Has been cancelled
check / go (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
android / build (push) Has been cancelled
2026-08-26 07:03:47 +00:00
Compare
nalum changed target branch from feat/household-visible-calendar to main 2026-08-26 18:33:12 +00:00
nalum merged commit 5ab42d7595 into main 2026-08-26 18:33:26 +00:00
nalum deleted branch feat/queue-grouped-writes 2026-08-26 18:33:34 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!206
No description provided.