perf+fix(footnotes): minimal-diff sync (no concurrent-edit loss); cache numbering
Release-cycle review found two hardening gaps: - The sync plugin deleted+rebuilt the WHOLE footnotesList on any reorder/orphan, replacing every definition's Yjs subtree -> a collaborator typing in a definition could lose in-flight characters on merge. Rework to targeted, minimal mutations: attr-only setNodeMarkup for collision re-ids, delete only genuine orphans, insert only genuinely-missing definitions (at the list end, not shifting existing subtrees), and consolidate multiple lists only in the abnormal paste/merge case. An unchanged (correct id, referenced) definition is left completely untouched. Numbering is decoration-only, so physical list order may drift after a reorder (accepted) while displayed numbers stay correct. Invariants preserved (reviewed + tested): one SYNC_META transaction, null when canonical (terminates), deterministic deriveFootnoteId, remote-skip -> no re-introduced freeze or divergence. - computeFootnoteNumbers ran per-NodeView-render (O(n^2)/keystroke in big docs). The numbering plugin now caches the number map in its state (computed once per docChanged); NodeViews read it O(1) via getFootnoteNumber. Tests: no-rebuild-on-reorder asserts unchanged definition node subtrees are identity-preserved; isRemoteTransaction skip; enableSync:false read-only; cache correctness. Browser re-smoke: insert (no freeze), number, persist across reload, cascade delete all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { NodeViewContent, NodeViewProps, NodeViewWrapper } from "@tiptap/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { computeFootnoteNumbers } from "@docmost/editor-ext";
|
||||
import { getFootnoteNumber } from "@docmost/editor-ext";
|
||||
import classes from "./footnote.module.css";
|
||||
|
||||
/**
|
||||
@@ -13,8 +13,9 @@ export default function FootnoteDefinitionView(props: NodeViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const id = node.attrs.id as string;
|
||||
|
||||
const numbers = computeFootnoteNumbers(editor.state.doc);
|
||||
const number = numbers.get(id) ?? "?";
|
||||
// Read the cached number from the numbering plugin (computed once per doc
|
||||
// change) rather than recomputing the whole map on every render.
|
||||
const number = getFootnoteNumber(editor.state, id) ?? "?";
|
||||
|
||||
const handleBack = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@floating-ui/dom";
|
||||
import {
|
||||
FOOTNOTE_DEFINITION_NAME,
|
||||
computeFootnoteNumbers,
|
||||
getFootnoteNumber,
|
||||
} from "@docmost/editor-ext";
|
||||
import { ActionIcon } from "@mantine/core";
|
||||
import { IconArrowDown } from "@tabler/icons-react";
|
||||
@@ -45,9 +45,10 @@ export default function FootnoteReferenceView(props: NodeViewProps) {
|
||||
const popoverRef = useRef<HTMLDivElement | null>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// Number is derived (not stored) — recompute from the current doc.
|
||||
const numbers = computeFootnoteNumbers(editor.state.doc);
|
||||
const number = numbers.get(id) ?? "?";
|
||||
// Number is derived (not stored). Read it from the numbering plugin's cached
|
||||
// map (computed once per doc change) instead of walking the whole document on
|
||||
// every render — recomputing per NodeView per render was O(n^2) per keystroke.
|
||||
const number = getFootnoteNumber(editor.state, id) ?? "?";
|
||||
const defText = open ? getDefinitionText(editor, id) : "";
|
||||
|
||||
const position = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user