-
Notifications
You must be signed in to change notification settings - Fork 81
Add proposal: Synchronous data at startup #793
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,313 @@ | ||||||
| # Proposal: Synchronous data at startup | ||||||
|
|
||||||
| **Summary** | ||||||
|
|
||||||
| A mechanism for extensions to specify values that are synchronously available when a content script or background script executes. | ||||||
|
|
||||||
| **Document Metadata** | ||||||
|
|
||||||
| **Author:** Rob--W | ||||||
|
|
||||||
| **Sponsoring Browser:** Mozilla | ||||||
|
|
||||||
| **Contributors:** @oliverdunk | ||||||
|
|
||||||
| **Created:** 2025-03-26 | ||||||
|
|
||||||
| **Related Issues:** | ||||||
|
|
||||||
| * [Issue 536 comment](https://github.com/w3c/webextensions/issues/536#issuecomment-2200692043): `scripting.globalParams` in content scripts | ||||||
| * [Issue 703](https://github.com/w3c/webextensions/issues/703): State in background scripts, synchronously available across restarts | ||||||
| * Seemingly related but explicitly out of scope: [Issue 284](https://github.com/w3c/webextensions/issues/284): Main world Content Script shared params | ||||||
| * Resolves use case: [Issue 501](https://github.com/w3c/webextensions/issues/501): Proposal: Toggleable event listeners | ||||||
| * Resolves use case: [issue 747](https://github.com/w3c/webextensions/issues/747): API to invalidate BFCache'd webpage with outdated content added by the extension | ||||||
|
|
||||||
| ## Motivation | ||||||
|
|
||||||
| ### Objective | ||||||
|
|
||||||
| Provide a way for extensions to set and update state that is synchronously | ||||||
| available at the start of script execution. The state can be set from | ||||||
| privileged extension contexts, such as background scripts and options pages. | ||||||
| Once the state has propagated (asynchronously), the state can *synchronously* | ||||||
| be read from content scripts or other privileged contexts. | ||||||
|
|
||||||
| This state may be session-scoped or persist across browser restarts and | ||||||
| extension updates. | ||||||
|
|
||||||
| Content scripts have the following additional limitations: | ||||||
| - They can only read this shared state, and not propagate modifications. | ||||||
| - They may not access state specific to privileged contexts. | ||||||
|
|
||||||
| #### Use Cases | ||||||
|
|
||||||
| Some use cases depend on state to be synchronously available at the start of | ||||||
| script execution. This applies both to content scripts and background scripts. | ||||||
|
|
||||||
| In the context of content scripts, initial state may be needed to run logic | ||||||
| at `document_start`, before the web page has had a chance to run. Content | ||||||
| scripts may also want to revalidate their state without latency after returning | ||||||
| from the bfcache ([issue 747](https://github.com/w3c/webextensions/issues/747)). | ||||||
|
|
||||||
| This concept has also been discussed before as `globalParams` in the context of | ||||||
| dynamic content scripts (see https://crbug.com/1054624). | ||||||
|
|
||||||
| In the context of background scripts, initial state may be needed for | ||||||
| initialization, for example registering listeners based on user configuration | ||||||
| ([issue 501](https://github.com/w3c/webextensions/issues/501)). | ||||||
|
|
||||||
|
|
||||||
| ### Known Consumers | ||||||
|
|
||||||
| Classes of privacy extensions, such as NoScript | ||||||
| ([issue 536](https://github.com/w3c/webextensions/issues/536)). | ||||||
|
|
||||||
| Classes of extensions that modify web pages based on user input, | ||||||
| where latency in state retrieval degrades the user experience. | ||||||
| The Violentmonkey author expressed a desire to use such an API in | ||||||
| [issue 747](https://github.com/w3c/webextensions/issues/747). | ||||||
|
|
||||||
|
|
||||||
| ## Specification | ||||||
|
|
||||||
| ### Schema | ||||||
|
|
||||||
| This proposal extends the `browser.storage` namespace with two new storage | ||||||
| areas. Their interface is inspired by the `StorageArea` type, except the | ||||||
| methods to look up stored keys/values are synchronous (`get` and `getKeys`). | ||||||
|
|
||||||
| The `set` method includes a new optional `options` parameter to configure the | ||||||
| persistence of the specified parameters. | ||||||
|
|
||||||
| ``` | ||||||
| // Methods available to extension contexts and content scripts: | ||||||
| any ConfigStorageArea.get(keyOrKeysOrObjectWithDefaults: string | string[] | null | object) | ||||||
| string[] ConfigStorageArea.getKeys(); | ||||||
| ExtensionEvent ConfigStorageArea.onChanged; | ||||||
|
|
||||||
| // Methods available to extension contexts only, not content scripts: | ||||||
| Promise<number> ConfigStorageArea.getBytesInUse() | ||||||
| Promise<undefined> ConfigStorageArea.set(items: object, options?: ConfigStorageAreaSetOptions) | ||||||
| Promise<undefined> ConfigStorageArea.remove(keyOrKeys: string | string[]) | ||||||
| Promise<undefined> ConfigStorageArea.clear() | ||||||
|
|
||||||
| number ConfigStorageArea.QUOTA_BYTES; | ||||||
|
|
||||||
| // In content scripts, the set/remove/clear methods are unavailable: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.contentScriptConfig; | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
|
|
||||||
| // Extension contexts only: | ||||||
| ConfigStorageArea browser.storage.extensionConfig; | ||||||
|
|
||||||
| dictionary ConfigStorageAreaSetOptions { | ||||||
| persistent: boolean // default false | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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? |
||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| #### Explicit list of APIs | ||||||
|
|
||||||
| This section describes the full list of APIs specified by this proposal. | ||||||
|
|
||||||
| APIs available to content scripts: | ||||||
|
|
||||||
| - `browser.storage.contentScriptConfig.get` | ||||||
| - `browser.storage.contentScriptConfig.getKeys` | ||||||
| - `browser.storage.contentScriptConfig.onChanged` | ||||||
| - `browser.storage.contentScriptConfig.QUOTA_BYTES` | ||||||
|
|
||||||
| APIs available to privileged extension contexts: | ||||||
|
|
||||||
| - `browser.storage.contentScriptConfig.get` | ||||||
| - `browser.storage.contentScriptConfig.getKeys` | ||||||
| - `browser.storage.contentScriptConfig.getBytesInUse` | ||||||
| - `browser.storage.contentScriptConfig.set` | ||||||
| - `browser.storage.contentScriptConfig.remove` | ||||||
| - `browser.storage.contentScriptConfig.clear` | ||||||
| - `browser.storage.contentScriptConfig.onChanged` | ||||||
| - `browser.storage.contentScriptConfig.QUOTA_BYTES` | ||||||
| - `browser.storage.extensionConfig.get` | ||||||
| - `browser.storage.extensionConfig.getKeys` | ||||||
| - `browser.storage.extensionConfig.getBytesInUse` | ||||||
| - `browser.storage.extensionConfig.set` | ||||||
| - `browser.storage.extensionConfig.remove` | ||||||
| - `browser.storage.extensionConfig.clear` | ||||||
| - `browser.storage.extensionConfig.onChanged` | ||||||
| - `browser.storage.extensionConfig.QUOTA_BYTES` | ||||||
|
|
||||||
| ### Behavior | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
|
|
||||||
| #### ConfigStorageArea | ||||||
|
|
||||||
| This proposal defines two fully independent `ConfigStorageArea` instances, | ||||||
| `contentScriptConfig` and `extensionConfig`. Privileged extension contexts have | ||||||
| full read and write access to both areas, whereas content scripts can only | ||||||
| read from `contentScriptConfig`. | ||||||
|
|
||||||
| The `ConfigStorageArea` type is based on the `StorageArea` interface, except | ||||||
| with the `get` and `getKeys` methods returning data synchronously instead of | ||||||
| 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 | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| `set`, `remove` and `clear` methods to update data, that eventually flushes. | ||||||
|
|
||||||
| Once flushed, saved data should immediately be readable. In particular: | ||||||
|
|
||||||
| - Background scripts / extension service workers should be able to read from | ||||||
| `extensionConfig` at any stage of their life cycle, including startup. | ||||||
| - Content scripts should be able to read from `contentScriptConfig`, even at | ||||||
| the earliest execution (`document_start`). | ||||||
|
|
||||||
| 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. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| #### get | ||||||
|
|
||||||
| The `get()` method has similar semantics as `StorageArea.get`, except it | ||||||
| *synchronously* returns keys and values that are known locally in the process. | ||||||
|
|
||||||
| #### getKeys | ||||||
|
|
||||||
| The `getKeys()` method has similar semantics as `StorageArea.getKeys`, except | ||||||
| it *synchronously* returns keys that are known locally in the process. | ||||||
|
|
||||||
| #### getBytesInUse | ||||||
|
|
||||||
| The `getBytesInUse()` method returns the amount of bytes in use by the data. | ||||||
|
|
||||||
| See `QUOTA_BYTES` for remarks about how quota is measured. | ||||||
|
|
||||||
|
|
||||||
| #### set | ||||||
|
|
||||||
| The `set()` method takes a (new) optional options object as a second parameter. | ||||||
|
|
||||||
| The `persistent` option defaults to false, which means that the data is not | ||||||
| persisted across browser restarts. When `persistent` is set to `true`, the | ||||||
| specified key-value pairs are persisted across browser and extension restarts. | ||||||
|
|
||||||
| 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. | ||||||
|
Comment on lines
+190
to
+194
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
|
|
||||||
| Persisted data should be stored locally, comparable to `storage.local`. | ||||||
|
|
||||||
| 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. | ||||||
|
Comment on lines
+198
to
+202
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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. |
||||||
|
|
||||||
| #### remove | ||||||
|
|
||||||
| Removes data associated with the specified keys. | ||||||
|
|
||||||
| See the `set` method for remarks about this method's asynchronous behavior. | ||||||
|
|
||||||
| #### clear | ||||||
|
|
||||||
| Clears all data in this storage area. | ||||||
|
|
||||||
| See the `set` method for remarks about this method's asynchronous behavior. | ||||||
|
|
||||||
| #### onChanged | ||||||
|
|
||||||
| When a storage change is observed, the `browser.storage.onChanged`, | ||||||
| `browser.storage.contentScriptConfig.onChanged` or | ||||||
| `browser.storage.extensionConfig.onChanged` events are dispatched as needed. | ||||||
|
|
||||||
| The `onChanged` event receives an object with all modified keys. | ||||||
|
|
||||||
| #### QUOTA_BYTES | ||||||
|
|
||||||
| The maximum amount (in bytes) of data that can be stored, as measured by the | ||||||
| key's string length plus the JSON stringification of every value, or the byte | ||||||
| consumption if the agent supports structured cloning. | ||||||
|
|
||||||
| The value is a balance between utility for extensions and performance impact. | ||||||
|
|
||||||
| The quota is `102400`. | ||||||
|
|
||||||
| ### New Permissions | ||||||
|
|
||||||
| This API builds upon the `storage` API, which already requires the `storage` | ||||||
| permission. The `storage` permission does not trigger a warning message. | ||||||
|
|
||||||
| ### Manifest File Changes | ||||||
|
|
||||||
| This proposal does not specify manifest changes. | ||||||
|
|
||||||
| ## Security and Privacy | ||||||
|
|
||||||
| ### Exposed Sensitive Data | ||||||
|
|
||||||
| This API does not expose sensitive data. | ||||||
|
|
||||||
| ### Abuse Mitigations | ||||||
|
|
||||||
| This API specifies data that should be made available across all processes, | ||||||
| which may affect the memory usage and startup cost of all processes. | ||||||
| To limit abuse, a reasonably tight storage quota is chosen. | ||||||
|
|
||||||
| ### Additional Security Considerations | ||||||
|
|
||||||
| The existing `storage.local` API is sometimes used by extension to store | ||||||
| sensitive information. Due to its default availability in content scripts, | ||||||
| this exposes extensions to data leakage in compromised content processes. | ||||||
|
|
||||||
| The proposed API design separates content script storage from the storage of | ||||||
| the higher-privileged parts of the extension, to encourage safe defaults. | ||||||
|
|
||||||
| ## Alternatives | ||||||
|
|
||||||
| ### Existing Workarounds | ||||||
|
|
||||||
| In content scripts, extensions use synchronous XMLHttpRequest to block the main | ||||||
| thread of the web page to fetch dynamic configuration that has been specified | ||||||
| by the extension. | ||||||
|
|
||||||
| In document-based background contexts, extensions can use the `localStorage` | ||||||
| API from the web platform as a synchronous storage mechanism. A limitation of | ||||||
| `localStorage` is that the data may not persist when the background context | ||||||
| runs in private browsing (incognito) mode, similar to | ||||||
| [issue 534](https://github.com/w3c/webextensions/issues/534). | ||||||
|
|
||||||
| Service worker-based background contexts do not have alternatives for | ||||||
| synchronous storage. | ||||||
|
|
||||||
| ### Open Web API | ||||||
|
|
||||||
| `contentScriptConfig` does not make sense on the web, as the concept of content | ||||||
| scripts is non-existent on the regular web platform. | ||||||
|
|
||||||
| Service workers use asynchronous APIs by design. Synchronously blocking storage | ||||||
| is incompatible with that design. | ||||||
|
|
||||||
| ## Implementation Notes | ||||||
|
|
||||||
|
|
||||||
| ## Future Work | ||||||
|
|
||||||
| ### Domain or Origin specific data | ||||||
|
|
||||||
| This proposal offers one global storage area for content scripts shared between | ||||||
| all websites. A potential enhancement could be to offer a way to scope storage | ||||||
| to specific domains or origins. While the proposal currently allows extensions | ||||||
| to implement the domain-specific values themselves by including the domain or | ||||||
| origin in the key, the data would still be exposed across all processes, which | ||||||
| makes the mechanism less suited for data that ought to only be shared with | ||||||
| processes from a specific origin. | ||||||
|
|
||||||
| ### onChanged event filter | ||||||
|
|
||||||
| The `onChanged` event fires for any storage change, which is inefficient if a | ||||||
| content script is only interested in changes to a specific key. | ||||||
|
|
||||||
| There is a proposal to add filter options to `StorageArea.onChanged` at | ||||||
| [Issue 475](https://github.com/w3c/webextensions/issues/475). | ||||||
|
|
||||||
| An alternative for extensions is to use `tabs.sendMessage` as a mechanism to | ||||||
| notify specific content scripts of data changes, but that would add overhead. | ||||||
There was a problem hiding this comment.
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.