Portal data plane
Portal data plane
Section titled “Portal data plane”Cross-cutting design aspect for how Portal UI and Habitat stay consistent about data: requests, in-memory views, durable snapshots, conditional HTTP cache, offline writes, sync, refresh, and live updates.
This is not a product feature module (unlike Chat or Diary). Sibling docs under docs/aspects/ hold the detailed mechanisms. Feature modules link here when they touch the shared data path.
Related terms: Portal data plane (EN) / Portal 数据面 (zh); also “data-flow aspect”.
Attention (Inbox vs reminder vs local interrupt, Habitat sleep-until-next, shell Attention hub) → Notification and reminder.
Why this aspect exists
Section titled “Why this aspect exists”Today the pieces below often evolve feature-by-feature. Without one vocabulary and one set of defaults, DX and UX suffer: full-list reloads after small writes, mixed cache meanings, hand-rolled loading state, and confusion between “offline” and “outbox pending”.
Map of the plane
Section titled “Map of the plane”flowchart LR write[Write] mem[In_memory_view] idb[IDB_snapshot] outbox[Outbox] rpc[Habitat_RPC] etag[ETag_304] sync[Sync_flush] refresh[Refresh] live[Live_channel] write --> mem write --> idb write --> outbox write --> rpc outbox --> sync sync --> refresh rpc --> etag etag --> mem rpc --> idb live --> mem refresh --> rpc
| Concern | Role | Primary doc / code |
|---|---|---|
| Habitat fetch gate | No RPC when offline or Habitat not connected | isHabitatFetchAvailable |
| In-memory view | What the React tree shows | Feature state / zustand; future shared hooks |
| IDB snapshot | Durable read cache; offline read; failure fallback | Offline platform |
| ETag / 304 | Transport-layer conditional GET; not the business snapshot | Habitat REST (If-None-Match) |
| Online write / outbox | Prefer Habitat; enqueue on transport failure | Offline platform |
| sync | Reconnect / visibility: flush outbox then module refreshAll | Page refresh, OfflineSyncBootstrap |
| refresh | User re-fetches current view | Page refresh |
| live channels | Limited auto (chat stream, conversation / pomodoro events) | Page refresh |
temp id / client_op_id | Offline create identity and idempotent retry | Offline platform |
| connectivity UI vs outbox UI | Link down ≠ pending writes | connectivity-notice vs sync toast |
| cache scope | Isolate by Habitat URL + subject | resolveHabitatCacheScope |
| PWA Service Worker | Shell static assets only; not business RPC | Remote access |
Unified principles
Section titled “Unified principles”- One vocabulary: snapshot, outbox, sync, refresh, live, gate. Do not narrate the plane in React Query terms.
- Three cache layers stay distinct: in-memory view ≠ IndexedDB business snapshot ≠ HTTP ETag/304.
- Limited auto: sync + existing live channels. No global list polling. Multi-device pull uses manual refresh unless a stronger protocol exists.
- Invalidate with granularity: prefer local reload or key-level invalidate after writes. Do not treat “reload the whole module list” as the only tool (except agreed sync →
refreshAll). - One optimistic write source: offline-store / outbox. Do not add a second optimistic mutation cache beside it.
- DX may grow on this plane: shared hooks are welcome; defaults follow these principles. Do not adopt the React Query library.
- Features must not fork the path: reads use
withOfflineCache(or identical semantics); writes follow the outbox /preferOnlineWritecontracts.
Best practices
Section titled “Best practices”- Route list/get reads through
withOfflineCache(or the same online-Habitat-first / offline-snapshot semantics). - On writable modules: online writes via
preferOnlineWrite; offline or transport failure via outbox; honor temp-id andclient_op_idcontracts. - Bind header Refresh and pull-to-refresh to the same page
reloadhandler. - After a local write, reload or patch the affected view; reserve full-module
refreshAllfor sync (and explicit product needs). - Keep connectivity chrome separate from outbox pending / failed toasts.
- When adding a module, register offline adapters and caps per Offline platform.
- Hand-roll a second IndexedDB or memory cache that bypasses portal-sdk.
- Conflate browser/PWA “reload” (shell assets) with business refresh.
- Treat 304/ETag success as “IDB is fresh” without an explicit protocol that ties them together.
- Default to focus-refetch or background list polling (React Query-style automation).
- Use React Query Persist (or similar) against the same business namespaces as
freeanima-portal-cache. - Show “reconnect and retry all” while Habitat is
connectedand the queue is merely working.
Relation to React Query
Section titled “Relation to React Query”| React Query | This plane | |
|---|---|---|
| Primary job | In-memory request lifecycle and UI subscriptions | Portal↔Habitat consistency including durable offline |
| Persist | Optional memory rehydrate; no outbox / temp-id / Habitat gate | IndexedDB snapshot + outbox + id-map |
| Defaults | Often focus refetch / broad invalidation | Limited auto + manual refresh |
| Backend cache protocol | Out of scope | May grow (ETag tied to snapshots, versions) |
Decision: do not introduce @tanstack/react-query. The plane may later grow RQ-like DX (hooks, explicit invalidate, controlled retry) on top of portal-sdk. That is one coordinate system, not “RQ + offline stacked”.
Automation must follow FreeAnima (this doc + Page refresh), not React Query defaults.
DX evolution (direction only)
Section titled “DX evolution (direction only)”Not implemented by this doc. Advance when pain is real (no speculative QueryClient clone):
- Shared read hook (
data/loading/error/reload) wrappingwithOfflineCache - Shared write path that always exits through one invalidate/reload entry
- Explicit invalidate by cache key or module
- Key helpers for
scope|namespace|id; inflight dedupe - Shared offline / outbox status primitives
- Consistent pagination / load-more state machine
- Optional devtools (IDB, outbox, gate, recent keys) and test fixtures
- Lint or review rules: no second cache path
- Separately: tighten ETag/304 with IDB into one protocol when bandwidth or multi-device cost justifies it
Child docs
Section titled “Child docs”| Doc | Scope |
|---|---|
| Offline platform | Snapshot, outbox shapes, flush, temp-id contracts |
| Page refresh | sync vs refresh verbs, page classes, limited auto |
See also
Section titled “See also”- Remote access — PWA vs business offline boundary
- Habitat RPC — transport
- UI dimensions (agent API: ui-dimensions rules) — pointer vs touch refresh affordances