-
Notifications
You must be signed in to change notification settings - Fork 16
/
index.bs
882 lines (659 loc) · 42.5 KB
/
index.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
<pre class=metadata>
Title: Web Locks API
Shortname: web-locks
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.
Status: ED
ED: https://w3c.github.io/web-locks/
Level: 1
TR: https://www.w3.org/TR/web-locks/
Editor: Joshua Bell, Google Inc. https://google.com, [email protected], w3cid 61302
Editor: Kagami Rosylight, Mozilla https://www.mozilla.org/, [email protected], w3cid 107856
Group: webapps
Repository: w3c/web-locks
Test Suite: https://github.com/web-platform-tests/wpt/tree/master/web-locks
Favicon: logo-lock.svg
Complain About: accidental-2119 yes, missing-example-ids yes
Markup Shorthands: css no, markdown yes
Assume Explicit For: yes
</pre>
<pre class=link-defaults>
spec:infra; type:dfn; text:user agent
</pre>
<pre class=anchors>
spec: ecma262; urlPrefix: https://tc39.github.io/ecma262/
type: dfn
text: agent; url: agent
spec: html; urlPrefix: https://html.spec.whatwg.org/multipage/
urlPrefix: webstorage.html
type: dfn
text: localStorage; url: dom-localstorage
urlPrefix: webappapis.html
type: dfn
text: agent cluster; url: integration-with-the-javascript-agent-cluster-formalism
spec: storage; urlPrefix: https://storage.spec.whatwg.org/
type: dfn
text: storage bucket; url: storage-bucket
text: storage bottle; url: storage-bottle
</pre>
<style>
.domintro::before {
content: 'For web developers (non-normative)';
text-transform: initial;
}
.domintro dt {
font-family: Menlo, Consolas, "DejaVu Sans Mono", Monaco, monospace;
padding-top: 0.5em;
padding-bottom: 1em;
}
.domintro dt a {
color: inherit; border-bottom-style: none;
}
.domintro dt code {
font-size: inherit;
}
</style>
<img id="speclogo" src="logo-lock.svg" alt="logo" width="100" height="100">
<style>
#speclogo { height: 100px; width: 100px; background-color: transparent; }
main #speclogo { position: absolute; right: 20px; top: 30px; }
.logo #speclogo { margin-top: 20px; }
</style>
<script>
(function() {
const logo = document.querySelector('.logo');
if (logo) logo.appendChild(document.querySelector('#speclogo'));
})();
</script>
<!-- ====================================================================== -->
# Introduction # {#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-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.
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 [=/agents=] sharing a [=/storage bucket=]; this may span multiple [=/agent clusters=].
NOTE: [=/Agents=] roughly correspond to windows (tabs), iframes, and workers. [=/Agent clusters=] correspond to independent processes in some user agent implementations.
<!-- ====================================================================== -->
## Usage Overview ## {#usage-overview}
<!-- ====================================================================== -->
The API is used as follows:
1. The lock is requested.
1. Work is done while holding the lock in an asynchronous task.
1. The lock is automatically released when the task completes.
<div class=example id=example-basic-usage>
A basic example of the API usage is as follows:
```js
navigator.locks.request('my_resource', async lock => {
// The lock has been acquired.
await do_something();
await do_something_else();
// Now the lock will be released.
});
```
Within an asynchronous function, the request itself can be awaited:
```js
// 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
```
</div>
<!-- ====================================================================== -->
## Motivating Use Cases ## {#motivations}
<!-- ====================================================================== -->
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.
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 [[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).
<!-- ====================================================================== -->
# Concepts # {#concepts}
<!-- ====================================================================== -->
For the purposes of this specification:
* Separate user profiles within a browser are considered separate user agents.
* Every [private mode](https://github.com/w3ctag/private-mode) browsing session is considered a separate user agent.
A [=/user agent=] has a <dfn>lock task queue</dfn> which is the result of [=starting a new parallel queue=].
The [=task source=] for [=parallel queue/enqueue steps|steps enqueued=] below is the <dfn export>web locks tasks source</dfn>.
<!-- ====================================================================== -->
## Resources Names ## {#resource-names}
<!-- ====================================================================== -->
A <dfn>resource name</dfn> 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 [=/agents=] sharing a [=/storage bucket=]. Web applications are free to use any resource naming scheme.
<div class=example id=example-indexeddb-transactions>
To mimic transaction locking over named stores within a named
database in [[IndexedDB-2]], a script might compose resource names as:
```js
encodeURIComponent(db_name) + '/' + encodeURIComponent(store_name)
```
</div>
Resource names starting with U+002D HYPHEN-MINUS (-) are reserved; requesting these will cause an exception.
<!-- ====================================================================== -->
## Lock Managers ## {#lock-managers}
<!-- ====================================================================== -->
A <dfn>lock manager</dfn> 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.
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=].
<div algorithm>
To <dfn>obtain a lock manager</dfn>, given an [=/environment settings object=] |environment|, run these steps:
1. Let |map| be the result of [=/obtaining a local storage bottle map=] given |environment| and "`web-locks`".
1. If |map| is failure, then return failure.
1. Let |bottle| be |map|'s associated [=/storage bottle=].
1. Return |bottle|'s associated [=/lock manager=].
</div>
Issue: Refine the integration with [[Storage]] here, including how to get the lock manager properly from the given environment.
<!-- ====================================================================== -->
## Modes and Scheduling ## {#modes-scheduling}
<!-- ====================================================================== -->
A <dfn>mode</dfn> 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}}".
Additional properties may influence scheduling, such as timeouts, fairness, and so on.
<!-- ====================================================================== -->
## Locks ## {#concept-lock}
<!-- ====================================================================== -->
A <dfn lt=lock-concept>lock</dfn> represents exclusive access to a shared resource.
<div dfn-for=lock-concept>
A [=lock-concept|lock=] has an <dfn>agent</dfn> which is an [=/agent=].
A [=lock-concept|lock=] has a <dfn>clientId</dfn> which is an opaque string.
A [=lock-concept|lock=] has a <dfn>manager</dfn> which is a [=/lock manager=].
A [=lock-concept|lock=] has a <dfn>name</dfn> which is a [=resource name=].
A [=lock-concept|lock=] has a <dfn>mode</dfn> which is one of "{{LockMode/exclusive}}" or "{{LockMode/shared}}".
A [=lock-concept|lock=] has a <dfn>waiting promise</dfn> which is a Promise.
A [=lock-concept|lock=] has a <dfn>released promise</dfn> which is a Promise.
<div class=note>
There are two promises associated with a lock's lifecycle:
* 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=].
* 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=].
```js
const p1 = navigator.locks.request('resource', lock => {
const p2 = new Promise(r => {
// Logic to use lock and resolve promise...
});
return p2;
});
```
In the above example, `p1` is the [=lock-concept/released promise=] and `p2` is the [=lock-concept/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:
```js
const p1 = navigator.locks.request('resource', async lock => {
// Logic to use lock...
});
```
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.
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.
</div>
Each [=/lock manager=] has a <dfn for="lock manager">held lock set</dfn> which is a [=/set=] of [=lock-concept|locks=].
<div algorithm="waiting promise settles">
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=]:
1. [=Release the lock=] |lock|.
1. [=/Resolve=] |lock|'s [=lock-concept/released promise=] with |lock|'s [=lock-concept/waiting promise=].
</div>
</div>
<!-- ====================================================================== -->
## Lock Requests ## {#concept-lock-request}
<!-- ====================================================================== -->
A <dfn>lock request</dfn> represents a pending request for a [=lock-concept|lock=].
<div dfn-for="lock request">
A [=lock request=] is a [=/struct=] with [=struct/items=] <dfn>agent</dfn>, <dfn>clientId</dfn>, <dfn>manager</dfn>, <dfn>name</dfn>, <dfn>mode</dfn>, <dfn>callback</dfn>, <dfn>promise</dfn>, and <dfn>signal</dfn>.
</div>
A <dfn>lock request queue</dfn> is a [=/queue=] of [=/lock requests=].
Each [=/lock manager=] has a <dfn for="lock manager">lock request queue map</dfn>, which is a [=map=] of [=resource names=] to [=/lock request queues=].
<div algorithm>
To <dfn>get the lock request queue</dfn> from [=lock manager/lock request queue map=] |queueMap| from [=/resource name=] |name|, run these steps:
1. If |queueMap|[|name|] does not [=map/exist=], [=map/set=] |queueMap|[|name|] to a new empty [=/lock request queue=].
1. Return |queueMap|[|name|].
</div>
<div algorithm>
A [=lock request=] |request| is said to be <dfn>grantable</dfn> if the following steps return true:
1. Let |manager| be |request|'s [=lock request/manager=].
1. Let |queueMap| be |manager|'s [=lock manager/lock request queue map=].
1. Let |name| be |request|'s [=lock request/name=].
1. Let |queue| be the result of [=/getting the lock request queue=] from |queueMap| for |name|.
1. Let |held| be |manager|'s [=lock manager/held lock set=]
1. Let |mode| be |request|'s [=lock request/mode=]
1. If |queue| [=queue/is empty|is not empty=] and |request| is not the first [=queue/item=] in |queue|, then return false.
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.
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.
</div>
<!-- ====================================================================== -->
## Termination of Locks ## {#termination-of-locks}
<!-- ====================================================================== -->
Whenever the [=/unloading document cleanup steps=] run with a [=/document=], [=/terminate remaining locks and requests=] with its [=/agent=].
When an [=/agent=] terminates, [=/terminate remaining locks and requests=] with the agent.
Issue: This is currently only for workers and is vaguely defined, since there is no normative way to run steps on worker termination.
<div algorithm>
To <dfn>terminate remaining locks and requests</dfn> with |agent|, [=parallel queue/enqueue the following steps=] on the [=lock task queue=]:
1. For each [=lock request=] |request| with [=lock request/agent=] equal to |agent|:
1. [=Abort the request=] |request|.
1. For each [=lock-concept|lock=] |lock| with [=lock-concept/agent=] equal to |agent|:
1. [=Release the lock=] |lock|.
</div>
<!-- ====================================================================== -->
# API # {#api}
<!-- ====================================================================== -->
<!-- ====================================================================== -->
## Navigator Mixins ## {#navigator-mixins}
<!-- ====================================================================== -->
<xmp class=idl>
[SecureContext]
interface mixin NavigatorLocks {
readonly attribute LockManager locks;
};
Navigator includes NavigatorLocks;
WorkerNavigator includes NavigatorLocks;
</xmp>
Each [=environment settings object=] has a {{LockManager}} object.
The <dfn attribute for=NavigatorLocks>locks</dfn> getter's steps are to return [=/this=]'s [=/relevant settings object=]'s {{LockManager}} object.
<!-- ====================================================================== -->
## {{LockManager}} class ## {#api-lock-manager}
<!-- ====================================================================== -->
<xmp class=idl>
[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 mode = "exclusive";
boolean ifAvailable = false;
boolean steal = false;
AbortSignal signal;
};
dictionary LockManagerSnapshot {
sequence<LockInfo> held;
sequence<LockInfo> pending;
};
dictionary LockInfo {
DOMString name;
LockMode mode;
DOMString clientId;
};
</xmp>
A {{LockManager}} instance allows script to make [=lock requests=] and query
the state of the [=lock manager=].
<!-- ====================================================================== -->
### The {{LockManager/request(name, callback)|request()}} method ### {#api-lock-manager-request}
<!-- ====================================================================== -->
<div class="domintro note">
: |promise| = navigator . locks . {{LockManager/request(name, callback)|request}}(|name|, |callback|)
: |promise| = navigator . locks . {{LockManager/request(name, options, callback)|request}}(|name|, |options|, |callback|)
:: The {{LockManager/request(name, callback)|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 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:
```js
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 {{LockOptions/mode}} option can be "{{LockMode/exclusive}}" (the default if not specified) or "{{LockMode/shared}}".
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.
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.
```js
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 {{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.)
```js
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 {{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:
```js
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 {{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.
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.
</div>
<div class=advisement>
Use the {{LockOptions/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.
</div>
<div algorithm="LockManager request method">
The <dfn method for=LockManager>request(|name|, |callback|)</dfn> and
<dfn method for=LockManager>request(|name|, |options|, |callback|)</dfn> method steps are:
1. If |options| was not passed, then let |options| be a new {{LockOptions}} dictionary with default members.
1. Let |environment| be [=/this=]'s [=/relevant settings object=].
1. If |environment|'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully active=], then return [=a promise rejected with=] a "{{InvalidStateError}}" {{DOMException}}.
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}}.
1. If |name| starts with U+002D HYPHEN-MINUS (-), then return [=a promise rejected with=] a "{{NotSupportedError}}" {{DOMException}}.
1. If both |options|["`steal`"] and |options|["`ifAvailable`"] are true, then return [=a promise rejected with=] a "{{NotSupportedError}}" {{DOMException}}.
1. If |options|["`steal`"] is true and |options|["`mode`"] is not "{{LockMode/exclusive}}", then return [=a promise rejected with=] a "{{NotSupportedError}}" {{DOMException}}.
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}}.
1. If |options|["`signal`"] [=map/exists=] and is [=AbortSignal/aborted=], then return [=a promise rejected with=] |options|["`signal`"]'s [=AbortSignal/abort reason=].
1. Let |promise| be [=a new promise=].
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`"].
1. Return |promise|.
</div>
<!-- ====================================================================== -->
### The {{LockManager/query()}} method ### {#api-lock-manager-query}
<!-- ====================================================================== -->
<div class="domintro note">
: |state| = await navigator . locks . {{LockManager/query()|query}}()
:: 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.
The returned promise resolves to |state|, a plain-old-data structure (i.e. JSON-like data) with this form:
```js
{
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 {{Client/id}} attribute.
</div>
<div class=advisement>
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.
</div>
<div algorithm>
The <dfn method for=LockManager>query()</dfn> method steps are:
1. Let |environment| be [=/this=]'s [=/relevant settings object=].
1. If |environment|'s [=relevant global object=]'s [=associated Document=] is not [=Document/fully active=], then return [=a promise rejected with=] a "{{InvalidStateError}}" {{DOMException}}.
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}}.
1. Let |promise| be [=a new promise=].
1. [=parallel queue/enqueue the following steps|Enqueue the steps=] to [=snapshot the lock state=] for |manager| with |promise| to the [=lock task queue=].
1. Return |promise|.
</div>
<!-- ====================================================================== -->
## {{Lock}} class ## {#api-lock}
<!-- ====================================================================== -->
<xmp class=idl>
[SecureContext, Exposed=(Window,Worker)]
interface Lock {
readonly attribute DOMString name;
readonly attribute LockMode mode;
};
</xmp>
A {{Lock}} object has an associated [=lock-concept|lock=].
The <dfn attribute for=Lock>name</dfn> getter's steps are to return the associated [=lock-concept|lock=]'s [=lock-concept/name=].
The <dfn attribute for=Lock>mode</dfn> getter's steps are to return the associated [=lock-concept|lock=]'s [=lock-concept/mode=].
<!-- ====================================================================== -->
# Algorithms # {#algorithms}
<!-- ====================================================================== -->
<!-- ====================================================================== -->
## Request a lock ## {#algorithm-request-lock}
<!-- ====================================================================== -->
<div algorithm>
To <dfn>request a lock</dfn> with |promise|, |agent|, |clientId|, |manager|, |callback|, |name|, |mode|, |ifAvailable|, |steal|, and |signal|:
1. Let |request| be a new [=lock request=] (|agent|, |clientId|, |manager|, |name|, |mode|, |callback|, |promise|, |signal|).
1. If |signal| is present, then [=AbortSignal/add=] the algorithm [=signal to abort the request=] |request| with |signal| to |signal|.
1. [=parallel queue/Enqueue the following steps=] to the [=lock task queue=]:
1. Let |queueMap| be |manager|'s [=lock manager/lock request queue map=].
1. Let |queue| be the result of [=/getting the lock request queue=] from |queueMap| for |name|.
1. Let |held| be |manager|'s [=lock manager/held lock set=].
1. If |steal| is true, then run these steps:
1. [=list/For each=] |lock| of |held|:
1. If |lock|'s [=lock-concept/name=] is |name|, then run these steps:
1. [=list/Remove=] [=lock-concept|lock=] from |held|.
1. [=Reject=] |lock|'s [=lock-concept/released promise=] with an "{{AbortError}}" {{DOMException}}.
1. [=list/Prepend=] |request| in |queue|.
1. Otherwise, run these steps:
1. If |ifAvailable| is true and |request| is not [=grantable=],
then [=parallel queue/enqueue the following steps=] on |callback|'s [=/relevant settings object=]'s [=environment settings object/responsible event loop=]:
1. Let |r| be the result of [=invoking=] |callback| with `null` as the only argument.
1. [=/Resolve=] |promise| with |r| and abort these steps.
1. [=queue/Enqueue=] |request| in |queue|.
1. [=Process the lock request queue=] |queue|.
1. Return |request|.
</div>
<!-- ====================================================================== -->
## Release a lock ## {#algorithm-release-lock}
<!-- ====================================================================== -->
<div algorithm>
To <dfn>release the lock</dfn> |lock|:
1. [=Assert=]: these steps are running on the [=lock task queue=].
1. Let |manager| be |lock|'s [=lock-concept/manager=].
1. Let |queueMap| be |manager|'s [=lock manager/lock request queue map=].
1. Let |name| be |lock|'s [=resource name=].
1. Let |queue| be the result of [=/getting the lock request queue=] from |queueMap| for |name|.
1. [=list/Remove=] [=lock-concept|lock=] from the |manager|'s [=lock manager/held lock set=].
1. [=Process the lock request queue=] |queue|.
</div>
<!-- ====================================================================== -->
## Abort a request ## {#algorithm-abort-request}
<!-- ====================================================================== -->
<div algorithm>
To <dfn>abort the request</dfn> |request|:
1. [=Assert=]: these steps are running on the [=lock task queue=].
1. Let |manager| be |request|'s [=lock request/manager=].
1. Let |name| be |request|'s [=lock request/name=].
1. Let |queueMap| be |manager|'s [=lock manager/lock request queue map=].
1. Let |queue| be the result of [=/getting the lock request queue=] from |queueMap| for |name|.
1. [=list/Remove=] |request| from |queue|.
1. [=Process the lock request queue=] |queue|.
</div>
<div algorithm>
To <dfn>signal to abort the request</dfn> |request| with |signal|:
1. [=parallel queue/enqueue the following steps|Enqueue the steps=] to [=abort the request=] |request| to the [=lock task queue=].
1. [=Reject=] |request|'s [=lock request/promise=] with |signal|'s [=AbortSignal/abort reason=].
</div>
<!-- ====================================================================== -->
## Process a lock request queue for a given resource name ## {#algorithm-process-request}
<!-- ====================================================================== -->
<div algorithm>
To <dfn>process the lock request queue</dfn> |queue|:
1. [=Assert=]: these steps are running on the [=lock task queue=].
1. [=list/For each=] |request| of |queue|:
1. 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.
1. [=list/Remove=] |request| from |queue|.
1. Let |agent| be |request|'s [=lock-concept/agent=].
1. Let |manager| be |request|'s [=lock request/manager=].
1. Let |clientId| be |request|'s [=lock request/clientId=].
1. Let |name| be |request|'s [=lock request/name=].
1. Let |mode| be |request|'s [=lock request/mode=].
1. Let |callback| be |request|'s [=lock request/callback=].
1. Let |p| be |request|'s [=lock request/promise=].
1. Let |signal| be |request|'s [=lock request/signal=].
1. Let |waiting| be [=a new promise=].
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|.
1. [=set/Append=] |lock| to |manager|'s [=lock manager/held lock set=].
1. [=parallel queue/Enqueue the following steps=] on |callback|'s [=/relevant settings object=]'s [=environment settings object/responsible event loop=]:
1. If |signal| is present, then run these steps:
1. If |signal| is [=AbortSignal/aborted=], then run these steps:
1. [=parallel queue/Enqueue the following step=] to the [=lock task queue=]:
1. [=Release the lock=] |lock|.
1. Return.
1. [=AbortSignal/Remove=] the algorithm [=signal to abort the request=] |request| from |signal|.
1. Let |r| be the result of [=invoking=] |callback| with a new {{Lock}} object associated with |lock| as the only argument.
1. [=/Resolve=] |waiting| with |r|.
</div>
<!-- ====================================================================== -->
## Snapshot the lock state ## {#algorithm-snapshot-state}
<!-- ====================================================================== -->
<div algorithm>
To <dfn>snapshot the lock state</dfn> for |manager| with |promise|:
1. [=Assert=]: these steps are running on the [=lock task queue=].
1. Let |pending| be a new [=/list=].
1. [=map/For each=] |queue| of |manager|'s [=lock manager/lock request queue map=]'s [=map/values=]:
1. [=list/For each=] |request| of |queue|:
1. [=list/Append=] «[ "name" → |request|'s [=lock request/name=], "mode" → |request|'s [=lock request/mode=], "clientId" → |request|'s [=lock request/clientId=] ]» to |pending|.
1. Let |held| be a new [=/list=].
1. [=list/For each=] |lock| of |manager|'s [=lock manager/held lock set=]:
1. [=list/Append=] «[ "name" → |lock|'s [=lock-concept/name=], "mode" → |lock|'s [=lock-concept/mode=], "clientId" → |lock|'s [=lock-concept/clientId=] ]» to |held|.
1. [=/Resolve=] |promise| with «[ "held" → |held|, "pending" → |pending| ]».
</div>
<div class=note>
For any given resource, the snapshot of the pending lock requests
will return the requests in the order in which they were made;
however, no guarantees are made with respect to the relative
ordering of requests across different resources. For example, if
pending lock requests A1 and A2 are made against resource A in
that order, and pending lock requests B1 and B2 are made against
resource B in that order, then both «A1, A2, B1, B2» and «A1, B1,
A2, B2» would be possible orderings for a snapshot's pending list.
No ordering guarantees exist for the snapshot of the held lock state.
</div>
<!-- ====================================================================== -->
# Usage Considerations # {#usage-considerations}
<!-- ====================================================================== -->
*This section is non-normative.*
<!-- ====================================================================== -->
## Deadlocks ## {#deadlocks}
<!-- ====================================================================== -->
[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.
<div class=example id=example-deadlocks>
An example of how deadlocks can be encountered through the use of this API is as follows.
Script 1:
```js
navigator.locks.request('A', async a => {
await navigator.locks.request('B', async b => {
// do stuff with A and B
});
});
```
Script 2:
```js
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.
</div>
Preventing deadlocks requires care. One approach is to always acquire multiple locks in a strict order.
<div class=example id=example-request-multiple-locks>
A helper function such as the following could be used to request multiple locks in a consistent order.
```js
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 their use.
</div>
<!-- ====================================================================== -->
# Security and Privacy Considerations # {#security-privacy}
<!-- ====================================================================== -->
<!-- ====================================================================== -->
## Lock Scope ## {#security-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.
<!-- ====================================================================== -->
## Private Browsing ## {#private-browsing}
<!-- ====================================================================== -->
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.
<!-- ====================================================================== -->
## Implementation Risks ## {#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).
<!-- ====================================================================== -->
## Checklist ## {#security-privacy-checklist}
<!-- ====================================================================== -->
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:
* 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 [[#security-scope]].
* Behavior in the context of a user agent's "incognito" mode is described in [[#private-browsing]].
* No data is persisted to a user's local device by this API.
* This API does not allow downgrading default security characteristics.
<!-- ====================================================================== -->
# Acknowledgements # {#acknowledgements}
<!-- ====================================================================== -->
Many thanks to
Alex Russell,
Andreas Butler,
Anne van Kesteren,
Boris Zbarsky,
Chris Messina,
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](https://github.com/tabatkins/bikeshed), the specification
authoring tool used to create this document, and for his general
authoring advice.