跳转到内容

Portal 数据面

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.

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”.

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
ConcernRolePrimary doc / code
Habitat fetch gateNo RPC when offline or Habitat not connectedisHabitatFetchAvailable
In-memory viewWhat the React tree showsFeature state / zustand; future shared hooks
IDB snapshotDurable read cache; offline read; failure fallbackOffline platform
ETag / 304Transport-layer conditional GET; not the business snapshotHabitat REST (If-None-Match)
Online write / outboxPrefer Habitat; enqueue on transport failureOffline platform
syncReconnect / visibility: flush outbox then module refreshAllPage refresh, OfflineSyncBootstrap
refreshUser re-fetches current viewPage refresh
live channelsLimited auto (chat stream, conversation / pomodoro events)Page refresh
temp id / client_op_idOffline create identity and idempotent retryOffline platform
connectivity UI vs outbox UILink down ≠ pending writesconnectivity-notice vs sync toast
cache scopeIsolate by Habitat URL + subjectresolveHabitatCacheScope
PWA Service WorkerShell static assets only; not business RPCRemote access
  1. One vocabulary: snapshot, outbox, sync, refresh, live, gate. Do not narrate the plane in React Query terms.
  2. Three cache layers stay distinct: in-memory view ≠ IndexedDB business snapshot ≠ HTTP ETag/304.
  3. Limited auto: sync + existing live channels. No global list polling. Multi-device pull uses manual refresh unless a stronger protocol exists.
  4. 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).
  5. One optimistic write source: offline-store / outbox. Do not add a second optimistic mutation cache beside it.
  6. DX may grow on this plane: shared hooks are welcome; defaults follow these principles. Do not adopt the React Query library.
  7. Features must not fork the path: reads use withOfflineCache (or identical semantics); writes follow the outbox / preferOnlineWrite contracts.
  • 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 and client_op_id contracts.
  • Bind header Refresh and pull-to-refresh to the same page reload handler.
  • After a local write, reload or patch the affected view; reserve full-module refreshAll for 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 connected and the queue is merely working.
React QueryThis plane
Primary jobIn-memory request lifecycle and UI subscriptionsPortal↔Habitat consistency including durable offline
PersistOptional memory rehydrate; no outbox / temp-id / Habitat gateIndexedDB snapshot + outbox + id-map
DefaultsOften focus refetch / broad invalidationLimited auto + manual refresh
Backend cache protocolOut of scopeMay 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.

Not implemented by this doc. Advance when pain is real (no speculative QueryClient clone):

  • Shared read hook (data / loading / error / reload) wrapping withOfflineCache
  • 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
DocScope
Offline platformSnapshot, outbox shapes, flush, temp-id contracts
Page refreshsync vs refresh verbs, page classes, limited auto