fix(muya): #4747 deep clone editor state without recursing by CookPiu · Pull Request #5259 · marktext/marktext · GitHub
Skip to content

fix(muya): #4747 deep clone editor state without recursing - #5259

Merged
Jocs merged 1 commit into
marktext:developfrom
CookPiu:fix/deep-nested-list-clone
Sep 7, 2026
Merged

Jocs merged 1 commit into
marktext:developfrom
CookPiu:fix/deep-nested-list-clone

Conversation

@CookPiu

@CookPiu CookPiu commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes #4747

Summary

JSONState.getState() copies the document with structuredClone, which recurses once per nesting level. A bullet list nested about 600 levels deep is roughly 2400 nested containers in the state tree (bullet-list → children → list-item → children per level), which exceeds the engine stack, so Editor.init() and Editor.setContent() throw RangeError before the document renders.

deepClone now walks arrays and plain objects with an explicit stack instead of recursing. Shared references inside the input stay shared in the copy (a source → copy map, which also rules out looping on cycles). Values that are neither arrays nor plain objects (Date, Map, typed arrays, ...) still go through structuredClone, so that behaviour is unchanged for callers that relied on it. Functions are the one input structuredClone rejected that the new walk does not: they are copied by reference. No caller passes them.

Muya playground (packages/muya/examples, Vite dev server) in Chrome 148 on Windows 11, muya.setContent() with the nested list from the issue's generator, then muya.getMarkdown():

levels develop (89d6988) this branch
300 ok, 702 ms, round-trip equal ok, 484 ms, round-trip equal
600 RangeError at deepClone ← JSONState.getState ← Editor.setContent ok, 1117 ms, round-trip equal, 600 .mu-bullet-list rendered
1000 RangeError, same stack ok, 3109 ms, round-trip equal, 1000 .mu-bullet-list rendered, deepest li at depth 1000

develop, 600 levels:

RangeError: Failed to execute 'structuredClone' on 'Window': Maximum call stack size exceeded
    at deepClone (packages/muya/src/utils/index.ts)
    at JSONState.getState (packages/muya/src/state/index.ts)
    at Editor.setContent (packages/muya/src/editor/index.ts)

Type of change

  • Bug fix (non-breaking, fixes an issue)
  • New feature (non-breaking, adds functionality)
  • Breaking change (causes existing functionality to change)
  • Documentation update

Test plan

  • New tests added: packages/muya/src/utils/__tests__/deepClone.spec.ts. The 600-level case fails on develop with the RangeError above; the other cases pin fresh containers in the copy, shared references staying shared, and the structuredClone fallback for non-plain values.
  • Manually tested on: Windows 11 (muya playground in Chrome 148, table above). The desktop app was not run on this machine: ced has no prebuilt binary and its node-gyp build needs Visual Studio Build Tools. The desktop path in the issue's stack (Muya.setContent → Editor.setContent → JSONState.getState → deepClone) is the same code the playground exercises.
pnpm -C packages/muya test           214 files, 1466 tests passed
pnpm -C packages/muya lint           clean
pnpm -C packages/muya lint:types     clean
pnpm -C packages/muya check-circular no circular dependency found

Notes for reviewers

  • Only deepClone changes. getState() still returns a defensive copy, so the public muya.getState() contract is untouched. The alternatives the issue lists (a non-cloning accessor for editor-internal paths, a nesting-depth guard in markdownToState) are not part of this change.
  • JSON.stringify, which buildReplaceOp uses to diff top-level blocks, also recurses and starts throwing at roughly 2000 list levels in V8, so lists that deep still fail further down the pipeline. The 600 to 1000 range the issue reports now loads.

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

structuredClone recurses once per nesting level and throws RangeError on a
bullet list a few hundred levels deep, so JSONState.getState() failed before
the document could render. deepClone now walks arrays and plain objects with
an explicit stack, keeps shared references shared, and still hands any other
object type to structuredClone.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Deeply nested list can crash the renderer with structuredClone stack overflow

2 participants