Web Locks API

(developer.mozilla.org)

213 points | by mooreds 3 days ago ago

103 comments

  • jauntywundrkind 3 days ago

    Not that it's super important, but Web Locks API is getting some circulation after a question about how you keep multiple pages from trying to use a (single-use only) oauth refresh token at the same time. Which is a pretty good use case for this feature! https://bsky.app/profile/ambarvm.bsky.social/post/3lakznzipt...

  • unilynx 3 days ago

    > navigator.locks.request("my_resource", async (lock) => {

    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...)
    • yoavm 3 days ago

      It doesn't actually feel more readable to me. I find the idea that the lock declaration sits at the same level as the lock content confusing.

      • zeroxfe 3 days ago

        It's readable if you're familiar with the RAII pattern which is used in languages like C++ and Go.

        • viraptor 3 days ago

          It's also similar to C#'s "using ...;" without a block. Syntax sugar there rather than RAII, but looks the same.

          • zigzag312 3 days ago

            C#'s using can be used without a block. It disposes the resource at the end of a current scope.

        • saagarjha 3 days ago

          I always felt that pattern was a bit too clever than it was a good design.

          • karmarepellent 3 days ago

            Makes me wonder what part of the pattern you think is "too clever"? I think it is fairly easy to reason about when the lock is restricted to the encompassing block and automatically dropped when you leave the block.

            • saagarjha 3 days ago

              It’s kind of a weird design that some of your variables (which you can define anywhere in the scope FWIW) just randomly define a critical section. I strongly prefer languages that do a

                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.
        • Yoric 3 days ago

          Well, Go doesn't quite support RAII.

          This syntax looks more like Python or Rust.

        • wbl 3 days ago

          It's not RAII, but closer to dynamic-wind.

      • nesarkvechnep 3 days ago

        Yes, it sits at the same level to signify the lifetime of the lock.

    • chrisfosterelli 3 days ago

      Why can't we just `await` the lock call?

      Edit: Nevermind, release of the lock is automatic when the callback resolves its promise. I get it now.

      • KTibow 3 days ago

        If you really wanted it to be at the top level you could probably turn it into an explicit `release` call using a wrapper and two `Promise` constructors, though that would probably be a bad idea since it could introduce bugs

    • creativenolo 3 days ago

      Subjectiveness aside on what’s more readable… your proposing a new language feature would be more readable than an API design. To me, the MDN proposal is declarative whereas your proposal is imperative. And with my subjectiveness, JavaScript shines in declarative programming.

    • mpweiher 3 days ago

         navigator whileLocked:'my_resource' do:{ :protected |
            protected doSomething.
            protected doOtherThing.
         }
    • sureIy 3 days ago

      You can probably wrap it to have that API

      • n2d4 3 days ago

        Sadly it needs a language feature that doesn't exist yet

        • sureIy 2 days ago

          What do you mean? You can definitely already wrap that into a Symbol.dispose-keyed object and use it via `using`

          • n2d4 a day ago

            `using` is not a thing in JS

        • wruza 3 days ago

          s/yet/thankfully/. We don’t need it cause it solves no real problem and the solution is loaded with implicit complexity.

          • unilynx 3 days ago

            'using' fixes having to do cleanup in a finally {} handler, which you can a) forget, and b) often messes up scoping of variables as you need to move them outside the 'try' block.

            • wruza 3 days ago

              It also creates additional finalization semantics for a using-ed value. Which has all sorts of implications. lock.do(async (lock) => {work}) is a similar construct - a scope-limited aquisition that requires nothing special.

              Catch/finally not sharing a scope with try is a repeated mistake, that I surely agree with!

    • pwdisswordfishz 3 days ago

      If it only weren’t for that crippled "using" syntax. On the other hand, though:

          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.
  • simonw 3 days ago

    I wish the compatibility tables on MDN gave an indicator of when a feature became available.

    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.

  • remram 3 days ago

    Why only in secure contexts? You can use storage APIs in insecure contexts, doing this by spinning, but the lock API which seems much more innocuous requires a secure context?

    • chrismorgan 3 days ago

      I believe most new capabilities are limited to secure contexts, in part as a way of discouraging bad habits, even when there’s no particular risk. The ideal is that everything should run in a secure context, but it’s a hard sell removing existing functionality from insecure contexts. If it were being added now, I’m pretty sure storage APIs would be secure-only.

      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.

    • berkes 3 days ago

      I'd presume that a MITM could set, release and read locks, by which it might determine that you have certain sites open.

      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.

      • eddd-ddde a day ago

        Or also just lock resources and stop other pages from working.

    • endgame 3 days ago

      If certain lock names become well-known, maybe you could DoS browsers by holding random locks and never releasing them?

      • Spivak 3 days ago

        I guess but since they're origin-bound can you do anything except DoS your page?

  • nitwit005 3 days ago

    How does a shared memory space work if you have different versions of scripts for the same domain?

    • horsawlarway 3 days ago

      Same way a shared database works across multiple client versions as you roll out a new deployment.

      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.

    • owaislone 3 days ago

      Why would that matter? The tabs don't share memory. Any code doesn't run when it tries to acquire a lot that another piece of code from another tab has already acquired. The two tabs don't even need to run the same app.

      • mmazing 3 days ago

        Well, it might matter for functionality in the application.

        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?

        • geocar 3 days ago

          What are you imagining doing with these web locks apis?

      • nitwit005 3 days ago

        The need to test both versions being active.

  • aabhay 3 days ago

    I’ve been using this for a while now. But one thing I recently worked on required these locks to be extremely efficient. Does anyone have any benchmarks on usage of these locks? Preferably compared to the use of rust’s tokio mutex from a wasm context.

    • jitl 3 days ago

      You should write your own benchmarks! I've been using mitata for microbenchmarks which is what the bun and deno people use for their cool benchmark charts. It's fast and tries to call the system GC between runs which helps reduce bias. github: https://github.com/evanwashere/mitata

      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

    • orf 3 days ago

      If you’ve been using them for a while, don’t you have any benchmarks?

      • afavour 3 days ago

        Don’t see why you’d assume that. Not all applications are time critical.

        • orf 3 days ago

          I’d assume that if you have used them for a while, you have a rough understanding of the performance characteristics and/or the knowledge of how to write even a quick, rough benchmark for them.

  • tantalor 3 days ago

    Weird API to release the lock. What if you want to hold on to it? Then you need to do some silly promise wrapper. Would be better if there was a matching release() function.

    • jauntywundrkind 3 days ago

      The ergonomics here for 99.999% of uses seem great to me. Whatever async function you have can run for as long as it needs. That's using the language, not adding more userland craft. It's a good move.

    • rockwotj 3 days ago

      Probably because there is no RAII semantics in JS and they don’t want to allow forgetting releasing the lock. Although the promise workaround is explicitly opting into this behavior

      • slimsag 3 days ago

        Javascript in browsers already has a full atomics API:

        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?

        • pavlov 3 days ago

          The Web Locks documentation explains that it works across tabs, i.e. separate processes:

          ”[…] 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.

        • whilenot-dev 3 days ago

          You mean Atomics + SharedArrayBuffer, otherwise it won't be shared across agents. I can imagine all the postMessage calls and message handlers swirling around in all agents to approximate something like the Web Locks API for a simple lock, but tbh I'd take the Web Locks API any day.

        • mrkeen 3 days ago

          It's "atomic" in the sense that it does one thing. It lets you do "actions", not "transactions". A transaction would allow you to do multiple things.

          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.

    • wruza 3 days ago

        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.
    • maxmcd 3 days ago

        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();
          }
        }
      • whilenot-dev 3 days ago

        You're not treating the aquire phase correctly:

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

        EDIT: think I fixed it, untested though

  • cammil 3 days ago

    I assume this is at the browser "session" level? In another browser, or private-session, would the locks be distinct?

  • gabrieledarrigo 3 days ago

    Ciao guys, what could be the use case for such API?

    • mooreds 3 days ago

      This was the use case[0] that brought it to my attention:

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

      • taosx 3 days ago

        Thanks for this, I've been using the localstorage for the same use case a while ago, I'll try to go back and update it.

      • bryanrasmussen 3 days ago

        I guess I don't actually get the point, because if I am locking a resource in one tab so that other tabs can't use that resource... how is that not going to lead to behavior that a user would think was broken.

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

        • mooreds 2 days ago

          It's not locking the UI refresh, it is only locking the access token refresh.

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

          • bryanrasmussen 2 days ago

            thanks, this was the part that clarified for me "After R2 has been stored, T2 could use R2 (or T2 could use A2)."

      • Voultapher 3 days ago

        Maybe it's old person yelling at cloud vibes, but I'm already annoyed enough at the majority of SPAs for breaking so many useful default browser features, such as stable links, navigation, multi-tab functionality, open in new tab etc. This is only gonna make them even more cancer my gut feeling tells me.

        • mooreds 2 days ago

          Oh yeah, I agree. I personally like the rails approach of light JS for necessary interaction, but keeping all logic serverside.

          The URL is the command line and all that.

          But like you, I sometimes feel like an old person yelling at the cloud.

    • 1oooqooq 3 days ago

      if a crypto miner infects a site you have several tabs open they won't fight for cpu at the same time.

    • loevborg 3 days ago

      Besides what sibling has written, write access to IndexedDB also often needs to be guarded by a mutex https://gist.github.com/pesterhazy/4de96193af89a6dd5ce682ce2...

      • CGamesPlay 3 days ago

        I don't get this complaint, because IndexedDB supports transactions. Why are these insufficient?

    • nchmy 3 days ago

      share a websocket/sse connection via a worker

    • sureIy 3 days ago

      Click the link, read the words. There's a list in it.

  • bitterblotter 2 days ago

    I've actually used this once! I built a slightly overengineered HTTP client where it would use a Reader Writer lock on the auth token. That way, if a token refresh request was taking place, all new requests would wait for it to finish writing, before being sent

  • SigmundA 3 days ago

    The only reason I know and have used this api is because it helps prevent the tab from going to sleep if you hold on to the lock: https://techcommunity.microsoft.com/discussions/edgeinsidera...

  • greatgib 2 days ago

    I'm not sure that this API is a good thing. That is already a pain in the ass when webapp are only working in a single tab and kind of log you out of other tabs.

    Also, when you have a lot of tabs, I can easily see web page strangely broken.

  • rdsubhas 3 days ago

    A lease is usually a better choice than a lock.

    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.

    • hn_throwaway_99 3 days ago

      I might agree in other contexts, but not with the use case here and how the API is designed.

      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.

    • sweetjuly 3 days ago

      There is no real safe way to use lock hold timeouts. While a waiter can timeout and possibly handle failing to acquire the lock, there's no generic safe way to steal the lock from the holder after a timeout since the holder may still be accessing the protected resources/have left the resources in an inconsistent state. Adding a wait timeout which generates telemetry on a long wait may be useful for helping catch failures in production, but seizing the lock is almost always the wrong way to go about this.

    • naasking 3 days ago

      Time limits are a recipe for non-determinism. Non-determinism is generally not what you want.

      • soulofmischief 3 days ago

        Put another way, combining side effects and timers invariably causes race conditions.

    • mooreds 3 days ago

      Is there a native browser API for leases?

  • eyeris 3 days ago

    Where did the steal method come from? Haven’t done much locking, but I haven’t ever seen lock stealing before

    • 0x457 3 days ago

      "steal" referres to "acquire unsafely"

      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.

  • anon291 3 days ago

    Sometimes I wonder why we even bothered with javascript and didn't just use POSIX.

  • IshKebab 3 days ago

    Using global names for this seems like a bad idea.

    • sureIy 3 days ago

      What's the problem? The whole concept is that it's locking a resource super-globally, not only in the current tab, but across tabs.

      • IshKebab 3 days ago

        Ah I misread. The intro made it sound like this was for locking within a tab, but it's within a origin.

    • RedShift1 3 days ago

      How many layers of namespacing do you want in between? 1? 2? 10? Perhaps we should go the SNMP route and start with 1.3.6.1.4.1?

    • koolba 3 days ago

      Using global for bad ideas is kind of par for the course for all things JavaScript.

  • shashashasha___ 3 days ago

    a lock API in single threaded js vm. is a var true/false really too hard these days that we need an API?

  • 3 days ago
    [deleted]
  • yazzku 3 days ago

    Expect deadlocks in web applications now? I wouldn't necessarily trust a JS programmer with a lock, sorry. They are hard enough in C++ or other languages that generally require a lot more discipline.

    • vendiddy 3 days ago

      I wish they adopted more of an actor model to emulate concurrency.

      Or at least made it easier. I think I introducing locks is a mistake for the browser.

    • ukuina 3 days ago

      > deadlocks only affect the locks themselves and code depending on them; the browser, other tabs, and other script in the page is not affected.

      Also, you don't need to use this API to lock up your web app.

      • yazzku 3 days ago

        I have a hard time picturing how an application can be considered anything other than completely broken once a couple threads/workers have deadlocked, so I don't know what any of that quote means. Yeah, I get that browsers isolate tabs and that the damage is contained.

        • CGamesPlay 3 days ago

          You seem to be expecting these locks to block a thread, but they do not. A "deadlock" with these locks is simply a chunk of heap space holding a bunch of promises that will never resolve, occupying a few slots in the global event loop's select statement.

          • 0x457 3 days ago

            Well yes, but some part of code that supposed to run, never runs because of a deadlock.