Skip to content

Add proposal: Synchronous data at startup#793

Open
Rob--W wants to merge 2 commits into
mainfrom
storage-for-synchronous-initialization
Open

Add proposal: Synchronous data at startup#793
Rob--W wants to merge 2 commits into
mainfrom
storage-for-synchronous-initialization

Conversation

@Rob--W
Copy link
Copy Markdown
Member

@Rob--W Rob--W commented Mar 27, 2025

This proposal offers a mechanism for extensions to specify values that are synchronously available when a content script or background script executes

This covers the use cases of #536, #703, #501, #747, among others.

@Rob--W Rob--W added proposal Proposal for a change or new feature topic: storage Issues related to persisting data. Topics include browser.storage, web storage, and new APIs. labels Mar 27, 2025
@Rob--W Rob--W requested review from oliverdunk and xeenon March 27, 2025 09:04
@hackademix
Copy link
Copy Markdown

hackademix commented Mar 27, 2025

Nice to see some progress to fix this very ancient problem, thank you!

I'm quite happy with this proposal, but I must signal two major problems making it unsuitable as a replacement for the current XHR work-around (one is a blocker, the second is slightly less critical):

  1. Blocker: without fixing request: allow to retrieve a tabId and documentId from the content script #469 there would be no way to tailor this configuration for certain tabs. Possible solution: adding filters such as includeTabIds[] / excludeTabIds[] to the ConfigStorageAreaSetOptions type.
  2. Site-specific configuration would require a very large amount of useless data to be marshaled within this storage blob, making the understandable size limit a potentially serious constrain. Similarly to n.1, a possible solution would be adding filters such as matches[] / excludeMatches[] to the ConfigStorageAreaSetOptions type.

@carlosjeurissen
Copy link
Copy Markdown
Contributor

@Rob--W Considering there seems to not be agreement on this PR yet, would it make sense to keep the issues open like #501? And potentially open a new issue to express support labels?

@xeenon
Copy link
Copy Markdown
Collaborator

xeenon commented Mar 29, 2025

@Rob--W Thanks for the proposal — this solves a real problem, but I strongly prefer a single storage.config area over the proposed two-area model.

Splitting into extensionConfig and contentScriptConfig introduces unnecessary complexity and developer confusion, with little benefit. The use cases are fully covered by a single area where:

  • Extension contexts have read/write access.
  • Content scripts have read-only access.

We already enforce content script access control for storage.session without needing separate namespaces, so duplicating the storage area purely for access restrictions feels unnecessary.

There’s also no precedent for changes in one namespace triggering events in another, and introducing that pattern (e.g., extensionConfig.set() firing contentScriptConfig.onChanged) adds avoidable complexity.

Also, I support keeping oldValue and newValue in the onChanged event — browsers should be able to detect changes without needing to deserialize values first (e.g., by comparing the binary representation of old and new values).

Copy link
Copy Markdown
Collaborator

@xeenon xeenon left a comment

Choose a reason for hiding this comment

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

Requesting changes based on my above comment.

@Rob--W
Copy link
Copy Markdown
Member Author

Rob--W commented Apr 9, 2026

I strongly prefer a single storage.config area over the proposed two-area model.

(...)
There’s also no precedent for changes in one namespace triggering events in another, and introducing that pattern (e.g., extensionConfig.set() firing contentScriptConfig.onChanged) adds avoidable complexity.

These two areas are not linked to each other; they are independent. One is meant to be used by content scripts, the other by the background script. I'll clarify.

Splitting into extensionConfig and contentScriptConfig introduces unnecessary complexity and developer confusion, with little benefit. The use cases are fully covered by a single area where:

  • Extension contexts have read/write access.
  • Content scripts have read-only access.

We already enforce content script access control for storage.session without needing separate namespaces, so duplicating the storage area purely for access restrictions feels unnecessary.

The availability to content scripts is controlled through the storage.session.setAccessLevel method, which is currently an all-or-nothing. To serve the use cases, we would need the ability to configure the availability per key (which is a future work already identified when setAccessLevel was offered in Chrome - https://crbug.com/40189208).

I see the following options:

  • Current version: two separate storage areas (and quotas)
  • alternative B: single area, with setAccessLevel that makes all or nothing available to content scripts.
  • alternative C: single area, with setAccessLevel updated to support configuration of keys (instead of the whole area).
  • alternative D: single area, with an option, e.g. ConfigStorageAreaSetOptions.accessLevel to specify what the access level should be.

Alternative B is better than nothing, but forces extension devs to choose sync data in content scripts XOR extension-private initialization data in background scripts.

Alternative C requires a bit more thought on the semantics of setAccessLevel:

  • What is the scope/lifetime of setAccessLevel? For the global storage the level is persistent. If we introduce key-specific access levels, we need to tie the access level to the key, or otherwise deal with access levels in limbo (e.g. if setAccessLevel is called before set(), or if remove() was called).
  • When should setAccessLevel be called, before or after set()? If called before (and there being some limbo state with access level defined for a non-existing key), storage.onChanged can fire, if called later, storage.onChanged is not guaranteed to fire.
  • When a key is removed, should its access level be cleared as well?

Alternative D is a version of C without the need to worry about the following getting out of sync: access level, persistent flag, key-value pair.

@ylemkimon
Copy link
Copy Markdown

I think splitting into two storage areas would both benefit security and developer's experience. Many developers don't realize and consider that a compromised renderer may access the extension storage, and put sensitive data on it. Naming the storage "contentScriptConfig" would definitely signal developers not to.

Rob--W added 2 commits April 9, 2026 17:18
Changes to the API:
- Drop getSync / getKeysSync; make `get` and `getKeys` sync instead.

- To avoid the odd situation of being to write without reading, the
  `contentScriptConfig` area is now also synchronously readable by
  extension contexts.

Updates to the proposal:

- Explicitly list all exposed APIs for clarity.

- Emphasize independence of storage areas for clarity.

- Add expectations for flushing and method resolution behavior.

- Drop "TODO" remark about potentially excluding oldValue/newValue
  from onChanged event as an optimization, because the consensus is
  to maintain the same semantics as other storage `onChanged` events.

- Expand "Future work"
@Rob--W Rob--W force-pushed the storage-for-synchronous-initialization branch from 5149b28 to 4440fe7 Compare April 10, 2026 12:18
@Rob--W
Copy link
Copy Markdown
Member Author

Rob--W commented Apr 10, 2026

I have updated the proposal based on previous feedback and the feedback from the WECG F2F meeting in London.

There was a request from @xeenon to use one namespace instead of the proposed two. @rdcronin expressed a strong preference for keeping them separate as initially proposed. I kept them separate, with extra clarifications in the proposal.

I dropped getSync and getKeysSync, and made the get and getKeys methods synchronous instead, following the request from @rdcronin .

One consequence of this change is that contentScriptConfig is now also synchronously readable by privileged extension contexts (background scripts). In my initial proposal I declared the async get function while withholding getSync to discourage the use of contentScriptConfig by background scripts, but with only one method that is not possible. I considered alternatives, but rejected them because they felt worse:

  • Omit contentScriptConfig.get (and getKeys) from privileged extension contexts. I rejected this option because it would rule out the ability for extensions to incrementally update a value (read + write), unless they use a content script to consult the current value.
  • Make contentScriptConfig.get (and getKeys) return a promise (instead of the value such as seen in content scripts). I rejected this option because different return types for the same API method would be confusing.

If anyone prefers the alternatives over the current approach, I'm willing to reconsider these.

Other than the above API change, the revised proposal adds a bit more detail to leave less to the imagination:

  • Explicitly list all exposed APIs for clarity.

  • Emphasize independence of storage areas for clarity.

  • Add expectations for flushing and method resolution behavior.

  • Drop "TODO" remark about potentially excluding oldValue/newValue from onChanged event as an optimization, because the consensus is to maintain the same semantics as other storage onChanged events.

  • Expand "Future work"

Copy link
Copy Markdown
Collaborator

@rdcronin rdcronin left a comment

Choose a reason for hiding this comment

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

Thanks, Rob! Excited to see this!


```
// Methods available to extension contexts and content scripts:
any ConfigStorageArea.get(keyOrKeysOrObjectWithDefaults: string | string[] | null | object)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

get() is pretty confusing on the existing StorageArea interface as a result of the various different kinds of inputs it can take. Do you think it would make sense to use this opportunity to straight this out? Especially since this is designed to be synchronous (and reads necessarily must be cheaper), it seems like we could just have a single get() call that takes a string and returns a value, and the getting-multiple-strings or getting-with-defaults cases can be handled extension side without the magic transformations we currently apply for storage.


number ConfigStorageArea.QUOTA_BYTES;

// In content scripts, the set/remove/clear methods are unavailable:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this available in user scripts? (Conditionally, configurably, as with messaging?)

ConfigStorageArea browser.storage.extensionConfig;

dictionary ConfigStorageAreaSetOptions {
persistent: boolean // default false
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Having this be optionally-persistent means that we need to have a pass-through storage mechanism where we write values to disk (in yet-another new storage area) and also update shared memory, which is passed through to all processes. That's doable, of course, but I wonder if we would get 95% of the benefit of this API with having this be in-memory / session only.

This:

  • Is simpler architecturally, and thus also provides more guarantees around consistency (less chance that the in-memory store and on-disk store get out of sync -- though, of course, they may still be out of sync between renderers during the course of a storage update)
  • Incentivizes using this for lightweight data and reduces the likelihood that it would grow with stale data that wasn't collected (though this is also mitigated by an aggressive storage limit)
  • Anecdotally, would be used by many of these use cases, which use it for "secrets" that are better generated at runtime rather than persisted.

Downside: Extensions that don't need dynamic / changing values would need to set this once per chrome session, possibly waking up unnecessarily at startup. It also means this probably wouldn't be available synchronous for the first content scripts that run at browser run -- but that's already not guaranteed and would also not be guaranteed with a disk-backed storage (since we wouldn't block renderer load on loading that storage, at least in Chrome).

And, of course, if we decide later we definitely do need persistence, we could always add it in later. WDYT?

number ConfigStorageArea.QUOTA_BYTES;

// In content scripts, the set/remove/clear methods are unavailable:
ConfigStorageArea browser.storage.contentScriptConfig;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I find the "contentScriptConfig" naming to be both clunky and confusing (I'd expect this to be a configuration around a content script, which is much more what scripting.RegisteredContentScript is).

Preferred alternative: cache? Maybe storage.cache for trusted contexts; storage.untrustedCache for untrusted contexts? IMO, that leans more into the "rapid, easy-access storage that probably shouldn't be super large." (And, see also below.)

Alternative: "lite"? Don't love that one, though.

asynchronously.

Updates to the storage area are propagated to all processes where a context may
exists with a need for synchronous data access. This proposal specifies the
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
exists with a need for synchronous data access. This proposal specifies the
exist with a need for synchronous data access. This proposal specifies the


By default, data only lasts for the duration of the browser session. The `set`
method has a `persistent` option that extends the lifetime past browser and
extension restarts. The data may be cleared when an extension is uninstalled.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
extension restarts. The data may be cleared when an extension is uninstalled.
extension restarts. The data will be cleared when an extension is uninstalled.

Comment on lines +198 to +202
Updates are asynchronous. The returned `Promise` may await writes to disk when
the persistent flag is set, but does not guarantee the delivery of data to
other processes. This ensures that non-responsive processes cannot delay the
resolution of the `Promise`. The caller resides in the extension process and
is guaranteed to receive the updated value when `get` or `getSync` are called.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This ensures that non-responsive processes cannot delay the resolution of the Promise

I understand the motivation, but that also means that the extension won't know if it can expect that a message it sends to a renderer would be able to access the data it set.

Brainstorming: Do you think it would be reasonable to say that it's guaranteed that any message sent after the promise resolves would arrive after the storage is updated in the renderer? I think we can make that guarantee in Chrome -- we'd update the shared memory and any messages sent would go across the same IPC channel that the shared memory update would have already gone over.

Comment on lines +190 to +194
Each key has its own associated `persistent` flag, which affect the behavior of
the latest `set()` call, and any following `remove()` or `clear()` calls.
The `set` method can update multiple keys at once, and the `persistent` option
applies to all specified keys. To have different `persistent` flags for keys,
call the `set()` method again, with a different `persistent` option value.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

see above about just not having "persistent", but if we do have it, we should call out the behavior of conflicting persistent values. I assume that if a mark a key as persistent, then later set it to a non-persistent value, the persistent value is wiped. That is, there can only be a single key, and the latest setting for persistent / non-persistent is used.

- `browser.storage.extensionConfig.onChanged`
- `browser.storage.extensionConfig.QUOTA_BYTES`

### Behavior
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should include a section on incognito.

I assume that, similar to the storage API, this does not respect incognito boundaries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

proposal Proposal for a change or new feature topic: storage Issues related to persisting data. Topics include browser.storage, web storage, and new APIs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants