Javascript, locking and sound, brought to you by the letters W, T and F

The Javascript party line is that you don't need locking, because no threads, no safety, no problem: every window or tab has its own Javascript context, and that context is guaranteed to behave as if it is single-threaded: sequential execution, timers fire on return to the event loop, processes cannot communicate with each other.

Except then they added localStorage. Oh, what's this? Is it shared memory that can be accessed asynchronously by multiple contexts? Why yes it is! So now you really do need locking. Except then they didn't add mutexes, or even an atomic-read-and-increment operator. Hooray. And they also didn't add any IPC, like a socket that one process can write to and that others can select() on. Hooray.

So let's say you've got an app, it spans multiple documents, and you want a sound to play in reaction to some external, asynchronous event if any of your documents are open. But if three windows are open, you only want that sound to play once per event. The obvious way to do this is to nominate one of those documents as the leader, and pass the conch to someone else if the leader document goes away.

Well, good luck with that, since you only have a shared whiteboard, not real IPC. This means that each of your contexts needs to constantly poll the whiteboard, doing something like, "Has it been too long since the leader has updated their heartbeat timestamp? If so, now I must race with my brethren to try and acquire the lock and become the leader." So aside from that being stupid -- all those timers, all that polling -- it's also inefficient, because it means that there's always an interregnum during which there's no leader (your polling interval). Try to burn fewer cycles by increasing your polling interval and you increase the window during which your asynchronous notifications can't happen.

What kind of Mickey Mouse operation is this? There was better multitasking on MacOS 6.

Oh, and speaking of sound: on iOS, you flat-out cannot play sound unless somewhere higher up on the call stack is a touch gesture. Asynchronous sounds can't be done using the HTML5 audio tag, because... Apple are dicks, I guess? I'm gonna go with that. So apparently there's a newer and completely separate audio interface with a way more convoluted API, and maybe that one works on iOS? But I haven't investigated, having at that point run out of fucks due to the fact that I was starting to have ALSA versus OSS flashbacks and expecting it to tell me my EMU10K1 PCM setting was wrong.

Previously, previously, previously.

Tags: , , , ,

54 Responses:

  1. Whenever I spend time delving into JavaScript, I come to the conclusion that a committee took a list of "worst coding practices for C", and designed a language to accommodate, if not require everything on that list.

    • James says:

      Give them a break, we only got a read method for readable streams two years ago. Some things take time.

  2. Jon says:

    This is what SharedWorker was supposed to be for, not localstorage hacks. Except Microsoft and Apple have refused to implement SharedWorker. Well, Apple implemented it in Safari 5, but then removed it in 6.1, I guess because they hate web developers and like to fuck with us?

    • foljs says:

      Or, you know because of the reason they stated: "Shared workers can not communicate across processes currently, so they should better be disabled.".

      Last time I checked, neither localStorage nor the new sound APIs were necessary for Web Development...

      • Anonnymoose says:

        I don't see the quote you mention. I do see:

        > Note: In Firefox, shared workers cannot be shared between private (i.e. browsing in a private window) and non-private documents...

        Am I missing the source of your quote, or were you mistaken?

      • Aaron says:

        Which is a cute way of saying proper IPC for SharedWorker would be too hard?

        Look, if you want to be taken seriously as a real application platform, you need to do the things real application platforms do.

      • Pavel Lishin says:

        Well, shit, by that reckoning, sound isn't required for computing at all! So I guess we should just count ourselves lucky that these dang ol' fabulistic future machines can output beeps and boops at all!

      • Jon says:

        So you're saying they implemented a feature in a way that was fundamentally broken, shipped it, then when they realised, decided just to remove it entirely instead of fixing it?

        I'm really not seeing how this is evidence that they don't hate web developers or like fucking with us.

  3. Wotsac says:

    On one hand, Apple are totally dicks. On the other, Apple's insistence on defanging any bit of JavaScript that they believe to have antisocial consequences has paid off. Since I can readily imagine the antisocial consequences of having sound effects pop on any sort of event, I can't entirely blame them, even as I want to grab someone by the shoulders and shout: let me do this you utter fucking dicks.

    • jwz says:

      I see no sensible way that the "protect me from people who write bad web pages" philosophy, even if you agree with it, should apply to mobile but should not apply to desktop. Having the two work differently is crazy.

      • Wotsac says:

        there are two reasons that Apple does things different on mobile, and either may apply here:
        -The Safari team tends to be paranoid about battery impact.
        -The iOS user base is assumed to be different, and not as sophisticated as the OSX user base. Not to mention that the tools system aren't as sophisticated (to the extent they exist at all)

        Not that you would bother, but Don Melton (former Safari high poobah) has done a number of episodes of the Debug podcast that offer wide ranging insight into how Apple thinks. He tends to noisily dispute critics before conceding many of their arguments.

    • Joe Shelby says:

      This totally conflicts with OSX's and IOS's practice of immediately starting to play music (including starting a non-running iTunes process) as soon as bluetooth has connected my headset.

  4. John says:

    Just thinking out loud here. Maybe writing an advanced audio application on top of a document parser & viewer (the web browser) is the wrong way to go about things. Maybe there's a reason javascript doesn't have this stuff? Maybe it's the wrong tool for this job...

    • jwz says:

      Well, first and most obviously, you're just wrong. But even if you weren't, then these features shouldn't exist at all, instead of only existing half way.

  5. Nek says:

    There is a also a global limit on the number of AudioContexts you can instantiate to play sounds using Web Audio API. You can't query for the number of already opened contexts neither you can control them in any way. Your sound related code silently (yep no errors thrown) stops working when some other pages have used that limit already.

    I'm talking about desktop Google Chrome but I'm sure the other browsers are not better in this regard 'cause the API's draft has no solution for this problem.

    • db48x says:

      Oh, and once you've created one of these audio contexts you can't get rid of it (short of closing the tab/window.)

      One of my projects (emularity.com) creates three of them at a time. (It needs to know the sample rate in order to generate an argument list to give to an emulated dosbox...)

  6. is postMessage not a sufficient IPC channel for this usecase?

    • jwz says:

      I think that only allows you to communicate with windows that you have explicitly created with window.open, not with arbitrary other tabs that happen to also be running your code.

      • d'oh. that's right.

        there's a hacker news thread with an interesting suggestion:
        when you play a sound, write to localStorage that the sound has played, and check before you play the sound that it hasn't already played. It then doesn't matter which one plays the sound (it's whichever gets to it first), it still gets played only once. I suspect you might have already tried this so… ducks

        also, webaudio api doesn't need a tap event. (for better or worse)

        I have a feeling you might have already tried this

        • it's also worth noting that current "best practice" is don't use localStorage. the browsers/standards people have come to their senses and realised that localStorage is broken (For some of the reasons you mention here) and invented "indexedDB" to replace it, with better asynchronous/transactional support. Apple has so far refused to implement it, but there's a polyfill called localForage that builds an asynchonous transactional version of localStorage on top of indexed DB (where that exists) and on webSQL (sqlite for javascript, which was apple's hot solution) where that exists.

          So, you can use localForage to kind of patch around the worst aspects of localStorage if you want.

          • (In fact, I've read some rumors that claim the fact that apple lost that argument as a leading reason for their current almost complete neglect of safari and web standards)

        • Joe Shelby says:

          "write to localStorage" - isn't that the heart of this problem, that localStorage isn't thread-safe...

        • bobo the hobo says:

          if "play a sound, write to localStorage" is a single atomic operation, then yes there might be an asinine kludge which could be built up into a barely serviceable workaround to a glaring design flaw.

          I have a feeling you might know whether that's true.

        • pcmt says:

          It doesn't work because localStorage doesn't have a CAS-like operation, you need some way to check the variable "sound played" is false and mark it as true, as a single atomic operation.

          • localStorage hasn't got that. but indexedDB does.
            what's happening here is jwz is using a deprecated api with known flaws, I've pointed out the replacement API that was specifically designed to correct those flaws (localForage), and somehow the response I'm getting is "localStorage has [known flaw]". yep, I'm getting that.

      • James says:

        What happens if you pretend a not-really-atomic test-and-set of localStorage is atomic for the purposes of pretending to have a mutex?

        • jwz says:

          Well sure, you can do a write, then do a read and see if you're the winner; plus store timestamps and decide how old is too old. There are ways. It's just fucking stupid.

          • James says:

            30 years from now, when Euramerica is trying to figure out why Oceanasia has all the credits, it's probably this.

  7. Neil Jenkins says:

    Yeh, we had to deal with this dance for very similar reasons. We used a mixture of random timers and polling to elect and maintain a "master" tab. We wrote this up on our blog here: http://blog.fastmail.com/2012/11/26/inter-tab-communication-using-local-storage/. Shame it's now 3 years later and still no better way to do it.

  8. DC says:

    Soundcloud does a good job of making sure only one tab is playing at a time. If you start a new song in new tab, the old one pauses.

    Looks like they are in fact using localStorage polling. Here's what that looks like with two tabs, one paused and one playing:


    > localStorage
    ...
    V2::local::inst_1437870496452: "{"playing":true}"
    V2::local::inst_1437874265670: "{"playing":false,"pong":"9529"}"

  9. Adam Goode says:

    Eventually, ServiceWorkers and navigator.connect should work for this.

    ServiceWorkers are in a few browsers, navigator.connect not at all yet.

    ServiceWorkers alone might do what you need.

  10. Logan Bowers says:

    So it turns out in that in IE (at least 8 and 9), JavaScript code is preemptible in an unfortunate circumstance:
    - You postMessage() to another window
    - That window immediately does a postMessage() back to you in its event handler
    - Your event handler for the received message will run before your postMessage() call returns

    So your postMessage() handler—and all code it could possibly call—must be reentrant. Figured that out the hard way. In production.

  11. jwz says:

    Hmm, well it seems that the window.storage event is useful for reducing latency: if you release your lock at window.unload, then every other document gets a notification that they can use to immediately wake up and race for the lock again, instead of waiting for their timer to fire. It's possible that the timers aren't necessary on non-leaders, but I'm not sure about that.

  12. db48x says:

    This type of brokenness is not exactly unique to javascript. A number of years ago I worked (indirectly) with a BHO for IE that communicated with other instances of itself (running in different IE windows or whatever) by writing data to the registry in exactly the same fashion.

  13. tobias says:

    If a lot of people are correct, instead of www, javascript should be filed under grim meathook future.

  14. Tom Boutell says:

    If there is a server in the picture, you could use a synchronous XMLHttpRequest to tell the server you're gone, and the server could notify the new lead window via a long-lasting websockets connection to eliminate the polling. sockets.io could be used as a wrapper around varying degrees of websockets support.

  15. Tom Boutell says:

    (The synchronous XMLHttpRequest would need to be fired by an unload handler. I find that if you stick to synchronous requests and don't dilly-dally the browser will allow them to fire before it kills your code.)

  16. John Adams says:

    I am surprised no one here has mentioned using socket.io as a backend coordinator for the playback of the sounds.

    1) Stand up a socket.io server, clients connect and wait for a push message. This eliminates the polling problem.
    2) When an action happens, the server-side script tells the socket.io server that it needs to ping the clients.
    3) Clients, on a per-logged-in username+ip pair key only get one ping from the socket.io server and thus, only one window can make sound at a time when the message is reflected out from the socket.io server.
    4) Clients can continue to background poll (or socket.io can push a 'you are the master message') to acquire a connection to the socket.io server should the 'master' client window get closed or dies.

    Alternately, you could allow all clients to connect but let the server arbitrate who is allowed to get the message.

    This is pretty much how presence notification works in many online chat systems. I use something similar for real-time updates on troupeIT right now and it's been great so far.

    Although, I'm guessing that localStorage will probably be a better solution for you because it doesn't involve standing up a new server and learning to work with an unknown system.

  17. Not Frank says:

    This is the closest thing to a Lazyweb message you've posted where I have personal experience, and my experience is... yes, it's awful. To get around some, but not all, of the insanity on iOS I'm currently playing with Phonegap, which is basically an app wrapping a local copy of the webpage.

    And then, even with all the background sound stuff enabled, it still currently sputters if I do something like, say, look at Maps.

  18. jwz says:

    Oh, here's another fun kink. Two windows. One is on Domain A. The other is on domain B, embeds an iFrame with the same URL as Window 1:

    • Window 1: URL-1 on Domain A
    • Window 2: URL-2 on Domain B
        • iFrame: URL-1 on Domain A

    In Safari, those two copies of URL-1 get different sandboxed localStorage, despite being on the same document.domain and protocol. They should count as same-origin but they don't. Apparently setting "Safari / Preferences / Privacy / Block cookies and other website data = Never" allows them to share localStorage.

    But here's the crazy part: this affects only localStorage, not cookies. In my example, URL-1 cannot be loaded without access to a login cookie, and it's loading fine inside that cross-domain iFrame, so they are both being considered same-origin with respect to cookies -- just not with respect to localStorage.

    Hooray.

    • Chris Brent says:

      Well that's simplet then. Just use cookies to sync your two localStorage sandboxes across the two windows.

      • jwz says:

        See, what I said was, "Here's yet another completely stupid, nonsensical thing."

        But then you -- like most of my blog commenters -- chose to interpret that as, "I'm too dumb to think of a workaround for this idiotic bug that shouldn't exist in the first place."

        I need a name for this affliction. Maybe we'll call it "That's Obvious-itis."

  19. LionsPhil says:

    If you get as far as playing audio, it looks like you can look forward to such delightful API as canPlayType(). In case MDN break that link, here is its documented return value:

    probably: if the specified type appears to be playable.
    maybe: if it's impossible to tell whether the type is playable without playing it.
    The empty string: if the specified type definitely cannot be played.

    I think we need a new name for this type. Perhaps a NoncommittalBoolean. It can go and be friends with type-coalescing equality operators.

  20. Buddy Casino says:

    I don't frivolously hand out slowclaps, but jesus, to whoever designed that API: you're doing God's work, son.