From b8164e479d317714bb88296b47014af3adf6ee74 Mon Sep 17 00:00:00 2001 From: Fadhlan Ridhwanallah Date: Thu, 15 Jan 2026 23:21:31 +0700 Subject: [PATCH 1/5] Prevent large amounts of data from getting into cards --- .../host/app/commands/check-correctness.ts | 9 ++ .../components/operator-mode/code-editor.gts | 27 +++++- .../components/operator-mode/code-submode.gts | 8 ++ .../code-submode/editor-indicator.gts | 21 +++- packages/host/app/services/card-service.ts | 97 ++++++++++++++++++- .../host/app/services/environment-service.ts | 4 +- packages/host/app/services/store.ts | 1 + packages/host/config/environment.js | 6 ++ .../tests/acceptance/code-submode-test.ts | 32 ++++++ .../acceptance/interact-submode-test.gts | 45 +++++++++ packages/host/tests/helpers/index.gts | 3 + .../commands/check-correctness-test.gts | 94 ++++++++++++++++++ packages/realm-server/main.ts | 3 + .../realm-server/prerender/manager-server.ts | 2 +- .../scripts/wait-for-prerender.sh | 2 +- packages/realm-server/server.ts | 8 ++ packages/realm-server/tests/helpers/index.ts | 3 + packages/runtime-common/index.ts | 1 + packages/runtime-common/realm.ts | 17 +++- .../runtime-common/write-size-validation.ts | 12 +++ 20 files changed, 379 insertions(+), 16 deletions(-) create mode 100644 packages/runtime-common/write-size-validation.ts diff --git a/packages/host/app/commands/check-correctness.ts b/packages/host/app/commands/check-correctness.ts index 324ff21aa5..69488008cd 100644 --- a/packages/host/app/commands/check-correctness.ts +++ b/packages/host/app/commands/check-correctness.ts @@ -78,6 +78,15 @@ export default class CheckCorrectnessCommand extends HostBaseCommand< let errors: string[] = []; let lintIssues: string[] = []; + let writeError = this.cardService.peekWriteError(input.targetRef); + if (writeError) { + return new CorrectnessResultCard({ + correct: false, + errors: [this.describeCardError(input.targetRef, writeError)], + warnings: [], + }); + } + if ( targetType === 'file' && (await this.isEmptyFileContent(input.targetRef)) diff --git a/packages/host/app/components/operator-mode/code-editor.gts b/packages/host/app/components/operator-mode/code-editor.gts index c8c29bbbe3..f4ae4b0927 100644 --- a/packages/host/app/components/operator-mode/code-editor.gts +++ b/packages/host/app/components/operator-mode/code-editor.gts @@ -64,6 +64,7 @@ interface Signature { onSetup: ( updateCursorByName: (name: string, fieldName?: string) => void, ) => void; + onWriteError?: (message: string | undefined) => void; }; } @@ -350,6 +351,7 @@ export default class CodeEditor extends Component { this.syncWithStore.perform(content); await timeout(this.environmentService.autoSaveDelayMs); + this.args.onWriteError?.(undefined); this.writeSourceCodeToFile( this.args.file, content, @@ -433,7 +435,11 @@ export default class CodeEditor extends Component { private waitForSourceCodeWrite = restartableTask(async () => { if (isReady(this.args.file)) { this.args.onFileSave('started'); - await all([this.args.file.writing, timeout(500)]); + try { + await all([this.args.file.writing, timeout(500)]); + } catch { + // Errors are surfaced via writeError banners, so don't rethrow. + } this.args.onFileSave('finished'); } }); @@ -459,10 +465,21 @@ export default class CodeEditor extends Component { // flush the loader so that the preview (when card instance data is shown), // or schema editor (when module code is shown) gets refreshed on save - return file.write(content, { - flushLoader: hasExecutableExtension(file.name), - saveType, - }); + return file + .write(content, { + flushLoader: hasExecutableExtension(file.name), + saveType, + }) + .catch((error) => { + if (error?.status === 413 && error?.title) { + this.args.onWriteError?.(error.title); + return; + } + let message = + error?.message ?? + (error?.title ? `${error.title}: ${error.message ?? ''}` : undefined); + this.args.onWriteError?.(message ? message.trim() : String(error)); + }); } private safeJSONParse(content: string) { diff --git a/packages/host/app/components/operator-mode/code-submode.gts b/packages/host/app/components/operator-mode/code-submode.gts index e13f4a404e..2a418a9aeb 100644 --- a/packages/host/app/components/operator-mode/code-submode.gts +++ b/packages/host/app/components/operator-mode/code-submode.gts @@ -147,6 +147,7 @@ export default class CodeSubmode extends Component { @tracked private loadFileError: string | null = null; @tracked private userHasDismissedURLError = false; @tracked private sourceFileIsSaving = false; + @tracked private writeError: string | undefined; @tracked private isCreateModalOpen = false; @tracked private itemToDelete: CardDef | URL | null | undefined; @tracked private cardResource: ReturnType | undefined; @@ -373,6 +374,11 @@ export default class CodeSubmode extends Component { this.sourceFileIsSaving = status === 'started'; } + @action + private onWriteError(message: string | undefined) { + this.writeError = message; + } + @action private onHorizontalLayoutChange(layout: number[]) { if (layout.length > 2) { @@ -731,6 +737,7 @@ export default class CodeSubmode extends Component { @saveSourceOnClose={{@saveSourceOnClose}} @selectDeclaration={{this.selectDeclaration}} @onFileSave={{this.onSourceFileSave}} + @onWriteError={{this.onWriteError}} @onSetup={{this.setupCodeEditor}} @isReadOnly={{this.isReadOnly}} /> @@ -738,6 +745,7 @@ export default class CodeSubmode extends Component { {{else if this.isLoading}} =