feat(tree): server-authoritative realtime tree updates #15

Merged
Ghost merged 5 commits from feat/realtime-tree-server into develop 2026-06-20 19:48:36 +03:00

5 Commits

Author SHA1 Message Date
vvzvlad
f650d2591b fix(tree): address realtime-tree-server review findings
- make addTreeNode receivers idempotent (invalidateOnCreatePage guard +
  buildTree dedup) so the author's self-echo no longer duplicates the node
- broadcast realtime tree updates for bulk copy/duplicate and import via a
  root refetch: PAGE_CREATED now carries spaceId and the WS listener falls
  back to refetchRootTreeNodeEvent when no per-node snapshot is present
- remove the now-dead client-relay inbound path (isTreeEvent/handleTreeEvent)
  that remained a stale-restriction-cache attack surface
- honest string|null cast for a root move's parent id
- add tests: buildTree dedup; onPageCreated per-node vs refetch branching

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:48:06 +03:00
claude code agent 227
be2530a0b9 chore(tree): document the restriction-cache primitive; drop dead notify code
Release-cycle audit flagged WsService.invalidateSpaceRestrictionCache and
WsTreeService.notifyPageRestricted/notifyPermissionGranted as never-wired dead
code. Investigation: this community fork has NO page-permission grant/revoke/
restrict mutation site (the page-access repo mutators have zero callers — that
flow is EE / not yet built), so there is nothing to wire them into.
- Keep invalidateSpaceRestrictionCache (it's the one-line correctness primitive
  the future permission-mutation path must call to avoid the 30s stale-cache
  window) but document exactly that + add a test that it deletes only the
  space-scoped cache key.
- Remove the untested, security-adjacent dead methods notifyPageRestricted /
  notifyPermissionGranted and their now-orphaned helpers emitToUsers /
  emitToSpaceExceptUsers (no remaining references; build confirms). A future
  permission-change realtime feature can reintroduce them wired + tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 15:50:53 +03:00
claude code agent 227
5d5f61fc6e fix(tree): place remote moves by position; remove stale node on move-into-restricted
Release-cycle review found two move-path issues:
- Remote moves were placed at index:0 (broadcastPageMoved hardcodes index:0),
  so every observer rendered the moved node at the TOP of its new siblings
  until refetch. Client moveTreeNode now places by fractional position
  (treeModel.placeByPosition, mirroring addTreeNode/insertByPosition) and
  applies the payload's pageData (title->name, icon, hasChildren) so receivers
  keep the node correct.
- Moving a page under a restricted ancestor left a stale named node (title/
  slugId/icon) in the trees of users who lost visibility. broadcastPageMoved
  now derives one FRESH hasRestrictedAncestor decision and drives both paths
  from it: when restricted, the move goes to authorized users only
  (emitToAuthorizedUsers, not the space-cache-gated emitTreeEvent) and a
  compensating deleteTreeNode goes to the unauthorized complement (same fresh
  getUserIdsWithPageAccess set) — disjoint, no stale-cache window. Non-restricted
  moves are unchanged (one moveTreeNode to the room).

Follow-up (noted): invalidateSpaceRestrictionCache is still unwired at
permission-mutation sites; the open-space fast path can lag up to the 30s TTL,
but the move/delete consistency above no longer depends on it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 14:01:37 +03:00
claude code agent 227
31d6498b24 docs: remove implemented realtime-tree-server-authoritative plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 08:27:56 +03:00
claude code agent 227
046132afc7 feat(tree): server-authoritative realtime tree updates
The sidebar page tree only updated on other clients when a change was made
via the UI tree, in an open tab, within a ~50ms client relay window — API/MCP/
AI/import changes never propagated. Move the source of truth to the server.

Server:
- Enrich PageEvent with thin TreeNodeSnapshot(s) so the WS listener never reads
  the DB (avoids the in-transaction visibility race). insertPage fills the
  create snapshot from its returning() row; removePage ships only the deleted
  subtree ROOT (client treeModel.remove drops descendants); restorePage carries
  spaceId.
- New PAGE_MOVED event from movePage with old/new parent + position + snapshot
  (generic PAGE_UPDATED stays for content/rename).
- WsService.emitTreeEvent mirrors emitCommentEvent (per-space restriction gate:
  spaceHasRestrictions -> hasRestrictedAncestor -> broadcastToAuthorizedUsers);
  author NOT excluded so non-UI creators see their own page (receiver is
  idempotent).
- WsTreeService.broadcastPageCreated/Deleted/Moved + broadcastRefetchRoot;
  new PageWsListener (create/delete/move/restore) registered in WsModule.

Client:
- Remove the client relay (emit + setTimeout(50)) from create/move/delete;
  keep optimistic local updates. Make the optimistic create insert id-idempotent
  (find-then-skip) so the now-fast server addTreeNode broadcast can't race it
  into a duplicate row. addTreeNode inserts by fractional position among loaded
  siblings (consistent order across clients).

Restore uses refetchRootTreeNodeEvent (robust for subtree re-attach). Rename/icon
updateOne and cross-space move realtime are deferred (commented as follow-ups).

Implements docs/backlog/realtime-tree-server-authoritative.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 08:27:56 +03:00