fix(browse): worker-free JSON in record editors to end Monaco worker OOM (#1370/#1499)#1543
fix(browse): worker-free JSON in record editors to end Monaco worker OOM (#1370/#1499)#1543dawsontoth wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a helper function, chooseRecordEditorLanguage, to dynamically switch the Monaco editor's language from 'json' to 'plaintext' when the payload size exceeds a safe threshold. This prevents the JSON worker from crashing on oversized records in both the AddTableRowModal and EditTableRowModal. The review feedback recommends defaulting empty or nullish content to 'json' instead of 'plaintext' to preserve helpful editor features (such as auto-closing brackets and syntax highlighting) when a user begins typing in an empty editor, along with updating the corresponding unit tests.
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Good call — applied in 26f5fc3. Defaulted empty/nullish content to |
kriszyp
left a comment
There was a problem hiding this comment.
The suggestions might help protect against OOM?
…1370/#1499) The browse Add/Edit record editors used `language="json"`, whose worker receives the model's full text (and every change delta) over a structured-clone postMessage. A large record — a big blob/array field, or a bulk-insert array pasted into Add row — overflows that clone and crashes the worker with "DataCloneError: ... out of memory.", which Monaco retries in a tight loop (the RUM recurrence of #1370/#1499 this PR was opened for). A size-based json->plaintext switch (the earlier approach) can't close the paste path: @monaco-editor/react applies a changed language prop in a later effect, and once the model is synced the worker's own change listener posts the oversized delta before any switch runs. Instead register a worker-free JSON language (WORKER_FREE_JSON_LANGUAGE_ID) — Monaco's main-thread JSON tokenizer plus the standard JSON language configuration, and none of the worker-backed providers — and use it for both record editors. There is no language worker to overflow, at any size; highlighting and bracket/auto-close are unchanged. Because that removes the worker's validation markers, the live onValidate signal is replaced with a cheap main-thread parse for content under the size limit (keeps "Save disabled while invalid" for normal records without parsing a huge buffer per keystroke) plus a caught JSON.parse at submit time that toasts on malformed input instead of throwing. EditTableRowModal also resets its draft when a different record is opened, since that modal instance is reused across rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
26f5fc3 to
7647364
Compare
Summary
RUM flagged a recurrence of the closed #1370 / #1499 Monaco-worker-OOM class through the browse record editors (
EditTableRowModal,AddTableRowModal), which hardcodedlanguage="json". Monaco'sjsonlanguage runs a worker and clones the model's full text (and every change delta) to it overpostMessage; a large record — a row with a big blob/array field, or a bulk-insert array pasted into "Add row" — overflows the structured clone and crashes the worker withDataCloneError: ... out of memory., which Monaco reports as an unhandled error and then retries in a tight loop, flooding the session.This revision replaces the earlier size-based
json→plaintextswitch with a worker-free JSON language, after review (@kriszyp) correctly pointed out the switch can't close the paste path (details below).Approach
json-worker-free(workerFreeJsonLanguage.ts): registers Monaco's main-thread JSON tokenizer plus the standard JSON language configuration (brackets, auto-closing pairs), and none of the worker-backed providers (validation, folding, colors, completion, hover, formatting). Registered insetup.tsalongside the built-injsoncontribution, which is left untouched for the config/roles editors that rely on its schema validation.json-worker-free. There is no language worker to overflow, at any size; syntax highlighting and bracket/auto-close are unchanged.onValidatesignal is replaced byrecordEditorJson.ts: a cheap main-thread parse for content underMAX_WORKER_MODEL_CHARSkeeps the "Save disabled while JSON is invalid" affordance for normal-size records without parsing a huge buffer on every keystroke, and a caughtJSON.parseat submit time toasts on malformed input (for both Add and Edit) instead of throwing an uncaughtSyntaxError.EditTableRowModalresets its draft/validity when a different record is opened (that modal instance is reused across rows).Why not the size-based switch
@monaco-editor/reactapplies a changedlanguageprop in a later React effect, and Monaco syncs the model to the worker on a debounce; once synced, the worker's ownonDidChangeContentlistener posts the oversized delta on the next change. So starting from a small sample and pasting a >512 KB array hands the payload to the worker before anyjson→plaintextswitch can run — the switch also doesn't stop an already-running model sync. A worker-free language sidesteps the race entirely rather than trying to win it.Where to focus review
createTokenizationSupportimport (monacoJsonTokenization.d.ts+workerFreeJsonLanguage.ts): reuses Monaco's own JSON tokenizer from an internal ESM path (same style as the existing deep imports insetup.ts/languageServices.ts) for pixel-identical highlighting. It ships without types, so it's declared locally. A Monaco version bump could move it —tsc -b/vite buildwould catch that.'*'provider), exactly as it does for everyplaintexteditor the app already treats as the safe fallback. This PR closes thejson-language-worker path the RUM data implicates; the base-worker exposure is pre-existing and shared, not addressed here.Testing
workerFreeJsonLanguage.test.ts: asserts the language registers tokenization + config and no worker-backed provider.recordEditorJson.test.ts: validity/parse helpers (valid, malformed, empty, oversized-deferred).AddTableRowModal.test.tsx(new) +EditTableRowModal.test.tsx(updated): assert both editors usejson-worker-free, a valid record saves the parsed value, and an oversized malformed paste is caught at submit time (toast, no save) — the large-paste integration regression asked for in review.tsc -bclean,oxlint+dprintclean,vite buildclean.Not browser-verified: reproducing the crash needs a live instance with a >512 KB record; verified at the mechanism/unit level and via a production build (the tokenizer import bundles into the
editor.api2chunk).🤖 Generated by an LLM (Claude Opus 4.8) via Claude Code.