Skip to content

Conversation

@skirsten
Copy link
Contributor

@skirsten skirsten commented Aug 26, 2025

On Next.js there is still some problems with the transformers library being loaded as node module somehow...

 ⚠ ./node_modules/.pnpm/@[email protected]/node_modules/@huggingface/transformers/dist/transformers.node.mjs
Critical dependency: Accessing import.meta directly is unsupported (only property access or destructuring is supported)

Import trace for requested module:
./node_modules/.pnpm/@[email protected]/node_modules/@huggingface/transformers/dist/transformers.node.mjs
./node_modules/.pnpm/@kixelated+hang@file+..+..+..+forks+moq+js+hang+dist+kixelated-hang-0.3.13.tgz_@[email protected][email protected]/node_modules/@kixelated/hang/publish/audio/captions-worker.js

but its not blocking anything.

Also turbopack just gives up without any error message and return code 0 😄

Summary by CodeRabbit

  • New Features

    • Added speaking capability to Audio for integrated speaking state.
    • Improved multi-channel audio handling with explicit channel count support.
  • Bug Fixes

    • Improved compatibility for audio and video worklet loading across build environments, reducing initialization failures.
    • Corrected support panel label from “Audio” to “Opus” for decoding details.
  • Chores

    • Added Prettier and an async utility dependency to development/package tooling.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Walkthrough

Replaces bundler-specific worker/worklet imports with a runtime loader (loadAudioWorklet) that temporarily patches navigator.serviceWorker.register; adds a Speaking subsystem to Audio, wires channelCount into AudioWorkletNode, updates video worker initialization, tweaks a UI label, and adds tooling/dependency changes.

Changes

Cohort / File(s) Summary of changes
Audio worklet loading (bundler-agnostic hack)
js/hang/src/publish/audio/captions.ts, js/hang/src/publish/audio/speaking.ts, js/hang/src/watch/audio/index.ts, js/hang/src/publish/audio/index.ts
Removed Vite-only ?worker&url imports. Replaced static worklet module imports with runtime loader calls: await loadAudioWorklet(() => navigator.serviceWorker.register(new URL(..., import.meta.url))) passed to audioWorklet.addModule. Existing worklet wiring (AudioWorkletNode creation, connections) preserved; added channelCount wiring in publish audio.
Speaking integration & audio settings
js/hang/src/publish/audio/index.ts
Imported Speaking and type SpeakingProps; added speaking?: SpeakingProps to AudioProps and a public speaking: Speaking member instantiated in the Audio constructor. Wired channelCount: settings.channelCount into AudioWorkletNode options.
Audio runtime helper
js/hang/src/util/hacks.ts
New exported loadAudioWorklet(registerFn) that serializes concurrent calls with an async Mutex, temporarily overrides navigator.serviceWorker.register to return the registerFn result (to produce a module URL for bundlers), restores original register on exit.
Video detection worker init
js/hang/src/publish/video/detection.ts
Removed Vite ?worker&url import and replaced worker creation with new Worker(new URL("./detection-worker", import.meta.url), { type: "module" }); rest of detection flow unchanged.
Watch audio worklet update
js/hang/src/watch/audio/index.ts
Replaced static RenderWorklet import with loadAudioWorklet usage via service worker registration; added Captions and Speaking imports; added type-aware message handler using Render.Status.
Support UI label tweak
js/hang/src/support/element.ts
Changed Decoding row label for Opus from "Audio" to "Opus"; value rendering remains binary(support.audio.decoding?.opus).
Package changes (JS tooling & deps)
js/package.json, js/hang/package.json
js/package.json: added devDependency prettier: ^3.6.2, reordered devDeps. js/hang/package.json: added runtime dependency async-mutex: ^0.5.0, reordered dependency entries. No other behavioral API changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant Audio as Audio (publish/watch)
  participant Hacks as loadAudioWorklet
  participant Nav as navigator.serviceWorker
  participant AW as AudioWorklet
  participant Speaking as Speaking

  App->>Audio: new Audio(props)
  activate Audio
  Audio->>Speaking: new Speaking(this, props?.speaking)
  Note right of Speaking #DFF2E1: initialize speaking/VAD

  Audio->>Nav: (temporarily patched) register -> returns module URL
  Audio->>Hacks: call loadAudioWorklet(registerFn)
  Hacks->>Nav: patch register, invoke registerFn, return URL, restore register
  Hacks-->>Audio: module URL
  Audio->>AW: audioContext.audioWorklet.addModule(module URL)
  AW-->>Audio: module loaded
  Audio->>Audio: new AudioWorkletNode(..., { channelCount: settings.channelCount })
  Audio->>Audio: connect nodes, encoding, framing, catalog
  deactivate Audio
Loading
sequenceDiagram
  autonumber
  actor App
  participant Detect as Video Detection
  participant W as Worker

  App->>Detect: start()
  Detect->>W: new Worker(new URL("./detection-worker", import.meta.url), { type: "module" })
  Note over Detect,W #FFF4C2: Replaces Vite `?worker&url` import
  Detect->>W: setup (Comlink, loop)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Separate VAD and captions. #555 — Touches the same audio/captions/speaking codepaths and likely overlaps in Speaking/VAD integration and capture worklet loading changes.
  • Change the JS API a little bit. #543 — Modifies watch-audio files (Captions/Audio) and could conflict with the runtime worklet loader and message-handling changes.
  • moq.dev #538 — Changes related JS utilities and build-time conversions that may intersect with bundler/worklet-loading helpers and shared utilities.

Poem

In cables of sound where packets hop,
I nudge a register, wake a worklet from slop.
I count the channels, I teach it to speak,
Mutexed and tidy — the loader’s technique.
Prettier trims fur, the rabbit takes a bow 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
js/hang/src/publish/audio/index.ts (3)

186-196: Guard against undefined channelCount in encoder config to avoid runtime errors.

MediaStreamTrack.getSettings() may omit channelCount. Passing undefined into u53() will crash and also breaks bitrate derivation. Compute channels with a safe fallback.

-    const config = {
+    const channels = settings.channelCount ?? worklet.channelCount ?? 1;
+    const config = {
       // TODO get codec and description from decoderConfig
       codec: "opus",
       // Firefox doesn't provide the sampleRate in the settings.
-      sampleRate: u53(settings.sampleRate ?? worklet?.context.sampleRate),
-      numberOfChannels: u53(settings.channelCount),
+      sampleRate: u53(settings.sampleRate ?? worklet.context.sampleRate),
+      numberOfChannels: u53(channels),
       // TODO configurable
-      bitrate: u53(settings.channelCount * 32_000),
+      bitrate: u53(channels * 32_000),
     };

237-246: Slice input channels defensively when channelCount is missing.

Avoid indexing with a possibly-undefined value.

-      const channels = data.channels.slice(0, settings.channelCount);
+      const channelLimit = settings.channelCount ?? data.channels.length;
+      const channels = data.channels.slice(0, channelLimit);

282-286: Close Speaking on teardown to prevent leaks.

Speaking is instantiated but never closed; add this.speaking.close() in close().

   close() {
     this.#signals.close();
     this.captions.close();
+    this.speaking.close();
     this.#track.close();
   }
🧹 Nitpick comments (6)
js/package.json (1)

12-16: Prettier + Biome can conflict; pick one formatter or scope their responsibilities

You already use Biome, which includes a formatter. Adding Prettier may lead to churn and inconsistent diffs if both run over the same files.

Options:

  • Prefer Biome for JS/TS/JSON; use Prettier only for markdown and non-Biome formats. Add explicit ignore patterns to avoid overlap.
  • Or drop Prettier to keep a single-source-of-truth formatter.

Example scripts (if you keep both), scoping Prettier to markdown only:

{
  "scripts": {
    "fmt": "biome format --write . && prettier --write \"**/*.{md,mdx}\"",
    "lint": "biome lint ."
  }
}
js/hang/src/watch/audio/index.ts (1)

95-103: Deduplicate the worklet-loading hack behind a small utility

The same pattern appears in multiple files. Extract a helper, e.g., loadAudioWorklet(context: AudioContext, path: string), to centralize the guard/restore logic and avoid drift.

I can draft a tiny js/hang/src/util/worklet.ts with typed loadAudioWorklet() and update call sites on request.

js/hang/src/publish/audio/captions.ts (1)

95-103: Consider extracting a shared loader helper

This is the same logic as in watch/audio. A shared loadAudioWorklet() reduces risk of inconsistent fixes.

Happy to add a small utility and update both call sites if you want.

js/hang/src/publish/audio/index.ts (3)

141-145: Honor explicit channel count on the AudioWorkletNode and provide a safe default.

Without an explicit mode, the graph may up/down-mix implicitly. Also, settings.channelCount can be undefined on some browsers. Recommend setting channelCountMode: "explicit" and defaulting to 1 when missing.

-      const worklet = new AudioWorkletNode(context, "capture", {
-        numberOfInputs: 1,
-        numberOfOutputs: 0,
-        channelCount: settings.channelCount,
-      });
+      const worklet = new AudioWorkletNode(context, "capture", {
+        numberOfInputs: 1,
+        numberOfOutputs: 0,
+        // Ensure we honor a specific channel count instead of letting the graph pick one.
+        channelCountMode: "explicit",
+        channelCount: settings.channelCount ?? 1,
+      });

10-11: Consider re-exporting Speaking to mirror Captions ergonomics.

If external callers need SpeakingProps/Speaking types, a re-export keeps the public API consistent with captions.

 export * from "./captions";
+export * from "./speaking";

26-38: Typo: “vidfeo” → “video”.

Very minor docstring fix.

-// This is a fork of the MediaTrackSettings interface with properties required for audio or vidfeo.
+// This is a fork of the MediaTrackSettings interface with properties required for audio or video.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 614d432 and 2f2fe19.

⛔ Files ignored due to path filters (1)
  • js/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • js/hang/src/publish/audio/captions.ts (1 hunks)
  • js/hang/src/publish/audio/index.ts (2 hunks)
  • js/hang/src/publish/video/detection.ts (1 hunks)
  • js/hang/src/support/element.ts (1 hunks)
  • js/hang/src/watch/audio/index.ts (1 hunks)
  • js/package.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
js/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place JS/TS packages under the js/ directory

Files:

  • js/package.json
  • js/hang/src/publish/video/detection.ts
  • js/hang/src/watch/audio/index.ts
  • js/hang/src/support/element.ts
  • js/hang/src/publish/audio/captions.ts
  • js/hang/src/publish/audio/index.ts
{rs,js}/hang/**

📄 CodeRabbit inference engine (CLAUDE.md)

All media logic belongs in the hang layer (rs/hang and js/hang)

Files:

  • js/hang/src/publish/video/detection.ts
  • js/hang/src/watch/audio/index.ts
  • js/hang/src/support/element.ts
  • js/hang/src/publish/audio/captions.ts
  • js/hang/src/publish/audio/index.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (6)
js/hang/src/support/element.ts (1)

303-307: Label change aligns audio decoding rows with encoding rows — looks good

Switching the second-column label from “Audio” to “Opus” under Decoding improves specificity and matches the Encoding section’s per-codec layout.

js/hang/src/publish/video/detection.ts (1)

53-54: Worker init via new URL is bundler-agnostic—nice

Switching to new Worker(new URL('./detection-worker', import.meta.url), { type: 'module' }) should work across Webpack/Vite and fixes the Vite-only path. Cleanup via terminate() is correct.

Please verify in Next.js + Turbopack and legacy Webpack builds that:

  • The worker chunk is emitted and reachable at runtime.
  • No “Accessing import.meta directly is unsupported” warnings propagate from nested imports.
js/hang/src/publish/audio/index.ts (4)

86-95: Speaking integration looks good; initialize order is correct.

Nice addition and consistent with Captions integration. Make sure to pair it with the teardown fix above.


4-4: LGTM on integer helpers import adjustment.

No functional impact; consistent with existing usage.


8-8: LGTM on Speaking import.

Matches the new public speaking surface and catalog wiring.


132-140: Ensure serviceWorker.register override is always restored and guard its absence

To prevent permanently leaking an overridden register (if addModule throws) and avoid crashes in browsers without ServiceWorker support:

  • File: js/hang/src/publish/audio/index.ts
    Location: inside the effect.spawn(async () => { … }) callback (around lines 130–140)

Proposed patch:

-      // Hacky workaround to support Webpack and Vite:
-      // https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
-
-      const { register } = navigator.serviceWorker;
-      // @ts-ignore hack to make webpack believe that it is registering a worker
-      navigator.serviceWorker.register = (url: URL) => context.audioWorklet.addModule(url);
-      await navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url));
-      navigator.serviceWorker.register = register;
+      // Hacky workaround to support Webpack and Vite:
+      // https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
+      // Guard against missing ServiceWorker and always restore the original register method.
+      const sw = (navigator as any).serviceWorker as ServiceWorkerContainer | undefined;
+      if (!sw || typeof sw.register !== "function") {
+        // If SW isn’t available, fall back to directly loading the worklet.
+        await context.audioWorklet.addModule(new URL("./capture-worklet", import.meta.url));
+      } else {
+        const originalRegister = sw.register;
+        try {
+          // @ts-expect-error: Trick bundlers into treating the URL as a worker.
+          sw.register = (url: URL) => context.audioWorklet.addModule(url);
+          await sw.register(new URL("./capture-worklet", import.meta.url));
+        } finally {
+          // Always restore—even if addModule rejects.
+          sw.register = originalRegister;
+        }
+      }

Please verify this change in your build environments (Next.js/Webpack, Turbopack, Vite) and across browsers (Safari secure contexts and Chromium) to confirm there are no side-effects or silent failures.

Comment on lines 95 to 103
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214

const { register } = navigator.serviceWorker;
// @ts-ignore hack to make webpack believe that it is registering a worker
navigator.serviceWorker.register = (url: URL) => ctx.audioWorklet.addModule(url);
await navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url));
navigator.serviceWorker.register = register;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Same global override concerns: add guard + try/finally and prefer @ts-expect-error

Mirror the safer pattern here to prevent leaking the patched register and to handle no-SW environments gracefully.

Apply this diff:

-      // Hacky workaround to support Webpack and Vite:
-      // https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
-
-      const { register } = navigator.serviceWorker;
-      // @ts-ignore hack to make webpack believe that it is registering a worker
-      navigator.serviceWorker.register = (url: URL) => ctx.audioWorklet.addModule(url);
-      await navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url));
-      navigator.serviceWorker.register = register;
+      // Hacky workaround to support Webpack and Vite:
+      // https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
+      const sw: any = (navigator as any).serviceWorker;
+      const workletUrl = new URL("./capture-worklet", import.meta.url);
+
+      if (!sw || typeof sw.register !== "function") {
+        await ctx.audioWorklet.addModule(workletUrl);
+      } else {
+        const originalRegister = sw.register.bind(sw);
+        // @ts-expect-error: Intentionally masquerading as ServiceWorker.register for bundlers.
+        sw.register = (url: URL) => ctx.audioWorklet.addModule(url);
+        try {
+          await sw.register(workletUrl);
+        } finally {
+          sw.register = originalRegister;
+        }
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
const { register } = navigator.serviceWorker;
// @ts-ignore hack to make webpack believe that it is registering a worker
navigator.serviceWorker.register = (url: URL) => ctx.audioWorklet.addModule(url);
await navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url));
navigator.serviceWorker.register = register;
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
const sw: any = (navigator as any).serviceWorker;
const workletUrl = new URL("./capture-worklet", import.meta.url);
if (!sw || typeof sw.register !== "function") {
await ctx.audioWorklet.addModule(workletUrl);
} else {
const originalRegister = sw.register.bind(sw);
// @ts-expect-error: Intentionally masquerading as ServiceWorker.register for bundlers.
sw.register = (url: URL) => ctx.audioWorklet.addModule(url);
try {
await sw.register(workletUrl);
} finally {
sw.register = originalRegister;
}
}
🤖 Prompt for AI Agents
In js/hang/src/publish/audio/captions.ts around lines 95 to 103, the code
overrides navigator.serviceWorker.register without a guard or try/finally and
uses @ts-ignore; change it to first check that navigator.serviceWorker and
navigator.serviceWorker.register exist, save the original register, replace it
with the worklet-adding function inside a try block, await registering the
worklet URL, and restore the original register in a finally block; also replace
the // @ts-ignore with // @ts-expect-error to indicate the intentional type
bypass and ensure the code no-ops gracefully when service workers are
unavailable.

Comment on lines 95 to 103
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214

const { register } = navigator.serviceWorker;
// @ts-ignore hack to make webpack believe that it is registering a worker
navigator.serviceWorker.register = (url: URL) => context.audioWorklet.addModule(url);
await navigator.serviceWorker.register(new URL("./render-worklet", import.meta.url));
navigator.serviceWorker.register = register;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard and always-restore when monkey-patching navigator.serviceWorker.register

Overriding a global API without a guard and without a try/finally can leave the environment in a broken state if addModule throws. It will also throw on platforms where navigator.serviceWorker is undefined (HTTP, some Safari contexts).

Apply this diff to add a guard and guarantee restoration:

-      // Hacky workaround to support Webpack and Vite:
-      // https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
-
-      const { register } = navigator.serviceWorker;
-      // @ts-ignore hack to make webpack believe that it is registering a worker
-      navigator.serviceWorker.register = (url: URL) => context.audioWorklet.addModule(url);
-      await navigator.serviceWorker.register(new URL("./render-worklet", import.meta.url));
-      navigator.serviceWorker.register = register;
+      // Hacky workaround to support Webpack and Vite:
+      // https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
+      const sw: any = (navigator as any).serviceWorker;
+      const workletUrl = new URL("./render-worklet", import.meta.url);
+
+      if (!sw || typeof sw.register !== "function") {
+        // Fallback: no Service Worker support—load directly.
+        await context.audioWorklet.addModule(workletUrl);
+      } else {
+        const originalRegister = sw.register.bind(sw);
+        // @ts-expect-error: Intentionally masquerading as ServiceWorker.register for bundlers.
+        sw.register = (url: URL) => context.audioWorklet.addModule(url);
+        try {
+          await sw.register(workletUrl);
+        } finally {
+          sw.register = originalRegister;
+        }
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
const { register } = navigator.serviceWorker;
// @ts-ignore hack to make webpack believe that it is registering a worker
navigator.serviceWorker.register = (url: URL) => context.audioWorklet.addModule(url);
await navigator.serviceWorker.register(new URL("./render-worklet", import.meta.url));
navigator.serviceWorker.register = register;
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
const sw: any = (navigator as any).serviceWorker;
const workletUrl = new URL("./render-worklet", import.meta.url);
if (!sw || typeof sw.register !== "function") {
// Fallback: no Service Worker support—load directly.
await context.audioWorklet.addModule(workletUrl);
} else {
const originalRegister = sw.register.bind(sw);
// @ts-expect-error: Intentionally masquerading as ServiceWorker.register for bundlers.
sw.register = (url: URL) => context.audioWorklet.addModule(url);
try {
await sw.register(workletUrl);
} finally {
sw.register = originalRegister;
}
}
🤖 Prompt for AI Agents
In js/hang/src/watch/audio/index.ts around lines 95 to 103, the code
monkey-patches navigator.serviceWorker.register without checking
navigator.serviceWorker exists and without a try/finally to guarantee
restoration; change it to first guard that navigator.serviceWorker and
navigator.serviceWorker.register are defined, save the original register, then
perform the temporary assignment and await addModule inside a try block, and
always restore the original register in a finally block so the global is never
left patched even if addModule throws or navigator.serviceWorker is absent.

@kixelated
Copy link
Collaborator

Hopefully it works with Vite. Worklets are awful.

@kixelated
Copy link
Collaborator

And yeah, the code rabbit review points out this could be racey. Maybe add a loadWorklet helper method that uses a mutex to make sure two (async) functions don't monkey patch navigator.serviceWorker.register at the same time and restores it even on exception. It won't actually matter at the moment, because we don't register serviceWorkers, but still.

@kixelated
Copy link
Collaborator

also just fix to format stuff.

@skirsten
Copy link
Contributor Author

also just fix to format stuff.

But my vscode formats on save and by default uses a different prettier version which causes a bunch of changes. If the prettier version is fixed in the project, it will always use that version -> no changes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
js/hang/src/publish/audio/speaking.ts (2)

87-95: Serialize monkey-patching to prevent races across async callers.

Two concurrent calls could overlap while register is patched. Consider a small module-scoped mutex and a shared helper so all worklet loaders use the same critical section. This also consolidates the hack in one place.

Example helper (new file suggestion):

// js/hang/src/worklet/load.ts
type Loader = (url: URL) => Promise<void>;

class Mutex {
  #p: Promise<void> = Promise.resolve();
  async run<T>(fn: () => Promise<T>): Promise<T> {
    const prev = this.#p;
    let release!: () => void;
    this.#p = new Promise<void>((r) => (release = r));
    await prev;
    try {
      return await fn();
    } finally {
      release();
    }
  }
}

const patchMutex = new Mutex();

export async function loadAudioWorkletModule(ctx: AudioContext, moduleUrl: URL): Promise<void> {
  return patchMutex.run(async () => {
    const sw = navigator.serviceWorker;
    if (sw?.register) {
      const original = sw.register;
      // @ts-expect-error spoof for bundlers
      sw.register = ((url: string | URL, ..._rest: unknown[]) =>
        ctx.audioWorklet.addModule(typeof url === "string" ? new URL(url, import.meta.url) : url)
      ) as typeof sw.register;
      try {
        await sw.register(moduleUrl);
      } finally {
        sw.register = original;
      }
    } else {
      await ctx.audioWorklet.addModule(moduleUrl);
    }
  });
}

Then here replace the patched block with:

await loadAudioWorkletModule(ctx, new URL("./capture-worklet", import.meta.url));

I can push a follow-up patch touching all similar sites (captions, watch/audio render worklet) to use this helper.


91-91: Prefer @ts-expect-error over @ts-ignore.

@ts-expect-error documents the intentional violation and will fail the build if/when it’s no longer needed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2f2fe19 and 240ff96.

📒 Files selected for processing (1)
  • js/hang/src/publish/audio/speaking.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{rs,js}/hang/**

📄 CodeRabbit inference engine (CLAUDE.md)

All media logic belongs in the hang layer (rs/hang and js/hang)

Files:

  • js/hang/src/publish/audio/speaking.ts
js/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place JS/TS packages under the js/ directory

Files:

  • js/hang/src/publish/audio/speaking.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check

Comment on lines 87 to 95
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214

const { register } = navigator.serviceWorker;
// @ts-ignore hack to make webpack believe that it is registering a worker
navigator.serviceWorker.register = (url: URL) => ctx.audioWorklet.addModule(url);
await navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url));
navigator.serviceWorker.register = register;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Double-check Webpack/Next dev servers where ServiceWorker may be disabled.

On non-HTTPS origins (except localhost) or certain Next.js dev configs, navigator.serviceWorker can be unavailable. The fallback path in the proposed diff handles this; please verify in your target environments.

Run to find all monkey-patch sites and ensure they either use the helper or include the same fallback + try/finally:


🏁 Script executed:

#!/bin/bash
# Look for monkey-patching and worklet loads across the repo
rg -nC2 -g 'js/**' -e 'serviceWorker\.register\s*=' -e 'new URL\(".*worklet", import\.meta\.url\)' -e 'audioWorklet\.addModule'

Length of output: 2802


Ensure all ServiceWorker monkey-patches include fallback handling and are wrapped in try/finally

We’ve identified four spots in the codebase where navigator.serviceWorker.register is temporarily overridden to load an AudioWorklet module. Each of these must:

  • Check for the existence of navigator.serviceWorker (to cover non-HTTPS or dev-server scenarios where it’s disabled).
  • Wrap the patch/revert sequence in a try { … } finally { … } block to guarantee restoration of the original register method, even if addModule throws.

Affected files:

  • js/hang/src/watch/audio/index.ts (lines 98–103)
  • js/hang/src/publish/audio/speaking.ts (lines 90–95)
  • js/hang/src/publish/audio/index.ts (lines 135–140)
  • js/hang/src/publish/audio/captions.ts (lines 98–103)

Please refactor each of these blocks to something like:

if (navigator.serviceWorker) {
  const originalRegister = navigator.serviceWorker.register;
  // @ts-ignore allow hack for bundlers
  navigator.serviceWorker.register = (url: URL) => ctx.audioWorklet.addModule(url);
  try {
    await navigator.serviceWorker.register(new URL("./…-worklet", import.meta.url));
  } finally {
    navigator.serviceWorker.register = originalRegister;
  }
} else {
  // fallback: directly load the worklet
  await ctx.audioWorklet.addModule(new URL("./…-worklet", import.meta.url));
}

This ensures both robust cleanup and support on origins without ServiceWorker.

🤖 Prompt for AI Agents
In js/hang/src/publish/audio/speaking.ts around lines 87 to 95, the temporary
override of navigator.serviceWorker.register lacks existence checks, guaranteed
restoration, and a fallback; update the block to first check if
navigator.serviceWorker exists, save the original register, override it (keeping
the @ts-ignore comment), then call the patched register inside a try { await ...
} finally { restore original register } so the original is always restored even
if addModule throws; if navigator.serviceWorker is not present, call
ctx.audioWorklet.addModule(...) directly as the fallback.

🛠️ Refactor suggestion

Ensure global patch is always reverted (try/finally) + add SW fallback + avoid destructuring trap.

If ctx.audioWorklet.addModule() throws, navigator.serviceWorker.register stays monkey‑patched, potentially breaking unrelated code. Also, destructuring register isn’t needed here and can be avoided. Add a guarded patch with a fallback when serviceWorker is unavailable, preserve the call signature, and always restore via try/finally.

Apply this diff:

@@
-      const { register } = navigator.serviceWorker;
-      // @ts-ignore hack to make webpack believe that it is registering a worker
-      navigator.serviceWorker.register = (url: URL) => ctx.audioWorklet.addModule(url);
-      await navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url));
-      navigator.serviceWorker.register = register;
+      const sw = navigator.serviceWorker;
+      const moduleUrl = new URL("./capture-worklet", import.meta.url);
+      if (sw?.register) {
+        const originalRegister = sw.register;
+        // @ts-expect-error We intentionally spoof the signature to trick bundlers.
+        sw.register = ((url: string | URL, ..._rest: unknown[]) =>
+          ctx.audioWorklet.addModule(typeof url === "string" ? new URL(url, import.meta.url) : url)
+        ) as typeof sw.register;
+        try {
+          await sw.register(moduleUrl);
+        } finally {
+          sw.register = originalRegister;
+        }
+      } else {
+        // Fallback: when ServiceWorker is unavailable (e.g., some environments), load directly.
+        await ctx.audioWorklet.addModule(moduleUrl);
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
const { register } = navigator.serviceWorker;
// @ts-ignore hack to make webpack believe that it is registering a worker
navigator.serviceWorker.register = (url: URL) => ctx.audioWorklet.addModule(url);
await navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url));
navigator.serviceWorker.register = register;
// Hacky workaround to support Webpack and Vite:
// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214
const sw = navigator.serviceWorker;
const moduleUrl = new URL("./capture-worklet", import.meta.url);
if (sw?.register) {
const originalRegister = sw.register;
// @ts-expect-error We intentionally spoof the signature to trick bundlers.
sw.register = ((url: string | URL, ..._rest: unknown[]) =>
ctx.audioWorklet.addModule(
typeof url === "string" ? new URL(url, import.meta.url) : url
)
) as typeof sw.register;
try {
await sw.register(moduleUrl);
} finally {
sw.register = originalRegister;
}
} else {
// Fallback: when ServiceWorker is unavailable (e.g., some environments), load directly.
await ctx.audioWorklet.addModule(moduleUrl);
}
🤖 Prompt for AI Agents
In js/hang/src/publish/audio/speaking.ts around lines 87–95, avoid destructuring
and ensure the global monkey-patch is always reverted and supports missing
serviceWorker: check if navigator.serviceWorker exists, save the original
register function (if any) into a variable (no destructuring), replace
navigator.serviceWorker.register with a function that preserves the original
call signature and delegates to ctx.audioWorklet.addModule(url) as the
workaround, perform the registration inside a try block and restore the original
register in a finally block so the global is always reverted, and if
navigator.serviceWorker is absent provide a safe fallback that directly calls
ctx.audioWorklet.addModule or rejects appropriately.

@kixelated
Copy link
Collaborator

also just fix to format stuff.

But my vscode formats on save and by default uses a different prettier version which causes a bunch of changes. If the prettier version is fixed in the project, it will always use that version -> no changes.

Yeah I'm using biome, not prettier. It's faster and IMO does a better job at formatting.

@kixelated
Copy link
Collaborator

yolo

I'll let you know if it breaks stuff for me.

@kixelated kixelated merged commit 9c42e0d into moq-dev:main Aug 26, 2025
1 of 3 checks passed
@skirsten
Copy link
Contributor Author

👍 🤞 I did a quick test in Vite and it looked good. But I have not tested all the caption stuff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
js/hang/src/watch/audio/index.ts (1)

100-119: Minor: keep channelCount in sync with worklet init and validate at runtime

You derive channelCount earlier (Line 81) and pass it in the init message. Consider also validating the created node’s channelCount matches expectations and logging once if not, to help diagnose platform-specific defaults.

Example (no diff necessary):

  • After constructing the node, compare worklet.channelCount vs the channelCount you computed and warn if they differ.
js/hang/src/publish/audio/index.ts (1)

139-144: Channel count may be undefined on some browsers; provide a sane default

Some implementations omit channelCount in MediaTrackSettings. Passing undefined can produce surprising defaults. Consider defaulting to 2 (stereo) or probing from the MediaStreamTrack capabilities.

Apply:

-      const worklet = new AudioWorkletNode(context, "capture", {
+      const worklet = new AudioWorkletNode(context, "capture", {
         numberOfInputs: 1,
         numberOfOutputs: 0,
-        channelCount: settings.channelCount,
+        channelCount: settings.channelCount ?? 2,
       });

If you prefer stricter behavior, throw when channelCount is undefined in dev builds to catch device quirks early.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 240ff96 and 2474fb4.

⛔ Files ignored due to path filters (1)
  • js/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • js/hang/package.json (1 hunks)
  • js/hang/src/publish/audio/captions.ts (2 hunks)
  • js/hang/src/publish/audio/index.ts (2 hunks)
  • js/hang/src/publish/audio/speaking.ts (2 hunks)
  • js/hang/src/util/hacks.ts (1 hunks)
  • js/hang/src/watch/audio/index.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • js/hang/src/publish/audio/speaking.ts
  • js/hang/src/publish/audio/captions.ts
🧰 Additional context used
📓 Path-based instructions (2)
{rs,js}/hang/**

📄 CodeRabbit inference engine (CLAUDE.md)

All media logic belongs in the hang layer (rs/hang and js/hang)

Files:

  • js/hang/src/util/hacks.ts
  • js/hang/package.json
  • js/hang/src/publish/audio/index.ts
  • js/hang/src/watch/audio/index.ts
js/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place JS/TS packages under the js/ directory

Files:

  • js/hang/src/util/hacks.ts
  • js/hang/package.json
  • js/hang/src/publish/audio/index.ts
  • js/hang/src/watch/audio/index.ts
🧬 Code graph analysis (2)
js/hang/src/publish/audio/index.ts (1)
js/hang/src/util/hacks.ts (1)
  • loadAudioWorklet (16-29)
js/hang/src/watch/audio/index.ts (1)
js/hang/src/util/hacks.ts (1)
  • loadAudioWorklet (16-29)
🔇 Additional comments (3)
js/hang/package.json (1)

41-47: async-mutex ESM/CJS interop verified

I’ve confirmed that [email protected] ships proper named exports for both ESM and CJS consumers:

  • The package.json exports field maps
    "import"./index.mjs (ESM)
    "require"./lib/index.js (CJS)
  • The root ESM entrypoint (index.mjs) ends with
    export { E_ALREADY_LOCKED, E_CANCELED, E_TIMEOUT, Mutex, Semaphore, tryAcquire, withTimeout };
  • The secondary ES build (es6/index.js) re-exports Mutex as a named export:
    export { default as Mutex } from './Mutex';

Imports in the codebase (found in js/hang/src/util/hacks.ts and js/moq/src/ietf/control.ts) use

import { Mutex } from "async-mutex";

which will correctly resolve in Node ESM (Next.js SSR), Vite, Webpack, and plain CJS contexts.

No changes needed here—named imports of Mutex are safe across all targeted environments. Approved.

js/hang/src/util/hacks.ts (1)

14-29: Mutex-based critical section is the right direction

Serializing the monkey-patch with a module-scoped Mutex eliminates the race flagged in the PR discussion and guarantees restoration with the finally block. Once the guard above is added, this becomes robust across async re-entrancy.

js/hang/src/publish/audio/index.ts (1)

186-257: Nice: end-to-end framing logic remains unchanged and Speaking integration is orthogonal

The encoder path and catalog updates remain stable; the Speaking addition fits cleanly without altering existing behavior.

Comment on lines +133 to +137
await context.audioWorklet.addModule(
await loadAudioWorklet(() =>
navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url)),
),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Adopt the guarded loadAudioWorklet(url, registerFn) and compute URL once

Prevents crashes on platforms without Service Worker support while preserving the bundler hint.

Apply:

-      await context.audioWorklet.addModule(
-        await loadAudioWorklet(() =>
-          navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url)),
-        ),
-      );
+      const workletUrl = new URL("./capture-worklet", import.meta.url);
+      await context.audioWorklet.addModule(
+        await loadAudioWorklet(workletUrl, () => navigator.serviceWorker.register(workletUrl)),
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await context.audioWorklet.addModule(
await loadAudioWorklet(() =>
navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url)),
),
);
// Compute the worklet URL once for bundler hints and reuse
const workletUrl = new URL("./capture-worklet", import.meta.url);
await context.audioWorklet.addModule(
await loadAudioWorklet(
workletUrl,
() => navigator.serviceWorker.register(workletUrl),
),
);
🤖 Prompt for AI Agents
In js/hang/src/publish/audio/index.ts around lines 133-137, the current call
inlines navigator.serviceWorker.register inside loadAudioWorklet which can crash
on platforms without Service Worker support; compute the worklet URL once into a
const (e.g. const workletUrl = new URL("./capture-worklet", import.meta.url))
and then call the guarded loadAudioWorklet(workletUrl, () =>
navigator.serviceWorker?.register(workletUrl)) so the helper can check for
service worker support before invoking the register function and the bundler
still sees the URL hint.

Comment on lines 3 to +8
// https://issues.chromium.org/issues/40504498
export const isChrome = navigator.userAgent.toLowerCase().includes("chrome");

// https://bugzilla.mozilla.org/show_bug.cgi?id=1967793
export const isFirefox = navigator.userAgent.toLowerCase().includes("firefox");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Top-level navigator access will crash in SSR and non-browser contexts

Accessing navigator at module load time throws in SSR (Next.js) and workers without Navigator. Gate userAgent reads.

Apply:

-// https://issues.chromium.org/issues/40504498
-export const isChrome = navigator.userAgent.toLowerCase().includes("chrome");
-
-// https://bugzilla.mozilla.org/show_bug.cgi?id=1967793
-export const isFirefox = navigator.userAgent.toLowerCase().includes("firefox");
+// https://issues.chromium.org/issues/40504498
+const ua = typeof navigator !== "undefined" && typeof navigator.userAgent === "string"
+  ? navigator.userAgent.toLowerCase()
+  : "";
+export const isChrome = ua.includes("chrome");
+
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1967793
+export const isFirefox = ua.includes("firefox");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// https://issues.chromium.org/issues/40504498
export const isChrome = navigator.userAgent.toLowerCase().includes("chrome");
// https://bugzilla.mozilla.org/show_bug.cgi?id=1967793
export const isFirefox = navigator.userAgent.toLowerCase().includes("firefox");
// https://issues.chromium.org/issues/40504498
const ua = typeof navigator !== "undefined" && typeof navigator.userAgent === "string"
? navigator.userAgent.toLowerCase()
: "";
export const isChrome = ua.includes("chrome");
// https://bugzilla.mozilla.org/show_bug.cgi?id=1967793
export const isFirefox = ua.includes("firefox");
🤖 Prompt for AI Agents
In js/hang/src/util/hacks.ts around lines 3 to 8, the module reads
navigator.userAgent at top-level which will throw in SSR or non-browser
contexts; change the exports to safe, lazily-evaluated checks that first ensure
navigator (or globalThis.navigator) exists and that navigator.userAgent is a
string (e.g., typeof globalThis !== "undefined" && typeof globalThis.navigator
!== "undefined" && typeof globalThis.navigator.userAgent === "string") before
calling toLowerCase().includes(...), or export functions/getters (isChrome(),
isFirefox()) that perform this guarded check at call time so the module load
never accesses navigator in non-browser environments.

Comment on lines +16 to +29
export async function loadAudioWorklet(registerFn: () => Promise<ServiceWorkerRegistration>) {
return await loadAudioWorkletMutex.runExclusive(async () => {
const { register } = navigator.serviceWorker;

// @ts-ignore hack to make webpack believe that it is registering a worker
navigator.serviceWorker.register = (url: URL) => Promise.resolve(url);

try {
return (await registerFn()) as unknown as URL;
} finally {
navigator.serviceWorker.register = register;
}
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Guard serviceWorker presence; avoid destructuring undefined and provide a safe fallback

As written, const { register } = navigator.serviceWorker; throws when serviceWorker is undefined (HTTP, some Safari, certain embedded webviews), and calling registerFn() will then also fail. We need a fallback that still satisfies bundlers but doesn’t execute a broken call when SW is absent. Also prefer @ts-expect-error over blanket @ts-ignore.

Proposed minimal API change: accept the worklet URL upfront and keep the current registerFn for bundler visibility. If SW is missing, return the URL without evaluating registerFn. This also solves SSR cases where navigator exists at runtime but serviceWorker does not.

Apply:

-const loadAudioWorkletMutex = new Mutex();
-
-export async function loadAudioWorklet(registerFn: () => Promise<ServiceWorkerRegistration>) {
-  return await loadAudioWorkletMutex.runExclusive(async () => {
-    const { register } = navigator.serviceWorker;
-
-    // @ts-ignore hack to make webpack believe that it is registering a worker
-    navigator.serviceWorker.register = (url: URL) => Promise.resolve(url);
-
-    try {
-      return (await registerFn()) as unknown as URL;
-    } finally {
-      navigator.serviceWorker.register = register;
-    }
-  });
-}
+const loadAudioWorkletMutex = new Mutex();
+
+export async function loadAudioWorklet(
+  url: URL,
+  registerFn?: () => Promise<ServiceWorkerRegistration>,
+): Promise<URL> {
+  return await loadAudioWorkletMutex.runExclusive(async () => {
+    const nav: any = typeof navigator !== "undefined" ? navigator : undefined;
+    const sw: any = nav?.serviceWorker;
+
+    // If no Service Worker environment or no register shim is provided, use the URL directly.
+    if (!sw || typeof sw.register !== "function" || !registerFn) {
+      return url;
+    }
+
+    const originalRegister = sw.register.bind(sw);
+    // Intentionally masquerade as ServiceWorkerContainer.register for bundlers; returns the URL.
+    // The call still exists in user code so bundlers can statically detect it.
+    // @ts-expect-error: return type intentionally diverges at runtime
+    sw.register = (_url: URL) => Promise.resolve(url);
+    try {
+      await registerFn();
+      return url;
+    } finally {
+      sw.register = originalRegister;
+    }
+  });
+}

Follow-up: this requires small call-site edits (see comments in watch/publish files). I can push a commit if you’d like.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function loadAudioWorklet(registerFn: () => Promise<ServiceWorkerRegistration>) {
return await loadAudioWorkletMutex.runExclusive(async () => {
const { register } = navigator.serviceWorker;
// @ts-ignore hack to make webpack believe that it is registering a worker
navigator.serviceWorker.register = (url: URL) => Promise.resolve(url);
try {
return (await registerFn()) as unknown as URL;
} finally {
navigator.serviceWorker.register = register;
}
});
}
const loadAudioWorkletMutex = new Mutex();
export async function loadAudioWorklet(
url: URL,
registerFn?: () => Promise<ServiceWorkerRegistration>,
): Promise<URL> {
return await loadAudioWorkletMutex.runExclusive(async () => {
const nav: any = typeof navigator !== "undefined" ? navigator : undefined;
const sw: any = nav?.serviceWorker;
// If no Service Worker environment or no register shim is provided, use the URL directly.
if (!sw || typeof sw.register !== "function" || !registerFn) {
return url;
}
const originalRegister = sw.register.bind(sw);
// Intentionally masquerade as ServiceWorkerContainer.register for bundlers; returns the URL.
// The call still exists in user code so bundlers can statically detect it.
// @ts-expect-error: return type intentionally diverges at runtime
sw.register = (_url: URL) => Promise.resolve(url);
try {
await registerFn();
return url;
} finally {
sw.register = originalRegister;
}
});
}
🤖 Prompt for AI Agents
In js/hang/src/util/hacks.ts around lines 16–29, the code destructures
navigator.serviceWorker and calls registerFn unguarded which throws when
serviceWorker is undefined; change the function signature to accept the worklet
URL upfront (preserve registerFn for bundler visibility), check if
navigator?.serviceWorker exists before touching or destructuring it, and if
absent immediately return the provided URL (avoid invoking registerFn); when
stubbing navigator.serviceWorker.register use @ts-expect-error instead of
@ts-ignore, and ensure the original register property is restored in finally.

Comment on lines 93 to +98
// Register the AudioWorklet processor
await context.audioWorklet.addModule(RenderWorklet);
await context.audioWorklet.addModule(
await loadAudioWorklet(() =>
navigator.serviceWorker.register(new URL("./render-worklet", import.meta.url)),
),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use the safer loadAudioWorklet(url, registerFn) form and avoid duplicate URL construction

This avoids executing a register call when Service Workers are unavailable and keeps the bundler-visible pattern. It also reduces duplication.

Apply:

-      // Register the AudioWorklet processor
-      await context.audioWorklet.addModule(
-        await loadAudioWorklet(() =>
-          navigator.serviceWorker.register(new URL("./render-worklet", import.meta.url)),
-        ),
-      );
+      // Register the AudioWorklet processor
+      const workletUrl = new URL("./render-worklet", import.meta.url);
+      await context.audioWorklet.addModule(
+        await loadAudioWorklet(workletUrl, () => navigator.serviceWorker.register(workletUrl)),
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Register the AudioWorklet processor
await context.audioWorklet.addModule(RenderWorklet);
await context.audioWorklet.addModule(
await loadAudioWorklet(() =>
navigator.serviceWorker.register(new URL("./render-worklet", import.meta.url)),
),
);
// Register the AudioWorklet processor
const workletUrl = new URL("./render-worklet", import.meta.url);
await context.audioWorklet.addModule(
await loadAudioWorklet(workletUrl, () => navigator.serviceWorker.register(workletUrl)),
);
🤖 Prompt for AI Agents
In js/hang/src/watch/audio/index.ts around lines 93–98, replace the inline
register-call form with the safer loadAudioWorklet(url, registerFn) pattern:
construct the worklet module URL once (e.g. new URL("./render-worklet",
import.meta.url) stored in a variable) and pass that URL as the first argument
to loadAudioWorklet and a register function that calls
navigator.serviceWorker.register with that same URL as the second argument; this
avoids executing registration when Service Workers are unavailable and
eliminates duplicate URL construction.

@kixelated
Copy link
Collaborator

@skirsten Unfortunately this seems to break vite in production (local dev is fine). I'm going to revert for now if that's okay while we figure it out.

kixelated added a commit that referenced this pull request Aug 27, 2025
@kixelated
Copy link
Collaborator

kixelated commented Aug 27, 2025

And for the record, dealing with AudioWorklet, AudioWorker, and WASM in bundlers is easily in my top 5 most despised things. I've lost days trying to get bundlers to work the same way.

@coderabbitai coderabbitai bot mentioned this pull request Aug 27, 2025
@skirsten
Copy link
Contributor Author

What is the issue you are seeing? I just quickly ran

web-build url='http://localhost:4443/anon':
	pnpm -r i
	cd hang-demo && VITE_RELAY_URL="{{url}}" pnpm vite build src

and it seems fine (apart from only building the index.html file), but the assets for the workers are there.

@skirsten
Copy link
Contributor Author

Hmm, nevermind that does not look right...

new URL(
	"data:video/mp2t;base64,...",
	import.meta.url,
),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants