Web Locks API
developer.mozilla.org
developer.mozilla.org
This would be so much more readable with `using`
{
using lock = navigator.lock('my_resource');
await do_something();
await do_something_else();
}
(https://github.com/tc39/proposal-explicit-resource-managemen...) with lock {
// do stuff
}
design. This could be C++ too to be honest because lambdas exist but RAII is just too common for people to design their locks like this.This syntax looks more like Python or Rust.
Edit: Nevermind, release of the lock is automatic when the callback resolves its promise. I get it now.
navigator whileLocked:'my_resource' do:{ :protected |
protected doSomething.
protected doOtherThing.
}https://github.com/nodejs/node/pull/48518 https://github.com/tc39/proposal-explicit-resource-managemen...
Catch/finally not sharing a scope with try is a repeated mistake, that I surely agree with!
const denialOfService = res => void navigator.lock(res);
I mean, one might write this just as well with the callback interface, but this is much easier to do accidentally.My ideal would be a thing that says "this hit 90% of deployed browsers 4 years ago", but just seeing the date it was added to each of the significant browser families would be amazingly useful.
Using the Web Locks API page as an example, let's say we want to know when `LockManager` was added to Chrome. Here are the steps:
1. View the page: https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_A... 2. Scroll down to the Browser compatibility table 3. Find the cell you're interested in, in this example we're looking at where the `LockManager` row meets the Chrome column. 4. We see a check and a version number (in this case 69).
So at this point we know that it has existed since version 69.
Now in the case that we don't know that Chrome's current version is already < 100 and we need to know when a feature gained support:
5. Click on the cell to view the timeline of that cell.
Now we know that it was released on 2018-09-04, and it never had a smaller release requiring browser flags/prefixes/experiment flags/etc...
Now I'm digging around in https://github.com/mdn/browser-compat-data/blob/main/api/Loc... and trying to find the browser release dates data...
Ooh, https://bcd.developer.mozilla.org/bcd/api/v0/current/api.Loc... is even better - it expands those browser dates and it's served with access-control-allow-origin: *
It lets you search for an API and then displays a timeline. Details on how I built it here: https://github.com/simonw/tools/commit/59323c6a30271c856aabb... and https://github.com/simonw/tools/commit/472b46fda02e912c43604...
Search around and you’ll find various information and explanations about it. https://blog.mozilla.org/security/2018/01/15/secure-contexts... is one, though https://w3ctag.github.io/design-principles/#secure-context has apparently been significantly watered down from what it was originally—see the initial proposal in https://github.com/w3ctag/design-principles/pull/75, and what was then merged in https://github.com/w3ctag/design-principles/pull/89.
In general¹, I presume that any API that uses "same origin" as bounding criteria, must be secure. Since there's no way to enforce this "same origin" in insecure contexts.
--
¹ Aside from the idea that maybe just make all new APIs "secure only", just to discourage insecure contexts.
Or the same way two completely different processes can access the same address space with shared memory IPC.
You aren't running in the same memory space, you're just communicating with a shared resource.
After you fix a lock-related bug for example, how do you deal with an open tab running a different version of your code that is erroneously misusing a lock?
You need to account for that when you release new code, yeah? Rename the lock maybe? Some other logic?
I find iterating in mitata super fun and a little addictive. It's hard to write a representative micro-benchmark, but optimizing them is still useful as long as you aren't making anything worse, which is often easy to avoid. I recently used mitata-benchmark-guided optimization to rewrite a core data structure at Notion for a 5% latency decrease on a few endpoints at p90/95/99. One of our returning interns used it to assess serialization libraries and she found one 3x faster. a+++ would recommend
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
I'm not sure why Web Locks is useful TBH. I guess if you don't understand atomics it's a friendlier API?
”[…] allows a web app running in multiple tabs or workers to coordinate work and the use of resources”
A locking API is much more natural and less error-prone for this use case than using shared memory and atomics.
If the atomics API gave you ability to do multiple things, you wouldn't need compareExchange, because you could just do compare and then exchange.
p = Promise.withResolvers()
navigator.locks.request(
"foo",
p.promise,
)
p.resolve()
I guess there’s room for .requestWithResolvers() still, they rarely learn the first lesson. Even the $subj article seems to be unaware of it and uses the silly wrapper way. class Lock {
#release: () => void | undefined;
constructor(public name: string) {}
acquire() {
if (this.#release !== undefined) throw new Error("Already locked");
navigator.locks.request(this.name, async (lock) => {
await new Promise<void>(resolve => {
this.#release = resolve;
});
});
}
release() {
if (this.#release === undefined) throw new Error("Not locked");
this.#release();
}
} class Lock {
#resolve: (() => void) | undefined;
#releasePromise: Promise<void> | undefined;
constructor(public name: string) {}
acquire(): Promise<void> {
if (this.#releasePromise !== undefined) {
throw new Error("Already aquired");
}
return new Promise((resolveAquire, _rejectAquire) => {
this.#releasePromise = new Promise((resolveRelease, _rejectRelease) => {
navigator.locks.request(this.name, async (lock) => {
await new Promise<void>((resolve, _reject) => {
this.#resolve = resolve;
resolveAquire();
});
resolveRelease();
});
});
});
}
async release(): Promise<void> {
if (this.#releasePromise === undefined) {
throw new Error("Already released");
}
this.#resolve();
await this.#releasePromise;
this.#resolve = undefined;
this.#releasePromise = undefined;
}
}
...the release phase still feels off without a Promise, but maybe somebody else can tackle that :DEDIT: think I fixed it, untested though
- single page application using access and refresh tokens to interact with an API
- refresh is one time use, as recommended by the OAuth security best practices[1]
- SPA is open in more than one tab
- two tabs try to refresh the token at the same time, second one fails because refresh token is used up
0: https://bsky.app/profile/ambarvm.bsky.social/post/3lakznzipt...
1: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-secur...
Two tabs try to refresh the token at the same time, user opened tab 2, tab 2 can't refresh because things locked in tab 1 - user thinks app is broken? Isn't that the way it would happen. I guess you can detect it is locked in another tab though so you could give the user a warning about this?
I guess I am missing something about the scenario..
Let's play it out.
- User authenticates, gets an access token A1 and a refresh token R1. R1 is one time use only.
- A1 and R1 are stored as cookies (secure and httponly to prevent XSS).
- A1 is used to access multiple APIs, but is only good for 5 minutes.
- The SPA opens up a new tab for whatever reason, so the browser is making requests to the APIs from both tabs (T1 and T2)
- 5 minutes passes, and T1 tries to call an API. The API call fails because A1 is not valid. T1 then makes a request to the OAuth server with R1, which returns a new access token (A2), a new refresh token (R2) and invalidates R1.
- 0.1 seconds later, T2 tries to call an API with A1, and also discovers it needs to get a new access token. It tries to use R1, but R1 is invalid because it has already been used
With this locking API, T1 could lock access to R1 when it was about to use it. T2 would then see the lock request fail, and not try to use R1. After R2 has been stored, T2 could use R2 (or T2 could use A2).
The URL is the command line and all that.
But like you, I sometimes feel like an old person yelling at the cloud.
A lease has a time limit. A lock does not. Clearing stale locks manually is a PITA. I still assume, being a Web-scale contract, the lock would be automatically cleared if the browser is restarted or something. But honestly a lease makes users do better design from the get-go.
It looks like the only potential for a "stale lock" is if somehow the async function passed to the request method hangs forever. But in web contexts I think that would be extremely unlikely for everyday use cases (e.g. most of the time I could imagine the async callback making remote calls using fetch, but normally that fetch has its own timeout). In contexts where it could happen, I'd argue it's better to make the caller explicitly handle that case (e.g. by using `steal`) than potentially leave things in an indeterminate state because a lease timeout expired.
Never seen it before for locks, but I guess it's to deal with some bugs caused from some other code running from that origin.
Also, when you have a lot of tabs, I can easily see web page strangely broken.
Or at least made it easier. I think I introducing locks is a mistake for the browser.
Also, you don't need to use this API to lock up your web app.