Skip to content

fix: prevent canvas component loss when saving default filter#9494

Merged
djbarnwal merged 1 commit into
mainfrom
Nishant-Bangarwa/fix-canvas-save-default-filter-race
May 25, 2026
Merged

fix: prevent canvas component loss when saving default filter#9494
djbarnwal merged 1 commit into
mainfrom
Nishant-Bangarwa/fix-canvas-save-default-filter-race

Conversation

@nishantmonu51
Copy link
Copy Markdown
Collaborator

@nishantmonu51 nishantmonu51 commented May 23, 2026

Root cause

CanvasEntity.saveDefaultFilters (web-common/src/features/canvas/stores/canvas-entity.ts) did a read → await → mutate → write on the canvas YAML, capturing the Document reference before the await:

  1. const yaml = get(this.parsedContent)parsedContent is a derived store over editorContent; each editorContent change emits a fresh parsed Document. The local yaml only points at one snapshot.
  2. await Promise.all(...) — one queryServiceConvertExpressionToMetricsSQL round-trip per metrics view in scope (100–500 ms typical).
  3. yaml.setIn(["defaults", "filters"], filterMap) then yaml.toString() then updateEditorContent(newContent, false, true) — writes the pre-await document back, immediately, via runtimeServicePutFile.

Anything that mutated editorContent during the await window was stranded on a newer Document that the saver never saw:

  • Grid edits. CanvasBuilder.updateContentsupdateEditorContent(..., false, true) runs saveContent immediately, not debounced. A drag/drop or resize finishing right around the click writes a newer YAML to disk.
  • File-watch echo. Each save triggers FILE_EVENT_WRITE from the runtime; file-invalidators.ts re-fetches the file and, when fileUntouched, pushes the fresh blob into editorContent, re-deriving parsedContent.

When the stale YAML hit disk, the runtime reconciled a truncated canvas spec. specStore.subscribeprocessSpecprocessRows then deleted every component name no longer present in rows[].items[].component from the in-memory componentsStore — which is what the user perceived as "components disappeared."

The pre-existing setTimeout(100) at the top of the method is unrelated (it waits for filter-input blur) and does nothing to make the read-modify-write atomic.

Fix

  • Collect pinned/time/comparison inputs and kick off Promise.all first.
  • Read parsedContent after the await, then apply all mutations and write in a single synchronous block.
  • clearDefaultFilters was already synchronous and is unchanged.
  • Behavior on disk is unchanged for the no-concurrent-edit case: only filters.pinned and defaults.* are touched; rows/items stay byte-identical.

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

Developed in collaboration with Claude Code

The previous flow captured the YAML Document before awaiting the
metrics-SQL conversion and then mutated and wrote that same reference.
Any component edit that landed in editorContent during the await was
stranded on a fresh Document, so the write reverted the canvas to a
pre-edit state and the runtime reconciliation deleted the missing
components from the in-memory store.

Collect inputs first, run Promise.all, then read parsedContent and apply
all mutations synchronously before writing. This keeps the
read-modify-write atomic relative to editorContent updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@djbarnwal djbarnwal merged commit 428428d into main May 25, 2026
18 of 20 checks passed
@djbarnwal djbarnwal deleted the Nishant-Bangarwa/fix-canvas-save-default-filter-race branch May 25, 2026 10:12
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.

2 participants