├── .github └── workflows │ └── auto-publish.yml ├── .gitignore ├── .pr-preview.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── EXPLAINER.md ├── LICENSE.md ├── README.md ├── alternate-api-proposals.md ├── index.bs ├── logo-lock.svg ├── proto-spec.md ├── security-privacy-self-assessment.md └── w3c.json /.github/workflows/auto-publish.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: {} 4 | push: 5 | branches: [main] 6 | jobs: 7 | main: 8 | name: Build, Validate and Deploy 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: w3c/spec-prod@v2 13 | with: 14 | GH_PAGES_BRANCH: gh-pages 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /index.html 2 | -------------------------------------------------------------------------------- /.pr-preview.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_file": "index.bs", 3 | "type": "bikeshed" 4 | } 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | All documentation, code and communication under this repository are covered by the [W3C Code of Ethics and Professional Conduct](https://www.w3.org/Consortium/cepc/). 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Web Platform Incubator Community Group 2 | 3 | This repository is being used for work in the W3C Web Platform Incubator Community Group, governed by the [W3C Community License 4 | Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To make substantive contributions, 5 | you must join the CG. 6 | 7 | If you are not the sole contributor to a contribution (pull request), please identify all 8 | contributors in the pull request comment. 9 | 10 | To add a contributor (other than yourself, that's automatic), mark them one per line as follows: 11 | 12 | ``` 13 | +@github_username 14 | ``` 15 | 16 | If you added a contributor by mistake, you can remove them in a comment with: 17 | 18 | ``` 19 | -@github_username 20 | ``` 21 | 22 | If you are making a pull request on behalf of someone else but you had no part in designing the 23 | feature, you can remove yourself with the above syntax. 24 | -------------------------------------------------------------------------------- /EXPLAINER.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Explainer: Web Locks API 4 | 5 | This document proposes a new web platform API that allows script to asynchronously acquire a lock over a resource, hold it while work is performed, then release it. While held, no other script in the origin can aquire a lock over the same resource. This allows contexts (windows, workers) within a web application to coordinate the usage of resources. 6 | 7 | Participate: [GitHub issues](https://github.com/w3c/web-locks/issues) — 8 | Docs: [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) — 9 | Tests: [web-platform-tests](https://github.com/web-platform-tests/wpt/tree/master/web-locks) 10 | 11 | ## Introduction 12 | 13 | The API is used as follows: 14 | 1. The lock is requested. 15 | 2. Work is done while holding the lock in an asynchronous task. 16 | 3. The lock is automatically released when the task completes. 17 | 18 | ```js 19 | navigator.locks.request('my_resource', async lock => { 20 | // The lock has been acquired. 21 | await do_something(); 22 | await do_something_else(); 23 | // Now the lock will be released. 24 | }); 25 | ``` 26 | The API provides optional functionality that may be used as needed, including: 27 | * returning values from the asynchronous task 28 | * shared and exclusive lock modes 29 | * conditional acquisition 30 | * diagnostics to query the state of locks in an origin 31 | * an escape hatch to protect against deadlocks 32 | 33 | Cooperative coordination takes place within the scope of same-origin [agents](https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-agent-formalism) (informally: frames/windows/tabs and workers); this may span multiple 34 | [agent clusters](https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-agent-cluster-formalism) (informally: process boundaries). 35 | 36 | In conjunction with this informal explainer, a [work-in-progress specification](https://w3c.github.io/web-locks/) defines the precise behavior of the proposed API. 37 | 38 | Previous discussions: 39 | * [Application defined "locks" [whatwg]](https://lists.w3.org/Archives/Public/public-whatwg-archive/2009Sep/0266.html) 40 | 41 | 42 | ## Use Case Examples 43 | 44 | A web-based document editor stores state in memory for fast access and persists changes (as a series of records) to a storage API such as IndexedDB for resiliency and offline use, and to a server for cross-device use. When the same document is opened for editing in two tabs the work must be coordinated across tabs, such as allowing only one tab to make changes to or synchronize the document at a time. This requires the tabs to coordinate on which will be actively making changes (and synchronizing the in-memory state with the storage API), knowing when the active tab goes away (navigated, closed, crashed) so that another tab can become active. 45 | 46 | In a data synchronization service, a "primary tab" is designated. This tab is the only one that should be performing some operations (e.g. network sync, cleaning up queued data, etc). It holds a lock and never releases it. Other tabs can attempt to acquire the lock, and such attempts will be queued. If the "primary tab" crashes or is closed then one of the other tabs will get the lock and become the new primary. 47 | 48 | The [Indexed Database API](https://w3c.github.io/IndexedDB/) defines a transaction model allowing shared read and exclusive write access across multiple named storage partitions within an origin. Exposing this concept as a primitive allows any Web Platform activity to be scheduled based on resource availability, for example allowing transactions to be composed for other storage types (such as Cache Storage), across storage types, even across non-storage APIs (e.g. network fetches). 49 | 50 | 51 | ## Concepts 52 | 53 | A _name_ is just a string chosen by the web application to represent an abstract resource. 54 | 55 | A _mode_ is either "exclusive" or "shared". 56 | 57 | A _lock request_ is made by script for a particular _name_ and _mode_. A scheduling algorithm looks at the state of current and previous requests, and eventually grants a lock request. 58 | 59 | A _lock_ is a granted request; it has the _name_ of the resource and _mode_ of the lock request. It is represented as an object returned to script. 60 | 61 | As long as the lock is _held_ it may prevent other lock requests from being granted (depending on the name and mode). 62 | 63 | A lock can be _released_ by script, at which point it may allow other lock requests to be granted. 64 | 65 | A user agent has a _lock manager_ for each origin, which encapsulates the state of all locks and requests for that origin. 66 | 67 | #### Lock Manager Scope 68 | 69 | For the purposes of this proposal: 70 | 71 | * Separate user profiles within a browser are considered separate user agents. 72 | * A [private mode](https://github.com/w3ctag/private-mode) browsing session is considered a separate user agent. 73 | 74 | Pages and workers (agents) on a single [origin](https://html.spec.whatwg.org/multipage/origin.html#concept-origin) opened in the same user agent share a lock manager even if they are in unrelated [browsing contexts](https://html.spec.whatwg.org/multipage/browsers.html#browsing-context). 75 | 76 | There is an equivalence between the following: 77 | 78 | * Agents that can communicate via [BroadcastChannel](https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts) 79 | * Agents that share [storage](https://storage.spec.whatwg.org/); e.g. a per-origin [local storage area](https://html.spec.whatwg.org/multipage/webstorage.html#the-localstorage-attribute), set of [Indexed DB](https://w3c.github.io/IndexedDB/) [https://w3c.github.io/IndexedDB/#database-construct](databases), or [Service Worker](https://w3c.github.io/ServiceWorker/) [caches](https://w3c.github.io/ServiceWorker/#cache-objects). 80 | * Agents that share a lock manager. 81 | 82 | This is important as it defines a privacy boundary. Locks can be used as a communication channel, and must be no more privileged than BroadcastChannel. Locks can be used as a state retention mechanism, and must be no more privileged than storage facilities. User agents that impose finer granularity on one of these services must impose it on others; for example, a user agent that exposes different storage partitions to a top-level page and a cross-origin iframe in the same origin for privacy reasons must similarly partition broadcasting and locking. 83 | 84 | This also provides reasonable expectations for web application authors; if a lock is acquired over a storage resource, or a broadcast is made signalling that updated data has been stored, all same-origin browsing contexts should observe the same state. 85 | 86 | > TODO: Migrate this definition to [HTML](https://html.spec.whatwg.org/multipage/) or [Storage](https://storage.spec.whatwg.org/) so it can be referenced by other standards. 87 | 88 | #### Resources Names 89 | 90 | The resource _name_ strings have no external meaning beyond the scheduling algorithm, but are global 91 | across browsing contexts within an origin. Web applications are free to use any resource naming 92 | scheme. For example, to mimic [IndexedDB](https://w3c.github.io/IndexedDB/#transaction-construct)'s transaction locking over named stores within a named 93 | database, an origin might use `encodeURIComponent(db_name) + '/' + encodeURIComponent(store_name)`. 94 | 95 | Names starting with `-` (dash) are reserved; requesting these will throw. 96 | 97 | #### Modes and Scheduling 98 | 99 | The _mode_ property can be used to model the common [readers-writer lock](http://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) pattern. If an "exclusive" lock is held, no other locks with that name can be granted. If "shared" lock is held, other "shared" locks with that name can be granted - but not any "exclusive" locks. The default mode in the API is "exclusive". 100 | 101 | Additional properties may influence scheduling, such as timeouts, fairness, and so on. 102 | 103 | 104 | ## API Proposal 105 | 106 | ```js 107 | async function get_lock_then_write() { 108 | await navigator.locks.request('resource', async lock => { 109 | await async_write_func(); 110 | }); 111 | } 112 | 113 | async function get_lock_then_read() { 114 | await navigator.locks.request('resource', {mode: 'shared'}, async lock => { 115 | await async_read_func(); 116 | }); 117 | } 118 | ``` 119 | 120 | The _name_ (required first argument) is a string, e.g. `'thing'`. 121 | 122 | The _callback_ (required final argument) is a callback invoked with the lock when granted. This "scoped release" API model encourages callers to pass in an async callback; the callback implicitly returns a promise and the lock is released when that promise resolves. In other words, the lock is held until the async callback completes. (If a non-async callback function is passed in, it is wrapped into a Promise that would resolve immediately, so the lock would only be held for the duration of the synchronous callback.) 123 | 124 | > See [alternate API proposals](alternate-api-proposals.md) for slightly different API styles which were considered. 125 | 126 | The method returns a promise that resolves/rejects with the result of the callback (so, after the lock is released), or rejects if the request is aborted. 127 | 128 | Example: 129 | ```js 130 | try { 131 | const result = await navigator.locks.request('resource', async lock => { 132 | // The lock is held here. 133 | await do_something(); 134 | await do_something_else(); 135 | return "ok"; 136 | // The lock will be released now. 137 | }); 138 | // |result| has the return value of the callback. 139 | } catch (ex) { 140 | // if the callback threw, it will be caught here. 141 | } 142 | ``` 143 | This guarantees that the lock will be released when the async callback exits for any reason - either when the code returns, or if it throws. 144 | 145 | ## Options 146 | 147 | An options dictionary may be specified as a second argument (bumping the callback to the third argument). 148 | 149 | ### `mode` option to `request()` 150 | 151 | An optional _mode_ member can be one of "exclusive" (the default if not specified) or "shared". 152 | ```js 153 | await navigator.locks.request('resource', {mode: 'shared'}, async lock => { 154 | // Use |lock| here. 155 | }); 156 | ``` 157 | 158 | ### `signal` option to `request()` 159 | 160 | An optional _signal_ member can be specified which is an [AbortSignal](https://dom.spec.whatwg.org/#interface-AbortSignal). This allows aborting a lock request, for example if the request is not granted in a timely manner: 161 | ```js 162 | const controller = new AbortController(); 163 | setTimeout(() => controller.abort(), 200); // wait at most 200ms 164 | 165 | try { 166 | await navigator.locks.request('resource', {signal: controller.signal}, async lock => { 167 | // Use |lock| here. 168 | }); 169 | // Done with lock here. 170 | } catch (ex) { 171 | // |ex| will be a DOMException with error name "AbortError" if timer fired. 172 | } 173 | ``` 174 | 175 | ### `ifAvailable` option to `request()` 176 | 177 | An optional _ifAvailable_ boolean member can be specified; the default is false. If true, then the lock is only granted if it can be without additional waiting. Note that this is still not _synchronous_; in many user agents this will require cross-process communication to see if the lock can be granted. If the lock cannot be granted, `null` is returned. (Since this is expected, the request is not rejected.) 178 | ```js 179 | await navigator.locks.request('resource', {ifAvailable: true}, async lock => { 180 | if (!lock) { 181 | // Didn't get it. Maybe take appropriate action. 182 | return; 183 | } 184 | // Use |lock| here. 185 | }); 186 | ``` 187 | See [issue #13](https://github.com/w3c/web-locks/issues/13) for discussion of this option. 188 | 189 | 190 | ## Management / Debugging 191 | 192 | One of the things we've learned from APIs with lots of hidden state like Indexed DB is that it makes diagnosing problems difficult. Developer tools can help locally, but not when a web application has been deployed and mysterious bug reports are coming in. The ability for a web app to introspect the state of such APIs is critical. 193 | 194 | To address this, a method called `query()` can be used which provides a snapshot of the lock manager state for an origin: 195 | 196 | ```js 197 | const state = await navigator.locks.query(); 198 | ``` 199 | 200 | This resolves to a plain-old-data structure (i.e. JSON-like) with this form: 201 | ```js 202 | { 203 | held: [ 204 | { name: "resource1", mode: "exclusive", clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, 205 | { name: "resource2", mode: "shared", clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, 206 | { name: "resource2", mode: "shared", clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, 207 | ], 208 | pending: [ 209 | { name: "resource1", mode: "exclusive", clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, 210 | { name: "resource1", mode: "exclusive", clientId: "d341a5d0-1d8d-4224-be10-704d1ef92a15" }, 211 | ] 212 | } 213 | ``` 214 | The `clientId` field corresponds to a unique context (frame/worker), and is the same value used in [Service Workers](https://w3c.github.io/ServiceWorker/#dom-client-id). 215 | 216 | This data is just a _snapshot_ of the lock manager state at some point in time. Once the data is returned to script, the lock state may have changed. It should therefore not usually be used by applications to make decisions about what locks are currently held or available. 217 | 218 | ### `steal` option to `request()` 219 | 220 | If a web application detects an unrecoverable state - for example, some coordination point like a Service Worker determines that a tab holding a lock is no longer responding - it can "steal" a lock by passing this option to `request()`. When specified, any held locks for the resource will be released (and the _released promise_ of such locks will resolve with `AbortError`), and the request will be granted, preempting any queued requests for it. This should only be used in exceptional cases; any code running in tabs that assume they hold the lock will continue to execute, violating any guarantee of exclusive access to the resource. 221 | 222 | Discussion about this controversial option is at: https://github.com/w3c/web-locks/issues/23 223 | 224 | 225 | ## Deadlocks 226 | 227 | [Deadlocks](https://en.wikipedia.org/wiki/Deadlock) are a concept in concurrent computing. Here's a simple example of how they can be encountered through the use of this API: 228 | 229 | ```js 230 | // Program 1 231 | navigator.locks.request('A', async a => { 232 | await navigator.locks.request('B', async b => { 233 | // do stuff with A and B 234 | }); 235 | }); 236 | 237 | // Elsewhere... 238 | 239 | // Program 2 240 | navigator.locks.request('B', async b => { 241 | await navigator.locks.request('A', async a => { 242 | // do stuff with A and B 243 | }); 244 | }); 245 | ``` 246 | If program 1 and program 2 run close to the same time, there is a chance that code 1 will hold lock A and code 2 will hold lock B and neither can make further progress - a deadlock. This will not affect the user agent as a whole, pause the tab, or affect other code in the origin, but this particular functionality will be blocked. 247 | 248 | Preventing deadlocks requires care. One approach is to always acquire multiple locks in a strict order, e.g.: 249 | 250 | ```js 251 | async function requestMultiple(resources, callback) { 252 | const sortedResources = Array.from(resources); 253 | sortedResources.sort(); // always request in the same order 254 | 255 | async function requestNext() { 256 | return await navigator.locks.request(sortedResources.shift(), async lock => { 257 | if (sortedResources.length > 0) { 258 | return await requestNext(); 259 | } else { 260 | return await callback(); 261 | } 262 | }); 263 | } 264 | return await requestNext(); 265 | } 266 | ``` 267 | 268 | In practice, the use of multiple locks is rarely as straightforward - libraries and other utilities may conceal their use. 269 | 270 | See issues for further discussion: 271 | 272 | * [Single vs multi-resource locks](https://github.com/w3c/web-locks/issues/20) 273 | * [Deadlock detection and resolution?](https://github.com/w3c/web-locks/issues/26) 274 | * [Avoid deadlocks entirely via crafty API design?](https://github.com/w3c/web-locks/issues/28) 275 | 276 | 277 | ## FAQ 278 | 279 | ### *Why can't [Atomics](https://tc39.github.io/ecmascript_sharedmem/shmem.html#AtomicsObject) be used for this?* 280 | 281 | The use cases for this API require coordination across multiple 282 | [agent clusters](https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-agent-cluster-formalism); 283 | whereas Atomics operations operate on [SharedArrayBuffers](https://tc39.github.io/ecmascript_sharedmem/shmem.html#StructuredData.SharedArrayBuffer) which are constrained to a single agent cluster. (Informally: tabs/workers can be multi-process and atomics only work same-process.) 284 | 285 | 286 | ### *Why is the _options_ argument not the last argument?* 287 | 288 | Since both callbacks and options are typically made the last argument, the best ordering is not obvious. Based on trying both, placing the options closer to the call site makes reading/writing the code much clearer, so the options dictionary is placed before the callback. Compare (a) and (b): 289 | 290 | ```js 291 | // a 292 | navigator.locks.request('resource', async lock => { 293 | // 294 | // 100 lines of code... 295 | // ... 296 | // 297 | }, {ifAvailable: 'true'}); 298 | 299 | // b 300 | navigator.locks.request('resource', {ifAvailable: true}, async lock => { 301 | // 302 | // 100 lines of code... 303 | // ... 304 | // 305 | }); 306 | ``` 307 | It's much clearer in (b) that the request will not wait if the lock is not available. In (a) you need to read all the way through the lock handling code (artificially short/simple here) before noting the very different behavior of the two requests. 308 | 309 | 310 | ### *What happens if a tab is throttled/suspended?* 311 | 312 | If a tab holds a lock and stops running code it can inhibit work done by other tabs. If this is because tabs are not appropriately breaking up work it's an application problem. But browsers could throttle or even suspend tabs (e.g. 313 | background background tabs) to reduce power and/or memory consumption. With an API like this — or with IndexedDB 314 | — this can result the [work in foreground tabs being throttled](https://bugs.chromium.org/p/chromium/issues/detail?id=675372). 315 | 316 | To mitigate this, browsers must ensure that apps are notified before being throttled or suspended so that 317 | they can release locks, and/or browsers must automatically release locks held by a context before it is 318 | suspended. See [A Lifecycle for the Web](https://github.com/spanicker/web-lifecycle) for possible thoughts 319 | around formalizing these states and notifications. 320 | 321 | 322 | ### *How do you _compose_ IndexedDB transactions with these locks?* 323 | 324 | * To wrap a lock around a transaction: 325 | 326 | ```js 327 | navigator.locks.request(name, options, lock => { 328 | return new Promise((resolve, reject) => { 329 | const tx = db.transaction(...); 330 | tx.oncomplete = resolve; 331 | tx.onabort = e => reject(tx.error); 332 | // use tx... 333 | }); 334 | }); 335 | ``` 336 | 337 | * To wrap a transaction around a lock is harder, since you can't keep an IndexedDB transaction alive arbitrarily. If [transactions supported `waitUntil()`](https://github.com/WICG/indexeddb-promises) this would be possible: 338 | 339 | ```js 340 | const tx = db.transaction(...); 341 | tx.waitUntil(locks.request(name, options, async lock => { 342 | // use lock and tx 343 | }); 344 | ``` 345 | 346 | Note that we don't want to _force_ IDBTransactions into this model of waiting for a resource before you can use it: in IDB you can open a transaction and schedule work against it immediately, even though that work will be delayed until the transaction is running. 347 | 348 | ### *Can we _define_ IndexedDB transactions in terms of this primitive?* 349 | 350 | Roughly: 351 | 352 | * The IDBTransaction requests a lock when created, and holds a "request queue" which operations are appended to. 353 | * When the lock is acquired it is waited on "complete promise". In addition an "active promise" is prepared. The request queue is then processed. 354 | * A processed request gets a hidden promise that is resolved when the request is done. The "active promise" is extended until one turn after every processed request has completed. (Similar to the trick used here, a dependent promise is created which, when run, schedules a microtask to do the work.) 355 | * Any new request is processed immediately. 356 | * When the "active promise" resolves, there are no further requests, and the transaction attempts to commit. 357 | * The "complete promise" is resolved when the transaction successfully commits or aborts. 358 | 359 | This doesn't precisely capture the "active" vs "inactive" semantics and several other details. We may want to go through the exercise of defining this more rigorously. 360 | 361 | 362 | ### *Why does a shared lock request need to wait until a previous exclusive lock request be granted/released?* 363 | 364 | This comes from developer expectations about file and database processing; if a write is scheduled 365 | before a read, the usual expectation is that the read will see the results of the write. When this 366 | was not enforced by IndexedDB implementations, developers expressed significant confusion. Given 367 | demand, we could add an option/mode to allow opting into the more subtle behavior. 368 | 369 | 370 | ### *Does this leak information from e.g. Incognito/Private Browsing/etc mode?* 371 | 372 | No - like storage APIs, browsers treat such anonymous sessions as if they were a completely separate 373 | user agent from the point of view of specs; the data is in a separate partition. This is similar 374 | to how some browsers support multiple user profiles; cookies, databases, certificates, etc. 375 | are all separated. Locks held in one user profile or anonymous session have no relationship to 376 | locks in another session, as if they in a distinct application or on another device. 377 | 378 | 379 | ### *Can you hold a lock for the lifetime of a tab?* 380 | 381 | Yes. Using the API, just pass in a promise that never resolves: 382 | ```js 383 | navigator.locks.request(name, lock => new Promise(r => {})); 384 | ``` 385 | In practice, you may want to reserve some ability to resolve the promise, e.g. in response to a "sign out" event or indication that the tab has become inactive. But in some scenarios (e.g. primary election) then never releasing the lock until the page is terminated is entirely reasonable. 386 | 387 | 388 | ### *If a tab is holding an exclusive lock, what happens if another lock request for the same resource is made?* 389 | 390 | The second request will block. A lock corresponds to a granted request, and each request is considered regardless of context. This allows libraries running in the same page to coordinate the use of a shared resource. As a consequence, nested requests for the same resource will deadlock: 391 | ```js 392 | await navigator.locks.request('mylock', async lock => { 393 | await navigator.locks.request('mylock', async lock => {}); 394 | }); 395 | ``` 396 | 397 | 398 | ## Related APIs 399 | 400 | * [Atomics](https://tc39.github.io/ecmascript_sharedmem/shmem.html#AtomicsObject) 401 | * Resource coordination within a SharedArrayBuffer, limiting use to a particular [agent cluster](https://html.spec.whatwg.org/multipage/webappapis.html#integration-with-the-javascript-agent-cluster-formalism). 402 | * [IndexedDB Transactions](https://w3c.github.io/IndexedDB/#transaction-concept) 403 | * No explicit control of transaction lifetimes. Requires use of full API (e.g. schema versioning). 404 | * [Screen Orientation API](https://w3c.github.io/screen-orientation/) 405 | * Acquisition of a single system-provided resource. 406 | * `screen.orientation.lock('portrait').then(...)` 407 | * [Pointer Lock](https://w3c.github.io/pointerlock/) 408 | * Acquisition of a single system-provided resource. 409 | * `element.requestPointerLock() ` 410 | * [Wake Lock API](https://w3c.github.io/wake-lock/) 411 | * Acquisition of a single system-provided resource. 412 | * `navigator.getWakeLock('screen').then(wakeLock => wakeLock.createRequest())` 413 | * [Keyboard Lock](https://w3c.github.io/keyboard-lock/) 414 | * Acquisition of a single system-provided resource. 415 | * `navigator.requestKeyboardLock().then(...)` (proposed) 416 | 417 | ## Acknowledgements 418 | 419 | Many thanks to 420 | Alex Russell, 421 | Anne van Kesteren, 422 | Boris Zbarsky, 423 | Darin Fisher, 424 | Domenic Denicola, 425 | Harald Alvestrand, 426 | Jake Archibald, 427 | L. David Baron, 428 | Luciano Pacheco, 429 | Marcos Caceres, 430 | Ralph Chelala, 431 | Ryan Fioravanti, 432 | and 433 | Victor Costan 434 | for helping craft this proposal. 435 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | All Reports in this Repository are licensed by Contributors 2 | under the 3 | [W3C Software and Document License](http://www.w3.org/Consortium/Legal/2015/copyright-software-and-document). 4 | 5 | Contributions to Specifications are made under the 6 | [W3C CLA](https://www.w3.org/community/about/agreements/cla/). 7 | 8 | Contributions to Test Suites are made under the 9 | [W3C 3-clause BSD License](https://www.w3.org/Consortium/Legal/2008/03-bsd-license.html) 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Web Locks API 4 | 5 | [![CI](https://github.com/w3c/web-locks/actions/workflows/auto-publish.yml/badge.svg)](https://github.com/w3c/web-locks/actions/workflows/auto-publish.yml) 6 | 7 | A web platform API that allows script to asynchronously acquire a lock over a resource, hold it while work is performed, then release it. While held, no other script in the origin can aquire a lock over the same resource. This allows contexts (windows, workers) within a web application to coordinate the usage of resources. 8 | 9 | Participate: [GitHub issues](https://github.com/w3c/web-locks/issues) — 10 | Docs: [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) — 11 | Tests: [web-platform-tests](https://github.com/web-platform-tests/wpt/tree/master/web-locks) 12 | 13 | * The [Explainer](EXPLAINER.md) is a developer-oriented preview of the API. 14 | * The [Specification](https://w3c.github.io/web-locks/) is for browser implementers. 15 | -------------------------------------------------------------------------------- /alternate-api-proposals.md: -------------------------------------------------------------------------------- 1 | 2 | ## Alternate API Proposals 3 | 4 | Alternate 1: Auto-Releasing with waitUntil(): 5 | ```js 6 | async function get_lock_then_write() { 7 | const lock = await requestLock('resource'); 8 | lock.waitUntil(async_write_func()); 9 | } 10 | 11 | async function get_lock_then_read() { 12 | const lock = await requestLock('resource', {mode: 'shared'}); 13 | lock.waitUntil(async_read_func()); 14 | } 15 | ``` 16 | 17 | Alternate 2: Explicit release: 18 | ```js 19 | async function get_lock_then_write() { 20 | const lock = await requestLock('resource'); 21 | await async_write_func(); 22 | lock.release(); 23 | } 24 | 25 | async function get_lock_then_read() { 26 | const lock = await requestLock('resource', {mode: 'shared'}); 27 | await async_read_func(); 28 | lock.release(); 29 | } 30 | ``` 31 | 32 | 33 | * The auto-release approach mirrors [Indexed DB's auto-committing transaction](https://w3c.github.io/IndexedDB/#transaction-construct) model where explicit action is needed to hold a resource, combined with [Service Worker's ExtendableEvent](https://w3c.github.io/ServiceWorker/#extendableevent-interface) `waitUntil()` method to allow promises to control the lifetime. 34 | * The explicit release model requires callers to always call the `release()` method, e.g. even if an exception is thrown. 35 | 36 | In the auto-release approach, a lock will automatically be released by a subsequent microtask if `waitUntil(p)` is not called with a promise to extend its lifetime within the callback from the initial acquisition promise. 37 | 38 | In the auto-release and explicit release approaches the method returns a promise that resolves with a lock, or rejects if the request was aborted. 39 | 40 | 41 | ## FAQ 42 | 43 | *Can you implement explicit release in terms of auto-release?* 44 | ```js 45 | async function requestExplicitLock(...args) { 46 | const lock = await requestLock(...args); 47 | lock.waitUntil(new Promise(resolve => { lock.release = resolve; })); 48 | return lock; 49 | } 50 | ``` 51 | 52 | *Can you implement auto-release in terms of explicit-release?* 53 | ```js 54 | function Extendable() { 55 | let resolve, reject; 56 | const promise = new Promise((res, rej) => { 57 | resolve = res; 58 | reject = rej; 59 | }); 60 | 61 | let ps; 62 | promise.waitUntil = function(p) { 63 | ps = ps ? Promise.all([ps, p]) : p; 64 | const snapshot = ps; 65 | ps.then( 66 | () => { if (snapshot === ps) resolve(); }, 67 | err => { if (snapshot === ps) reject(err); } 68 | ); 69 | }; 70 | return promise; 71 | } 72 | 73 | function requestAutoReleaseLock(...args) { 74 | const lock = await requestLock(...args); 75 | const ext = Extendable(); 76 | ext.then(() => lock.release(), () => lock.release()); 77 | ext.waitUntil(Promise.resolve().then(() => Promise.resolve().then())); 78 | lock.waitUntil = ext.waitUntil.bind(ext); 79 | return lock; 80 | } 81 | ``` 82 | 83 | *Can you implement explicit release in terms of scoped release?* 84 | ```js 85 | function requestExplicitLock(scope, ...rest) { 86 | return new Promise(resolve => { 87 | requestLock(scope, lock => { 88 | // p waits until lock.release() is called 89 | const p = new Promise(r => { lock.release = r; }); 90 | resolve(lock); 91 | return p; 92 | }, ...rest); 93 | }); 94 | } 95 | ``` 96 | 97 | *Can you implement scoped release in terms of explict release?* 98 | ```js 99 | async function requestScopedLock(scope, callback, ...rest) { 100 | const lock = await requestLock(scope, ...rest); 101 | try { 102 | await callback(lock); 103 | } finally { 104 | lock.release(); 105 | } 106 | } 107 | ``` 108 | 109 | *Can you implement...* 110 | 111 | Staaahp! 112 | -------------------------------------------------------------------------------- /index.bs: -------------------------------------------------------------------------------- 1 |
  2 | Title: Web Locks API
  3 | Shortname: web-locks
  4 | Abstract: This document defines a web platform API that allows script to asynchronously acquire a lock over a resource, hold it while work is performed, then release it. While held, no other script in the origin can acquire a lock over the same resource. This allows contexts (windows, workers) within a web application to coordinate the usage of resources.
  5 | Status: ED
  6 | ED: https://w3c.github.io/web-locks/
  7 | Level: 1
  8 | TR: https://www.w3.org/TR/web-locks/
  9 | Editor: Kagami Rosylight, Mozilla https://www.mozilla.org/, krosylight@mozilla.com, w3cid 107856
 10 | Former Editor: Joshua Bell, Google Inc. https://google.com, jsbell@google.com, w3cid 61302
 11 | Group: webapps
 12 | Repository: w3c/web-locks
 13 | Test Suite: https://github.com/web-platform-tests/wpt/tree/master/web-locks
 14 | Favicon: logo-lock.svg
 15 | Complain About: accidental-2119 yes, missing-example-ids yes
 16 | Markup Shorthands: css no, markdown yes
 17 | Assume Explicit For: yes
 18 | 
19 | 20 | 23 | 24 |
 25 | spec: ecma262; urlPrefix: https://tc39.github.io/ecma262/
 26 |     type: dfn
 27 |         text: agent; url: agent
 28 | spec: html; urlPrefix: https://html.spec.whatwg.org/multipage/
 29 |     urlPrefix: webstorage.html
 30 |         type: dfn
 31 |             text: localStorage; url: dom-localstorage
 32 |     urlPrefix: webappapis.html
 33 |         type: dfn
 34 |             text: agent cluster; url: integration-with-the-javascript-agent-cluster-formalism
 35 | spec: storage; urlPrefix: https://storage.spec.whatwg.org/
 36 |     type: dfn
 37 |         text: storage bucket; url: storage-bucket
 38 |         text: storage bottle; url: storage-bottle
 39 | 
40 | 41 | 59 | 60 | 61 | 66 | 72 | 73 | 74 | 75 | # Introduction # {#introduction} 76 | 77 | 78 | *This section is non-normative.* 79 | 80 | A [=lock request=] is made by script for a particular [=resource name=] and [=/mode=]. A scheduling algorithm looks at the state of current and previous requests, and eventually grants a lock request. A [=lock-concept|lock=] is a granted request; it has a [=resource name=] and [=/mode=]. It is represented as an object returned to script. As long as the lock is held it may prevent other lock requests from being granted (depending on the name and mode). A lock can be released by script, at which point it may allow other lock requests to be granted. 81 | 82 | The API provides optional functionality that may be used as needed, including: 83 | 84 | * returning values from the asynchronous task, 85 | * shared and exclusive lock modes, 86 | * conditional acquisition, 87 | * diagnostics to query the state of locks, and 88 | * an escape hatch to protect against deadlocks. 89 | 90 | Cooperative coordination takes place within the scope of [=/agents=] sharing a [=/storage bucket=]; this may span multiple [=/agent clusters=]. 91 | 92 | NOTE: [=/Agents=] roughly correspond to windows (tabs), iframes, and workers. [=/Agent clusters=] correspond to independent processes in some user agent implementations. 93 | 94 | 95 | ## Usage Overview ## {#usage-overview} 96 | 97 | 98 | The API is used as follows: 99 | 100 | 1. The lock is requested. 101 | 1. Work is done while holding the lock in an asynchronous task. 102 | 1. The lock is automatically released when the task completes. 103 | 104 |
105 | 106 | A basic example of the API usage is as follows: 107 | 108 | ```js 109 | navigator.locks.request('my_resource', async lock => { 110 | // The lock has been acquired. 111 | await do_something(); 112 | await do_something_else(); 113 | // Now the lock will be released. 114 | }); 115 | ``` 116 | 117 | Within an asynchronous function, the request itself can be awaited: 118 | 119 | ```js 120 | // Before requesting the lock. 121 | await navigator.locks.request('my_resource', async lock => { 122 | // The lock has been acquired. 123 | await do_something(); 124 | // Now the lock will be released. 125 | }); 126 | // After the lock has been released 127 | ``` 128 | 129 |
130 | 131 | 132 | ## Motivating Use Cases ## {#motivations} 133 | 134 | 135 | A web-based document editor stores state in memory for fast access and persists changes (as a series of records) to a storage API such as the [[IndexedDB-2|Indexed Database API]] for resiliency and offline use, and to a server for cross-device use. When the same document is opened for editing in two tabs the work must be coordinated across tabs, such as allowing only one tab to make changes to or synchronize the document at a time. This requires the tabs to coordinate on which will be actively making changes (and synchronizing the in-memory state with the storage API), knowing when the active tab goes away (navigated, closed, crashed) so that another tab can become active. 136 | 137 | In a data synchronization service, a "primary tab" is designated. This tab is the only one that should be performing some operations (e.g. network sync, cleaning up queued data, etc). It holds a lock and never releases it. Other tabs can attempt to acquire the lock, and such attempts will be queued. If the "primary tab" crashes or is closed then one of the other tabs will get the lock and become the new primary. 138 | 139 | The [[IndexedDB-2|Indexed Database API]] defines a transaction model allowing shared read and exclusive write access across multiple named storage partitions within an origin. Exposing this concept as a primitive allows any Web Platform activity to be scheduled based on resource availability, for example allowing transactions to be composed for other storage types (such as Caches [[Service-Workers]]), across storage types, even across non-storage APIs (e.g. network fetches). 140 | 141 | 142 | 143 | # Concepts # {#concepts} 144 | 145 | 146 | For the purposes of this specification: 147 | 148 | * Separate user profiles within a browser are considered separate user agents. 149 | * Every [private mode](https://github.com/w3ctag/private-mode) browsing session is considered a separate user agent. 150 | 151 | A [=/user agent=] has a lock task queue which is the result of [=starting a new parallel queue=]. 152 | 153 | The [=task source=] for [=parallel queue/enqueue steps|steps enqueued=] below is the web locks tasks source. 154 | 155 | 156 | 157 | ## Resources Names ## {#resource-names} 158 | 159 | 160 | A resource name is a [=JavaScript string=] chosen by the web application to represent an abstract resource. 161 | 162 | A resource name has no external meaning beyond the scheduling algorithm, but is global 163 | across [=/agents=] sharing a [=/storage bucket=]. Web applications are free to use any resource naming scheme. 164 | 165 |
166 | To mimic transaction locking over named stores within a named 167 | database in [[IndexedDB-2]], a script might compose resource names as: 168 | ```js 169 | encodeURIComponent(db_name) + '/' + encodeURIComponent(store_name) 170 | ``` 171 |
172 | 173 | Resource names starting with U+002D HYPHEN-MINUS (-) are reserved; requesting these will cause an exception. 174 | 175 | 176 | ## Lock Managers ## {#lock-managers} 177 | 178 | 179 | A lock manager encapsulates the state of [=lock-concept|locks=] and [=lock requests=]. Each [=/storage bucket=] includes one [=/lock manager=] through an associated [=/storage bottle=] for the Web Locks API. 180 | 181 | NOTE: Pages and workers ([=/agents=]) sharing a [=/storage bucket=] opened in the same user agent share a [=/lock manager=] even if they are in unrelated [=/browsing contexts=]. 182 | 183 |
184 | To obtain a lock manager, given an [=/environment settings object=] |environment|, run these steps: 185 | 186 | 1. Let |map| be the result of [=/obtaining a local storage bottle map=] given |environment| and "`web-locks`". 187 | 1. If |map| is failure, then return failure. 188 | 1. Let |bottle| be |map|'s associated [=/storage bottle=]. 189 | 1. Return |bottle|'s associated [=/lock manager=]. 190 | 191 |
192 | 193 | Issue: Refine the integration with [[Storage]] here, including how to get the lock manager properly from the given environment. 194 | 195 | 196 | 197 | ## Modes and Scheduling ## {#modes-scheduling} 198 | 199 | 200 | A mode is either "{{LockMode/exclusive}}" or "{{LockMode/shared}}". Modes can be used to model the common [readers-writer lock](http://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) pattern. If an "{{LockMode/exclusive}}" lock is held, then no other locks with that name can be granted. If a "{{LockMode/shared}}" lock is held, other "{{LockMode/shared}}" locks with that name can be granted — but not any "{{LockMode/exclusive}}" locks. The default mode in the API is "{{LockMode/exclusive}}". 201 | 202 | Additional properties may influence scheduling, such as timeouts, fairness, and so on. 203 | 204 | 205 | 206 | ## Locks ## {#concept-lock} 207 | 208 | 209 | A lock represents exclusive access to a shared resource. 210 | 211 |
212 | 213 | A [=lock-concept|lock=] has an agent which is an [=/agent=]. 214 | 215 | A [=lock-concept|lock=] has a clientId which is an opaque string. 216 | 217 | A [=lock-concept|lock=] has a manager which is a [=/lock manager=]. 218 | 219 | A [=lock-concept|lock=] has a name which is a [=resource name=]. 220 | 221 | A [=lock-concept|lock=] has a mode which is one of "{{LockMode/exclusive}}" or "{{LockMode/shared}}". 222 | 223 | A [=lock-concept|lock=] has a waiting promise which is a Promise. 224 | 225 | A [=lock-concept|lock=] has a released promise which is a Promise. 226 | 227 |
228 | There are two promises associated with a lock's lifecycle: 229 | 230 | * A promise provided either implicitly or explicitly by the callback when the lock is granted which determines how long the lock is held. When this promise settles, the lock is released. This is known as the lock's [=lock-concept/waiting promise=]. 231 | * A promise returned by {{LockManager}}'s {{LockManager/request(name, callback)|request()}} method that settles when the lock is released or the request is aborted. This is known as the lock's [=lock-concept/released promise=]. 232 | 233 | ```js 234 | const p1 = navigator.locks.request('resource', lock => { 235 | const p2 = new Promise(r => { 236 | // Logic to use lock and resolve promise... 237 | }); 238 | return p2; 239 | }); 240 | ``` 241 | 242 | In the above example, `p1` is the [=lock-concept/released promise=] and `p2` is the [=lock-concept/waiting promise=]. 243 | Note that in most code the callback would be implemented as an `async` function and the returned promise would be implicit, as in the following example: 244 | 245 | ```js 246 | const p1 = navigator.locks.request('resource', async lock => { 247 | // Logic to use lock... 248 | }); 249 | ``` 250 | 251 | The [=lock-concept/waiting promise=] is not named in the above code, but is still present as the return value from the anonymous `async` callback. 252 | Further note that if the callback is not `async` and returns a non-promise, the return value is wrapped in a promise that is immediately resolved; the lock will be released in an upcoming microtask, and the [=lock-concept/released promise=] will also resolve in a subsequent microtask. 253 |
254 | 255 | Each [=/lock manager=] has a held lock set which is a [=/set=] of [=lock-concept|locks=]. 256 | 257 |
258 | 259 | When [=lock-concept|lock=] |lock|'s [=lock-concept/waiting promise=] settles (fulfills or rejects), [=parallel queue/enqueue the following steps=] on the [=lock task queue=]: 260 | 261 | 1. [=Release the lock=] |lock|. 262 | 1. [=/Resolve=] |lock|'s [=lock-concept/released promise=] with |lock|'s [=lock-concept/waiting promise=]. 263 | 264 |
265 | 266 |
267 | 268 | 269 | ## Lock Requests ## {#concept-lock-request} 270 | 271 | 272 | A lock request represents a pending request for a [=lock-concept|lock=]. 273 | 274 |
275 | 276 | A [=lock request=] is a [=/struct=] with [=struct/items=] agent, clientId, manager, name, mode, callback, promise, and signal. 277 | 278 |
279 | 280 | A lock request queue is a [=/queue=] of [=/lock requests=]. 281 | 282 | Each [=/lock manager=] has a lock request queue map, which is a [=map=] of [=resource names=] to [=/lock request queues=]. 283 | 284 |
285 | 286 | To get the lock request queue from [=lock manager/lock request queue map=] |queueMap| from [=/resource name=] |name|, run these steps: 287 | 288 | 1. If |queueMap|[|name|] does not [=map/exist=], [=map/set=] |queueMap|[|name|] to a new empty [=/lock request queue=]. 289 | 1. Return |queueMap|[|name|]. 290 | 291 |
292 | 293 | 294 |
295 | 296 | A [=lock request=] |request| is said to be grantable if the following steps return true: 297 | 298 | 1. Let |manager| be |request|'s [=lock request/manager=]. 299 | 1. Let |queueMap| be |manager|'s [=lock manager/lock request queue map=]. 300 | 1. Let |name| be |request|'s [=lock request/name=]. 301 | 1. Let |queue| be the result of [=/getting the lock request queue=] from |queueMap| for |name|. 302 | 1. Let |held| be |manager|'s [=lock manager/held lock set=] 303 | 1. Let |mode| be |request|'s [=lock request/mode=] 304 | 1. If |queue| [=queue/is empty|is not empty=] and |request| is not the first [=queue/item=] in |queue|, then return false. 305 | 1. If |mode| is "{{LockMode/exclusive}}", then return true if no [=lock-concept|lock=] in |held| has [=lock-concept/name=] equal to |name|, and false otherwise. 306 | 1. Otherwise, |mode| is "{{LockMode/shared}}"; return true if no [=lock-concept|lock=] in |held| has [=lock-concept/mode=] "{{LockMode/exclusive}}" and has [=lock-concept/name=] equal to |name|, and false otherwise. 307 | 308 |
309 | 310 | 311 | ## Termination of Locks ## {#termination-of-locks} 312 | 313 | 314 | Whenever the [=/unloading document cleanup steps=] run with a [=/document=], [=/terminate remaining locks and requests=] with its [=/agent=]. 315 | 316 | When an [=/agent=] terminates, [=/terminate remaining locks and requests=] with the agent. 317 | 318 | Issue: This is currently only for workers and is vaguely defined, since there is no normative way to run steps on worker termination. 319 | 320 |
321 | 322 | To terminate remaining locks and requests with |agent|, [=parallel queue/enqueue the following steps=] on the [=lock task queue=]: 323 | 324 | 1. For each [=lock request=] |request| with [=lock request/agent=] equal to |agent|: 325 | 1. [=Abort the request=] |request|. 326 | 1. For each [=lock-concept|lock=] |lock| with [=lock-concept/agent=] equal to |agent|: 327 | 1. [=Release the lock=] |lock|. 328 | 329 |
330 | 331 | 332 | # API # {#api} 333 | 334 | 335 | 336 | ## Navigator Mixins ## {#navigator-mixins} 337 | 338 | 339 | 340 | [SecureContext] 341 | interface mixin NavigatorLocks { 342 | readonly attribute LockManager locks; 343 | }; 344 | Navigator includes NavigatorLocks; 345 | WorkerNavigator includes NavigatorLocks; 346 | 347 | 348 | Each [=environment settings object=] has a {{LockManager}} object. 349 | 350 | The locks getter's steps are to return [=/this=]'s [=/relevant settings object=]'s {{LockManager}} object. 351 | 352 | 353 | ## {{LockManager}} class ## {#api-lock-manager} 354 | 355 | 356 | 357 | [SecureContext, Exposed=(Window,Worker)] 358 | interface LockManager { 359 | Promise<any> request(DOMString name, 360 | LockGrantedCallback callback); 361 | Promise<any> request(DOMString name, 362 | LockOptions options, 363 | LockGrantedCallback callback); 364 | 365 | Promise<LockManagerSnapshot> query(); 366 | }; 367 | 368 | callback LockGrantedCallback = Promise<any> (Lock? lock); 369 | 370 | enum LockMode { "shared", "exclusive" }; 371 | 372 | dictionary LockOptions { 373 | LockMode mode = "exclusive"; 374 | boolean ifAvailable = false; 375 | boolean steal = false; 376 | AbortSignal signal; 377 | }; 378 | 379 | dictionary LockManagerSnapshot { 380 | sequence<LockInfo> held; 381 | sequence<LockInfo> pending; 382 | }; 383 | 384 | dictionary LockInfo { 385 | DOMString name; 386 | LockMode mode; 387 | DOMString clientId; 388 | }; 389 | 390 | 391 | A {{LockManager}} instance allows script to make [=lock requests=] and query 392 | the state of the [=lock manager=]. 393 | 394 | 395 | ### The {{LockManager/request(name, callback)|request()}} method ### {#api-lock-manager-request} 396 | 397 | 398 |
399 | : |promise| = navigator . locks . {{LockManager/request(name, callback)|request}}(|name|, |callback|) 400 | : |promise| = navigator . locks . {{LockManager/request(name, options, callback)|request}}(|name|, |options|, |callback|) 401 | 402 | :: The {{LockManager/request(name, callback)|request()}} method is called to request a lock. 403 | 404 | The |name| (initial argument) is a [=resource name=] string. 405 | 406 | The |callback| (final argument) is a [=callback function=] invoked with the {{Lock}} when granted. This is specified by script, and is usually an `async` function. The lock is held until the callback function completes. If a non-async callback function is passed in, then it is automatically wrapped in a promise that resolves immediately, so the lock is only held for the duration of the synchronous callback. 407 | 408 | The returned |promise| resolves (or rejects) with the result of the callback after the lock is released, or rejects if the request is aborted. 409 | 410 | Example: 411 | ```js 412 | try { 413 | const result = await navigator.locks.request('resource', async lock => { 414 | // The lock is held here. 415 | await do_something(); 416 | await do_something_else(); 417 | return "ok"; 418 | // The lock will be released now. 419 | }); 420 | // |result| has the return value of the callback. 421 | } catch (ex) { 422 | // if the callback threw, it will be caught here. 423 | } 424 | ``` 425 | 426 | The lock will be released when the callback exits for any reason — either when the code returns, or if it throws. 427 | 428 | An |options| dictionary can be specified as a second argument; the |callback| argument is always last. 429 | 430 | 431 | : |options| . mode 432 | 433 | :: The {{LockOptions/mode}} option can be "{{LockMode/exclusive}}" (the default if not specified) or "{{LockMode/shared}}". 434 | Multiple tabs/workers can hold a lock for the same resource in "{{LockMode/shared}}" mode, but only one tab/worker can hold a lock for the resource in "{{LockMode/exclusive}}" mode. 435 | 436 | The most common use for this is to allow multiple readers to access a resource simultaneously but prevent changes. 437 | Once reader locks are released a single exclusive writer can acquire the lock to make changes, followed by another exclusive writer or more shared readers. 438 | 439 | 440 | ```js 441 | await navigator.locks.request('resource', {mode: 'shared'}, async lock => { 442 | // Lock is held here. Other contexts might also hold the lock in shared mode, 443 | // but no other contexts will hold the lock in exclusive mode. 444 | }); 445 | ``` 446 | 447 | 448 | : |options| . ifAvailable 449 | 450 | :: If the {{LockOptions/ifAvailable}} option is `true`, then the lock is only granted if it can be without additional waiting. Note that this is still not *synchronous*; in many user agents this will require cross-process communication to see if the lock can be granted. If the lock cannot be granted, the callback is invoked with `null`. (Since this is expected, the request is *not* rejected.) 451 | 452 | ```js 453 | await navigator.locks.request('resource', {ifAvailable: true}, async lock => { 454 | if (!lock) { 455 | // Didn't get it. Maybe take appropriate action. 456 | return; 457 | } 458 | // Lock is held here. 459 | }); 460 | ``` 461 | 462 | : |options| . signal 463 | 464 | :: The {{LockOptions/signal}} option can be set to an {{AbortSignal}}. This allows aborting a lock request, for example if the request is not granted in a timely manner: 465 | 466 | ```js 467 | const controller = new AbortController(); 468 | setTimeout(() => controller.abort(), 200); // Wait at most 200ms. 469 | 470 | try { 471 | await navigator.locks.request( 472 | 'resource', {signal: controller.signal}, async lock => { 473 | // Lock is held here. 474 | }); 475 | // Done with lock here. 476 | } catch (ex) { 477 | // |ex| will be a DOMException with error name "AbortError" if timer fired. 478 | } 479 | ``` 480 | 481 | If an abort is signalled before the lock is granted, then the request promise will reject with an {{AbortError}}. 482 | Once the lock has been granted, the signal is ignored. 483 | 484 | 485 | : |options| . steal 486 | 487 | :: If the {{LockOptions/steal}} option is `true`, then any held locks for the resource will be released (and the [=lock-concept/released promise=] of such locks will resolve with {{AbortError}}), and the request will be granted, preempting any queued requests for it. 488 | 489 | If a web application detects an unrecoverable state — for example, some coordination point like a Service Worker determines that a tab holding a lock is no longer responding — then it can "steal" a lock using this option. 490 | 491 |
492 | 493 |
494 | Use the {{LockOptions/steal}} option with caution. 495 | When used, code previously holding a lock will now be executing without guarantees that it is the sole context with access to the resource. 496 | Similarly, the code that used the option has no guarantees that other contexts will not still be executing as if they have access to the abstract resource. 497 | It is intended for use by web applications that need to attempt recovery in the face of application and/or user-agent defects, where behavior is already unpredictable. 498 |
499 | 500 |
501 | 502 | The request(|name|, |callback|) and 503 | request(|name|, |options|, |callback|) method steps are: 504 | 505 | 1. If |options| was not passed, then let |options| be a new {{LockOptions}} dictionary with default members. 506 | 1. Let |environment| be [=/this=]'s [=/relevant settings object=]. 507 | 1. If |environment|'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully active=], then return [=a promise rejected with=] a "{{InvalidStateError}}" {{DOMException}}. 508 | 1. Let |manager| be the result of [=/obtaining a lock manager=] given |environment|. If that returned failure, then return [=a promise rejected with=] a "{{SecurityError}}" {{DOMException}}. 509 | 1. If |name| starts with U+002D HYPHEN-MINUS (-), then return [=a promise rejected with=] a "{{NotSupportedError}}" {{DOMException}}. 510 | 1. If both |options|["`steal`"] and |options|["`ifAvailable`"] are true, then return [=a promise rejected with=] a "{{NotSupportedError}}" {{DOMException}}. 511 | 1. If |options|["`steal`"] is true and |options|["`mode`"] is not "{{LockMode/exclusive}}", then return [=a promise rejected with=] a "{{NotSupportedError}}" {{DOMException}}. 512 | 1. If |options|["`signal`"] [=map/exists=], and either of |options|["`steal`"] or |options|["`ifAvailable`"] is true, then return [=a promise rejected with=] a "{{NotSupportedError}}" {{DOMException}}. 513 | 1. If |options|["`signal`"] [=map/exists=] and is [=AbortSignal/aborted=], then return [=a promise rejected with=] |options|["`signal`"]'s [=AbortSignal/abort reason=]. 514 | 1. Let |promise| be [=a new promise=]. 515 | 1. [=Request a lock=] with |promise|, the current [=/agent=], |environment|'s [=environment/id=], |manager|, |callback|, |name|, |options|["`mode`"], |options|["`ifAvailable`"], |options|["`steal`"], and |options|["`signal`"]. 516 | 1. Return |promise|. 517 | 518 |
519 | 520 | 521 | ### The {{LockManager/query()}} method ### {#api-lock-manager-query} 522 | 523 | 524 |
525 | 526 | : |state| = await navigator . locks . {{LockManager/query()|query}}() 527 | 528 | :: The {{LockManager/query()}} method can be used to produce a snapshot of the [=lock manager=] state for an origin, which allows a web application to introspect its usage of locks, for logging or debugging purposes. 529 | 530 | The returned promise resolves to |state|, a plain-old-data structure (i.e. JSON-like data) with this form: 531 | 532 | ```js 533 | { 534 | held: [ 535 | { name: "resource1", mode: "exclusive", 536 | clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, 537 | { name: "resource2", mode: "shared", 538 | clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, 539 | { name: "resource2", mode: "shared", 540 | clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, 541 | ], 542 | pending: [ 543 | { name: "resource1", mode: "exclusive", 544 | clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, 545 | { name: "resource1", mode: "exclusive", 546 | clientId: "d341a5d0-1d8d-4224-be10-704d1ef92a15" }, 547 | ] 548 | } 549 | ``` 550 | 551 | The `clientId` field corresponds to a unique context (frame or worker), and is the same value returned by {{Client}}'s {{Client/id}} attribute. 552 | 553 |
554 | 555 |
556 | This data is just a *snapshot* of the [=lock manager=] state at some point in time. By the time the data is returned to script, the actual lock state might have changed. 557 |
558 | 559 | 560 |
561 | 562 | The query() method steps are: 563 | 564 | 1. Let |environment| be [=/this=]'s [=/relevant settings object=]. 565 | 1. If |environment|'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully active=], then return [=a promise rejected with=] a "{{InvalidStateError}}" {{DOMException}}. 566 | 1. Let |manager| be the result of [=/obtaining a lock manager=] given |environment|. If that returned failure, then return [=a promise rejected with=] a "{{SecurityError}}" {{DOMException}}. 567 | 1. Let |promise| be [=a new promise=]. 568 | 1. [=parallel queue/enqueue the following steps|Enqueue the steps=] to [=snapshot the lock state=] for |manager| with |promise| to the [=lock task queue=]. 569 | 1. Return |promise|. 570 | 571 |
572 | 573 | 574 | ## {{Lock}} class ## {#api-lock} 575 | 576 | 577 | 578 | [SecureContext, Exposed=(Window,Worker)] 579 | interface Lock { 580 | readonly attribute DOMString name; 581 | readonly attribute LockMode mode; 582 | }; 583 | 584 | 585 | A {{Lock}} object has an associated [=lock-concept|lock=]. 586 | 587 | The name getter's steps are to return the associated [=lock-concept|lock=]'s [=lock-concept/name=]. 588 | 589 | The mode getter's steps are to return the associated [=lock-concept|lock=]'s [=lock-concept/mode=]. 590 | 591 | 592 | # Algorithms # {#algorithms} 593 | 594 | 595 | 596 | ## Request a lock ## {#algorithm-request-lock} 597 | 598 | 599 |
600 | To request a lock with |promise|, |agent|, |clientId|, |manager|, |callback|, |name|, |mode|, |ifAvailable|, |steal|, and |signal|: 601 | 602 | 1. Let |request| be a new [=lock request=] (|agent|, |clientId|, |manager|, |name|, |mode|, |callback|, |promise|, |signal|). 603 | 1. If |signal| is present, then [=AbortSignal/add=] the algorithm [=signal to abort the request=] |request| with |signal| to |signal|. 604 | 1. [=parallel queue/Enqueue the following steps=] to the [=lock task queue=]: 605 | 1. Let |queueMap| be |manager|'s [=lock manager/lock request queue map=]. 606 | 1. Let |queue| be the result of [=/getting the lock request queue=] from |queueMap| for |name|. 607 | 1. Let |held| be |manager|'s [=lock manager/held lock set=]. 608 | 1. If |steal| is true, then run these steps: 609 | 1. [=list/For each=] |lock| of |held|: 610 | 1. If |lock|'s [=lock-concept/name=] is |name|, then run these steps: 611 | 1. [=list/Remove=] [=lock-concept|lock=] from |held|. 612 | 1. [=Reject=] |lock|'s [=lock-concept/released promise=] with an "{{AbortError}}" {{DOMException}}. 613 | 1. [=list/Prepend=] |request| in |queue|. 614 | 1. Otherwise, run these steps: 615 | 1. If |ifAvailable| is true and |request| is not [=grantable=], 616 | then [=parallel queue/enqueue the following steps=] on |callback|'s [=/relevant settings object=]'s [=environment settings object/responsible event loop=]: 617 | 1. Let |r| be the result of [=invoking=] |callback| with `null` as the only argument. 618 | 1. [=/Resolve=] |promise| with |r| and abort these steps. 619 | 1. [=queue/Enqueue=] |request| in |queue|. 620 | 1. [=Process the lock request queue=] |queue|. 621 | 1. Return |request|. 622 | 623 |
624 | 625 | 626 | ## Release a lock ## {#algorithm-release-lock} 627 | 628 | 629 |
630 | To release the lock |lock|: 631 | 632 | 1. [=Assert=]: these steps are running on the [=lock task queue=]. 633 | 1. Let |manager| be |lock|'s [=lock-concept/manager=]. 634 | 1. Let |queueMap| be |manager|'s [=lock manager/lock request queue map=]. 635 | 1. Let |name| be |lock|'s [=resource name=]. 636 | 1. Let |queue| be the result of [=/getting the lock request queue=] from |queueMap| for |name|. 637 | 1. [=list/Remove=] [=lock-concept|lock=] from the |manager|'s [=lock manager/held lock set=]. 638 | 1. [=Process the lock request queue=] |queue|. 639 | 640 |
641 | 642 | 643 | ## Abort a request ## {#algorithm-abort-request} 644 | 645 | 646 |
647 | To abort the request |request|: 648 | 649 | 1. [=Assert=]: these steps are running on the [=lock task queue=]. 650 | 1. Let |manager| be |request|'s [=lock request/manager=]. 651 | 1. Let |name| be |request|'s [=lock request/name=]. 652 | 1. Let |queueMap| be |manager|'s [=lock manager/lock request queue map=]. 653 | 1. Let |queue| be the result of [=/getting the lock request queue=] from |queueMap| for |name|. 654 | 1. [=list/Remove=] |request| from |queue|. 655 | 1. [=Process the lock request queue=] |queue|. 656 | 657 |
658 | 659 |
660 | To signal to abort the request |request| with |signal|: 661 | 662 | 1. [=parallel queue/enqueue the following steps|Enqueue the steps=] to [=abort the request=] |request| to the [=lock task queue=]. 663 | 1. [=Reject=] |request|'s [=lock request/promise=] with |signal|'s [=AbortSignal/abort reason=]. 664 | 665 |
666 | 667 | 668 | ## Process a lock request queue for a given resource name ## {#algorithm-process-request} 669 | 670 | 671 |
672 | To process the lock request queue |queue|: 673 | 674 | 1. [=Assert=]: these steps are running on the [=lock task queue=]. 675 | 1. [=list/For each=] |request| of |queue|: 676 | 1. If |request| is not [=grantable=], then return. 677 | 678 | NOTE: Only the first item in a queue is grantable. Therefore, if something is not grantable then all the following items are automatically not grantable. 679 | 680 | 1. [=list/Remove=] |request| from |queue|. 681 | 1. Let |agent| be |request|'s [=lock-concept/agent=]. 682 | 1. Let |manager| be |request|'s [=lock request/manager=]. 683 | 1. Let |clientId| be |request|'s [=lock request/clientId=]. 684 | 1. Let |name| be |request|'s [=lock request/name=]. 685 | 1. Let |mode| be |request|'s [=lock request/mode=]. 686 | 1. Let |callback| be |request|'s [=lock request/callback=]. 687 | 1. Let |p| be |request|'s [=lock request/promise=]. 688 | 1. Let |signal| be |request|'s [=lock request/signal=]. 689 | 1. Let |waiting| be [=a new promise=]. 690 | 1. Let |lock| be a new [=lock-concept|lock=] with [=lock-concept/agent=] |agent|, [=lock-concept/clientId=] |clientId|, [=lock-concept/manager=] |manager|, [=lock-concept/mode=] |mode|, [=lock-concept/name=] |name|, [=lock-concept/released promise=] |p|, and [=lock-concept/waiting promise=] |waiting|. 691 | 1. [=set/Append=] |lock| to |manager|'s [=lock manager/held lock set=]. 692 | 1. [=parallel queue/Enqueue the following steps=] on |callback|'s [=/relevant settings object=]'s [=environment settings object/responsible event loop=]: 693 | 1. If |signal| is present, then run these steps: 694 | 1. If |signal| is [=AbortSignal/aborted=], then run these steps: 695 | 1. [=parallel queue/Enqueue the following step=] to the [=lock task queue=]: 696 | 1. [=Release the lock=] |lock|. 697 | 1. Return. 698 | 1. [=AbortSignal/Remove=] the algorithm [=signal to abort the request=] |request| from |signal|. 699 | 1. Let |r| be the result of [=invoking=] |callback| with a new {{Lock}} object associated with |lock| as the only argument. 700 | 1. [=/Resolve=] |waiting| with |r|. 701 | 702 |
703 | 704 | 705 | ## Snapshot the lock state ## {#algorithm-snapshot-state} 706 | 707 | 708 |
709 | To snapshot the lock state for |manager| with |promise|: 710 | 711 | 1. [=Assert=]: these steps are running on the [=lock task queue=]. 712 | 1. Let |pending| be a new [=/list=]. 713 | 1. [=map/For each=] |queue| of |manager|'s [=lock manager/lock request queue map=]'s [=map/values=]: 714 | 1. [=list/For each=] |request| of |queue|: 715 | 1. [=list/Append=] «[ "name" → |request|'s [=lock request/name=], "mode" → |request|'s [=lock request/mode=], "clientId" → |request|'s [=lock request/clientId=] ]» to |pending|. 716 | 1. Let |held| be a new [=/list=]. 717 | 1. [=list/For each=] |lock| of |manager|'s [=lock manager/held lock set=]: 718 | 1. [=list/Append=] «[ "name" → |lock|'s [=lock-concept/name=], "mode" → |lock|'s [=lock-concept/mode=], "clientId" → |lock|'s [=lock-concept/clientId=] ]» to |held|. 719 | 1. [=/Resolve=] |promise| with «[ "held" → |held|, "pending" → |pending| ]». 720 | 721 |
722 | 723 |
724 | For any given resource, the snapshot of the pending lock requests 725 | will return the requests in the order in which they were made; 726 | however, no guarantees are made with respect to the relative 727 | ordering of requests across different resources. For example, if 728 | pending lock requests A1 and A2 are made against resource A in 729 | that order, and pending lock requests B1 and B2 are made against 730 | resource B in that order, then both «A1, A2, B1, B2» and «A1, B1, 731 | A2, B2» would be possible orderings for a snapshot's pending list. 732 | 733 | No ordering guarantees exist for the snapshot of the held lock state. 734 |
735 | 736 | 737 | # Usage Considerations # {#usage-considerations} 738 | 739 | 740 | *This section is non-normative.* 741 | 742 | 743 | ## Deadlocks ## {#deadlocks} 744 | 745 | 746 | [Deadlocks](https://en.wikipedia.org/wiki/Deadlock) are a concept in concurrent computing, and deadlocks scoped to a particular [=lock manager=] can be introduced by this API. 747 | 748 |
749 | An example of how deadlocks can be encountered through the use of this API is as follows. 750 | 751 | Script 1: 752 | ```js 753 | navigator.locks.request('A', async a => { 754 | await navigator.locks.request('B', async b => { 755 | // do stuff with A and B 756 | }); 757 | }); 758 | ``` 759 | 760 | Script 2: 761 | ```js 762 | navigator.locks.request('B', async b => { 763 | await navigator.locks.request('A', async a => { 764 | // do stuff with A and B 765 | }); 766 | }); 767 | ``` 768 | 769 | If script 1 and script 2 run close to the same time, there is a chance that script 1 will hold lock A and script 2 will hold lock B and neither can make further progress - a deadlock. This will not affect the user agent as a whole, pause the tab, or affect other script in the origin, but this particular functionality will be blocked. 770 |
771 | 772 | Preventing deadlocks requires care. One approach is to always acquire multiple locks in a strict order. 773 | 774 |
775 | 776 | A helper function such as the following could be used to request multiple locks in a consistent order. 777 | 778 | ```js 779 | async function requestMultiple(resources, callback) { 780 | const sortedResources = [...resources]; 781 | sortedResources.sort(); // Always request in the same order. 782 | 783 | async function requestNext(locks) { 784 | return await navigator.locks.request(sortedResources.shift(), async lock => { 785 | // Now holding this lock, plus all previously requested locks. 786 | locks.push(lock); 787 | 788 | // Recursively request the next lock in order if needed. 789 | if (sortedResources.length > 0) 790 | return await requestNext(locks); 791 | 792 | // Otherwise, run the callback. 793 | return await callback(locks); 794 | 795 | // All locks will be released when the callback returns (or throws). 796 | }); 797 | } 798 | return await requestNext([]); 799 | } 800 | ``` 801 | 802 | In practice, the use of multiple locks is rarely as straightforward — libraries and other utilities can often unintentionally obfuscate their use. 803 |
804 | 805 | 806 | 807 | # Security and Privacy Considerations # {#security-privacy} 808 | 809 | 810 | 811 | ## Lock Scope ## {#security-scope} 812 | 813 | 814 | The definition of a [=lock manager=]'s scope is important as it defines a privacy boundary. Locks can be used as an ephemeral state retention mechanism and, like storage APIs, can be used as a communication mechanism, and must be no more privileged than storage facilities. User agents that impose finer granularity on one of these services must impose it on others; for example, a user agent that exposes different storage partitions to a top-level page (first-party) and a cross-origin iframe (third-party) in the same origin for privacy reasons must similarly partition locking. 815 | 816 | This also provides reasonable expectations for web application authors; if a lock is acquired over a storage resource, all same-origin browsing contexts must observe the same state. 817 | 818 | 819 | ## Private Browsing ## {#private-browsing} 820 | 821 | 822 | Every [private mode](https://github.com/w3ctag/private-mode) browsing session is considered a separate user agent for the purposes of this API. That is, locks requested/held outside such a session have no affect on requested/held inside such a session, and vice versa. This prevents a website from determining that a session is "incognito" while also not allowing a communication mechanism between such sessions. 823 | 824 | 825 | ## Implementation Risks ## {#implementation-risks} 826 | 827 | 828 | Implementations must ensure that locks do not span origins. Failure to do so would provide a side-channel for communication between script running in two origins, or allow one script in one origin to disrupt the behavior of another (e.g. denying service). 829 | 830 | 831 | ## Checklist ## {#security-privacy-checklist} 832 | 833 | 834 | The W3C TAG has developed a [Self-Review Questionnaire: Security and Privacy](https://www.w3.org/TR/security-privacy-questionnaire/) for editors of specifications to informatively answer. Revisiting the questions here: 835 | 836 | * The specification does not deal with personally identifiable information, or high-value data. 837 | * No new state for an origin that persists across browsing sessions is introduced. 838 | * No new persistent, cross-origin state is exposed to the web. 839 | * No new data is exposed to an origin that it doesn't currently have access to (e.g. via polling [[IndexedDB-2]].) 840 | * No new script execution/loading mechanisms are enabled. 841 | * This specification does not allow an origin access to any of the following: 842 | * The user's location. 843 | * Sensors on a user's device. 844 | * Aspects of a user's local computing environment. 845 | * Access to other devices. 846 | * Any measure of control over a user agent's native UI. 847 | * No temporary identifiers to the web are exposed to the web. All [=resource names=] are provided by the web application itself. 848 | * Behavior in first-party and third-party contexts is distinguished in a user agent if storage is distinguished. See [[#security-scope]]. 849 | * Behavior in the context of a user agent's "incognito" mode is described in [[#private-browsing]]. 850 | * No data is persisted to a user's local device by this API. 851 | * This API does not allow downgrading default security characteristics. 852 | 853 | 854 | # Acknowledgements # {#acknowledgements} 855 | 856 | 857 | Many thanks to 858 | Alex Russell, 859 | Andreas Butler, 860 | Anne van Kesteren, 861 | Boris Zbarsky, 862 | Chris Messina, 863 | Darin Fisher, 864 | Domenic Denicola, 865 | Gus Caplan, 866 | Harald Alvestrand, 867 | Jake Archibald, 868 | Kagami Sascha Rosylight, 869 | L. David Baron, 870 | Luciano Pacheco, 871 | Marcos Caceres, 872 | Ralph Chelala, 873 | Raymond Toy, 874 | Ryan Fioravanti, 875 | and 876 | Victor Costan 877 | for helping craft this proposal. 878 | 879 | Special thanks to Tab Atkins, Jr. for creating and maintaining 880 | [Bikeshed](https://github.com/tabatkins/bikeshed), the specification 881 | authoring tool used to create this document, and for his general 882 | authoring advice. 883 | -------------------------------------------------------------------------------- /logo-lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /proto-spec.md: -------------------------------------------------------------------------------- 1 | ## Moved ## 2 | 3 | Work has started on a real spec: https://w3c.github.io/web-locks/ 4 | 5 | Bikeshed Source: [index.bs](index.bs) 6 | -------------------------------------------------------------------------------- /security-privacy-self-assessment.md: -------------------------------------------------------------------------------- 1 | https://www.w3.org/TR/security-privacy-questionnaire/ 2 | 3 | ### 3.1 Does this specification deal with personally-identifiable information? 4 | 5 | No. 6 | 7 | ### 3.2 Does this specification deal with high-value data? 8 | 9 | No. 10 | 11 | ### 3.3 Does this specification introduce new state for an origin that persists across browsing sessions? 12 | 13 | No. State is shared within an origin across active agents (tabs, workers) but does not persist beyond the lifetime of the agents. 14 | 15 | ### 3.4 Does this specification expose persistent, cross-origin state to the web? 16 | 17 | No. 18 | 19 | ### 3.5 Does this specification expose any other data to an origin that it doesn’t currently have access to? 20 | 21 | No. 22 | 23 | ### 3.6 Does this specification enable new script execution/loading mechanisms? 24 | 25 | No. 26 | 27 | ### 3.7 Does this specification allow an origin access to a user’s location? 28 | 29 | No. 30 | 31 | ### 3.8 Does this specification allow an origin access to sensors on a user’s device? 32 | 33 | No. 34 | 35 | ### 3.9 Does this specification allow an origin access to aspects of a user’s local computing environment? 36 | 37 | No. 38 | 39 | ### 3.10 Does this specification allow an origin access to other devices? 40 | 41 | No. 42 | 43 | ### 3.11 Does this specification allow an origin some measure of control over a user agent’s native UI? 44 | 45 | No. 46 | 47 | ### 3.12 Does this specification expose temporary identifiers to the web? 48 | 49 | No. All identifiers (the arbitrary resource names) are provided by the web application itself. 50 | 51 | ### 3.13 Does this specification distinguish between behavior in first-party and third-party contexts? 52 | 53 | Depending on the browser, yes. The scope of locks is expressly tied to the scope of storage. Browsers that partition storage separately for origins depending on first-party or third-party context (ee.g. "double keying") would reflect that for locks as well. 54 | 55 | ### 3.14 How should this specification work in the context of a user agent’s "incognito" mode? 56 | 57 | The collection of agents within an incognito browsing session behave as a separate user agent for the purposes of this API. 58 | That is, locks requested/held outside such a session have no affect on requested/held inside such a session, and vice versa. 59 | This prevents a website from determining that a session is "incognito" while also not allowing a communication mechanism between such sessions. 60 | 61 | ### 3.15 Does this specification persist data to a user’s local device? 62 | 63 | No. 64 | 65 | ### 3.16 Does this specification have a "Security Considerations" and "Privacy Considerations" section? 66 | 67 | Yes. 68 | 69 | ### 3.17 Does this specification allow downgrading default security characteristics? 70 | 71 | No. 72 | -------------------------------------------------------------------------------- /w3c.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": [114929] 3 | , "contacts": ["marcoscaceres"] 4 | , "repo-type": "rec-track" 5 | } 6 | --------------------------------------------------------------------------------