Follow-up from the #13–#26 integration review (quality-only, non-blocking; deferred deliberately because the web layer has no test net — UI testing is browser-only/locked — so a blind refactor carries regression risk).
What
The optimistic / pending / reconcile machinery in web/src/store.ts grew across #19/#20/#21/#22/#23 and now has parallel near-copies:
-
Post-dating-by-sha is copy-pasted 3× (move, create, edit reconcile blocks in applySnapshot):
const postDates = board.head != null && (p.issuedHead == null || board.head.sha !== p.issuedHead.sha);
→ extract postDatesIssued(head, issuedHead): boolean.
-
The three pending-write bodies (move, createTask, editTask) repeat the same scaffold: capture issuedHead, optimistic apply, set the pending entry, clearDelayTimer + an identical 200ms setTimeout that flips showPending.
→ extract a startPendingTimer(key, set, mapName) (or similar) helper for the delay-gate.
The #23 slice already unified the failure path (handleWriteFailure); this is the remaining issue-path + reconcile-loop duplication.
Out of scope
Leave the per-type landed checks as-is — their semantics genuinely differ (lane membership for move, uuid-in-board for create, confirm-and-drop for edit). This is a DRY pass on the shared scaffold only.
Verification
No automated UI tests exist (locked: browser-only). After the refactor, browser-verify move/create/edit happy + failure paths on the single-origin server, and gate on tsc --noEmit (root + web) + web build.
Follow-up from the #13–#26 integration review (quality-only, non-blocking; deferred deliberately because the web layer has no test net — UI testing is browser-only/locked — so a blind refactor carries regression risk).
What
The optimistic / pending / reconcile machinery in
web/src/store.tsgrew across #19/#20/#21/#22/#23 and now has parallel near-copies:Post-dating-by-sha is copy-pasted 3× (move, create, edit reconcile blocks in
applySnapshot):→ extract
postDatesIssued(head, issuedHead): boolean.The three pending-write bodies (
move,createTask,editTask) repeat the same scaffold: captureissuedHead, optimistic apply,setthe pending entry,clearDelayTimer+ an identical 200mssetTimeoutthat flipsshowPending.→ extract a
startPendingTimer(key, set, mapName)(or similar) helper for the delay-gate.The #23 slice already unified the failure path (
handleWriteFailure); this is the remaining issue-path + reconcile-loop duplication.Out of scope
Leave the per-type landed checks as-is — their semantics genuinely differ (lane membership for move, uuid-in-board for create, confirm-and-drop for edit). This is a DRY pass on the shared scaffold only.
Verification
No automated UI tests exist (locked: browser-only). After the refactor, browser-verify move/create/edit happy + failure paths on the single-origin server, and gate on
tsc --noEmit(root + web) +webbuild.