Skip to content

Preserve every config change during rapid storage updates - #1037

Open
PeterDaveHello wants to merge 1 commit into
masterfrom
fix/config-storage-update-race
Open

Preserve every config change during rapid storage updates#1037
PeterDaveHello wants to merge 1 commit into
masterfrom
fix/config-storage-update-race

Conversation

@PeterDaveHello

@PeterDaveHello PeterDaveHello commented Aug 6, 2026

Copy link
Copy Markdown
Member

Problem

useConfig merged each storage event into the config object captured by the current render. Multiple storage callbacks can run before Preact refreshes that closure, so a later object update could overwrite an earlier queued change.

The effect also removed and re-added the storage listener after every config update because config was in its dependency list.

Changes

  • Convert storage patches into functional state updates that always receive the latest queued config.
  • Keep the storage listener stable unless the session-filter option changes.
  • Isolate the listener logic for deterministic unit coverage.
  • Preserve and test both session-filter modes, including the Independent Panel path that keeps session-only updates.

Validation

  • node --test tests/unit/hooks/config-storage-listener.test.mjs — 4 tests passed.
  • The combined focused suite for all five independent fixes — 39 tests passed.
  • node --check passed for the changed JavaScript modules.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change extracts configuration storage-change handling into createConfigStorageListener. useConfig uses the shared listener, and unit tests cover queued updates, session filtering, mixed changes, and unfiltered session updates.

Changes

Config storage synchronization

Layer / File(s) Summary
Shared storage listener
src/hooks/config-storage-listener.mjs
Adds createConfigStorageListener, which filters session-only changes and merges changed values through setConfig.
Hook integration and listener tests
src/hooks/use-config.mjs, tests/unit/hooks/config-storage-listener.test.mjs
Updates listener registration and effect dependencies in useConfig. Tests cover queued updates, session-only changes, mixed changes, and disabled session filtering.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving configuration changes during rapid storage updates.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/config-storage-update-race

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown

Greptile Summary

The PR replaces stale-closure config merges with functional state updates and keeps the storage listener stable across config changes.

  • Extracts storage-event handling into a dedicated listener factory.
  • Applies each storage patch to the latest queued config state.
  • Adds focused coverage for rapid updates and enabled, disabled, and mixed session filtering.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains, and the previously requested disabled-session-filter coverage is now present.

Important Files Changed

Filename Overview
src/hooks/config-storage-listener.mjs Extracts storage patch processing and uses functional updates while preserving session-filter behavior.
src/hooks/use-config.mjs Installs the extracted listener and limits effect recreation to changes in the session-filter option.
tests/unit/hooks/config-storage-listener.test.mjs Covers queued config merges and session-only or mixed events with filtering both enabled and disabled.

Sequence Diagram

sequenceDiagram
    participant Storage as Browser storage
    participant Listener as Config storage listener
    participant State as Config state
    Storage->>Listener: onChanged(changes)
    alt Session-only and filtering enabled
        Listener-->>Storage: Ignore event
    else Update accepted
        Listener->>State: "setConfig(current => merge(current, changes))"
        State-->>Listener: Latest queued config preserved
    end
Loading

Reviews (2): Last reviewed commit: "Preserve every config change during rapi..." | Re-trigger Greptile

@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix config storage race with functional updates and stable listener

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Apply storage patches via functional state updates to avoid stale-render overwrites.
• Keep the storage change listener stable unless session-filtering settings change.
• Add unit tests that replay consecutive storage callbacks and session filtering behavior.
Diagram

graph TD
  A["Browser.storage.onChanged"] --> B["createConfigStorageListener"] --> C["listener(changes)"] --> D["setConfig(fn)"] --> E["Config state"] --> F["useConfig consumers"]
  G["getUserConfig/init"] --> E["Config state"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep latest config in a ref (useRef) and merge imperatively
  • ➕ Avoids functional updates if downstream code expects object updates
  • ➕ Can centralize merge logic without producing multiple updater functions
  • ➖ Introduces ref/state dual sources of truth and synchronization pitfalls
  • ➖ Still requires careful listener lifecycle handling
2. Switch config state management to useReducer with patch actions
  • ➕ Explicit patch semantics and easier to extend (e.g., batching, validation)
  • ➕ Naturally avoids stale closure issues via reducer updates
  • ➖ Larger refactor and more boilerplate for a narrow bug fix
  • ➖ Potential churn across consumers if API changes

Recommendation: The chosen approach (extract a listener factory + functional setConfig updates + stable subscription keyed by ignoreSession) is the most targeted fix: it eliminates stale-closure overwrites without changing the hook’s public API, and it enables deterministic unit tests for the previously timing-dependent behavior.

Files changed (3) +75 / -11

Bug fix (2) +14 / -11
config-storage-listener.mjsExtract deterministic storage listener with functional config merging +11/-0

Extract deterministic storage listener with functional config merging

• Adds a small factory that builds a storage-change listener, optionally ignoring session-only updates. The listener converts the storage change map into a patch and applies it via a functional state update to avoid stale closure merges.

src/hooks/config-storage-listener.mjs

use-config.mjsUse extracted listener and stabilize subscription dependencies +3/-11

Use extracted listener and stabilize subscription dependencies

• Replaces the inline storage listener with createConfigStorageListener and switches config updates to functional merges. Adjusts the effect dependency list so the listener is not re-registered on every config change, only when ignoreSession changes.

src/hooks/use-config.mjs

Tests (1) +61 / -0
config-storage-listener.test.mjsAdd unit tests for queued updates and session filtering behavior +61/-0

Add unit tests for queued updates and session filtering behavior

• Introduces tests that replay back-to-back storage callbacks to verify functional updates merge against the latest queued state. Adds coverage for default session-only ignore behavior and mixed-change handling when sessions are present.

tests/unit/hooks/config-storage-listener.test.mjs

Comment thread tests/unit/hooks/config-storage-listener.test.mjs
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Storage callbacks can queue before Preact refreshes the hook closure. A
second object update built from that stale snapshot can overwrite the
change queued immediately before it.

Use functional state updates and keep the listener stable until its
session-filter option changes. Cover both filtering modes in tests.
@PeterDaveHello
PeterDaveHello force-pushed the fix/config-storage-update-race branch from 182115c to 3bd3e61 Compare August 6, 2026 18:45
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@PeterDaveHello
PeterDaveHello requested a lite review from Copilot August 6, 2026 18:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Refactors the configuration storage onChanged handler into a reusable listener factory and updates the React hook to use it, with new unit tests verifying queue-safe merging and session filtering behavior.

Changes:

  • Added createConfigStorageListener to centralize storage-change -> config-update logic.
  • Updated useConfig to use a functional setConfig update (avoids stale config closures) and reduced effect re-subscriptions.
  • Added unit tests covering merge behavior and session filtering modes.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
tests/unit/hooks/config-storage-listener.test.mjs Adds unit tests validating listener merge semantics and session filtering behavior.
src/hooks/use-config.mjs Switches to shared storage listener and updates effect dependencies.
src/hooks/config-storage-listener.mjs Introduces a factory for the storage change listener using functional state updates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +5 to +9
const configUpdate = {}
for (const key of Object.keys(changes)) {
configUpdate[key] = changes[key].newValue
}
setConfig((currentConfig) => ({ ...currentConfig, ...configUpdate }))
Comment on lines +3 to +6
if (ignoreSession && Object.keys(changes).length === 1 && 'sessions' in changes) return

const configUpdate = {}
for (const key of Object.keys(changes)) {
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