a6ff7623db
The editor lagged while typing (worse with doc size, and under collaboration the same cost is paid for every REMOTE keystroke). ProseMirror itself was fine — the overhead was the surrounding work done on every transaction. Behavior is 1:1; only WHEN work runs changed. - getJSON() off the keystroke path: `onUpdate` no longer serializes the whole doc synchronously — the serialization now runs inside a 3s debounce (new hook use-page-content-cache.ts), flushed on unmount so the last snapshot isn't lost. - footnote numbering: merged 3 per-docChanged O(n) doc walks into one, and short-circuit the whole-doc renumber when the doc has no footnotes and the transaction didn't insert one (step-slice scan — covers typing/paste/collab). - toolbar: replaced per-keystroke `editor.can().undo()/.redo()` dry-runs with cheap history-depth reads (Yjs undoManager stack length / pm-history depth). - render side-effect bug: `remote.attach()` moved out of the render body into a useEffect. - debounced the TOC all-headings rescan and memoized the slash-command suggestion build (was rebuilt twice per keystroke). - node menus (image/video/audio/pdf/callout/subpages): the per-transaction selectors early-return a cheap isActive check instead of running getAttributes + multiple alignment probes while their node type is inactive (shouldShow still controls display — appears exactly when it did). - code blocks: the global selectionUpdate listener is now added only for mermaid blocks (the only consumer of the selected state), eliminating N listeners + N setStates per caret move for normal code blocks. Deferred (documented, collab hot-path risk): full conditional menu MOUNTING (menu-less-frame risk on same-tx context switch) and code-block re-tokenization debounce / language-persist (self-dispatching meta tx + node-attr writes interact with collab/undo). The route split from #342 already keeps lowlight off startup. Gate: editor-ext build + 252/252 tests, client editor tests pass, tsc --noEmit 0, client build ok. New tests: footnote no-footnote-doc → 0 traversals + numbering unchanged; page-content-cache onUpdate-no-sync-getJSON + flush-on-unmount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
import type { MutableRefObject } from "react";
|
|
import { useDebouncedCallback } from "@mantine/hooks";
|
|
import type { Editor } from "@tiptap/react";
|
|
import { queryClient } from "@/main.tsx";
|
|
import { IPage } from "@/features/page/types/page.types.ts";
|
|
|
|
/**
|
|
* Off-keystroke local page-cache updater (issue #343, PART 3).
|
|
*
|
|
* The editor's `onUpdate` fires on every keystroke — and, under collaboration,
|
|
* on every REMOTE keystroke too. Serializing the WHOLE document with
|
|
* `editor.getJSON()` on that hot path is expensive, and the previous 3s debounce
|
|
* only guarded the cache WRITE, not the serialization: `getJSON()` still ran per
|
|
* keystroke.
|
|
*
|
|
* This hook moves the serialization INSIDE the debounced callback, so the
|
|
* full-doc traversal happens at most once per `delay`, not per keystroke. Call
|
|
* the returned function from `onUpdate` (it only schedules the debounce); the
|
|
* `getJSON()` snapshot is taken when the debounce fires.
|
|
*
|
|
* On unmount/navigation the pending snapshot is FLUSHED (via `flushOnUnmount`)
|
|
* so the last edits within the debounce window aren't lost from the local cache.
|
|
* The source of truth is collab/Yjs, but the cache must not go stale.
|
|
*
|
|
* IMPORTANT: call this hook BEFORE `useEditor`. React runs effect cleanups in
|
|
* declaration order on unmount, so the debounce's flush cleanup must be declared
|
|
* before `useEditor`'s teardown to run while the editor is still alive; the
|
|
* `isDestroyed` guard keeps a flush that still races teardown safe (it skips).
|
|
*/
|
|
export function usePageContentCache(
|
|
editorRef: MutableRefObject<Editor | null>,
|
|
slugId: string | undefined,
|
|
delay = 3000,
|
|
) {
|
|
return useDebouncedCallback(
|
|
() => {
|
|
const e = editorRef.current;
|
|
if (!e || e.isDestroyed || e.isEmpty) return;
|
|
const pageData = queryClient.getQueryData<IPage>(["pages", slugId]);
|
|
if (pageData) {
|
|
// getJSON() (full-doc serialization) runs HERE, off the keystroke path.
|
|
queryClient.setQueryData(["pages", slugId], {
|
|
...pageData,
|
|
content: e.getJSON(),
|
|
});
|
|
}
|
|
},
|
|
{ delay, flushOnUnmount: true },
|
|
);
|
|
}
|