mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:02:51 +08:00
* Integrate database blob diff and lazy row loading * chore: load blob * chore: fix cell render * chore: create csv api endpoint * chore: add test * chore: fix lint
8.3 KiB
8.3 KiB
Database Row Loading and Storage (Web)
Overview
- Database content is modeled as Yjs documents: a view document (per viewId) plus row documents (per rowId).
- View docs store row ordering and view config; row docs store the actual row cells.
- Yjs docs are persisted locally with
y-indexeddb, while a Dexie cache stores small metadata and row versions.
Row loading flow
- A database view is opened via
useViewOperations.loadViewinsrc/components/app/hooks/useViewOperations.ts.- It calls
service.getPageDoc(...), which opens a Yjs doc from IndexedDB viaopenCollabDB(viewId). - For database layouts,
loadViewresolves the databaseId and setsdoc.guid = databaseIdbefore sync.
- It calls
Databaseinsrc/components/database/Database.tsxkeeps a row doc map but does not open every row up front.row_ordersis still read from the view doc when rendering.
- Row docs are opened lazily per row:
useRowData(rowId)/useRowDataSelector(rowId)callensureRowDoc(rowId)fromDatabaseContext.ensureRowDocwaits for the blob diff prefetch to finish (success or failure) before binding row sync.ensureRowDocusesgetRowKey(doc.guid, rowId)andcreateRowDoc(rowKey)to open the row doc and register sync asTypes.DatabaseRow.
rowDocMapinDatabaseContextis incremental (only loaded rows):- Grid/Board/Calendar rows read
YjsDatabaseKey.cellsfrom row docs when available.
- Grid/Board/Calendar rows read
- Sorting/filtering is computed in
useRowOrdersSelectorinsrc/application/database-yjs/selector.ts:- When row docs are incomplete, the selector returns the base
row_orders. - If sorts/filters exist, it loads missing row docs from IndexedDB (no sync binding) in the background and re-applies conditions once all rows are available.
- When row docs are incomplete, the selector returns the base
Where row data comes from
- WebSocket sync:
registerSyncContextinsrc/components/ws/useSync.tscallsinitSyncinsrc/application/services/js-services/sync-protocol.ts, which sends a sync request and applies updates to docs. - Database blob diff prefetch (new):
prefetchDatabaseBlobDiffinsrc/application/database-blob/index.tsis called fromuseViewOperations.loadViewfor database layouts.- It calls
databaseBlobDiffinsrc/application/services/js-services/http/http_api.tsand appliesCollabDocState(doc_state+encoder_version) into IndexedDB row docs withopenCollabDBWithProvider+applyYDoc(V1 or V2 based on encoder version). - Errors are logged and the UI falls back to the existing per-row sync path.
- Fetch + revalidate (mainly for publish or cache refresh):
getPageCollabinsrc/application/services/js-services/http/http_api.tsreturnsencoded_collabplusrow_data.revalidateViewinsrc/application/services/js-services/cache/index.tsappliesrow_dataviaupdateRows.getPublishViewreturnsdatabase_collabanddatabase_row_collabs, which follow the sameupdateRowspath.
Storage layers
Yjs + IndexedDB (y-indexeddb)
openCollabDBinsrc/application/db/index.tsusesIndexeddbPersistence(name, doc)and waits for the providersyncedevent before returning.- Each collab doc is stored in its own IndexedDB database named by
name:- View docs:
name = viewId. - Row docs:
name = getRowKey(databaseId, rowId)=>${databaseId}_rows_${rowId}. - Workspace database doc:
name = databaseStorageId.
- View docs:
closeCollabDBdestroys the provider, andclearDatadeletes all IndexedDB databases.
Dexie cache
- Dexie database name:
${databasePrefix}_cache=>af_database_cacheinsrc/application/db/index.ts. - Tables (schemas in
src/application/db/tables/*.ts):view_metas(publish metadata and view relations).users(cached user profiles).workspace_member_profiles.rows(row_id, row_key, version).
updateRowsinsrc/application/services/js-services/cache/index.tsapplies row updates and increments therows.versionentry for each row.
In-memory caches
rowDocsMap insrc/application/services/js-services/cache/index.tsavoids reopening row docs.openedSetinsrc/application/db/index.tstracks which collab databases are open.- LocalStorage RID cache:
af_database_blob_rid:{databaseId}stores the last RID seen from blob diff.
Key data structures
- View doc root:
YjsEditorKey.databasewithYjsDatabaseKey.row_orders. - Row doc root:
YjsEditorKey.database_rowwithYjsDatabaseKey.cells. - Yjs key/type definitions live in
src/application/types.ts.
Notes
getPageDocinsrc/application/services/js-services/index.tsusesStrategyType.CACHE_ONLYfor app views, so initial data is expected to come from IndexedDB or the sync protocol rather than an immediate fetch.- Database row docs are keyed by
databaseId, so multiple view tabs that share the same database reuse the same row documents.
Desktop blob/diff row loading (reference)
This section summarizes how the desktop client (Rust) avoids per-row sync by using the
database blob diff API. Source of truth: AppFlowy-Premium/frontend/rust-lib/flowy-database2.
Diff-based row loader
- Row loader is selected in
database_row_loader.rs;DatabaseBlobRowLoaderis currently enabled. DatabaseBlobRowLoaderrequestsdatabase_blob_diff(database_id, max_known_rid)viaDatabaseCollabServiceImpl::database_blob_diffincollab_service.rs.max_known_ridis stored per database in sqlite tabledb_database_blob_rid_cache(database_blob_rid_cache.rs).- A shared in-memory
BlobDiffCachestores empty diffs for 30s to avoid repeated "no changes" calls per RID.
Applying the diff
- The diff contains row updates with
encoded_collab_v1plus optional row document updates. DatabaseBlobRowLoader::prepare_diffdecodes rows and tracks missing/decode failures.collab_service.merge_blob_rows(...)merges the diff into local collab storage:- It loads any local collab first, applies the server snapshot update on top (preserving offline edits), and writes back to the KV store.
- Row document updates are persisted; delete markers from blob diffs are ignored to avoid dropping offline edits.
- After merge, the loader builds row snapshots by opening
DatabaseRowfrom the mergedEncodedCollab.
Missing rows and fallbacks
- If the diff is empty and a RID is known, the loader reads missing rows from local cache first; missing rows can fall back to collab loading.
- If the diff is partial, it still reads local cache for remaining rows before falling back to collab.
- If the diff request fails or returns non-ready status, the loader falls back immediately to the collab row loader.
- When
auto_fetch=false, the loader only reads local cache and skips remote fallback.
Background prefetch (desktop)
DatabaseEditor::maybe_spawn_prefetch_missing_row_collabschooses:- Blob loader prefetch (auto-fetch diff + merge) when blob backend is active.
- Missing-only collab prefetch otherwise (
prefetch_missing_row_collabsincollab_service.rs) which fetches and persists missing encoded collabs without binding rows.
Desktop storage model
- Row collabs are persisted in the local KV store (
CollabKVDB). - The blob diff RID watermark persists in sqlite
db_database_blob_rid_cache. - Derived view caches and calculations are stored in sqlite (see
database_rows_init.mdfor the cache tables and backfill behavior).
Key desktop files
AppFlowy-Premium/frontend/rust-lib/flowy-database2/src/services/database/database_blob_row_loader.rsAppFlowy-Premium/frontend/rust-lib/flowy-database2/src/services/database/database_row_loader.rsAppFlowy-Premium/frontend/rust-lib/flowy-database2/src/services/database/database_blob_rid_cache.rsAppFlowy-Premium/frontend/rust-lib/flowy-database2/src/collab_service.rsAppFlowy-Premium/frontend/rust-lib/flowy-database2/database_rows_init.md
Related files
src/components/app/hooks/useViewOperations.tssrc/components/database/Database.tsxsrc/application/database-blob/index.tssrc/application/database-yjs/row_meta.tssrc/application/database-yjs/context.tssrc/application/database-yjs/selector.tssrc/application/db/index.tssrc/application/db/tables/rows.tssrc/application/services/js-services/cache/index.tssrc/application/services/js-services/http/http_api.tssrc/application/services/js-services/sync-protocol.ts