feat(offline): PWA shell, Yjs-backed titles, and offline read cache (M0–M2)
Implements docs/offline-sync-plan.md milestones M0–M2. M0 (PWA shell): - Add vite-plugin-pwa (generateSW, registerType: 'prompt', manifest:false); NetworkOnly for /api,/collab,/socket.io, NetworkFirst for GET /api, navigateFallback to index.html. - Register SW via useRegisterSW with a Mantine update prompt; skip registration inside Capacitor native WebView (is-capacitor guard). M1 (harden CRDT body + title into Yjs): - Lift the per-page Y.Doc/Hocuspocus providers into a shared hook+context so body and title editors share one doc. - Move the page title into a dedicated 'title' Yjs fragment (CRDT, offline- tolerant); drop the REST title save. Server persists the title fragment to page.title and seeds it for legacy pages (empty-fragment guard); a collab rename emits a treeUpdate so other users' tree/breadcrumbs refresh. - Persist the rebuilt ydoc on the content->ydoc path to neutralize the Yjs duplication trap. Add a 3-state sync indicator. M2 (offline read/navigation): - Persist React Query to IndexedDB (idb-keyval persister, version buster, selected roots only). - "Make available offline" action warms page, space, tree (root+ancestors+ children) and comments under exact hook keys, plus the page ydoc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,7 @@ import { generateHTML, generateJSON } from '../common/helpers/prosemirror/html';
|
||||
import { Node, Schema } from '@tiptap/pm/model';
|
||||
import * as Y from 'yjs';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { TiptapTransformer } from '@hocuspocus/transformer';
|
||||
|
||||
export const tiptapExtensions = [
|
||||
StarterKit.configure({
|
||||
@@ -143,6 +144,34 @@ export function jsonToText(tiptapJson: JSONContent) {
|
||||
return generateText(tiptapJson, tiptapExtensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a standalone Y.Doc that holds ONLY the page title, in a dedicated Yjs
|
||||
* fragment named exactly 'title' (the collaborative title-editor contract with
|
||||
* the client). The ProseMirror shape is a doc with a single level-1 heading
|
||||
* whose text is the title (empty title => heading with no text child).
|
||||
*
|
||||
* The encoded state of the returned doc can be merged into a body doc via
|
||||
* `Y.applyUpdate(doc, Y.encodeStateAsUpdate(titleSeed))` to seed the title
|
||||
* fragment for legacy pages. Seeding MUST be guarded by an emptiness check on
|
||||
* the existing 'title' fragment to avoid the Yjs duplication trap.
|
||||
*/
|
||||
export function buildTitleSeedYdoc(title: string): Y.Doc {
|
||||
return TiptapTransformer.toYdoc(
|
||||
{
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: { level: 1 },
|
||||
content: title ? [{ type: 'text', text: title }] : [],
|
||||
},
|
||||
],
|
||||
},
|
||||
'title',
|
||||
tiptapExtensions,
|
||||
);
|
||||
}
|
||||
|
||||
export function jsonToNode(tiptapJson: JSONContent) {
|
||||
const schema = getSchema(tiptapExtensions);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user