What this post covers
I traced the flow from one screen saving data in a browser extension to another screen showing the latest list. This is not about a specific product implementation or metric; it covers only the message contract and state update principles that apply anywhere when connecting separated execution contexts such as Content Scripts, Background Service Workers, and Side Panels.
Concepts to know first
RPC (Remote Procedure Call) is a way to send a structured request to another execution context and receive the result. Cache invalidation marks a saved query result as stale so the latest value is read again. A single source of truth means keeping the final authority for the same data in one storage location.
Screens that are not connected by shared memory
Consider a common browser extension feature: save something the user selected on a web page and show the list in the extension's Side Panel. The Content Script can read the current tab's DOM, the Background Service Worker handles storage and permissions, and the Side Panel renders the list. Even when all three are active at the same time, they do not share the same in-memory object.
So I traced each step from the save button to the list refresh. The UI's sendRequest() sends a { type, payload } request through runtime.sendMessage(), and the Background listener checks the received value with a runtime schema. Only validated requests reach the registered handler through the operation router, and the handler returns the result as { ok, data | error } after completing its transaction.
→ Message-based RPC in browser extensions
A save response does not update other screens by itself
It is easy to assume that once a save response arrives, every screen knows about the new data. But the response returns only to the screen that made the request. A separate Side Panel needs another path that announces the state change after the save.
In this flow, the event does not copy the entire new data payload. It broadcasts only a small signal that says, "data in this scope changed." The Side Panel receives the signal, invalidates the related TanStack Query cache, and reads the latest state from IndexedDB again. The response that reports whether a command succeeded and the notification that gives other screens a reason to re-fetch have different responsibilities, so separating them made the flow easier to reason about.
→ invalidateQueries and staleTime
Making sure data does not disappear when an event is missed
If the Side Panel is closed, no listener may receive the broadcast. That is fine when the change signal is only a reason to re-fetch, not the data itself. When the Side Panel opens later, its initial query reads IndexedDB and gets the latest state.
In this structure, IndexedDB is the single source of truth and the query cache is a rebuildable copy. Because receiving an event is not a requirement for preserving data, consistency can recover even when execution contexts have different lifetimes. By contrast, if event payloads are stored as the new source of truth, screens that missed the event and screens that received it can diverge.
What I learned
Chrome's message APIs and the application's RPC layer need to be treated as different things. The platform moves values, but the application has to decide which requests are allowed, which handler runs, and what shape failures take.
I also should not treat request responses and change notifications as one mechanism. The requester receives the operation result, while other screens receive a change signal and re-fetch only the necessary scope. Finally, when the cache remains a convenience copy and the latest data has exactly one authoritative store, it is easier to recover state after a screen closes and reopens or the Background context restarts.