From d2c8fa6244ccd9f84e346c9dab6e7396f57c3d6e Mon Sep 17 00:00:00 2001 From: gilpanal Date: Wed, 17 Jun 2026 12:40:27 +0200 Subject: [PATCH 1/3] fix: add legacy ESM export subpath, patch Phase 6 docs findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - package.json: expose dist/latency-test.legacy.esm.js via a "./legacy" exports subpath so bundler consumers can `import "@adasp/latency-test/legacy"` instead of reaching into node_modules directly. Documented in install.md and build-output.md. - docs/examples/svelte.md: explicit close tags instead of self-closing (Svelte 5 compiler warning). - docs/examples/angular.md: documents re-enabling zone.js on CLI 22+ zoneless scaffolds (npm install, polyfill load order, provideZoneChangeDetection, markForCheck()). - docs/examples/{react,nextjs}.md: JSX IntrinsicElements augmentation now works on all React versions via `declare module 'react'` with the required `import type {} from 'react'` (verified against @types/react 19.2.17 — the previous "React 19+ auto-detects" claim was false). - docs/examples/vanilla-js.md: CDN snippet adds , SRI hash + crossorigin on the pinned script tag, and type="module" on the inline script to remove a custom-element upgrade race. - connect() examples (vanilla-js, react, vue, svelte x2, angular, nextjs): clear stale error state on retry and close the AudioContext created before a failed getUserMedia() call, instead of leaking it. - docs/index.md, docs/install.md, docs/examples/host-gain.md: same AudioContext-leak pattern fixed in the Quick Start / host-gain snippets. Closes #29 Closes #30 --- docs/build-output.md | 2 +- docs/examples/angular.md | 59 +++++++++++++++++++++++++++++++++++++ docs/examples/host-gain.md | 22 +++++++++----- docs/examples/nextjs.md | 29 ++++++++++-------- docs/examples/react.md | 27 ++++++++++------- docs/examples/svelte.md | 13 +++++--- docs/examples/vanilla-js.md | 13 ++++++-- docs/examples/vue.md | 3 ++ docs/index.md | 17 +++++++---- docs/install.md | 23 +++++++++++---- package.json | 5 ++++ 11 files changed, 165 insertions(+), 48 deletions(-) diff --git a/docs/build-output.md b/docs/build-output.md index ef4acee..8c6dfea 100644 --- a/docs/build-output.md +++ b/docs/build-output.md @@ -15,7 +15,7 @@ | File | Purpose | |------|---------| -| `latency-test.legacy.esm.js` | Legacy ESM — private fields, optional chaining, and nullish coalescing lowered | +| `latency-test.legacy.esm.js` | Legacy ESM — private fields, optional chaining, and nullish coalescing lowered; reachable via `import '@adasp/latency-test/legacy'` | | `latency-test.legacy.iife.js` | Legacy IIFE — same lowering; used by the demo page | `npm run build:component:all` runs both builds in sequence and is used by CI and `prepublishOnly`. diff --git a/docs/examples/angular.md b/docs/examples/angular.md index 5f0bb12..d6f521f 100644 --- a/docs/examples/angular.md +++ b/docs/examples/angular.md @@ -92,6 +92,7 @@ export class LatencyTesterComponent implements AfterViewInit, OnDestroy { } async connect() { + this.error = null try { this.audioCtx = new AudioContext({ latencyHint: 0 }) this.micStream = await navigator.mediaDevices.getUserMedia(MIC_CONSTRAINTS) @@ -101,6 +102,8 @@ export class LatencyTesterComponent implements AfterViewInit, OnDestroy { } catch (e: any) { this.micStream?.getTracks().forEach(t => t.stop()) this.micStream = null + await this.audioCtx?.close() + this.audioCtx = null this.error = `Could not access mic: ${e.message}` } } @@ -114,6 +117,62 @@ export class LatencyTesterComponent implements AfterViewInit, OnDestroy { --- +## Zoneless apps (Angular CLI 22+ default) + +Angular CLI 22 scaffolds a fully zoneless app by default — zone.js is not included. The `connect()` example above relies on change detection running after `await getUserMedia()` resolves and inside the CustomEvent listeners (`onResult`, `onComplete`, `onError`). Without zone.js, these updates won't reliably reach the template. + +Install zone.js and re-enable it explicitly: + +```bash +npm install zone.js --save +``` + +Load the polyfill before bootstrap — `provideZoneChangeDetection` has nothing to patch until zone.js itself is loaded. Add it to `main.ts`, before the `bootstrapApplication` call: + +```ts +// main.ts +import 'zone.js' +import { bootstrapApplication } from '@angular/platform-browser' +``` + +(Equivalent: add `"polyfills": ["zone.js"]` to the `build` target options in `angular.json` instead of the manual import.) + +```ts +// app.config.ts +import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core' + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + // ...other providers + ] +} +``` + +Even with zone.js re-enabled, call `ChangeDetectorRef.markForCheck()` explicitly after `await getUserMedia()` and inside the CustomEvent callbacks — zone.js does not reliably trigger change detection for these paths on its own: + +```ts +import { ChangeDetectorRef, inject } from '@angular/core' + +export class LatencyTesterComponent implements AfterViewInit, OnDestroy { + private cdr = inject(ChangeDetectorRef) + + async connect() { + try { + // ... + this.isConnected = true + this.cdr.markForCheck() + } catch (e: any) { + // ... + this.error = `Could not access mic: ${e.message}` + this.cdr.markForCheck() + } + } +} +``` + +--- + ## Sharing audio resources from a host app When your application already owns a mic stream and `AudioContext`, pass both to the element. The component will not stop the stream or close the context — the host owns both lifetimes. diff --git a/docs/examples/host-gain.md b/docs/examples/host-gain.md index 1fed88b..dc60b0b 100644 --- a/docs/examples/host-gain.md +++ b/docs/examples/host-gain.md @@ -29,14 +29,20 @@ This pattern introduces an extra browser-managed Web Audio / MediaStream bridge. ```js const ac = new AudioContext({ latencyHint: 0 }) -const stream = await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: false, - noiseSuppression: false, - autoGainControl: false, - channelCount: 1 - } -}) +let stream +try { + stream = await navigator.mediaDevices.getUserMedia({ + audio: { + echoCancellation: false, + noiseSuppression: false, + autoGainControl: false, + channelCount: 1 + } + }) +} catch (e) { + await ac.close() + throw e +} // Build the gain chain. // Adjust gainValue as needed — 50 is a starting point for Safari >= 16 diff --git a/docs/examples/nextjs.md b/docs/examples/nextjs.md index 2f4bca4..8989a64 100644 --- a/docs/examples/nextjs.md +++ b/docs/examples/nextjs.md @@ -59,6 +59,7 @@ export function LatencyTester({ numberOfTests = 5 }: { numberOfTests?: number }) useLatencyTest(onReady) async function connect() { + setError(null) try { const ac = new AudioContext({ latencyHint: 0 }) audioCtxRef.current = ac @@ -70,6 +71,8 @@ export function LatencyTester({ numberOfTests = 5 }: { numberOfTests?: number }) } catch (e: any) { micStreamRef.current?.getTracks().forEach(t => t.stop()) micStreamRef.current = null + await audioCtxRef.current?.close() + audioCtxRef.current = null setError(`Could not access mic: ${e.message}`) } } @@ -209,21 +212,23 @@ el?.start() el?.audioContext // ✅ typed ``` -**React 19+** — `<latency-test>` in JSX picks up types from `HTMLElementTagNameMap` automatically. No manual declarations needed. - -**React < 19** (most current Next.js projects) — add a JSX namespace declaration: +A JSX namespace declaration is required regardless of React version — verified against Next.js 16 + `@types/react` 19.2.17, where `<latency-test>` in JSX does **not** automatically pick up types from `HTMLElementTagNameMap`. Declare it under `declare module 'react'` (a bare `declare namespace JSX { ... }` targets the wrong namespace in React 19 and has no effect): ```ts // types/custom-elements.d.ts -declare namespace JSX { - interface IntrinsicElements { - 'latency-test': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { - 'number-of-tests'?: number - 'mls-bits'?: number - 'max-lag-ms'?: number - 'recording-mode'?: 'mediarecorder' | 'mediarecorder-1ch' | 'audioworklet' - 'signal-type'?: 'mls' - 'buffer-size'?: number +import type {} from 'react' + +declare module 'react' { + namespace JSX { + interface IntrinsicElements { + 'latency-test': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { + 'number-of-tests'?: number + 'mls-bits'?: number + 'max-lag-ms'?: number + 'recording-mode'?: 'mediarecorder' | 'mediarecorder-1ch' | 'audioworklet' + 'signal-type'?: 'mls' + 'buffer-size'?: number + } } } } diff --git a/docs/examples/react.md b/docs/examples/react.md index 00d0487..c22b83e 100644 --- a/docs/examples/react.md +++ b/docs/examples/react.md @@ -41,6 +41,7 @@ export function LatencyTester({ numberOfTests = 5 }) { const [error, setError] = useState(null) async function connect() { + setError(null) try { const ac = new AudioContext({ latencyHint: 0 }) audioCtxRef.current = ac @@ -52,6 +53,8 @@ export function LatencyTester({ numberOfTests = 5 }) { } catch (e) { micStreamRef.current?.getTracks().forEach(t => t.stop()) micStreamRef.current = null + await audioCtxRef.current?.close() + audioCtxRef.current = null setError(`Could not access mic: ${e.message}`) } } @@ -132,20 +135,22 @@ el?.start() el?.audioContext // ✅ typed ``` -**React 19+** — `<latency-test>` in JSX picks up types from `HTMLElementTagNameMap` automatically. No manual declarations needed. - -**React < 19** — add a JSX namespace declaration to avoid template type errors: +A JSX namespace declaration is required regardless of React version — `<latency-test>` in JSX does **not** automatically pick up types from `HTMLElementTagNameMap`. Declare it under `declare module 'react'` (a bare `declare namespace JSX { ... }` targets the wrong namespace in React 19 and has no effect): ```ts // src/custom-elements.d.ts -declare namespace JSX { - interface IntrinsicElements { - 'latency-test': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { - 'number-of-tests'?: number - 'mls-bits'?: number - 'max-lag-ms'?: number - 'recording-mode'?: 'mediarecorder' | 'mediarecorder-1ch' | 'audioworklet' - 'signal-type'?: 'mls' +import type {} from 'react' + +declare module 'react' { + namespace JSX { + interface IntrinsicElements { + 'latency-test': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { + 'number-of-tests'?: number + 'mls-bits'?: number + 'max-lag-ms'?: number + 'recording-mode'?: 'mediarecorder' | 'mediarecorder-1ch' | 'audioworklet' + 'signal-type'?: 'mls' + } } } } diff --git a/docs/examples/svelte.md b/docs/examples/svelte.md index b6a48ff..7eddfa1 100644 --- a/docs/examples/svelte.md +++ b/docs/examples/svelte.md @@ -38,6 +38,7 @@ The recommended pattern is a two-step flow: connect audio first, then run tests. let error = null async function connect() { + error = null try { audioCtx = new AudioContext({ latencyHint: 0 }) micStream = await navigator.mediaDevices.getUserMedia(MIC_CONSTRAINTS) @@ -47,6 +48,8 @@ The recommended pattern is a two-step flow: connect audio first, then run tests. } catch (e) { micStream?.getTracks().forEach(t => t.stop()) micStream = null + await audioCtx?.close() + audioCtx = null error = `Could not access mic: ${e.message}` } } @@ -70,7 +73,7 @@ The recommended pattern is a two-step flow: connect audio first, then run tests. }) </script> -<latency-test bind:this={lt} number-of-tests="5" /> +<latency-test bind:this={lt} number-of-tests="5"></latency-test> {#if !isConnected} <button on:click={connect}>Connect Audio</button> @@ -121,7 +124,7 @@ When your application already owns a mic stream and `AudioContext`, pass both to $: if (lt && inputStream) lt.inputStream = inputStream </script> -<latency-test bind:this={lt} /> +<latency-test bind:this={lt}></latency-test> ``` --- @@ -168,13 +171,15 @@ SvelteKit runs components on the server during SSR. Custom elements that access } catch (e) { micStream?.getTracks().forEach(t => t.stop()) micStream = null + await audioCtx?.close() + audioCtx = null console.error('Could not access mic:', e.message) } } </script> {#if browser && isReady} - <latency-test bind:this={lt} /> + <latency-test bind:this={lt}></latency-test> {#if !isConnected} <button on:click={connect}>Connect Audio</button> {:else} @@ -193,7 +198,7 @@ Types ship with the package. Import `LatencyTestElement` directly: import type { LatencyTestElement } from '@adasp/latency-test' let lt: LatencyTestElement -// <latency-test bind:this={lt} /> +// <latency-test bind:this={lt}></latency-test> lt?.start() lt?.audioContext // ✅ typed diff --git a/docs/examples/vanilla-js.md b/docs/examples/vanilla-js.md index 12a353a..5c53e93 100644 --- a/docs/examples/vanilla-js.md +++ b/docs/examples/vanilla-js.md @@ -16,7 +16,13 @@ The recommended pattern is a two-step flow: connect audio first, then run tests. <!DOCTYPE html> <html lang="en"> <head> - <script type="module" src="https://cdn.jsdelivr.net/npm/@adasp/latency-test@1.2.0/dist/latency-test.esm.js"></script> + <title>Latency Test Demo + @@ -29,7 +35,7 @@ The recommended pattern is a two-step flow: connect audio first, then run tests. -