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 aquire a lock over the same resource. This allows contexts (windows, workers) within a web application to coordinate the usage of resources.

1. Introduction

This section is non-normative.

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 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.

The API provides optional functionality that may be used as needed, including:

returning values from the asynchronous task,

shared and exclusive lock modes,

conditional acquisition,

diagnostics to query the state of locks in an origin, and

an escape hatch to protect against deadlocks.

Cooperative coordination takes place within the scope of same-origin agents; this may span multiple agent clusters.

navigator.locks.request('my_resource', async lock =>{// The lock has been acquired.
await do_something();
await do_somethng_else();// Now the lock will be released.});

Within an asynchronous function, the request itself can be awaited:

// Before requesting the lock.
await navigator.locks.request('my_resource', async lock =>{// The lock has been acquired.
await do_something();// Now the lock will be released.});// After the lock has been released

1.2. Motivating Use Cases

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 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.

In a data synchronization service, a "master 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 "master tab" crashes or is closed then one of the other tabs will get the lock and become the new master.

The 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).

2. Concepts

For the purposes of this specification:

Separate user profiles within a browser are considered separate user agents.

Every private mode browsing session is considered a separate user agent.

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 waiting promise.

In the above example, p1 is the released promise and p2 is the waiting promise.
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:

The waiting promise is not named in the above code, but is still present as the return value from the anonymous async callback.
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 released promise will also resolve in a subsequent microtask.

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.

The returned promise resolves (or rejects) with the result of the callback after the lock is released, or rejects if the request is aborted.

Example:

try{const result = await navigator.locks.request('resource', async lock =>{// The lock is held here.
await do_something();
await do_something_else();return"ok";// The lock will be released now.});// |result| has the return value of the callback.}catch(ex){// if the callback threw, it will be caught here.}

The lock will be released when the callback exits for any reason — either when the code returns, or if it throws.

An options dictionary can be specified as a second argument; the callback argument is always last.

options . mode

The mode option can be "exclusive" (the default if not specified) or "shared".
Multiple tabs/workers can hold a lock for the same resource in "shared" mode, but only one tab/worker can hold a lock for the resource in "exclusive" mode.

The most common use for this is to allow multiple readers to access a resource simultaneously but prevent changes.
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.

await navigator.locks.request('resource',{mode:'shared'}, async lock =>{// Lock is held here. Other contexts might also hold the lock in shared mode,// but no other contexts will hold the lock in exclusive mode.});

options . ifAvailable

If the 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.)

The 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:

const controller =new AbortController();
setTimeout(()=> controller.abort(),200);// Wait at most 200ms.try{
await navigator.locks.request('resource',{signal: controller.signal}, async lock =>{// Lock is held here.});// Done with lock here.}catch(ex){// |ex| will be a DOMException with error name "AbortError" if timer fired.}

If an abort is signalled before the lock is granted, then the request promise will reject with an AbortError.
Once the lock has been granted, the signal is ignored.

options . steal

If the steal option is true, then 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.

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.

Use the steal option with caution.
When used, code previously holding a lock will now be executing without guarantees that it is the sole context with access to the resource.
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.
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.

The request(name, callback) and request(name, options, callback) methods, when invoked, must run these steps:

Let promise be a new promise.

If options was not passed, then let options be a new LockOptions dictionary with default members.

Otherwise, if option’s signal dictionary member is present, and either of options’ steal dictionary member or options’ ifAvailable dictionary member is true, then reject promise with a "NotSupportedError" DOMException.

Otherwise, if options’ signal dictionary member is present and its aborted flag is set, then reject promise with an "AbortError" {{DOMException}.

Otherwise, run these steps:

Let request be the result of running the steps to request a lock with promise, the current agent, environment’s id, origin, callback, name, options’ mode dictionary member, options’ ifAvailable dictionary member, and options’ steal dictionary member.

5. Usage Considerations

5.1. Deadlocks

Deadlocks are a concept in concurrent computing, and deadlocks scoped to a particular lock manager can be introduced by this API.

An example of how deadlocks can be encountered through the use of this API is as follows.

Script 1:

navigator.locks.request('A', async a =>{
await navigator.locks.request('B', async b =>{// do stuff with A and B});});

Script 2:

navigator.locks.request('B', async b =>{
await navigator.locks.request('A', async a =>{// do stuff with A and B});});

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.

Preventing deadlocks requires care. One approach is to always acquire multiple locks in a strict order.

A helper function such as the following could be used to request multiple locks in a consistent order.

async function requestMultiple(resources, callback){const sortedResources =[...resources];
sortedResources.sort();// Always request in the same order.
async function requestNext(locks){return await navigator.locks.request(sortedResources.shift(), async lock =>{// Now holding this lock, plus all previously requested locks.
locks.push(lock);// Recursively request the next lock in order if needed.if(sortedResources.length >0)return await requestNext(locks);// Otherwise, run the callback.return await callback(locks);// All locks will be released when the callback returns (or throws).});}return await requestNext([]);}

In practice, the use of multiple locks is rarely as straightforward — libraries and other utilities can often unintentionally obfuscate thier use.

6. Security and Privacy Considerations

6.1. Lock Scope

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.

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.

6.2. Private Browsing

Every 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.

6.3. Implementation Risks

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).

Special thanks to Tab Atkins, Jr. for creating and maintaining Bikeshed, the specification
authoring tool used to create this document, and for his general
authoring advice.

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology.
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL”
in the normative parts of this document
are to be interpreted as described in RFC 2119.
However, for readability,
these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative
except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example”
or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note”
and are set apart from the normative text with class="note", like this: