mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +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
142 lines
8.3 KiB
Markdown
142 lines
8.3 KiB
Markdown
# 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
|
|
1. A database view is opened via `useViewOperations.loadView` in `src/components/app/hooks/useViewOperations.ts`.
|
|
- It calls `service.getPageDoc(...)`, which opens a Yjs doc from IndexedDB via `openCollabDB(viewId)`.
|
|
- For database layouts, `loadView` resolves the databaseId and sets `doc.guid = databaseId` before sync.
|
|
2. `Database` in `src/components/database/Database.tsx` keeps a row doc map but does **not** open every row up front.
|
|
- `row_orders` is still read from the view doc when rendering.
|
|
3. Row docs are opened lazily per row:
|
|
- `useRowData(rowId)` / `useRowDataSelector(rowId)` call `ensureRowDoc(rowId)` from `DatabaseContext`.
|
|
- `ensureRowDoc` waits for the blob diff prefetch to finish (success or failure) before binding row sync.
|
|
- `ensureRowDoc` uses `getRowKey(doc.guid, rowId)` and `createRowDoc(rowKey)` to open the row doc and register sync as `Types.DatabaseRow`.
|
|
4. `rowDocMap` in `DatabaseContext` is incremental (only loaded rows):
|
|
- Grid/Board/Calendar rows read `YjsDatabaseKey.cells` from row docs when available.
|
|
5. Sorting/filtering is computed in `useRowOrdersSelector` in `src/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.
|
|
|
|
## Where row data comes from
|
|
- WebSocket sync: `registerSyncContext` in `src/components/ws/useSync.ts` calls `initSync` in
|
|
`src/application/services/js-services/sync-protocol.ts`, which sends a sync request and applies updates to docs.
|
|
- Database blob diff prefetch (new):
|
|
- `prefetchDatabaseBlobDiff` in `src/application/database-blob/index.ts` is called from
|
|
`useViewOperations.loadView` for database layouts.
|
|
- It calls `databaseBlobDiff` in `src/application/services/js-services/http/http_api.ts` and applies
|
|
`CollabDocState` (`doc_state` + `encoder_version`) into IndexedDB row docs with
|
|
`openCollabDBWithProvider` + `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):
|
|
- `getPageCollab` in `src/application/services/js-services/http/http_api.ts` returns `encoded_collab` plus `row_data`.
|
|
- `revalidateView` in `src/application/services/js-services/cache/index.ts` applies `row_data` via `updateRows`.
|
|
- `getPublishView` returns `database_collab` and `database_row_collabs`, which follow the same `updateRows` path.
|
|
|
|
## Storage layers
|
|
### Yjs + IndexedDB (y-indexeddb)
|
|
- `openCollabDB` in `src/application/db/index.ts` uses `IndexeddbPersistence(name, doc)` and waits for the
|
|
provider `synced` event 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`.
|
|
- `closeCollabDB` destroys the provider, and `clearData` deletes all IndexedDB databases.
|
|
|
|
### Dexie cache
|
|
- Dexie database name: `${databasePrefix}_cache` => `af_database_cache` in `src/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).
|
|
- `updateRows` in `src/application/services/js-services/cache/index.ts` applies row updates and
|
|
increments the `rows.version` entry for each row.
|
|
|
|
### In-memory caches
|
|
- `rowDocs` Map in `src/application/services/js-services/cache/index.ts` avoids reopening row docs.
|
|
- `openedSet` in `src/application/db/index.ts` tracks 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.database` with `YjsDatabaseKey.row_orders`.
|
|
- Row doc root: `YjsEditorKey.database_row` with `YjsDatabaseKey.cells`.
|
|
- Yjs key/type definitions live in `src/application/types.ts`.
|
|
|
|
## Notes
|
|
- `getPageDoc` in `src/application/services/js-services/index.ts` uses `StrategyType.CACHE_ONLY` for 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`; `DatabaseBlobRowLoader` is currently enabled.
|
|
- `DatabaseBlobRowLoader` requests `database_blob_diff(database_id, max_known_rid)` via
|
|
`DatabaseCollabServiceImpl::database_blob_diff` in `collab_service.rs`.
|
|
- `max_known_rid` is stored per database in sqlite table `db_database_blob_rid_cache`
|
|
(`database_blob_rid_cache.rs`).
|
|
- A shared in-memory `BlobDiffCache` stores empty diffs for 30s to avoid repeated
|
|
"no changes" calls per RID.
|
|
|
|
### Applying the diff
|
|
- The diff contains row updates with `encoded_collab_v1` plus optional row document updates.
|
|
- `DatabaseBlobRowLoader::prepare_diff` decodes 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 `DatabaseRow` from the
|
|
merged `EncodedCollab`.
|
|
|
|
### 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_collabs` chooses:
|
|
- Blob loader prefetch (auto-fetch diff + merge) when blob backend is active.
|
|
- Missing-only collab prefetch otherwise (`prefetch_missing_row_collabs` in
|
|
`collab_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.md` for the cache tables and backfill behavior).
|
|
|
|
### Key desktop files
|
|
- `AppFlowy-Premium/frontend/rust-lib/flowy-database2/src/services/database/database_blob_row_loader.rs`
|
|
- `AppFlowy-Premium/frontend/rust-lib/flowy-database2/src/services/database/database_row_loader.rs`
|
|
- `AppFlowy-Premium/frontend/rust-lib/flowy-database2/src/services/database/database_blob_rid_cache.rs`
|
|
- `AppFlowy-Premium/frontend/rust-lib/flowy-database2/src/collab_service.rs`
|
|
- `AppFlowy-Premium/frontend/rust-lib/flowy-database2/database_rows_init.md`
|
|
|
|
## Related files
|
|
- `src/components/app/hooks/useViewOperations.ts`
|
|
- `src/components/database/Database.tsx`
|
|
- `src/application/database-blob/index.ts`
|
|
- `src/application/database-yjs/row_meta.ts`
|
|
- `src/application/database-yjs/context.ts`
|
|
- `src/application/database-yjs/selector.ts`
|
|
- `src/application/db/index.ts`
|
|
- `src/application/db/tables/rows.ts`
|
|
- `src/application/services/js-services/cache/index.ts`
|
|
- `src/application/services/js-services/http/http_api.ts`
|
|
- `src/application/services/js-services/sync-protocol.ts`
|