{"id":20513871,"url":"https://github.com/webreflection/sabayon","last_synced_at":"2025-10-06T03:18:27.545Z","repository":{"id":246515781,"uuid":"821357640","full_name":"WebReflection/sabayon","owner":"WebReflection","description":"SharedArrayBuffer always on.","archived":false,"fork":false,"pushed_at":"2025-03-27T09:40:20.000Z","size":151,"stargazers_count":30,"open_issues_count":1,"forks_count":0,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-03-27T10:34:57.302Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/WebReflection.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-28T11:07:19.000Z","updated_at":"2025-03-27T09:40:23.000Z","dependencies_parsed_at":"2024-06-28T12:28:36.940Z","dependency_job_id":"19d07645-43f1-4647-86b7-e27fb48bcce9","html_url":"https://github.com/WebReflection/sabayon","commit_stats":null,"previous_names":["webreflection/sabayon"],"tags_count":40,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Fsabayon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Fsabayon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Fsabayon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Fsabayon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/WebReflection","download_url":"https://codeload.github.com/WebReflection/sabayon/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248799933,"owners_count":21163403,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-15T21:13:37.090Z","updated_at":"2025-10-06T03:18:27.515Z","avatar_url":"https://github.com/WebReflection.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sabayon 😋\n\n\u003csup\u003e**S**hared**A**rray**B**uffer **a**lwa**y**s **on** - how sweet!\u003c/sup\u003e\n\n\u003csup\u003e**Social Media Photo by [You Le](https://unsplash.com/@le_y0u) on [Unsplash](https://unsplash.com/)**\u003c/sup\u003e\n\n### 📢 A new Polyfill approach\n\n`sabayon/polyfill` and its `sabayon/polyfill-sw-listeners` plus `sabayon/polyfill-sw` just landed, with a whole new *polyfill* like approach for both `Atomics.wait` **synchronous** dance and `Atomics.waitAsync` **asynchronous** operation between a *Worker* and its *Main* thread.\n\n```js\n// Main thread\nimport sabayon from 'sabayon/polyfill';\n// provide the ServiceWorker file (+ optional RegistrationOptions)\nawait sabayon('/polyfill-sw.js'/*[, options]*/);\n// that's it 🥳\n\n\n// Worker thread\nimport sabayon from 'sabayon/polyfill';\nawait sabayon();\n// that's it 🥳\n```\n\nThe new approach is based on:\n\n  * global patches to provide a seamless experience with *SharedArrayBuffer* and *Atomics.wait* (Worker) plus *Atomics.notify* (Main) operations\n  * a uniquely randomized *BroadcastChannel* *API* to simplify even further the orchestration\n  * one module to rule both *main* and *worker* threads, allowing CDN cache to work best\n  * works out of the box with *MessageChannel* *API* too\n\nOne does not need to hook *sabayon* in its project anymore, just load it before any other module only when you think you'll need it and call it a day:\n\n```js\n// Main thread top module as OPTIONAL DEPENDENCY\nif (!crossOriginIsolated) {\n  const { default: sabayon } = await import('https://esm.run/sabayon/polyfill');\n  await sabayon('/sw-polyfill.js');\n}\n\n// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n\n// Worker thread top module as OPTIONAL DEPENDENCY\nif (!crossOriginIsolated) {\n  const events = [];\n  const push = events.push.bind(events);\n  addEventListener('message', push);\n  const { default: sabayon } = await import('https://esm.run/sabayon/polyfill');\n  // main sends the first message to the worker to resolve the promise\n  self.dispatchEvent(events.shift());\n  await sabayon();\n  removeEventListener('message', push);\n  // give the rest of the code an event tick to bootstrap\n  // so that possible added listeners won't be lost\n  setTimeout(() =\u003e {\n    for (const event of events)\n      self.dispatchEvent(event);\n  });\n}\n```\n\n\n#### When is the polifyll needed?\n\n  * when you don't want to use headers that are compatible with *Safari* and *iOS* too\n  * when you don't want to use special headers at all, not even via a *ServiceWorker*\n\nIn all other cases, you probably won't need it\n\n#### How to save the *ServiceWorker* locally?\n\n```sh\n# store the polyfill version in ./public/sw.js\nnpx sabayon --polyfill ./public/sw.js\n```\n\n- - -\n\n\n## Usage\n\nThese examples represent a dual-exchange between tha *Main* thread and the *Worker* one.\n\nTo see more examples, please check the test folder:\n\n  * [main async-waiting a worker](./test/wait-async/)\n  * [worker async-waiting the main](./test/async-wait/)\n  * [worker sync-waiting the main](./test/wait-int/)\n\nRemember to run a local server to test these modules, or check the **[live test page](https://webreflection.github.io/sabayon/test/)**.\n\n##### Worker\n\n```js\nimport {\n  Atomics,\n  BigInt64Array,\n  Int32Array,\n  SharedArrayBuffer,\n  addEventListener, // use this to add worker listeners\n  postMessage,      // use this to post worker messages\n  ignore,           // use this to bypass data parsing\n} from 'sabayon/worker';\n\n// intercept things to be notified\naddEventListener('message', event =\u003e {\n  const { handle, complex } = event.data;\n  handle[0] = 1;\n  // release the waiter\n  Atomics.notify(handle, 0);\n});\n\nconst sab = new SharedArrayBuffer(4);\nconst view = new Int32Array(sab);\n\npostMessage({\n  // will be automatically handled by sabayon\n  handle: view,\n  // this value is passed AS-IS\n  passThrough: ignore({ complex: \"data\" })\n});\n\n// use the async version out of the box\nAtomics.waitAsync(view, 0).value.then(result =\u003e {\n  // result === 'ok'\n  console.log('view changed', [...view]);\n});\n\n// or use the sync one ... please note:\n// sync wait requires serviceWorker option\n// when the worker is created on the main thread\nAtomics.wait(view, 0);\nconsole.log('view changed', [...view]);\n```\n\n##### Main\n\n```js\nimport {\n  Atomics,\n  BigInt64Array,\n  Int32Array,\n  SharedArrayBuffer,\n  Worker,\n  ignore,           // use this to bypass data parsing\n} from 'sabayon/main';\n\nconst w = new Worker('./worker.js', {\n  type: 'module',\n  // optional ServiceWorker to enable sync wait\n  // on the Worker side of affairs\n  serviceWorker: './sw.js',\n});\n\nw.addEventListener('message', event =\u003e {\n  const { handle, complex } = event.data;\n  handle[0] = 1;\n  // release the waiter sync or async\n  Atomics.notify(handle, 0);\n});\n\nconst sab = new SharedArrayBuffer(4);\nconst view = new Int32Array(sab);\n\npostMessage({\n  // will be automatically handled by sabayon\n  handle: view,\n  // this value is passed AS-IS\n  passThrough: ignore({ complex: \"data\" })\n});\n\n// on main thread, only waitAsync is possible by standard\nAtomics.waitAsync(view, 0).value.then(result =\u003e {\n  // result === 'ok'\n  console.log('view changed', [...view]);\n});\n```\n\n##### Service Worker\n\nThis module comes with a basic, yet handy, *CLI* utility that saves `sabayon/sw` export into a path.\n\n```sh\nnpx sabayon ./public/sw.js\n```\n\n**Please note** that if you have your own Service Worker logic already in place, you can use the `sabayon/sw-listeners` export to simply augment your file:\n\n```js\nimport { activate, fetch, message } from 'sabayon/sw-listeners';\n\n// Way No #1\n// you can either add these listeners first\n// as `event.stopImmediatePropagation()` is used\n// whenever the `event` is handled\naddEventListener('fetch', fetch);\naddEventListener('message', message);\n\n// Way No #2\n// alternatively, you can use those callbacks\n// and eventually do nothing if preventDefault()\n// was called via that `event`\naddEventListener('fetch', event =\u003e {\n  fetch(event);\n  if (event.defaultPrevented) return;\n  // your previously implemented logic\n});\n\naddEventListener('message', event =\u003e {\n  message(event);\n  if (event.defaultPrevented) return;\n  // your previously implemented logic\n});\n```\n\nIt is still important to add at least both `fetch` and `message` listeners, while `activate` ensures that `event.waitUntil(clients.claim())` is invoked so that workers can bootstrap right after when the `serviceWorker` option is passed along.\n\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eWhy is this needed?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv markdown=1\u003e\n\nBoth [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) and some [Atomics](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics) operations require special headers to work out of the box.\n\nThis has been an endless source of pain for various projects, where the suggested solutions can be summarized as such:\n\n  * there is no way around the fact to enable both technologies one needs [special headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) to have native functionality and performance and this is still the preferred way to use these APIs\n  * a *ServiceWorker* based workaround, such as [mini-coi](https://github.com/WebReflection/mini-coi#readme), could be used to automatically enable, whenever it's possible, those headers where it's not possible to change these otherwise (like on *GitHub pages* or other similar hosts)\n    * ... and yet, even using *mini-coi* might create friction for edge cases where embedding *YouTube* content or other 3rd party domains might not like augmented headers for their services\n  * there is no *polyfill* for any of these primitives, one that can actually be used as \"*drop-in*\" replacement for all the globals that surround this part of the Web (*SharedArrayBuffer*, *Int32Array*, *Atomics.wait*, *Atomics.waitAsync* and *Atomics.notify*)\n\nThis **project goal** is to **enable all of that**, like a polyfill would do, without needing to patch anything at the global context level 🎉\n\nP.S. this module also enables out of the box *Firefox* missing `notifyAsync` via its own logic.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eHow does this work?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv markdown=1\u003e\n\nUsing a minimal runtime feature detection, such as:\n\n```js\ntry {\n  new SharedArrayBuffer(4);\n}\ncatch (polyfillRequired) {\n  // the polyfill\n}\n```\n\nIt is possible to detect when the current page is capable of using native features and simply export these without affecting at all performance or standard behavior.\n\nWhen such constructor does not exist or it fails at allocating anything more than `0` bytes though, a workaround is orchestrated in both the *main* thread and each *worker* created through such *main*, and here is how.\n\nIf we remove the *Shared* prefix, it's all about *ArrayBuffer*, and that's indeed how *SAB* class is created:\n\n```js\nSharedArrayBuffer = class extends ArrayBuffer {}\n```\n\nOnce that's done, the only wrappers able to deal with that kind of buffer are *Int32Array* and *BigInt64Array*.\n\n```js\nconst extend = (Class, SharedArrayBuffer) =\u003e class extends Class {\n  constructor(value, ...rest) {\n    super(value, ...rest);\n    if (value instanceof SharedArrayBuffer) {\n      // logic to track / recognize these wrappers\n      // when postMessage are used to send data\n      // and \"message\" listeners intercept such data\n    }\n  }\n};\n\nBigInt64Array = extend(BigInt64Array, SharedArrayBuffer);\nInt32Array = extend(Int32Array, SharedArrayBuffer);\n```\n\nThere is a little known, yet wonderful, API that is [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone). Its functionality is used in various APIs such as *IndexedDB* and *postMessage*.\n\nWhat makes it special and useful for these scenarios is its ability to deal with recursion, which in turns means it's able to send the same reference over the wire only once, still preserving the identity at the receiver side of affairs:\n\n```js\nconst complexData = { huge: \"payload\" };\n\n// Worker\n// this will send complexData same reference at\n// both index 0 and index 1 .data\npostMessage([complexData, { data: complexData }]);\n\n// Main\nworker.addEventListener('message', event =\u003e {\n  const [complexData, obj] = event.data;\n  // true - no assertion failed\n  console.assert(complexData === obj.data);\n});\n```\n\nConnecting the dots, so far we have a way to recognize and track *views* that are meant to be posted and received around plus a way to intercept such *views* on the other side, using a basic *CHANNEL* based protocol, nothing really too different from the way *MQTT* works.\n\n```js\n// main page - ensure a unique channel per page/tab\nconst CHANNEL = crypto.randomUUID();\n\n// when post message is used and there are views to send\npostMessage([CHANNEL, action, views, data])\n\n// views will be a Set of views that is also contained in data\n```\n\nOn the other side, when these kind of `message` are received, all *views* are temporarily stored so that any *Atomics* operation that would like to `wait`, `waitAsync` or `notify` these *views*, the logic knows these have a unique identifier themselves (that is just a forever increasing `i++`) so that such *view* knows that it should be updated on the other side and *release the lock* after some cleanup.\n\nWhile this orchestration seems reasonable enough, *Atomics.wait* is a blocking operation that must pause the *worker* until that *view* has been notified at some index on the other side (the *main* thread).\n\nTo provide this pause/blocking mechanism we need a way to block the current *worker* until such *view* has been notified ... but we need something not blocked behind the scene to make this happen 🤔\n\nAbusing *XMLHttpRequest* in *sync* mode it is then, so we can *POST* a message with enough details that will produce a pending *Response* until such details are forwarded to any page or tab that is registered and that recognize the unique *CHANNEL*, to then wait for that page to tell us back the *view* has been notified, by sending the *view* content that is then returned as *JSON* response, so that the *worker* can access the `xhr.responseText`, *parse* that array, update the *view* that was waiting to be notified, and finally get out of the `Atomics.wait(view, index)` operation in a 100% *synchronous* fashion that moved asynchronously that Service Worker and one *main* thread in the meanwhile.\n\n**As summary**\n\n  * there is a unique (per page/tab) communication *CHANNEL* that is both sent and intercepted on `message` events, able to orchestrate via *actions* all needed operations on any side of affair\n  * there is a mechanism to automatically crawl and track *views* that contain the semi-fake *SharedArrayBuffer* but one can also opt-out via an `ignore` utility\n  * there is an asynchronous communication for `Atomics.waitAsync` that just works by updating and awaiting back and forward those *views*\n  * there is an optional *Service Worker* where data is posted that can orchestrate blocking-like operations on the *worker* side, when `Atomics.wait` is used instead of *waitAsync*\n\n... and that's pretty much it.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n#### Performance\n\nMeasured \"*on my machine*\", these are results passing along a `{ some: 'value', view }` object where the `view` is a `new Int32Array(new SharedArrayBuffer(4))` base ref.\n\n**Native**\n\n  * **waitAsync** from *Main* - use a *Worker* to `notify(...)`: 11ms\n  * **waitAsync** from a *Worker* - use *Main* to `notify(...)`: 0.4ms\n  * **wait** *sync* from a *Worker* - use *Main* to `notify(...)`: 0.3ms\n\n**Polyfill**\n\n  * **waitAsync** from *Main* - use a *Worker* to `notify(...)`: 12ms \u003csub\u003e\u003csup\u003e(about the same)\u003c/sup\u003e\u003c/sub\u003e\n  * **waitAsync** from a *Worker* - use *Main* to `notify(...)`: 0.8ms \u003csub\u003e\u003csup\u003e(~2x slower)\u003c/sup\u003e\u003c/sub\u003e\n  * **wait** *sync* from a *Worker* - use *Main* to `notify(...)`: 2ms \u003csub\u003e\u003csup\u003e(~7x slower due *ServiceWorker ↔ Main* roundtrip taking 90% of the time)\u003c/sup\u003e\u003c/sub\u003e\n\n**Note** that due lack of real *SharedArrayBuffer* primitive the memory consumption can be temporarily duplicated on both *Main* and *Workers* but fear not, no leaks happen so this should never be a real-world issue.\n\n#### Caveats\n\n  * the optional **Service Worker**, if *sync* `Atomics.wait(...)` is desired, *MUST* be a local file. It cannot be downloaded as module, even if `sabayon/sw` export exists.\n  * **no interrupts** (timeout handlers) possible when in emulation mode. These are complex to implement via *ServiceWorker* and quite possibly not super common out there. If proper headers are used though, everything would work natively without any issue whatsoever.\n  * **notify(view, index)** are currently the only supported arguments when running in emulation mode. This is due the inability to make sense of a `count` argument and due the fact *interrupts* don't work so that a `delay` makes little sense. You can still pass these values if you like but in emulation these will be ignored.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebreflection%2Fsabayon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwebreflection%2Fsabayon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwebreflection%2Fsabayon/lists"}