Skip to content

fix(browse): worker-free JSON in record editors to end Monaco worker OOM (#1370/#1499)#1543

Open
dawsontoth wants to merge 1 commit into
stagefrom
claude/rum-browse-record-editor-worker-oom
Open

fix(browse): worker-free JSON in record editors to end Monaco worker OOM (#1370/#1499)#1543
dawsontoth wants to merge 1 commit into
stagefrom
claude/rum-browse-record-editor-worker-oom

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

RUM flagged a recurrence of the closed #1370 / #1499 Monaco-worker-OOM class through the browse record editors (EditTableRowModal, AddTableRowModal), which hardcoded language="json". Monaco's json language runs a worker and clones the model's full text (and every change delta) to it over postMessage; 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 with DataCloneError: ... 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 jsonplaintext switch with a worker-free JSON language, after review (@kriszyp) correctly pointed out the switch can't close the paste path (details below).

Approach

  • New Monaco language 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 in setup.ts alongside the built-in json contribution, which is left untouched for the config/roles editors that rely on its schema validation.
  • Both record editors switch to json-worker-free. There is no language worker to overflow, at any size; syntax highlighting and bracket/auto-close are unchanged.
  • Worker validation markers go away with the worker, so the onValidate signal is replaced by recordEditorJson.ts: a cheap main-thread parse for content under MAX_WORKER_MODEL_CHARS keeps the "Save disabled while JSON is invalid" affordance for normal-size records without parsing a huge buffer on every keystroke, and a caught JSON.parse at submit time toasts on malformed input (for both Add and Edit) instead of throwing an uncaught SyntaxError.
  • EditTableRowModal resets its draft/validity when a different record is opened (that modal instance is reused across rows).

Why not the size-based switch

@monaco-editor/react applies a changed language prop in a later React effect, and Monaco syncs the model to the worker on a debounce; once synced, the worker's own onDidChangeContent listener 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 any jsonplaintext switch 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

  • Tradeoff — live validation: worker-free means no inline red-squiggle validation. Mitigated by the main-thread Save gating (normal records) + submit-time parse/toast (all records). This is a deliberate behavior change for the browse record editors; if the team wants richer inline validation back, a main-thread JSON diagnostics provider would be a follow-up.
  • createTokenizationSupport import (monacoJsonTokenization.d.ts + workerFreeJsonLanguage.ts): reuses Monaco's own JSON tokenizer from an internal ESM path (same style as the existing deep imports in setup.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 build would catch that.
  • Out of scope: Monaco's base editor worker still syncs models for link detection (a '*' provider), exactly as it does for every plaintext editor the app already treats as the safe fallback. This PR closes the json-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 use json-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.
  • Full suite green (1786 passed / 11 skipped), tsc -b clean, oxlint + dprint clean, vite build clean.

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.api2 chunk).

🤖 Generated by an LLM (Claude Opus 4.8) via Claude Code.

@dawsontoth
dawsontoth requested a review from a team as a code owner July 21, 2026 14:09
@dawsontoth dawsontoth added the rum From real user monitoring where we aim to keep users happy label Jul 21, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/features/instance/databases/modals/recordEditorLanguage.ts Outdated
Comment thread src/features/instance/databases/modals/recordEditorLanguage.test.ts Outdated
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 52.28% 5618 / 10745
🔵 Statements 52.8% 6007 / 11375
🔵 Functions 44.64% 1385 / 3102
🔵 Branches 45.19% 3765 / 8330
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/databases/modals/AddTableRowModal.tsx 70.45% 47.82% 55.55% 70.45% 59, 80-98, 113, 123-124, 173, 202, 208, 215
src/features/instance/databases/modals/EditTableRowModal.tsx 82.85% 77.5% 71.42% 82.85% 62, 91-92, 165-167
src/features/instance/databases/modals/recordEditorJson.ts 100% 100% 100% 100%
src/lib/monaco/workerFreeJsonLanguage.ts 100% 100% 100% 100%
Generated in workflow #1573 for commit 7647364 by the Vitest Coverage Report Action

@dawsontoth

Copy link
Copy Markdown
Contributor Author

Good call — applied in 26f5fc3. Defaulted empty/nullish content to json so the record editor keeps auto-close brackets, indentation, and highlighting from the first keystroke; the OOM guard now only trips once content actually exceeds MAX_WORKER_MODEL_CHARS. Updated the doc comment and the unit test accordingly.

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestions might help protect against OOM?

Comment thread src/features/instance/databases/modals/AddTableRowModal.tsx Outdated
Comment thread src/features/instance/databases/modals/EditTableRowModal.tsx Outdated
Comment thread src/features/instance/databases/modals/recordEditorLanguage.ts Outdated
…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>
@dawsontoth
dawsontoth force-pushed the claude/rum-browse-record-editor-worker-oom branch from 26f5fc3 to 7647364 Compare July 23, 2026 15:37
@dawsontoth dawsontoth changed the title fix(browse): guard record editors against Monaco worker OOM (RUM recurrence of #1370/#1499) fix(browse): worker-free JSON in record editors to end Monaco worker OOM (#1370/#1499) Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rum From real user monitoring where we aim to keep users happy

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Monitoring] Monaco editor worker OOM — DataCloneError / postMessage failure (regression since v2.115.4)

3 participants