T-LNJA-md-engine-large-file-guard
State: closed/done · Impact: high · Complexity: small
A very large note in live mode rebuilds decorations it can never afford, and nothing stops a host
from opening one that way. This task adds the large-file guard: sourceModeThreshold,
initialMode forcing 'source' with modeForced: 'large-file', a setMode refusal, and
decoration layers that stand down over the threshold.
| Location | Role today |
|---|---|
packages/ts/obsidian-editor/src/state.ts | modeFacet, getMode, setMode; no size threshold and no forced-mode state |
packages/ts/obsidian-editor/src/types.ts#ObsidianEditorOptions | mode?: EditorMode; no sourceModeThreshold |
packages/ts/obsidian-editor/src/decorations.ts | Decoration layers run regardless of document size |
packages/ts/obsidian-editor/src/vue/index.ts#useObsidianEditor | No mode or modeForced refs |
packages/ts/obsidian-editor/test/mode.test.ts | Source/live switch tests, the base the guard tests extend |
The catalogue’s editor.toggle-mode comes from T-EXQ7-md-engine-command-catalogue; the bench
from T-0GV4-md-engine-line-endings-corpus.
Proposed
Section titled “Proposed”sourceModeThreshold?: numberonObsidianEditorOptions, default 600 000 — the UTF-8 byte length of the document.docBytesField: StateField<number>computes the byte length once at creation and updates incrementally from change lengths, never rescanning the document per transaction.initialMode(doc, options): { mode: EditorMode; modeForced: 'large-file' | null }— over the threshold the mode is'source'andmodeForcedis'large-file'regardless ofoptions.mode.obsidianEditorExtensionsuses it;getModeForced(state)reads the forced state back.setMode(view, 'live')returnsfalseand leaves the mode'source'while the document is over the threshold; the refusal clears once edits bring it under.buildInlineDecorationsand the block widget field returnDecoration.nonewhile over the threshold, so even a host that forces a reconfigure gets no decoration cost.- The catalogue’s
editor.toggle-modereportsenabled: falsewhilemodeForcedis set — the chrome mode toggle disables Live with a tooltip off this state. useObsidianEditorreturnsmode(effective, recomputed on reconfigure and change) andmodeForcedshallow refs.- The bench gains an
open-600k-sourcecase so opening at the threshold is baselined and gated like every other measure; the browser-level 500 ms open budget is proven by T-I630-md-ui-playground-e2e-perf.
Approach
Section titled “Approach”- Extend
packages/ts/obsidian-editor/test/mode.test.tsfirst: a 600 001-byte document opens in'source'withgetModeForced(state) === 'large-file';setMode(view, 'live')returnsfalseandgetModestays'source'; a 599 999-byte document opens peroptions.modeand switches freely; deleting half the large document thensetMode(view, 'live')succeeds. - Add a decoration case: over the threshold,
buildInlineDecorationsand the block field produce zero decorations. - Implement
docBytesField,initialMode,getModeForced, thesetModerefusal and the threshold option; assertdocBytesFieldequals the byte length ofstate.sliceDoc()after an insert, a delete, and over the CRLF corpus fixture. - Wire
editor.toggle-mode’sisEnabledto the forced state; extend the catalogue test. - Extend
useObsidianEditor(mode,modeForced) and the Vue test. - Add the
open-600k-sourcebench case; re-record the baseline entry for it only;bun test,tsc --noEmit,moon run obsidian-editor:bench.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
packages/ts/obsidian-editor/src/state.ts | modify | docBytesField, initialMode, getModeForced, setMode refusal |
packages/ts/obsidian-editor/src/types.ts | modify | sourceModeThreshold?: number |
packages/ts/obsidian-editor/src/decorations.ts | modify | Decoration.none over the threshold |
packages/ts/obsidian-editor/src/commands/catalogue.ts | new | Created by T-EXQ7-md-engine-command-catalogue; editor.toggle-mode disabled while forced |
packages/ts/obsidian-editor/src/vue/index.ts | modify | mode and modeForced shallow refs |
packages/ts/obsidian-editor/src/index.ts | modify | Export initialMode, getModeForced, docBytesField |
packages/ts/obsidian-editor/test/mode.test.ts | modify | Guard, refusal and byte-field tests |
packages/ts/obsidian-editor/test/vue.test.ts | modify | mode / modeForced refs |
packages/ts/obsidian-editor/bench/run.ts | new | Created by T-0GV4-md-engine-line-endings-corpus; gains the open-600k-source case |
packages/ts/obsidian-editor/bench/baseline.json | new | Created by T-0GV4-md-engine-line-endings-corpus; entry for the new case |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
bun test packages/ts/obsidian-editorpasses, including the extended mode and Vue suites. - AC-2: a 600 001-byte document opens in
'source'withgetModeForced(state) === 'large-file';setMode(view, 'live')returnsfalseandgetMode(view.state)stays'source'; a 599 999-byte document is unaffected. - AC-3: over the threshold,
buildInlineDecorationsand the block widget field return zero decorations. - AC-4:
commandStates(state)['editor.toggle-mode'].enabledisfalsewhile forced andtrueotherwise. - AC-5:
docBytesFieldequals the UTF-8 byte length ofstate.sliceDoc()after each of the three edits in the test (insert, delete, CRLF fixture load). - AC-6:
useObsidianEditorreturnsmodeandmodeForced; the Vue test provesmodeForcedis'large-file'for an over-threshold document andnullafter edits shrink it. - AC-7:
moon run obsidian-editor:benchpasses with theopen-600k-sourcecase recorded inbench/baseline.jsonand gated at 1.5× like the rest.
Out of scope
Section titled “Out of scope”- The chrome mode toggle’s disabled state and tooltip (T-A0HT-md-ui-layer-skeleton’s
MdModeTogglereadsmodeForced). - The read mode;
'read'is a chrome-level mode outside the engine’sEditorMode. - The browser-level open-time budget (500 ms for a 600 KB note), proven by T-I630-md-ui-playground-e2e-perf.
Dependencies
Section titled “Dependencies”- T-EXQ7-md-engine-command-catalogue for
editor.toggle-modeandcommandStates. - T-0GV4-md-engine-line-endings-corpus for the bench the new case joins.
Discovery context
Section titled “Discovery context”Member of milestone M-931L; engine step E10 of decision D-K3EM-markdown-editor-chrome-and-plugin-environment.
Depends on
Section titled “Depends on”T-EXQ7-md-engine-command-catalogue