From aad0d25463b8fb9e6e9f4cf19bd86a89e73756cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C5=ABdolfs=20O=C5=A1i=C5=86=C5=A1?= Date: Wed, 28 Oct 2026 10:59:11 +0100 Subject: [PATCH] Add ADR for data access and reactivity Both apps should react to changes in their backend's data without disturbing the user. Record how data is cached, invalidated by ref updates and shown when newer data arrives. --- docs/adr/0005-reactivity.md | 68 +++++++++++++++++++++++++++++++++++++ shared/ui/AGENTS.md | 4 +++ 2 files changed, 72 insertions(+) create mode 100644 docs/adr/0005-reactivity.md diff --git a/docs/adr/0005-reactivity.md b/docs/adr/0005-reactivity.md new file mode 100644 index 000000000..8c4bea94a --- /dev/null +++ b/docs/adr/0005-reactivity.md @@ -0,0 +1,68 @@ +# 5. Data access and reactivity + +Date: 2026-10-28 + +## Status + +Accepted + +## Context + +Each app fetches its data in its own way. Desktop caches some reads through +a `cached()` helper, and explorer through its own copy. Neither +reacts to changes in its backend's data: desktop's backend emits only node +status and download progress, its repository list polls while replication is +pending, and radicle-httpd has no push API. + +Both apps should react to such changes without disturbing the user. Lists +and counts can refresh in place, but a revision or diff under review must +stay as it is, showing that newer data is available until the user applies +it. + +We also considered events that carry the new data, which ties them to each +transport's response shapes; a cache of our own on Svelte stores, which would +have to rebuild deduplication, retries and structural sharing; and polling in +explorer, which loads the third-party nodes it browses. + +## Decision + +- Core defines the keys of the data shared code reads, by domain identity and + nested under the repository, so that one prefix covers all of a + repository's data. Each app supplies the fetchers for them. +- A shared query cache on TanStack Query keeps the results, deduplicates reads + and applies writes. It lives in `@radicle-gui/ui` because it needs Svelte's + reactivity, and only the apps' views use it; shared components keep taking + props, as in ADR 3. +- Route loaders load through the cache and views read from it, so that a + view's first data is already cached, and paginated lists keep their pages + in it. +- Content-addressed data, keyed by an oid, never changes, so it is cached + without expiry and never invalidated. +- Each app forwards its backend's ref updates: the repository and the names of + the refs that changed. A pure function in core maps them to the keys they + affect, which the cache invalidates and refetches while they are shown. +- A view whose content the user is acting on, such as a revision under review, + renders a pinned snapshot and shows that newer data is available when the + cache's data is a different object. Refetches keep unchanged objects + identical, so a different object means the data changed. +- Desktop's backend forwards its node's events, and invalidates more broadly + when an event names no refs or events may have been missed. +- Explorer has no event source until radicle-httpd can push ref updates. It + refetches mutable data on navigation and when the window regains focus, and + so does desktop while its node is stopped. + +## Consequences + +- Reads are deduplicated, and updates no longer reset what the user is + doing. +- Both apps depend on TanStack Query, which replaces `cached()` and explorer's + copy of it. Events replace desktop's polling of its repository list. +- Desktop's backend has to subscribe to its node's events. +- Explorer views move to runes before they use the cache. +- The rule in `shared/ui/AGENTS.md` that components keep their local state + when props change becomes essential. + +## Open questions + +- Whether httpd grows finer endpoints, such as COB timelines, and a push API + for ref updates. diff --git a/shared/ui/AGENTS.md b/shared/ui/AGENTS.md index 8188da121..719e441c3 100644 --- a/shared/ui/AGENTS.md +++ b/shared/ui/AGENTS.md @@ -43,6 +43,10 @@ interactions such as editing, but the app owns all data and I/O. in, and never fetched, cached or persisted by the component - No side effects: no `fetch`, `invoke`, `localStorage`, `location`, polling or app context. Context defined in this package, such as the theme, is fine +- Props can change at any time when newer data arrives. Keep local state such + as drafts, scroll position, selection and expanded threads across such + updates, and key lists by stable ids so items update instead of being + recreated - Style only with scoped CSS and the shared design tokens ESLint enforces the import and global restrictions.