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, 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 .
1.1. Usage Overview
The API is used as follows:
-
The lock is requested.
-
Work is done while holding the lock in an asynchronous task.
-
The lock is automatically released when the task completes.
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 "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.
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 user agent has a lock task queue which is the result of starting a new parallel queue .
The task source for steps enqueued below is the web locks tasks source .
2.1. Resources Names
A resource name is a JavaScript string chosen by the web application to represent an abstract resource.
A resource name has no external meaning beyond the scheduling algorithm, but is global across browsing contexts within an origin . Web applications are free to use any resource naming scheme.
Resource names starting with U+002D HYPHEN-MINUS (-) are reserved; requesting these will cause an exception.
2.2. Lock Managers
A user agent has a lock manager for each origin , which encapsulates the state of all locks and lock requests for that origin.
Note: Pages and workers ( agents ) on a single origin opened in the same user agent share a lock manager even if they are in unrelated browsing contexts .
-
Let origin be environment ’s origin .
-
If origin is an opaque origin , then return failure.
-
Return origin ’s associated lock manager .
2.3. Modes and Scheduling
A
mode
is
either
"
exclusive
"
or
"
shared
".
Modes
can
be
used
to
model
the
common
readers-writer
lock
pattern.
If
an
"
exclusive
"
lock
is
held,
then
no
other
locks
with
that
name
can
be
granted.
If
a
"
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
".
Additional properties may influence scheduling, such as timeouts, fairness, and so on.
2.4. Locks
A lock represents exclusive access to a shared resource.
A lock has an agent which is an agent .
A lock has a clientId which is an opaque string.
A lock has a manager which is a lock manager .
A lock has a name which is a resource name .
A
lock
has
a
mode
which
is
one
of
"
exclusive
"
or
"
shared
".
A lock has a waiting promise which is a Promise.
A lock has a released promise which is a Promise.
Each lock manager has a held lock set which is a set of locks .
When lock lock ’s waiting promise settles (fulfills or rejects), enqueue the following steps on the lock task queue :
-
Release the lock lock .
-
Resolve lock ’s released promise with lock ’s waiting promise .
2.5. Lock Requests
A lock request represents a pending request for a lock .
A lock request is a struct with items agent , clientId , manager , name , mode , callback , promise , and signal .
A lock request queue is a queue of lock requests .
Each lock manager has a lock request queue map , which is a map of resource names to lock request queues .
To get the lock request queue from lock request queue map queueMap from resource name name , run these steps:
-
If queueMap [ name ] does not exist , set queueMap [ name ] to a new empty lock request queue .
-
Return queueMap [ name ].
A lock request request is said to be grantable if the following steps return true:
-
Let manager be request ’s manager .
-
Let queueMap be manager ’s lock request queue map .
-
Let name be request ’s name .
-
Let queue be the result of getting the lock request queue from queueMap for name .
-
Let held be manager ’s held lock set
-
Let mode be request ’s mode
-
If queue is not empty and request is not the first item in queue , then return false.
-
If mode is "
exclusive
", then return true if no lock in held has name equal to name , and false otherwise. -
Otherwise, mode is "
shared
"; return true if no lock in held has mode "exclusive
" and has name equal to name , and false otherwise.
2.6. Agent Integration
Identify a normative reference for terminates .
-
For each lock request request with agent equal to the terminating agent:
-
Abort the request request .
-
-
For each lock lock with agent equal to the terminating agent:
-
Release the lock lock .
-
3. API
3.1. Navigator Mixins
[SecureContext ]interface mixin {
NavigatorLocks readonly attribute LockManager locks ; };Navigator includes NavigatorLocks ;WorkerNavigator includes NavigatorLocks ;
Each
environment
settings
object
has
a
LockManager
object.
In only one current engine.
Opera 56+ Edge 79+
Edge (Legacy) None IE None
Firefox for Android None iOS Safari None Chrome for Android 69+ Android WebView 69+ Samsung Internet 10.0+ Opera Mobile 48+
The
locks
getter’s
steps
are
to
return
this
's
relevant
settings
object
's
LockManager
object.
3.2.
LockManager
class
In only one current engine.
Opera 56+ Edge 79+
Edge (Legacy) None IE None
Firefox for Android None iOS Safari None Chrome for Android 69+ Android WebView 69+ Samsung Internet 10.0+ Opera Mobile 48+
[SecureContext ,Exposed =(Window ,Worker )]interface {
LockManager Promise <any >request (DOMString ,
name LockGrantedCallback );
callback Promise <any >request (DOMString ,
name LockOptions ,
options LockGrantedCallback );
callback Promise <LockManagerSnapshot >query (); };callback =
LockGrantedCallback Promise <any > (Lock ?);
lock enum {
LockMode ,
"shared" };
"exclusive" dictionary {
LockOptions LockMode = "exclusive";
mode boolean =
ifAvailable false ;boolean =
steal false ;AbortSignal ; };
signal dictionary {
LockManagerSnapshot sequence <LockInfo >;
held sequence <LockInfo >; };
pending dictionary {
LockInfo DOMString ;
name LockMode ;
mode DOMString ; };
clientId
A
LockManager
instance
allows
script
to
make
lock
requests
and
query
the
state
of
the
lock
manager
.
3.2.1.
The
request()
method
In only one current engine.
Opera 56+ Edge 79+
Edge (Legacy) None IE None
Firefox for Android None iOS Safari None Chrome for Android 69+ Android WebView 69+ Samsung Internet 10.0+ Opera Mobile 48+
-
promise
=
navigator
.
locks
.
request
( name , callback )- promise = navigator . locks .
request
( name , options , callback ) - promise = navigator . locks .
-
The
request()
method is called to request a lock.The name (initial argument) is a resource name string.
The callback (final argument) is a callback function invoked with the
Lock
when granted. This is specified by script, and is usually anasync
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 istrue
, 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 withnull
. (Since this is expected, the request is not rejected.)
await navigator. locks. request( 'resource' , { ifAvailable: true }, async lock=> { if ( ! lock) { // Didn’t get it. Maybe take appropriate action. return ; } // Lock is held here. });
- options . signal
-
The
signal
option can be set to anAbortSignal
. 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 istrue
, then any held locks for the resource will be released (and the released promise of such locks will resolve withAbortError
), 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.
The
request(
name
,
callback
)
and
request(
name
,
options
,
callback
)
method
steps
are:
-
If options was not passed, then let options be a new
LockOptions
dictionary with default members. -
Let environment be this 's relevant settings object .
-
If environment ’s responsible document is not fully active , then return a promise rejected with a "
InvalidStateError
"DOMException
. -
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
. -
If name starts with U+002D HYPHEN-MINUS (-), then return a promise rejected with a "
NotSupportedError
"DOMException
. -
If both options ["
steal
"] and options ["ifAvailable
"] are true, then return a promise rejected with a "NotSupportedError
"DOMException
. -
If options ["
steal
"] is true and options ["mode
"] is not "exclusive
", then return a promise rejected with a "NotSupportedError
"DOMException
. -
If options ["
signal
"] exists , and either of options ["steal
"] or options ["ifAvailable
"] is true, then return a promise rejected with a "NotSupportedError
"DOMException
. -
If options ["
signal
"] exists and is aborted , then return a promise rejected withan " AbortError " {{DOMException}.signal ’s abort reason . -
Let promise be a new promise .
-
Request a lock with promise , the current agent , environment ’s id , manager , callback , name , options ["
mode
"], options ["ifAvailable
"], options ["steal
"], and options ["signal
"]. -
Return promise .
3.2.2.
The
query()
method
In only one current engine.
Opera 56+ Edge 79+
Edge (Legacy) None IE None
Firefox for Android None iOS Safari None Chrome for Android 69+ Android WebView 69+ Samsung Internet 10.0+ Opera Mobile 48+
-
state
=
await
navigator
.
locks
.
query
() -
The
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.The returned promise resolves to state , a plain-old-data structure (i.e. JSON-like data) with this form:
{ held: [ { name: "resource1" , mode: "exclusive" , clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, { name: "resource2" , mode: "shared" , clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, { name: "resource2" , mode: "shared" , clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, ], pending: [ { name: "resource1" , mode: "exclusive" , clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, { name: "resource1" , mode: "exclusive" , clientId: "d341a5d0-1d8d-4224-be10-704d1ef92a15" }, ] }
The
clientId
field
corresponds
to
a
unique
context
(frame
or
worker),
and
is
the
same
value
returned
by
Client
's
id
attribute.
The
query()
method
steps
are:
-
Let environment be this 's relevant settings object .
-
If environment ’s responsible document is not fully active , then return a promise rejected with a "
InvalidStateError
"DOMException
. -
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
. -
Let promise be a new promise .
-
Enqueue the steps to snapshot the lock state for manager with promise to the lock task queue .
-
Return promise .
3.3.
Lock
class
In only one current engine.
Opera 56+ Edge 79+
Edge (Legacy) None IE None
Firefox for Android None iOS Safari None Chrome for Android 69+ Android WebView 69+ Samsung Internet 10.0+ Opera Mobile 48+
[SecureContext ,Exposed =(Window ,Worker )]interface {
Lock readonly attribute DOMString name ;readonly attribute LockMode mode ; };
A
Lock
object
has
an
associated
lock
.
In only one current engine.
Opera 56+ Edge 79+
Edge (Legacy) None IE None
Firefox for Android None iOS Safari None Chrome for Android 69+ Android WebView 69+ Samsung Internet 10.0+ Opera Mobile 48+
The
name
getter’s
steps
are
to
return
the
associated
lock
's
name
.
In only one current engine.
Opera 56+ Edge 79+
Edge (Legacy) None IE None
Firefox for Android None iOS Safari None Chrome for Android 69+ Android WebView 69+ Samsung Internet 10.0+ Opera Mobile 48+
The
mode
getter’s
steps
are
to
return
the
associated
lock
's
mode
.
4. Algorithms
4.1. Request a lock
-
Let request be a new lock request ( agent , clientId , manager , name , mode , callback , promise , signal ).
-
If signal is present, then add the algorithm signal to abort the request request with signal to signal .
-
Enqueue the following steps to the lock task queue :
-
Let queueMap be manager ’s lock request queue map .
-
Let queue be the result of getting the lock request queue from queueMap for name .
-
Let held be manager ’s held lock set .
-
If steal is true, then run these steps:
-
For each lock of held :
-
If lock ’s name is name , then run these steps:
-
Reject lock ’s released promise with an "
AbortError
"DOMException
.
-
-
Prepend request in queue .
-
-
Otherwise, run these steps:
-
If ifAvailable is true and request is not grantable , then enqueue the following steps on callback ’s relevant settings object 's responsible event loop :
-
Enqueue request in queue .
-
-
Process the lock request queue queue .
-
-
Return request .
4.2. Release a lock
-
Assert : these steps are running on the lock task queue .
-
Let manager be lock ’s manager .
-
Let queueMap be manager ’s lock request queue map .
-
Let name be lock ’s resource name .
-
Let queue be the result of getting the lock request queue from queueMap for name .
-
Remove lock from the manager ’s held lock set .
-
Process the lock request queue queue .
4.3. Abort a request
-
Assert : these steps are running on the lock task queue .
-
Let manager be request ’s manager .
-
Let name be request ’s name .
-
Let queueMap be manager ’s lock request queue map .
-
Let queue be the result of getting the lock request queue from queueMap for name .
-
Remove request from queue .
-
Process the lock request queue queue .
-
Enqueue the steps to abort the request request to the lock task queue .
-
Reject request ’s promise with
an " AbortError " DOMException .signal ’s abort reason .
4.4. Process a lock request queue for a given resource name
-
Assert : these steps are running on the lock task queue .
-
For each request of queue :
-
If request is not grantable , then return.
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.
-
Remove request from queue .
-
Let agent be request ’s agent .
-
Let manager be request ’s manager .
-
Let clientId be request ’s clientId .
-
Let name be request ’s name .
-
Let mode be request ’s mode .
-
Let callback be request ’s callback .
-
Let p be request ’s promise .
-
Let signal be request ’s signal .
-
Let waiting be a new promise .
-
Let lock be a new lock with agent agent , clientId clientId , manager manager , mode mode , name name , released promise p , and waiting promise waiting .
-
Append lock to manager ’s held lock set .
-
Enqueue the following steps on callback ’s relevant settings object 's responsible event loop :
-
If signal is present, then run these steps:
-
If signal is aborted , then run these steps:
-
Enqueue the following step to the lock task queue :
-
Release the lock lock .
-
-
Return.
-
-
Remove the algorithm signal to abort the request request from signal .
-
-
Let r be the result of invoking callback with a new
Lock
object associated with lock as the only argument. -
Resolve waiting with r .
-
-
4.5. Snapshot the lock state
-
Assert : these steps are running on the lock task queue .
-
Let pending be a new list .
-
For each queue of manager ’s lock request queue map 's values :
-
Let held be a new list .
-
For each lock of manager ’s held lock set :
-
Resolve promise with «[ "held" → held , "pending" → pending ]».
5. Usage Considerations
This section is non-normative.
5.1. Deadlocks
Deadlocks are a concept in concurrent computing, and deadlocks scoped to a particular lock manager can be introduced by this API.
Preventing deadlocks requires care. One approach is to always acquire multiple locks in a strict order.
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).
6.4. Checklist
The W3C TAG has developed a Self-Review Questionnaire: Security and Privacy for editors of specifications to informatively answer. Revisiting the questions here:
-
The specification does not deal with personally identifiable information, or high-value data.
-
No new state for an origin that persists across browsing sessions is introduced.
-
No new persistent, cross-origin state is exposed to the web.
-
No new data is exposed to an origin that it doesn’t currently have access to (e.g. via polling [IndexedDB-2] .)
-
No new script execution/loading mechanisms are enabled.
-
This specification does not allow an origin access to any of the following:
-
The user’s location.
-
Sensors on a user’s device.
-
Aspects of a user’s local computing environment.
-
Access to other devices.
-
Any measure of control over a user agent’s native UI.
-
-
No temporary identifiers to the web are exposed to the web. All resource names are provided by the web application itself.
-
Behavior in first-party and third-party contexts is distinguished in a user agent if storage is distinguished. See § 6.1 Lock Scope .
-
Behavior in the context of a user agent’s "incognito" mode is described in § 6.2 Private Browsing .
-
No data is persisted to a user’s local device by this API.
-
This API does not allow downgrading default security characteristics.
7. Acknowledgements
Many thanks to Alex Russell, Andreas Butler, Anne van Kesteren, Boris Zbarsky, Darin Fisher, Domenic Denicola, Gus Caplan, Harald Alvestrand, Jake Archibald, Kagami Sascha Rosylight, L. David Baron, Luciano Pacheco, Marcos Caceres, Ralph Chelala, Raymond Toy, Ryan Fioravanti, and Victor Costan for helping craft this proposal.
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.