@crimson_dev/use-resize-observer 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,37 @@ All notable changes to `@crimson_dev/use-resize-observer` will be documented in
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.1] - 2026-03-06
9
+
10
+ ### Changed
11
+ - Dependency upgrades: Biome 2.4.6, tsdown 0.21.0 (stable), Vitest 4.1.0-beta.6, @vitejs/plugin-react 5.1.4, Playwright 1.59.0-alpha-2026-03-06, publint 0.3.18
12
+ - `core.ts`: Replaced `Object.assign(EventTarget)` with proper `ResizeObservableImpl` class extending `EventTarget` — eliminates runtime overhead, cleaner prototype chain
13
+ - `hook-multi.ts`: Replaced closure-based cleanup array with tuple tracking `[Element, ResizeCallback]` — avoids intermediate closure allocation per ref
14
+ - `worker/hook.ts`: Combined separate `width`/`height` `useState` into single state object — batches into 1 render instead of 2 per frame
15
+ - `worker/protocol.ts`: Replaced manual `for` loop in `allocateSlot` with `Int32Array.prototype.indexOf()` — delegates to native code for faster slot scanning
16
+ - Biome schema updated to 2.4.6
17
+
18
+ ## [0.4.0] - 2026-03-06
19
+
20
+ ### Added
21
+ - Benchmark suite modernization: JSON results output to `bench/results/`, 500-element scheduler tier, writeSlot/readSlot roundtrip latency benchmark, heap delta tracking
22
+ - Concurrency stress tests: 1000 elements in same rAF, rapid observe/unobserve cycling, concurrent callbacks, 1000-element scheduler flush
23
+ - Memory pressure tests: 10k cycle leak detection, mass observe/unobserve verification, repeated pool disposal, worker slot bitmap full recycling
24
+ - CI benchmark workflow (`.github/workflows/bench.yml`): auto-runs on push/PR, uploads artifacts, posts PR comments with results
25
+ - Accessibility: ARIA live regions (`aria-live="polite"`, `role="status"`) on visualizer dimension readout, `role="img"` on bar chart
26
+ - Accessibility documentation section in visualizer demo page
27
+
28
+ ### Changed
29
+ - Worker hook now respects `box` option (previously ignored — always read content-box dimensions)
30
+ - Worker hook effect dependency array includes `box` for proper re-subscription on box model change
31
+ - Deep modernization audit of all 18 source files and 14 test files — all confirmed ES2026-compliant
32
+ - V8 optimization audit: all hot paths (`observe`, `unobserve`, `schedule`, `writeSlot`, `readSlot`) confirmed monomorphic with no deoptimizations
33
+ - Performance guide updated with V8 audit results and actual benchmark numbers
34
+ - Test suite expanded: 102 tests across 14 suites (up from 94 tests in 12 suites)
35
+
36
+ ### Fixed
37
+ - Worker hook `box` option was declared but not used — border-box measurements now correctly read from `borderWidth`/`borderHeight` slot values
38
+
8
39
  ## [0.3.0] - 2026-03-05
9
40
 
10
41
  ### Added
@@ -101,6 +132,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
101
132
  - Stable callback identity via ref pattern (React Compiler safe)
102
133
  - Worker mode: SharedArrayBuffer + Float16Array + Atomics for off-main-thread
103
134
 
135
+ [0.4.1]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.4.1
136
+ [0.4.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.4.0
104
137
  [0.3.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.3.0
105
138
  [0.2.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.2.0
106
139
  [0.1.1]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.1.1
package/README.md CHANGED
@@ -14,25 +14,12 @@
14
14
 
15
15
  <br />
16
16
 
17
- [![npm version][npm-badge]][npm-url]
18
- [![bundle size][size-badge]][size-url]
19
- [![CI][ci-badge]][ci-url]
20
- [![license][license-badge]][license-url]
21
- [![TypeScript][ts-badge]][ts-url]
22
- [![docs][docs-badge]][docs-url]
23
-
24
- [npm-badge]: https://img.shields.io/npm/v/@crimson_dev/use-resize-observer?style=flat-square&color=DC143C&labelColor=0D1117
25
- [npm-url]: https://npmjs.com/package/@crimson_dev/use-resize-observer
26
- [size-badge]: https://img.shields.io/bundlephobia/minzip/@crimson_dev/use-resize-observer?style=flat-square&color=DC143C&labelColor=0D1117&label=gzip
27
- [size-url]: https://bundlephobia.com/package/@crimson_dev/use-resize-observer
28
- [ci-badge]: https://img.shields.io/github/actions/workflow/status/ABCrimson/use-resize-observer/ci.yml?style=flat-square&labelColor=0D1117&label=CI
29
- [ci-url]: https://github.com/ABCrimson/use-resize-observer/actions/workflows/ci.yml
30
- [license-badge]: https://img.shields.io/npm/l/@crimson_dev/use-resize-observer?style=flat-square&color=DC143C&labelColor=0D1117
31
- [license-url]: https://github.com/ABCrimson/use-resize-observer/blob/main/LICENSE
32
- [ts-badge]: https://img.shields.io/badge/TypeScript-6.0-3178C6?style=flat-square&labelColor=0D1117
33
- [ts-url]: https://www.typescriptlang.org/
34
- [docs-badge]: https://img.shields.io/badge/docs-GitHub%20Pages-DC143C?style=flat-square&labelColor=0D1117
35
- [docs-url]: https://abcrimson.github.io/use-resize-observer/
17
+ <a href="https://npmjs.com/package/@crimson_dev/use-resize-observer"><img src="https://img.shields.io/npm/v/@crimson_dev/use-resize-observer?style=flat-square&color=DC143C&labelColor=0D1117" alt="npm version"></a>
18
+ <a href="https://bundlephobia.com/package/@crimson_dev/use-resize-observer"><img src="https://img.shields.io/bundlephobia/minzip/@crimson_dev/use-resize-observer?style=flat-square&color=DC143C&labelColor=0D1117&label=gzip" alt="bundle size"></a>
19
+ <a href="https://github.com/ABCrimson/use-resize-observer/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/ABCrimson/use-resize-observer/ci.yml?style=flat-square&labelColor=0D1117&label=CI" alt="CI"></a>
20
+ <a href="https://github.com/ABCrimson/use-resize-observer/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@crimson_dev/use-resize-observer?style=flat-square&color=DC143C&labelColor=0D1117" alt="license"></a>
21
+ <a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-6.0-3178C6?style=flat-square&labelColor=0D1117" alt="TypeScript"></a>
22
+ <a href="https://abcrimson.github.io/use-resize-observer/"><img src="https://img.shields.io/badge/docs-GitHub%20Pages-DC143C?style=flat-square&labelColor=0D1117" alt="docs"></a>
36
23
 
37
24
  </div>
38
25
 
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","names":[],"sources":["../src/core.ts"],"mappings":";;;;;UAqBiB,iBAAA;EAAA,SACN,KAAA;EAAA,SACA,MAAA;EAAA,SACA,KAAA,EAAO,mBAAA;AAAA;;cAIL,WAAA,SAAoB,WAAA,CAAY,iBAAA;EAC3C,WAAA,CAAY,MAAA,EAAQ,iBAAA;AAAA;;UAML,6BAAA;EAPjB;EASE,GAAA,GAAM,wBAAA;AAAA;;UAIS,gBAAA,SAAyB,WAAA,EAAa,UAAA;;EAErD,UAAA;AAAA;;;;;;;;AARF;;;cAqBa,sBAAA,GACX,MAAA,EAAQ,OAAA,EACR,OAAA,GAAS,6BAAA,KACR,gBAAA"}
1
+ {"version":3,"file":"core.d.ts","names":[],"sources":["../src/core.ts"],"mappings":";;;;;UAqBiB,iBAAA;EAAA,SACN,KAAA;EAAA,SACA,MAAA;EAAA,SACA,KAAA,EAAO,mBAAA;AAAA;;cAIL,WAAA,SAAoB,WAAA,CAAY,iBAAA;EAC3C,WAAA,CAAY,MAAA,EAAQ,iBAAA;AAAA;;UAML,6BAAA;EAPjB;EASE,GAAA,GAAM,wBAAA;AAAA;;UAIS,gBAAA,SAAyB,WAAA,EAAa,UAAA;;EAErD,UAAA;AAAA;;;;;;;;AARF;;;cAsDa,sBAAA,GACX,MAAA,EAAQ,OAAA,EACR,OAAA,GAAS,6BAAA,KACR,gBAAA"}
package/dist/core.js CHANGED
@@ -25,6 +25,35 @@ var ResizeEvent = class extends CustomEvent {
25
25
  }
26
26
  };
27
27
  /**
28
+ * Concrete implementation: extends EventTarget directly for zero-overhead
29
+ * event dispatching, avoiding Object.assign runtime cost.
30
+ * @internal
31
+ */
32
+ var ResizeObservableImpl = class extends EventTarget {
33
+ #observer;
34
+ constructor(target, box) {
35
+ super();
36
+ this.#observer = new ResizeObserver((entries) => {
37
+ for (const entry of entries) {
38
+ const sizeEntry = extractBoxSize(entry, box);
39
+ const detail = {
40
+ width: sizeEntry?.inlineSize ?? 0,
41
+ height: sizeEntry?.blockSize ?? 0,
42
+ entry
43
+ };
44
+ this.dispatchEvent(new ResizeEvent(detail));
45
+ }
46
+ });
47
+ this.#observer.observe(target, { box });
48
+ }
49
+ disconnect() {
50
+ this.#observer.disconnect();
51
+ }
52
+ [Symbol.dispose]() {
53
+ this.disconnect();
54
+ }
55
+ };
56
+ /**
28
57
  * Create a framework-agnostic resize observable for an element.
29
58
  *
30
59
  * Wraps a `ResizeObserver` with `EventTarget` dispatching — consumers
@@ -36,28 +65,7 @@ var ResizeEvent = class extends CustomEvent {
36
65
  */
37
66
  const createResizeObservable = (target, options = {}) => {
38
67
  const { box = "content-box" } = options;
39
- const eventTarget = new EventTarget();
40
- const observer = new ResizeObserver((entries) => {
41
- for (const entry of entries) {
42
- const sizeEntry = extractBoxSize(entry, box);
43
- const detail = {
44
- width: sizeEntry?.inlineSize ?? 0,
45
- height: sizeEntry?.blockSize ?? 0,
46
- entry
47
- };
48
- eventTarget.dispatchEvent(new ResizeEvent(detail));
49
- }
50
- });
51
- observer.observe(target, { box });
52
- const disconnect = () => {
53
- observer.disconnect();
54
- };
55
- return Object.assign(eventTarget, {
56
- disconnect,
57
- [Symbol.dispose]() {
58
- disconnect();
59
- }
60
- });
68
+ return new ResizeObservableImpl(target, box);
61
69
  };
62
70
 
63
71
  //#endregion
package/dist/core.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","names":[],"sources":["../src/core.ts"],"sourcesContent":["/**\n * Framework-agnostic core observable for ResizeObserver events.\n *\n * Uses the `EventTarget` API for zero-dependency event dispatching.\n * Can be adapted by any framework (React, Solid, Vue, Svelte, vanilla).\n *\n * Implements `Disposable` for ES2026 `using` declarations.\n *\n * @example\n * ```ts\n * using observable = createResizeObservable(element, { box: 'content-box' });\n * observable.addEventListener('resize', (e) => {\n * console.log(e.detail.width, e.detail.height);\n * });\n * ```\n */\n\nimport { extractBoxSize } from './extract.js';\nimport type { ResizeObserverBoxOptions } from './types.js';\n\n/** Detail payload for resize events dispatched by the observable. */\nexport interface ResizeEventDetail {\n readonly width: number;\n readonly height: number;\n readonly entry: ResizeObserverEntry;\n}\n\n/** Custom event type for resize observations. */\nexport class ResizeEvent extends CustomEvent<ResizeEventDetail> {\n constructor(detail: ResizeEventDetail) {\n super('resize', { detail });\n }\n}\n\n/** Options for the framework-agnostic observable. */\nexport interface CreateResizeObservableOptions {\n /** Which box model to report. @default 'content-box' */\n box?: ResizeObserverBoxOptions;\n}\n\n/** Framework-agnostic resize observable with EventTarget-based dispatching. */\nexport interface ResizeObservable extends EventTarget, Disposable {\n /** Stop observing and clean up resources. */\n disconnect(): void;\n}\n\n/**\n * Create a framework-agnostic resize observable for an element.\n *\n * Wraps a `ResizeObserver` with `EventTarget` dispatching — consumers\n * subscribe via `addEventListener('resize', handler)`.\n *\n * @param target - The DOM element to observe.\n * @param options - Configuration options.\n * @returns A `ResizeObservable` with `addEventListener` and `disconnect`.\n */\nexport const createResizeObservable = (\n target: Element,\n options: CreateResizeObservableOptions = {},\n): ResizeObservable => {\n const { box = 'content-box' } = options;\n const eventTarget = new EventTarget();\n\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const sizeEntry = extractBoxSize(entry, box);\n\n const detail: ResizeEventDetail = {\n width: sizeEntry?.inlineSize ?? 0,\n height: sizeEntry?.blockSize ?? 0,\n entry,\n };\n\n eventTarget.dispatchEvent(new ResizeEvent(detail));\n }\n });\n\n observer.observe(target, { box });\n\n const disconnect = (): void => {\n observer.disconnect();\n };\n\n return Object.assign(eventTarget, {\n disconnect,\n [Symbol.dispose](): void {\n disconnect();\n },\n });\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,cAAb,cAAiC,YAA+B;CAC9D,YAAY,QAA2B;AACrC,QAAM,UAAU,EAAE,QAAQ,CAAC;;;;;;;;;;;;;AA0B/B,MAAa,0BACX,QACA,UAAyC,EAAE,KACtB;CACrB,MAAM,EAAE,MAAM,kBAAkB;CAChC,MAAM,cAAc,IAAI,aAAa;CAErC,MAAM,WAAW,IAAI,gBAAgB,YAAY;AAC/C,OAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,YAAY,eAAe,OAAO,IAAI;GAE5C,MAAM,SAA4B;IAChC,OAAO,WAAW,cAAc;IAChC,QAAQ,WAAW,aAAa;IAChC;IACD;AAED,eAAY,cAAc,IAAI,YAAY,OAAO,CAAC;;GAEpD;AAEF,UAAS,QAAQ,QAAQ,EAAE,KAAK,CAAC;CAEjC,MAAM,mBAAyB;AAC7B,WAAS,YAAY;;AAGvB,QAAO,OAAO,OAAO,aAAa;EAChC;EACA,CAAC,OAAO,WAAiB;AACvB,eAAY;;EAEf,CAAC"}
1
+ {"version":3,"file":"core.js","names":["#observer"],"sources":["../src/core.ts"],"sourcesContent":["/**\n * Framework-agnostic core observable for ResizeObserver events.\n *\n * Uses the `EventTarget` API for zero-dependency event dispatching.\n * Can be adapted by any framework (React, Solid, Vue, Svelte, vanilla).\n *\n * Implements `Disposable` for ES2026 `using` declarations.\n *\n * @example\n * ```ts\n * using observable = createResizeObservable(element, { box: 'content-box' });\n * observable.addEventListener('resize', (e) => {\n * console.log(e.detail.width, e.detail.height);\n * });\n * ```\n */\n\nimport { extractBoxSize } from './extract.js';\nimport type { ResizeObserverBoxOptions } from './types.js';\n\n/** Detail payload for resize events dispatched by the observable. */\nexport interface ResizeEventDetail {\n readonly width: number;\n readonly height: number;\n readonly entry: ResizeObserverEntry;\n}\n\n/** Custom event type for resize observations. */\nexport class ResizeEvent extends CustomEvent<ResizeEventDetail> {\n constructor(detail: ResizeEventDetail) {\n super('resize', { detail });\n }\n}\n\n/** Options for the framework-agnostic observable. */\nexport interface CreateResizeObservableOptions {\n /** Which box model to report. @default 'content-box' */\n box?: ResizeObserverBoxOptions;\n}\n\n/** Framework-agnostic resize observable with EventTarget-based dispatching. */\nexport interface ResizeObservable extends EventTarget, Disposable {\n /** Stop observing and clean up resources. */\n disconnect(): void;\n}\n\n/**\n * Concrete implementation: extends EventTarget directly for zero-overhead\n * event dispatching, avoiding Object.assign runtime cost.\n * @internal\n */\nclass ResizeObservableImpl extends EventTarget implements ResizeObservable {\n readonly #observer: ResizeObserver;\n\n constructor(target: Element, box: ResizeObserverBoxOptions) {\n super();\n this.#observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const sizeEntry = extractBoxSize(entry, box);\n const detail: ResizeEventDetail = {\n width: sizeEntry?.inlineSize ?? 0,\n height: sizeEntry?.blockSize ?? 0,\n entry,\n };\n this.dispatchEvent(new ResizeEvent(detail));\n }\n });\n this.#observer.observe(target, { box });\n }\n\n disconnect(): void {\n this.#observer.disconnect();\n }\n\n [Symbol.dispose](): void {\n this.disconnect();\n }\n}\n\n/**\n * Create a framework-agnostic resize observable for an element.\n *\n * Wraps a `ResizeObserver` with `EventTarget` dispatching — consumers\n * subscribe via `addEventListener('resize', handler)`.\n *\n * @param target - The DOM element to observe.\n * @param options - Configuration options.\n * @returns A `ResizeObservable` with `addEventListener` and `disconnect`.\n */\nexport const createResizeObservable = (\n target: Element,\n options: CreateResizeObservableOptions = {},\n): ResizeObservable => {\n const { box = 'content-box' } = options;\n return new ResizeObservableImpl(target, box);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA4BA,IAAa,cAAb,cAAiC,YAA+B;CAC9D,YAAY,QAA2B;AACrC,QAAM,UAAU,EAAE,QAAQ,CAAC;;;;;;;;AAqB/B,IAAM,uBAAN,cAAmC,YAAwC;CACzE,CAASA;CAET,YAAY,QAAiB,KAA+B;AAC1D,SAAO;AACP,QAAKA,WAAY,IAAI,gBAAgB,YAAY;AAC/C,QAAK,MAAM,SAAS,SAAS;IAC3B,MAAM,YAAY,eAAe,OAAO,IAAI;IAC5C,MAAM,SAA4B;KAChC,OAAO,WAAW,cAAc;KAChC,QAAQ,WAAW,aAAa;KAChC;KACD;AACD,SAAK,cAAc,IAAI,YAAY,OAAO,CAAC;;IAE7C;AACF,QAAKA,SAAU,QAAQ,QAAQ,EAAE,KAAK,CAAC;;CAGzC,aAAmB;AACjB,QAAKA,SAAU,YAAY;;CAG7B,CAAC,OAAO,WAAiB;AACvB,OAAK,YAAY;;;;;;;;;;;;;AAcrB,MAAa,0BACX,QACA,UAAyC,EAAE,KACtB;CACrB,MAAM,EAAE,MAAM,kBAAkB;AAChC,QAAO,IAAI,qBAAqB,QAAQ,IAAI"}
package/dist/index.js CHANGED
@@ -313,7 +313,7 @@ const useResizeObserverEntries = (refs, options = {}) => {
313
313
  const boxRef = useRef(box);
314
314
  boxRef.current = box;
315
315
  useEffect(() => {
316
- const cleanups = [];
316
+ const observed = [];
317
317
  for (const ref of refs) {
318
318
  const element = ref.current;
319
319
  if (!element) continue;
@@ -332,10 +332,10 @@ const useResizeObserverEntries = (refs, options = {}) => {
332
332
  });
333
333
  };
334
334
  pool.observe(element, { box: currentBox }, callback);
335
- cleanups.push(() => pool.unobserve(element, callback));
335
+ observed.push([element, callback]);
336
336
  }
337
337
  return () => {
338
- for (const cleanup of cleanups) cleanup();
338
+ for (const [element, callback] of observed) getSharedPool(root ?? element.ownerDocument).unobserve(element, callback);
339
339
  };
340
340
  }, [refs, root]);
341
341
  return entries;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#queue","#requestFlush","#rafId","#flush","#scheduler","#registry","#finalizer","#observer","#size"],"sources":["../src/context.ts","../src/scheduler.ts","../src/pool.ts","../src/factory.ts","../src/hook.ts","../src/hook-multi.ts"],"sourcesContent":["'use client';\n\nimport type React from 'react';\nimport { createContext, useContext } from 'react';\n\n/**\n * Context for injecting a custom `ResizeObserver` constructor.\n *\n * Useful for:\n * - **Testing**: Inject a mock `ResizeObserver` for deterministic tests.\n * - **SSR**: Inject a no-op implementation to avoid `ReferenceError`.\n * - **Polyfills**: Inject a polyfill without modifying `globalThis`.\n *\n * @example\n * ```tsx\n * // In tests:\n * <ResizeObserverContext.Provider value={MockResizeObserver}>\n * <ComponentThatUsesResize />\n * </ResizeObserverContext.Provider>\n * ```\n */\nexport const ResizeObserverContext: React.Context<typeof ResizeObserver | null> = createContext<\n typeof ResizeObserver | null\n>(null);\n\nResizeObserverContext.displayName = 'ResizeObserverContext';\n\n/**\n * Access the injected ResizeObserver constructor, falling back to the global.\n * @internal\n */\nexport const useResizeObserverConstructor = (): typeof ResizeObserver => {\n const contextValue = useContext(ResizeObserverContext);\n return contextValue ?? globalThis.ResizeObserver;\n};\n","'use client';\n\nimport { startTransition } from 'react';\n\nimport type { ResizeCallback } from './types.js';\n\n/**\n * Per-frame flush entry — snapshot of callbacks + latest entry for one element.\n * @internal\n */\ninterface FlushEntry {\n readonly callbacks: ReadonlySet<ResizeCallback>;\n readonly entry: ResizeObserverEntry;\n}\n\n/**\n * Batching scheduler that coalesces all ResizeObserver callbacks into a single\n * `requestAnimationFrame` flush, wrapped in React `startTransition` for\n * non-urgent update scheduling.\n *\n * Uses a `Map<Element, FlushEntry>` with last-write-wins semantics so that\n * 100 simultaneous resize events produce exactly 1 React render cycle.\n *\n * Implements `Disposable` for ES2026 `using` declarations.\n *\n * @internal\n */\nexport class RafScheduler implements Disposable {\n readonly #queue = new Map<Element, FlushEntry>();\n #rafId: number | null = null;\n\n /** Enqueue a resize observation for the next rAF flush. */\n schedule(target: Element, entry: ResizeObserverEntry, cbs: ReadonlySet<ResizeCallback>): void {\n this.#queue.set(target, { callbacks: cbs, entry });\n this.#requestFlush();\n }\n\n #requestFlush(): void {\n if (this.#rafId !== null) return;\n this.#rafId = requestAnimationFrame(() => {\n this.#rafId = null;\n this.#flush();\n });\n }\n\n #flush(): void {\n // Snapshot and clear before dispatching to avoid re-entrant mutations\n const snapshot = new Map(this.#queue);\n this.#queue.clear();\n\n startTransition(() => {\n for (const { callbacks, entry } of snapshot.values()) {\n for (const cb of callbacks) {\n cb(entry);\n }\n }\n });\n }\n\n /** Cancel any pending rAF and clear the queue. */\n cancel(): void {\n if (this.#rafId !== null) {\n cancelAnimationFrame(this.#rafId);\n this.#rafId = null;\n }\n this.#queue.clear();\n }\n\n /** Disposable contract (ES2026 explicit resource management). */\n [Symbol.dispose](): void {\n this.cancel();\n }\n}\n\n/** Create a new scheduler instance. @internal */\nexport const createScheduler = (): RafScheduler => new RafScheduler();\n","import { createScheduler, type RafScheduler } from './scheduler.js';\nimport type { ResizeCallback } from './types.js';\n\n/**\n * Shared observer pool that multiplexes many element observations through a\n * single `ResizeObserver` instance per document root.\n *\n * Uses `WeakMap` + `FinalizationRegistry` for GC-backed cleanup of detached\n * elements, and `RafScheduler` for batched, non-urgent React state updates.\n *\n * Implements `Disposable` for ES2026 `using` declarations.\n *\n * @internal\n */\nexport class ObserverPool implements Disposable {\n readonly #scheduler: RafScheduler;\n readonly #registry = new WeakMap<Element, Set<ResizeCallback>>();\n readonly #finalizer = new FinalizationRegistry<WeakRef<Element>>((ref) => {\n const el = ref.deref();\n if (el) {\n this.#observer.unobserve(el);\n this.#size--;\n }\n });\n readonly #observer: ResizeObserver;\n #size = 0;\n\n constructor(scheduler?: RafScheduler) {\n this.#scheduler = scheduler ?? createScheduler();\n this.#observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const callbacks = this.#registry.get(entry.target);\n if (callbacks?.size) {\n this.#scheduler.schedule(entry.target, entry, callbacks);\n }\n }\n });\n }\n\n /** Begin observing an element with the given options and callback. */\n observe(target: Element, options: ResizeObserverOptions, cb: ResizeCallback): void {\n let callbacks = this.#registry.get(target);\n if (!callbacks) {\n callbacks = new Set();\n this.#registry.set(target, callbacks);\n this.#finalizer.register(target, new WeakRef(target), target);\n this.#observer.observe(target, options);\n this.#size++;\n }\n callbacks.add(cb);\n }\n\n /** Stop a specific callback from observing the target. */\n unobserve(target: Element, cb: ResizeCallback): void {\n const callbacks = this.#registry.get(target);\n if (!callbacks) return;\n callbacks.delete(cb);\n if (callbacks.size === 0) {\n this.#registry.delete(target);\n this.#finalizer.unregister(target);\n this.#observer.unobserve(target);\n this.#size--;\n }\n }\n\n /** Number of currently observed elements. */\n get observedCount(): number {\n return this.#size;\n }\n\n /** Disposable contract (ES2026 explicit resource management). */\n [Symbol.dispose](): void {\n this.#observer.disconnect();\n this.#scheduler.cancel();\n this.#size = 0;\n }\n}\n\n/**\n * Module-level weak registry of pools per document/shadow root.\n * Ensures a single shared pool per root context.\n */\nconst poolRegistry = new WeakMap<Document | ShadowRoot, ObserverPool>();\n\n/**\n * Get or create the shared observer pool for the given root.\n * Uses `Promise.try()` (ES2026) for safe async-context creation\n * with synchronous return path.\n *\n * @param root - Document or ShadowRoot to scope the pool to.\n * @returns The shared `ObserverPool` for the given root.\n * @internal\n */\nexport const getSharedPool = (root: Document | ShadowRoot): ObserverPool => {\n const existing = poolRegistry.get(root);\n if (existing) return existing;\n\n // Fire-and-forget: validates ResizeObserver availability asynchronously.\n // Errors surface via console.error, not thrown to caller (sync return path).\n //\n // Promise.try() (ES2026) — safely wraps synchronous pool creation in a\n // microtask-aware context, catching any constructor exceptions into a\n // rejected promise for diagnostics while returning synchronously.\n Promise.try(() => {\n if (typeof globalThis.ResizeObserver === 'undefined') {\n throw new Error(\n '[@crimson_dev/use-resize-observer] ResizeObserver is not available. ' +\n 'Import the /shim entry or use the /server entry for SSR.',\n );\n }\n }).catch((error: unknown) => {\n console.error(Error.isError(error) ? error : new Error(String(error)));\n });\n\n const pool = new ObserverPool();\n poolRegistry.set(root, pool);\n return pool;\n};\n","import { getSharedPool } from './pool.js';\nimport type {\n CreateResizeObserverOptions,\n ResizeCallback,\n ResizeObserverFactory,\n} from './types.js';\n\n/**\n * Framework-agnostic factory for creating a ResizeObserver subscription\n * using the shared pool architecture.\n *\n * Uses the same pool and scheduler as the React hook — no duplicate observers.\n * Implements cleanup tracking with `Map` for efficient iteration.\n *\n * @param options - Configuration options.\n * @returns An object with `observe`, `unobserve`, and `disconnect` methods.\n *\n * @example\n * ```ts\n * using observer = createResizeObserver({ box: 'border-box' });\n * observer.observe(element, (entry) => {\n * console.log(entry.contentRect.width);\n * });\n * ```\n */\nexport const createResizeObserver = (\n options: CreateResizeObserverOptions = {},\n): ResizeObserverFactory & Disposable => {\n const { box = 'content-box', root = globalThis.document } = options;\n const pool = getSharedPool(root);\n const tracked = new Map<Element, Set<ResizeCallback>>();\n\n const observe = (target: Element, callback: ResizeCallback): void => {\n pool.observe(target, { box }, callback);\n\n let cbs = tracked.get(target);\n if (!cbs) {\n cbs = new Set();\n tracked.set(target, cbs);\n }\n cbs.add(callback);\n };\n\n const unobserve = (target: Element, callback: ResizeCallback): void => {\n pool.unobserve(target, callback);\n const cbs = tracked.get(target);\n if (cbs) {\n cbs.delete(callback);\n if (cbs.size === 0) tracked.delete(target);\n }\n };\n\n const disconnect = (): void => {\n for (const [target, cbs] of tracked) {\n for (const cb of cbs) {\n pool.unobserve(target, cb);\n }\n }\n tracked.clear();\n };\n\n return {\n observe,\n unobserve,\n disconnect,\n [Symbol.dispose](): void {\n disconnect();\n },\n };\n};\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { extractDimensions } from './extract.js';\nimport { getSharedPool } from './pool.js';\nimport type {\n ResizeCallback,\n ResizeObserverBoxOptions,\n UseResizeObserverOptions,\n UseResizeObserverResult,\n} from './types.js';\n\n/** Internal state shape — single object to batch width+height+entry in one setState. */\ninterface ObserverState {\n readonly width: number;\n readonly height: number;\n readonly entry: ResizeObserverEntry;\n}\n\n/**\n * Primary React hook for observing element resize events.\n *\n * Features:\n * - Single shared `ResizeObserver` per document root (pool architecture)\n * - `requestAnimationFrame` batching with `startTransition` wrapping\n * - GC-backed cleanup via `FinalizationRegistry`\n * - React Compiler-safe (stable callback identity via ref pattern)\n * - Sub-1.1 kB gzip bundle contribution\n *\n * @param options - Configuration options.\n * @returns Ref, width, height, and raw entry.\n *\n * @example\n * ```tsx\n * const { ref, width, height } = useResizeObserver<HTMLDivElement>();\n * return <div ref={ref}>Size: {width} x {height}</div>;\n * ```\n */\nexport const useResizeObserver = <T extends Element = Element>(\n options: UseResizeObserverOptions<T> = {},\n): UseResizeObserverResult<T> => {\n const { ref: externalRef, box = 'content-box', root, onResize } = options;\n\n const internalRef = useRef<T | null>(null);\n const targetRef = externalRef ?? internalRef;\n\n const [state, setState] = useState<ObserverState | undefined>(undefined);\n\n // Stable callback ref — survives re-renders without triggering re-observation.\n // Follows useEffectEvent semantics: latest closure captured, identity stable.\n const onResizeRef = useRef(onResize);\n onResizeRef.current = onResize;\n\n const boxRef = useRef(box);\n boxRef.current = box;\n\n useEffect(() => {\n const element = targetRef.current;\n if (!element) return;\n\n const observerRoot = root ?? element.ownerDocument;\n const pool = getSharedPool(observerRoot);\n\n const callback: ResizeCallback = (resizeEntry) => {\n const { width: w, height: h } = extractDimensions(resizeEntry, boxRef.current);\n setState({ width: w, height: h, entry: resizeEntry });\n onResizeRef.current?.(resizeEntry);\n };\n\n pool.observe(element, { box }, callback);\n\n return () => {\n pool.unobserve(element, callback);\n };\n }, [targetRef, box, root]);\n\n return {\n ref: targetRef,\n width: state?.width,\n height: state?.height,\n entry: state?.entry,\n };\n};\n\nexport type { ResizeObserverBoxOptions, UseResizeObserverOptions, UseResizeObserverResult };\n","'use client';\n\nimport type { RefObject } from 'react';\nimport { useEffect, useRef, useState } from 'react';\n\nimport { extractBoxSize } from './extract.js';\nimport { getSharedPool } from './pool.js';\nimport type { ResizeCallback, ResizeObserverBoxOptions } from './types.js';\n\n/** Entry data for a single observed element in the multi-element hook. */\nexport interface ResizeEntry {\n readonly width: number;\n readonly height: number;\n readonly entry: ResizeObserverEntry;\n}\n\n/** Options for `useResizeObserverEntries`. */\nexport interface UseResizeObserverEntriesOptions {\n /** Which box model to report. @default 'content-box' */\n box?: ResizeObserverBoxOptions;\n /** Document or ShadowRoot scoping the pool. @default document */\n root?: Document | ShadowRoot;\n}\n\n/**\n * Multi-element variant: observe multiple elements simultaneously through\n * a single pool subscription.\n *\n * @param refs - Array of refs pointing to elements to observe.\n * @param options - Configuration options.\n * @returns A `Map<Element, ResizeEntry>` keyed by observed element.\n *\n * @example\n * ```tsx\n * const ref1 = useRef<HTMLDivElement>(null);\n * const ref2 = useRef<HTMLDivElement>(null);\n * const entries = useResizeObserverEntries([ref1, ref2]);\n * ```\n */\nexport const useResizeObserverEntries = (\n refs: ReadonlyArray<RefObject<Element | null>>,\n options: UseResizeObserverEntriesOptions = {},\n): Map<Element, ResizeEntry> => {\n const { box = 'content-box', root } = options;\n const [entries, setEntries] = useState<Map<Element, ResizeEntry>>(new Map());\n const boxRef = useRef(box);\n boxRef.current = box;\n\n useEffect(() => {\n const cleanups: Array<() => void> = [];\n\n for (const ref of refs) {\n const element = ref.current;\n if (!element) continue;\n\n const observerRoot = root ?? element.ownerDocument;\n const pool = getSharedPool(observerRoot);\n const currentBox = boxRef.current;\n\n const callback: ResizeCallback = (resizeEntry) => {\n const sizeEntry = extractBoxSize(resizeEntry, currentBox);\n\n setEntries((prev) => {\n const next = new Map(prev);\n next.set(element, {\n width: sizeEntry?.inlineSize ?? 0,\n height: sizeEntry?.blockSize ?? 0,\n entry: resizeEntry,\n });\n return next;\n });\n };\n\n pool.observe(element, { box: currentBox }, callback);\n cleanups.push(() => pool.unobserve(element, callback));\n }\n\n return () => {\n for (const cleanup of cleanups) {\n cleanup();\n }\n };\n }, [refs, root]);\n\n return entries;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#queue","#requestFlush","#rafId","#flush","#scheduler","#registry","#finalizer","#observer","#size"],"sources":["../src/context.ts","../src/scheduler.ts","../src/pool.ts","../src/factory.ts","../src/hook.ts","../src/hook-multi.ts"],"sourcesContent":["'use client';\n\nimport type React from 'react';\nimport { createContext, useContext } from 'react';\n\n/**\n * Context for injecting a custom `ResizeObserver` constructor.\n *\n * Useful for:\n * - **Testing**: Inject a mock `ResizeObserver` for deterministic tests.\n * - **SSR**: Inject a no-op implementation to avoid `ReferenceError`.\n * - **Polyfills**: Inject a polyfill without modifying `globalThis`.\n *\n * @example\n * ```tsx\n * // In tests:\n * <ResizeObserverContext.Provider value={MockResizeObserver}>\n * <ComponentThatUsesResize />\n * </ResizeObserverContext.Provider>\n * ```\n */\nexport const ResizeObserverContext: React.Context<typeof ResizeObserver | null> = createContext<\n typeof ResizeObserver | null\n>(null);\n\nResizeObserverContext.displayName = 'ResizeObserverContext';\n\n/**\n * Access the injected ResizeObserver constructor, falling back to the global.\n * @internal\n */\nexport const useResizeObserverConstructor = (): typeof ResizeObserver => {\n const contextValue = useContext(ResizeObserverContext);\n return contextValue ?? globalThis.ResizeObserver;\n};\n","'use client';\n\nimport { startTransition } from 'react';\n\nimport type { ResizeCallback } from './types.js';\n\n/**\n * Per-frame flush entry — snapshot of callbacks + latest entry for one element.\n * @internal\n */\ninterface FlushEntry {\n readonly callbacks: ReadonlySet<ResizeCallback>;\n readonly entry: ResizeObserverEntry;\n}\n\n/**\n * Batching scheduler that coalesces all ResizeObserver callbacks into a single\n * `requestAnimationFrame` flush, wrapped in React `startTransition` for\n * non-urgent update scheduling.\n *\n * Uses a `Map<Element, FlushEntry>` with last-write-wins semantics so that\n * 100 simultaneous resize events produce exactly 1 React render cycle.\n *\n * Implements `Disposable` for ES2026 `using` declarations.\n *\n * @internal\n */\nexport class RafScheduler implements Disposable {\n readonly #queue = new Map<Element, FlushEntry>();\n #rafId: number | null = null;\n\n /** Enqueue a resize observation for the next rAF flush. */\n schedule(target: Element, entry: ResizeObserverEntry, cbs: ReadonlySet<ResizeCallback>): void {\n this.#queue.set(target, { callbacks: cbs, entry });\n this.#requestFlush();\n }\n\n #requestFlush(): void {\n if (this.#rafId !== null) return;\n this.#rafId = requestAnimationFrame(() => {\n this.#rafId = null;\n this.#flush();\n });\n }\n\n #flush(): void {\n // Snapshot and clear before dispatching to avoid re-entrant mutations\n const snapshot = new Map(this.#queue);\n this.#queue.clear();\n\n startTransition(() => {\n for (const { callbacks, entry } of snapshot.values()) {\n for (const cb of callbacks) {\n cb(entry);\n }\n }\n });\n }\n\n /** Cancel any pending rAF and clear the queue. */\n cancel(): void {\n if (this.#rafId !== null) {\n cancelAnimationFrame(this.#rafId);\n this.#rafId = null;\n }\n this.#queue.clear();\n }\n\n /** Disposable contract (ES2026 explicit resource management). */\n [Symbol.dispose](): void {\n this.cancel();\n }\n}\n\n/** Create a new scheduler instance. @internal */\nexport const createScheduler = (): RafScheduler => new RafScheduler();\n","import { createScheduler, type RafScheduler } from './scheduler.js';\nimport type { ResizeCallback } from './types.js';\n\n/**\n * Shared observer pool that multiplexes many element observations through a\n * single `ResizeObserver` instance per document root.\n *\n * Uses `WeakMap` + `FinalizationRegistry` for GC-backed cleanup of detached\n * elements, and `RafScheduler` for batched, non-urgent React state updates.\n *\n * Implements `Disposable` for ES2026 `using` declarations.\n *\n * @internal\n */\nexport class ObserverPool implements Disposable {\n readonly #scheduler: RafScheduler;\n readonly #registry = new WeakMap<Element, Set<ResizeCallback>>();\n readonly #finalizer = new FinalizationRegistry<WeakRef<Element>>((ref) => {\n const el = ref.deref();\n if (el) {\n this.#observer.unobserve(el);\n this.#size--;\n }\n });\n readonly #observer: ResizeObserver;\n #size = 0;\n\n constructor(scheduler?: RafScheduler) {\n this.#scheduler = scheduler ?? createScheduler();\n this.#observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const callbacks = this.#registry.get(entry.target);\n if (callbacks?.size) {\n this.#scheduler.schedule(entry.target, entry, callbacks);\n }\n }\n });\n }\n\n /** Begin observing an element with the given options and callback. */\n observe(target: Element, options: ResizeObserverOptions, cb: ResizeCallback): void {\n let callbacks = this.#registry.get(target);\n if (!callbacks) {\n callbacks = new Set();\n this.#registry.set(target, callbacks);\n this.#finalizer.register(target, new WeakRef(target), target);\n this.#observer.observe(target, options);\n this.#size++;\n }\n callbacks.add(cb);\n }\n\n /** Stop a specific callback from observing the target. */\n unobserve(target: Element, cb: ResizeCallback): void {\n const callbacks = this.#registry.get(target);\n if (!callbacks) return;\n callbacks.delete(cb);\n if (callbacks.size === 0) {\n this.#registry.delete(target);\n this.#finalizer.unregister(target);\n this.#observer.unobserve(target);\n this.#size--;\n }\n }\n\n /** Number of currently observed elements. */\n get observedCount(): number {\n return this.#size;\n }\n\n /** Disposable contract (ES2026 explicit resource management). */\n [Symbol.dispose](): void {\n this.#observer.disconnect();\n this.#scheduler.cancel();\n this.#size = 0;\n }\n}\n\n/**\n * Module-level weak registry of pools per document/shadow root.\n * Ensures a single shared pool per root context.\n */\nconst poolRegistry = new WeakMap<Document | ShadowRoot, ObserverPool>();\n\n/**\n * Get or create the shared observer pool for the given root.\n * Uses `Promise.try()` (ES2026) for safe async-context creation\n * with synchronous return path.\n *\n * @param root - Document or ShadowRoot to scope the pool to.\n * @returns The shared `ObserverPool` for the given root.\n * @internal\n */\nexport const getSharedPool = (root: Document | ShadowRoot): ObserverPool => {\n const existing = poolRegistry.get(root);\n if (existing) return existing;\n\n // Fire-and-forget: validates ResizeObserver availability asynchronously.\n // Errors surface via console.error, not thrown to caller (sync return path).\n //\n // Promise.try() (ES2026) — safely wraps synchronous pool creation in a\n // microtask-aware context, catching any constructor exceptions into a\n // rejected promise for diagnostics while returning synchronously.\n Promise.try(() => {\n if (typeof globalThis.ResizeObserver === 'undefined') {\n throw new Error(\n '[@crimson_dev/use-resize-observer] ResizeObserver is not available. ' +\n 'Import the /shim entry or use the /server entry for SSR.',\n );\n }\n }).catch((error: unknown) => {\n console.error(Error.isError(error) ? error : new Error(String(error)));\n });\n\n const pool = new ObserverPool();\n poolRegistry.set(root, pool);\n return pool;\n};\n","import { getSharedPool } from './pool.js';\nimport type {\n CreateResizeObserverOptions,\n ResizeCallback,\n ResizeObserverFactory,\n} from './types.js';\n\n/**\n * Framework-agnostic factory for creating a ResizeObserver subscription\n * using the shared pool architecture.\n *\n * Uses the same pool and scheduler as the React hook — no duplicate observers.\n * Implements cleanup tracking with `Map` for efficient iteration.\n *\n * @param options - Configuration options.\n * @returns An object with `observe`, `unobserve`, and `disconnect` methods.\n *\n * @example\n * ```ts\n * using observer = createResizeObserver({ box: 'border-box' });\n * observer.observe(element, (entry) => {\n * console.log(entry.contentRect.width);\n * });\n * ```\n */\nexport const createResizeObserver = (\n options: CreateResizeObserverOptions = {},\n): ResizeObserverFactory & Disposable => {\n const { box = 'content-box', root = globalThis.document } = options;\n const pool = getSharedPool(root);\n const tracked = new Map<Element, Set<ResizeCallback>>();\n\n const observe = (target: Element, callback: ResizeCallback): void => {\n pool.observe(target, { box }, callback);\n\n let cbs = tracked.get(target);\n if (!cbs) {\n cbs = new Set();\n tracked.set(target, cbs);\n }\n cbs.add(callback);\n };\n\n const unobserve = (target: Element, callback: ResizeCallback): void => {\n pool.unobserve(target, callback);\n const cbs = tracked.get(target);\n if (cbs) {\n cbs.delete(callback);\n if (cbs.size === 0) tracked.delete(target);\n }\n };\n\n const disconnect = (): void => {\n for (const [target, cbs] of tracked) {\n for (const cb of cbs) {\n pool.unobserve(target, cb);\n }\n }\n tracked.clear();\n };\n\n return {\n observe,\n unobserve,\n disconnect,\n [Symbol.dispose](): void {\n disconnect();\n },\n };\n};\n","'use client';\n\nimport { useEffect, useRef, useState } from 'react';\n\nimport { extractDimensions } from './extract.js';\nimport { getSharedPool } from './pool.js';\nimport type {\n ResizeCallback,\n ResizeObserverBoxOptions,\n UseResizeObserverOptions,\n UseResizeObserverResult,\n} from './types.js';\n\n/** Internal state shape — single object to batch width+height+entry in one setState. */\ninterface ObserverState {\n readonly width: number;\n readonly height: number;\n readonly entry: ResizeObserverEntry;\n}\n\n/**\n * Primary React hook for observing element resize events.\n *\n * Features:\n * - Single shared `ResizeObserver` per document root (pool architecture)\n * - `requestAnimationFrame` batching with `startTransition` wrapping\n * - GC-backed cleanup via `FinalizationRegistry`\n * - React Compiler-safe (stable callback identity via ref pattern)\n * - Sub-1.1 kB gzip bundle contribution\n *\n * @param options - Configuration options.\n * @returns Ref, width, height, and raw entry.\n *\n * @example\n * ```tsx\n * const { ref, width, height } = useResizeObserver<HTMLDivElement>();\n * return <div ref={ref}>Size: {width} x {height}</div>;\n * ```\n */\nexport const useResizeObserver = <T extends Element = Element>(\n options: UseResizeObserverOptions<T> = {},\n): UseResizeObserverResult<T> => {\n const { ref: externalRef, box = 'content-box', root, onResize } = options;\n\n const internalRef = useRef<T | null>(null);\n const targetRef = externalRef ?? internalRef;\n\n const [state, setState] = useState<ObserverState | undefined>(undefined);\n\n // Stable callback ref — survives re-renders without triggering re-observation.\n // Follows useEffectEvent semantics: latest closure captured, identity stable.\n const onResizeRef = useRef(onResize);\n onResizeRef.current = onResize;\n\n const boxRef = useRef(box);\n boxRef.current = box;\n\n useEffect(() => {\n const element = targetRef.current;\n if (!element) return;\n\n const observerRoot = root ?? element.ownerDocument;\n const pool = getSharedPool(observerRoot);\n\n const callback: ResizeCallback = (resizeEntry) => {\n const { width: w, height: h } = extractDimensions(resizeEntry, boxRef.current);\n setState({ width: w, height: h, entry: resizeEntry });\n onResizeRef.current?.(resizeEntry);\n };\n\n pool.observe(element, { box }, callback);\n\n return () => {\n pool.unobserve(element, callback);\n };\n }, [targetRef, box, root]);\n\n return {\n ref: targetRef,\n width: state?.width,\n height: state?.height,\n entry: state?.entry,\n };\n};\n\nexport type { ResizeObserverBoxOptions, UseResizeObserverOptions, UseResizeObserverResult };\n","'use client';\n\nimport type { RefObject } from 'react';\nimport { useEffect, useRef, useState } from 'react';\n\nimport { extractBoxSize } from './extract.js';\nimport { getSharedPool } from './pool.js';\nimport type { ResizeCallback, ResizeObserverBoxOptions } from './types.js';\n\n/** Entry data for a single observed element in the multi-element hook. */\nexport interface ResizeEntry {\n readonly width: number;\n readonly height: number;\n readonly entry: ResizeObserverEntry;\n}\n\n/** Options for `useResizeObserverEntries`. */\nexport interface UseResizeObserverEntriesOptions {\n /** Which box model to report. @default 'content-box' */\n box?: ResizeObserverBoxOptions;\n /** Document or ShadowRoot scoping the pool. @default document */\n root?: Document | ShadowRoot;\n}\n\n/**\n * Multi-element variant: observe multiple elements simultaneously through\n * a single pool subscription.\n *\n * @param refs - Array of refs pointing to elements to observe.\n * @param options - Configuration options.\n * @returns A `Map<Element, ResizeEntry>` keyed by observed element.\n *\n * @example\n * ```tsx\n * const ref1 = useRef<HTMLDivElement>(null);\n * const ref2 = useRef<HTMLDivElement>(null);\n * const entries = useResizeObserverEntries([ref1, ref2]);\n * ```\n */\nexport const useResizeObserverEntries = (\n refs: ReadonlyArray<RefObject<Element | null>>,\n options: UseResizeObserverEntriesOptions = {},\n): Map<Element, ResizeEntry> => {\n const { box = 'content-box', root } = options;\n const [entries, setEntries] = useState<Map<Element, ResizeEntry>>(new Map());\n const boxRef = useRef(box);\n boxRef.current = box;\n\n useEffect(() => {\n // Track observed pairs for cleanup — avoids separate closure array\n const observed: Array<readonly [Element, ResizeCallback]> = [];\n\n for (const ref of refs) {\n const element = ref.current;\n if (!element) continue;\n\n const observerRoot = root ?? element.ownerDocument;\n const pool = getSharedPool(observerRoot);\n const currentBox = boxRef.current;\n\n const callback: ResizeCallback = (resizeEntry) => {\n const sizeEntry = extractBoxSize(resizeEntry, currentBox);\n\n setEntries((prev) => {\n const next = new Map(prev);\n next.set(element, {\n width: sizeEntry?.inlineSize ?? 0,\n height: sizeEntry?.blockSize ?? 0,\n entry: resizeEntry,\n });\n return next;\n });\n };\n\n pool.observe(element, { box: currentBox }, callback);\n observed.push([element, callback] as const);\n }\n\n return () => {\n for (const [element, callback] of observed) {\n const observerRoot = root ?? element.ownerDocument;\n getSharedPool(observerRoot).unobserve(element, callback);\n }\n };\n }, [refs, root]);\n\n return entries;\n};\n"],"mappings":"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"}
package/dist/worker.js CHANGED
@@ -19,7 +19,7 @@ const SLOT_BYTES = 8;
19
19
  /** Maximum number of simultaneously observable elements. */
20
20
  const MAX_ELEMENTS = 256;
21
21
  /** Total SharedArrayBuffer size in bytes. */
22
- const SAB_SIZE = SLOT_BYTES * MAX_ELEMENTS;
22
+ const SAB_SIZE = 8 * 256;
23
23
  /** Offsets within a single Float16Array slot. */
24
24
  const SlotOffset = {
25
25
  InlineSize: 0,
@@ -37,7 +37,7 @@ const SlotOffset = {
37
37
  * @param entry - ResizeObserverEntry from the Worker's observer.
38
38
  */
39
39
  const writeSlot = (sab, slotId, entry) => {
40
- const view = new Float16Array(sab, slotId * SLOT_BYTES, 4);
40
+ const view = new Float16Array(sab, slotId * 8, 4);
41
41
  const cs = entry.contentBoxSize[0];
42
42
  const bs = entry.borderBoxSize[0];
43
43
  view[SlotOffset.InlineSize] = cs?.inlineSize ?? 0;
@@ -54,7 +54,7 @@ const writeSlot = (sab, slotId, entry) => {
54
54
  * @returns Measurement object with width, height, and border dimensions.
55
55
  */
56
56
  const readSlot = (sab, slotId) => {
57
- const view = new Float16Array(sab, slotId * SLOT_BYTES, 4);
57
+ const view = new Float16Array(sab, slotId * 8, 4);
58
58
  return {
59
59
  width: view[SlotOffset.InlineSize] ?? 0,
60
60
  height: view[SlotOffset.BlockSize] ?? 0,
@@ -64,17 +64,15 @@ const readSlot = (sab, slotId) => {
64
64
  };
65
65
  /**
66
66
  * Allocate a slot from the bitmap tracker.
67
- * Scans for the first unallocated slot in O(n) worst case.
67
+ * Uses `Int32Array.prototype.indexOf()` for fast native-code scan.
68
68
  *
69
69
  * @param bitmap - Int32Array tracking allocated slots (1 = in use).
70
70
  * @returns The allocated slot index, or -1 if all slots are in use.
71
71
  */
72
72
  const allocateSlot = (bitmap) => {
73
- for (let i = 0; i < MAX_ELEMENTS; i++) if (bitmap[i] === 0) {
74
- bitmap[i] = 1;
75
- return i;
76
- }
77
- return -1;
73
+ const i = bitmap.indexOf(0);
74
+ if (i !== -1) bitmap[i] = 1;
75
+ return i;
78
76
  };
79
77
  /**
80
78
  * Release a slot back to the bitmap tracker.
@@ -83,7 +81,7 @@ const allocateSlot = (bitmap) => {
83
81
  * @param slotId - The slot index to release.
84
82
  */
85
83
  const releaseSlot = (bitmap, slotId) => {
86
- if (slotId >= 0 && slotId < MAX_ELEMENTS) bitmap[slotId] = 0;
84
+ if (slotId >= 0 && slotId < 256) bitmap[slotId] = 0;
87
85
  };
88
86
 
89
87
  //#endregion
@@ -91,7 +89,7 @@ const releaseSlot = (bitmap, slotId) => {
91
89
  /** Shared Worker instance — lazy-initialized, lives until last observer unmounts. */
92
90
  let sharedWorker = null;
93
91
  let sharedSab = null;
94
- const slotBitmap = new Int32Array(MAX_ELEMENTS);
92
+ const slotBitmap = new Int32Array(256);
95
93
  let activeObserverCount = 0;
96
94
  let workerReady = false;
97
95
  /** Promise that resolves when the Worker is initialized and ready. */
@@ -154,19 +152,20 @@ const terminateWorkerIfIdle = () => {
154
152
  * @returns Ref, width, height, and raw entry (entry is `undefined` in Worker mode).
155
153
  */
156
154
  const useResizeObserverWorker = (options = {}) => {
157
- const { ref: externalRef, onResize } = options;
155
+ const { ref: externalRef, box = "content-box", onResize } = options;
158
156
  const internalRef = useRef(null);
159
157
  const targetRef = externalRef ?? internalRef;
160
- const [width, setWidth] = useState(void 0);
161
- const [height, setHeight] = useState(void 0);
158
+ const [state, setState] = useState(void 0);
162
159
  const onResizeRef = useRef(onResize);
163
160
  onResizeRef.current = onResize;
161
+ const boxRef = useRef(box);
162
+ boxRef.current = box;
164
163
  useEffect(() => {
165
164
  const element = targetRef.current;
166
165
  if (!element) return;
167
166
  const slotId = allocateSlot(slotBitmap);
168
167
  if (slotId === -1) {
169
- console.error(`[@crimson_dev/use-resize-observer/worker] Maximum ${String(MAX_ELEMENTS)} simultaneous observations exceeded.`);
168
+ console.error(`[@crimson_dev/use-resize-observer/worker] Maximum ${String(256)} simultaneous observations exceeded.`);
170
169
  return;
171
170
  }
172
171
  activeObserverCount++;
@@ -175,9 +174,14 @@ const useResizeObserverWorker = (options = {}) => {
175
174
  const startPolling = () => {
176
175
  const poll = () => {
177
176
  if (cancelled || !sharedSab) return;
178
- const { width: w, height: h } = readSlot(sharedSab, slotId);
179
- setWidth(w);
180
- setHeight(h);
177
+ const slot = readSlot(sharedSab, slotId);
178
+ const useBorder = boxRef.current === "border-box";
179
+ const w = useBorder ? slot.borderWidth : slot.width;
180
+ const h = useBorder ? slot.borderHeight : slot.height;
181
+ setState({
182
+ width: w,
183
+ height: h
184
+ });
181
185
  onResizeRef.current?.({
182
186
  width: w,
183
187
  height: h
@@ -208,11 +212,11 @@ const useResizeObserverWorker = (options = {}) => {
208
212
  activeObserverCount--;
209
213
  terminateWorkerIfIdle();
210
214
  };
211
- }, [targetRef]);
215
+ }, [targetRef, box]);
212
216
  return {
213
217
  ref: targetRef,
214
- width,
215
- height,
218
+ width: state?.width,
219
+ height: state?.height,
216
220
  entry: void 0
217
221
  };
218
222
  };
@@ -1 +1 @@
1
- {"version":3,"file":"worker.js","names":[],"sources":["../src/worker/protocol.ts","../src/worker/hook.ts"],"sourcesContent":["/**\n * SharedArrayBuffer protocol for Worker-based resize observations.\n *\n * Layout:\n * - 4 Float16 values per element slot (2 bytes each = 8 bytes per slot)\n * - Int32Array overlay for `Atomics.notify()` / `Atomics.waitAsync()` synchronization\n * - Supports up to 256 simultaneous element observations\n *\n * @internal\n */\n\n/** Bytes per observation slot: 4 x Float16 (2 bytes each) = 8 bytes. */\nexport const SLOT_BYTES: number = 8;\n\n/** Maximum number of simultaneously observable elements. */\nexport const MAX_ELEMENTS: number = 256;\n\n/** Total SharedArrayBuffer size in bytes. */\nexport const SAB_SIZE: number = SLOT_BYTES * MAX_ELEMENTS;\n\n/** Offsets within a single Float16Array slot. */\nexport const SlotOffset = {\n InlineSize: 0,\n BlockSize: 1,\n BorderInline: 2,\n BorderBlock: 3,\n} as const;\n\nexport type SlotOffsetKey = keyof typeof SlotOffset;\n\n/** Discriminated union of all Worker protocol messages. */\nexport type WorkerMessage =\n | { readonly op: 'init'; readonly sab: SharedArrayBuffer }\n | { readonly op: 'observe'; readonly slotId: number; readonly elementId: string }\n | { readonly op: 'unobserve'; readonly slotId: number }\n | { readonly op: 'terminate' }\n | { readonly op: 'ready' }\n | { readonly op: 'error'; readonly message: string };\n\n/**\n * Write resize measurements into a SharedArrayBuffer slot.\n * Uses `Float16Array` (ES2026) for compact storage and\n * `Atomics.notify()` for cross-thread signaling.\n *\n * @param sab - SharedArrayBuffer backing the measurement protocol.\n * @param slotId - Zero-based slot index for this element.\n * @param entry - ResizeObserverEntry from the Worker's observer.\n */\nexport const writeSlot = (\n sab: SharedArrayBuffer,\n slotId: number,\n entry: ResizeObserverEntry,\n): void => {\n const view = new Float16Array(sab, slotId * SLOT_BYTES, 4);\n const cs = entry.contentBoxSize[0];\n const bs = entry.borderBoxSize[0];\n view[SlotOffset.InlineSize] = cs?.inlineSize ?? 0;\n view[SlotOffset.BlockSize] = cs?.blockSize ?? 0;\n view[SlotOffset.BorderInline] = bs?.inlineSize ?? 0;\n view[SlotOffset.BorderBlock] = bs?.blockSize ?? 0;\n // Signal main thread that new data is available\n Atomics.notify(new Int32Array(sab), slotId, 1);\n};\n\n/**\n * Read resize measurements from a SharedArrayBuffer slot.\n *\n * @param sab - SharedArrayBuffer backing the measurement protocol.\n * @param slotId - Zero-based slot index for this element.\n * @returns Measurement object with width, height, and border dimensions.\n */\nexport const readSlot = (\n sab: SharedArrayBuffer,\n slotId: number,\n): {\n readonly width: number;\n readonly height: number;\n readonly borderWidth: number;\n readonly borderHeight: number;\n} => {\n const view = new Float16Array(sab, slotId * SLOT_BYTES, 4);\n return {\n width: view[SlotOffset.InlineSize] ?? 0,\n height: view[SlotOffset.BlockSize] ?? 0,\n borderWidth: view[SlotOffset.BorderInline] ?? 0,\n borderHeight: view[SlotOffset.BorderBlock] ?? 0,\n };\n};\n\n/**\n * Allocate a slot from the bitmap tracker.\n * Scans for the first unallocated slot in O(n) worst case.\n *\n * @param bitmap - Int32Array tracking allocated slots (1 = in use).\n * @returns The allocated slot index, or -1 if all slots are in use.\n */\nexport const allocateSlot = (bitmap: Int32Array): number => {\n for (let i = 0; i < MAX_ELEMENTS; i++) {\n if (bitmap[i] === 0) {\n bitmap[i] = 1;\n return i;\n }\n }\n return -1;\n};\n\n/**\n * Release a slot back to the bitmap tracker.\n *\n * @param bitmap - Int32Array tracking allocated slots.\n * @param slotId - The slot index to release.\n */\nexport const releaseSlot = (bitmap: Int32Array, slotId: number): void => {\n if (slotId >= 0 && slotId < MAX_ELEMENTS) {\n bitmap[slotId] = 0;\n }\n};\n","'use client';\n\nimport type { RefObject } from 'react';\nimport { useEffect, useRef, useState } from 'react';\n\nimport type { ResizeObserverBoxOptions, UseResizeObserverResult } from '../types.js';\nimport type { WorkerMessage } from './protocol.js';\nimport { allocateSlot, MAX_ELEMENTS, readSlot, releaseSlot, SAB_SIZE } from './protocol.js';\n\n/** Options for the Worker-based resize observer hook. */\nexport interface UseResizeObserverWorkerOptions<T extends Element = Element> {\n /** Pre-existing ref to observe. If omitted, an internal ref is created. */\n ref?: RefObject<T | null>;\n /** Which box model to report. @default 'content-box' */\n box?: ResizeObserverBoxOptions;\n /**\n * Called on every resize event. Identity is stable across renders\n * (powered by ref pattern) — do NOT wrap in useCallback.\n */\n onResize?: (dimensions: { readonly width: number; readonly height: number }) => void;\n}\n\n/** Shared Worker instance — lazy-initialized, lives until last observer unmounts. */\nlet sharedWorker: Worker | null = null;\nlet sharedSab: SharedArrayBuffer | null = null;\nconst slotBitmap = new Int32Array(MAX_ELEMENTS);\nlet activeObserverCount = 0;\nlet workerReady = false;\n\n/** Promise that resolves when the Worker is initialized and ready. */\nlet initPromise: Promise<void> | null = null;\n\n/**\n * Lazily initialize the shared Worker with `Promise.withResolvers()` (ES2024+).\n * Uses `Error.isError()` (ES2026) for robust error discrimination.\n */\nconst ensureWorker = (): Promise<void> => {\n if (initPromise) return initPromise;\n\n const { promise, resolve, reject } = Promise.withResolvers<void>();\n initPromise = promise;\n\n Promise.try(() => {\n if (!globalThis.crossOriginIsolated) {\n throw new Error(\n '[@crimson_dev/use-resize-observer/worker] ' +\n 'crossOriginIsolated is false. Worker mode requires COOP/COEP headers. ' +\n 'See: https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated',\n );\n }\n\n sharedSab = new SharedArrayBuffer(SAB_SIZE);\n const workerUrl = new URL('./worker.js', import.meta.url);\n sharedWorker = new Worker(workerUrl, { type: 'module' });\n\n sharedWorker.addEventListener('message', (event: MessageEvent<WorkerMessage>) => {\n if (event.data.op === 'ready') {\n workerReady = true;\n resolve();\n } else if (event.data.op === 'error') {\n reject(new Error(event.data.message));\n }\n });\n\n sharedWorker.addEventListener('error', (event) => {\n const errorMessage = event instanceof ErrorEvent ? event.message : 'Worker error';\n reject(new Error(errorMessage));\n\n // Auto-restart on crash\n sharedWorker = null;\n initPromise = null;\n workerReady = false;\n });\n\n sharedWorker.postMessage({ op: 'init', sab: sharedSab } satisfies WorkerMessage);\n }).catch((error: unknown) => {\n reject(Error.isError(error) ? error : new Error(String(error)));\n });\n\n return promise;\n};\n\nconst terminateWorkerIfIdle = (): void => {\n if (activeObserverCount === 0 && sharedWorker) {\n sharedWorker.postMessage({ op: 'terminate' } satisfies WorkerMessage);\n sharedWorker.terminate();\n sharedWorker = null;\n sharedSab = null;\n initPromise = null;\n workerReady = false;\n slotBitmap.fill(0);\n }\n};\n\n/**\n * Worker-based resize observer hook.\n *\n * Moves all `ResizeObserver` measurement off the main thread using\n * `SharedArrayBuffer` + `Float16Array` + `Atomics`.\n *\n * Requires `crossOriginIsolated === true` (COOP/COEP headers).\n *\n * @param options - Configuration options.\n * @returns Ref, width, height, and raw entry (entry is `undefined` in Worker mode).\n */\nexport const useResizeObserverWorker = <T extends Element = Element>(\n options: UseResizeObserverWorkerOptions<T> = {},\n): UseResizeObserverResult<T> => {\n const { ref: externalRef, onResize } = options;\n\n const internalRef = useRef<T | null>(null);\n const targetRef = externalRef ?? internalRef;\n\n const [width, setWidth] = useState<number | undefined>(undefined);\n const [height, setHeight] = useState<number | undefined>(undefined);\n\n const onResizeRef = useRef(onResize);\n onResizeRef.current = onResize;\n\n useEffect(() => {\n const element = targetRef.current;\n if (!element) return;\n\n const slotId = allocateSlot(slotBitmap);\n if (slotId === -1) {\n console.error(\n `[@crimson_dev/use-resize-observer/worker] ` +\n `Maximum ${String(MAX_ELEMENTS)} simultaneous observations exceeded.`,\n );\n return;\n }\n\n activeObserverCount++;\n let cancelled = false;\n let rafId: number | null = null;\n\n const startPolling = (): void => {\n const poll = (): void => {\n if (cancelled || !sharedSab) return;\n\n const { width: w, height: h } = readSlot(sharedSab, slotId);\n setWidth(w);\n setHeight(h);\n onResizeRef.current?.({ width: w, height: h });\n rafId = requestAnimationFrame(poll);\n };\n rafId = requestAnimationFrame(poll);\n };\n\n ensureWorker()\n .then(() => {\n if (cancelled) return;\n sharedWorker?.postMessage({\n op: 'observe',\n slotId,\n elementId: element.id || `slot-${String(slotId)}`,\n } satisfies WorkerMessage);\n startPolling();\n })\n .catch((error: unknown) => {\n console.error(\n '[@crimson_dev/use-resize-observer/worker] Init failed:',\n Error.isError(error) ? error : new Error(String(error)),\n );\n });\n\n return () => {\n cancelled = true;\n if (rafId !== null) cancelAnimationFrame(rafId);\n\n if (workerReady && sharedWorker) {\n sharedWorker.postMessage({\n op: 'unobserve',\n slotId,\n } satisfies WorkerMessage);\n }\n\n releaseSlot(slotBitmap, slotId);\n activeObserverCount--;\n terminateWorkerIfIdle();\n };\n }, [targetRef]);\n\n return { ref: targetRef, width, height, entry: undefined };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../src/worker/protocol.ts","../src/worker/hook.ts"],"sourcesContent":["/**\n * SharedArrayBuffer protocol for Worker-based resize observations.\n *\n * Layout:\n * - 4 Float16 values per element slot (2 bytes each = 8 bytes per slot)\n * - Int32Array overlay for `Atomics.notify()` / `Atomics.waitAsync()` synchronization\n * - Supports up to 256 simultaneous element observations\n *\n * @internal\n */\n\n/** Bytes per observation slot: 4 x Float16 (2 bytes each) = 8 bytes. */\nexport const SLOT_BYTES: number = 8;\n\n/** Maximum number of simultaneously observable elements. */\nexport const MAX_ELEMENTS: number = 256;\n\n/** Total SharedArrayBuffer size in bytes. */\nexport const SAB_SIZE: number = SLOT_BYTES * MAX_ELEMENTS;\n\n/** Offsets within a single Float16Array slot. */\nexport const SlotOffset = {\n InlineSize: 0,\n BlockSize: 1,\n BorderInline: 2,\n BorderBlock: 3,\n} as const;\n\nexport type SlotOffsetKey = keyof typeof SlotOffset;\n\n/** Discriminated union of all Worker protocol messages. */\nexport type WorkerMessage =\n | { readonly op: 'init'; readonly sab: SharedArrayBuffer }\n | { readonly op: 'observe'; readonly slotId: number; readonly elementId: string }\n | { readonly op: 'unobserve'; readonly slotId: number }\n | { readonly op: 'terminate' }\n | { readonly op: 'ready' }\n | { readonly op: 'error'; readonly message: string };\n\n/**\n * Write resize measurements into a SharedArrayBuffer slot.\n * Uses `Float16Array` (ES2026) for compact storage and\n * `Atomics.notify()` for cross-thread signaling.\n *\n * @param sab - SharedArrayBuffer backing the measurement protocol.\n * @param slotId - Zero-based slot index for this element.\n * @param entry - ResizeObserverEntry from the Worker's observer.\n */\nexport const writeSlot = (\n sab: SharedArrayBuffer,\n slotId: number,\n entry: ResizeObserverEntry,\n): void => {\n const view = new Float16Array(sab, slotId * SLOT_BYTES, 4);\n const cs = entry.contentBoxSize[0];\n const bs = entry.borderBoxSize[0];\n view[SlotOffset.InlineSize] = cs?.inlineSize ?? 0;\n view[SlotOffset.BlockSize] = cs?.blockSize ?? 0;\n view[SlotOffset.BorderInline] = bs?.inlineSize ?? 0;\n view[SlotOffset.BorderBlock] = bs?.blockSize ?? 0;\n // Signal main thread that new data is available\n Atomics.notify(new Int32Array(sab), slotId, 1);\n};\n\n/**\n * Read resize measurements from a SharedArrayBuffer slot.\n *\n * @param sab - SharedArrayBuffer backing the measurement protocol.\n * @param slotId - Zero-based slot index for this element.\n * @returns Measurement object with width, height, and border dimensions.\n */\nexport const readSlot = (\n sab: SharedArrayBuffer,\n slotId: number,\n): {\n readonly width: number;\n readonly height: number;\n readonly borderWidth: number;\n readonly borderHeight: number;\n} => {\n const view = new Float16Array(sab, slotId * SLOT_BYTES, 4);\n return {\n width: view[SlotOffset.InlineSize] ?? 0,\n height: view[SlotOffset.BlockSize] ?? 0,\n borderWidth: view[SlotOffset.BorderInline] ?? 0,\n borderHeight: view[SlotOffset.BorderBlock] ?? 0,\n };\n};\n\n/**\n * Allocate a slot from the bitmap tracker.\n * Uses `Int32Array.prototype.indexOf()` for fast native-code scan.\n *\n * @param bitmap - Int32Array tracking allocated slots (1 = in use).\n * @returns The allocated slot index, or -1 if all slots are in use.\n */\nexport const allocateSlot = (bitmap: Int32Array): number => {\n const i = bitmap.indexOf(0);\n if (i !== -1) bitmap[i] = 1;\n return i;\n};\n\n/**\n * Release a slot back to the bitmap tracker.\n *\n * @param bitmap - Int32Array tracking allocated slots.\n * @param slotId - The slot index to release.\n */\nexport const releaseSlot = (bitmap: Int32Array, slotId: number): void => {\n if (slotId >= 0 && slotId < MAX_ELEMENTS) {\n bitmap[slotId] = 0;\n }\n};\n","'use client';\n\nimport type { RefObject } from 'react';\nimport { useEffect, useRef, useState } from 'react';\n\nimport type { ResizeObserverBoxOptions, UseResizeObserverResult } from '../types.js';\nimport type { WorkerMessage } from './protocol.js';\nimport { allocateSlot, MAX_ELEMENTS, readSlot, releaseSlot, SAB_SIZE } from './protocol.js';\n\n/** Options for the Worker-based resize observer hook. */\nexport interface UseResizeObserverWorkerOptions<T extends Element = Element> {\n /** Pre-existing ref to observe. If omitted, an internal ref is created. */\n ref?: RefObject<T | null>;\n /** Which box model to report. @default 'content-box' */\n box?: ResizeObserverBoxOptions;\n /**\n * Called on every resize event. Identity is stable across renders\n * (powered by ref pattern) — do NOT wrap in useCallback.\n */\n onResize?: (dimensions: { readonly width: number; readonly height: number }) => void;\n}\n\n/** Shared Worker instance — lazy-initialized, lives until last observer unmounts. */\nlet sharedWorker: Worker | null = null;\nlet sharedSab: SharedArrayBuffer | null = null;\nconst slotBitmap = new Int32Array(MAX_ELEMENTS);\nlet activeObserverCount = 0;\nlet workerReady = false;\n\n/** Promise that resolves when the Worker is initialized and ready. */\nlet initPromise: Promise<void> | null = null;\n\n/**\n * Lazily initialize the shared Worker with `Promise.withResolvers()` (ES2024+).\n * Uses `Error.isError()` (ES2026) for robust error discrimination.\n */\nconst ensureWorker = (): Promise<void> => {\n if (initPromise) return initPromise;\n\n const { promise, resolve, reject } = Promise.withResolvers<void>();\n initPromise = promise;\n\n Promise.try(() => {\n if (!globalThis.crossOriginIsolated) {\n throw new Error(\n '[@crimson_dev/use-resize-observer/worker] ' +\n 'crossOriginIsolated is false. Worker mode requires COOP/COEP headers. ' +\n 'See: https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated',\n );\n }\n\n sharedSab = new SharedArrayBuffer(SAB_SIZE);\n const workerUrl = new URL('./worker.js', import.meta.url);\n sharedWorker = new Worker(workerUrl, { type: 'module' });\n\n sharedWorker.addEventListener('message', (event: MessageEvent<WorkerMessage>) => {\n if (event.data.op === 'ready') {\n workerReady = true;\n resolve();\n } else if (event.data.op === 'error') {\n reject(new Error(event.data.message));\n }\n });\n\n sharedWorker.addEventListener('error', (event) => {\n const errorMessage = event instanceof ErrorEvent ? event.message : 'Worker error';\n reject(new Error(errorMessage));\n\n // Auto-restart on crash\n sharedWorker = null;\n initPromise = null;\n workerReady = false;\n });\n\n sharedWorker.postMessage({ op: 'init', sab: sharedSab } satisfies WorkerMessage);\n }).catch((error: unknown) => {\n reject(Error.isError(error) ? error : new Error(String(error)));\n });\n\n return promise;\n};\n\nconst terminateWorkerIfIdle = (): void => {\n if (activeObserverCount === 0 && sharedWorker) {\n sharedWorker.postMessage({ op: 'terminate' } satisfies WorkerMessage);\n sharedWorker.terminate();\n sharedWorker = null;\n sharedSab = null;\n initPromise = null;\n workerReady = false;\n slotBitmap.fill(0);\n }\n};\n\n/**\n * Worker-based resize observer hook.\n *\n * Moves all `ResizeObserver` measurement off the main thread using\n * `SharedArrayBuffer` + `Float16Array` + `Atomics`.\n *\n * Requires `crossOriginIsolated === true` (COOP/COEP headers).\n *\n * @param options - Configuration options.\n * @returns Ref, width, height, and raw entry (entry is `undefined` in Worker mode).\n */\nexport const useResizeObserverWorker = <T extends Element = Element>(\n options: UseResizeObserverWorkerOptions<T> = {},\n): UseResizeObserverResult<T> => {\n const { ref: externalRef, box = 'content-box', onResize } = options;\n\n const internalRef = useRef<T | null>(null);\n const targetRef = externalRef ?? internalRef;\n\n const [state, setState] = useState<\n { readonly width: number; readonly height: number } | undefined\n >(undefined);\n\n const onResizeRef = useRef(onResize);\n onResizeRef.current = onResize;\n\n const boxRef = useRef(box);\n boxRef.current = box;\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: box intentionally in deps to re-subscribe on box model change\n useEffect(() => {\n const element = targetRef.current;\n if (!element) return;\n\n const slotId = allocateSlot(slotBitmap);\n if (slotId === -1) {\n console.error(\n `[@crimson_dev/use-resize-observer/worker] ` +\n `Maximum ${String(MAX_ELEMENTS)} simultaneous observations exceeded.`,\n );\n return;\n }\n\n activeObserverCount++;\n let cancelled = false;\n let rafId: number | null = null;\n\n const startPolling = (): void => {\n const poll = (): void => {\n if (cancelled || !sharedSab) return;\n\n const slot = readSlot(sharedSab, slotId);\n // Select dimensions based on box model (device-pixel-content-box\n // falls back to content-box — no DPCB data in worker protocol)\n const useBorder = boxRef.current === 'border-box';\n const w = useBorder ? slot.borderWidth : slot.width;\n const h = useBorder ? slot.borderHeight : slot.height;\n setState({ width: w, height: h });\n onResizeRef.current?.({ width: w, height: h });\n rafId = requestAnimationFrame(poll);\n };\n rafId = requestAnimationFrame(poll);\n };\n\n ensureWorker()\n .then(() => {\n if (cancelled) return;\n sharedWorker?.postMessage({\n op: 'observe',\n slotId,\n elementId: element.id || `slot-${String(slotId)}`,\n } satisfies WorkerMessage);\n startPolling();\n })\n .catch((error: unknown) => {\n console.error(\n '[@crimson_dev/use-resize-observer/worker] Init failed:',\n Error.isError(error) ? error : new Error(String(error)),\n );\n });\n\n return () => {\n cancelled = true;\n if (rafId !== null) cancelAnimationFrame(rafId);\n\n if (workerReady && sharedWorker) {\n sharedWorker.postMessage({\n op: 'unobserve',\n slotId,\n } satisfies WorkerMessage);\n }\n\n releaseSlot(slotBitmap, slotId);\n activeObserverCount--;\n terminateWorkerIfIdle();\n };\n }, [targetRef, box]);\n\n return { ref: targetRef, width: state?.width, height: state?.height, entry: undefined };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crimson_dev/use-resize-observer",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Zero-dependency, Worker-native, ESNext-first React 19 ResizeObserver hook",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -67,7 +67,7 @@
67
67
  }
68
68
  },
69
69
  "devDependencies": {
70
- "@biomejs/biome": "2.4.5",
70
+ "@biomejs/biome": "2.4.6",
71
71
  "@changesets/cli": "2.30.0",
72
72
  "@size-limit/preset-small-lib": "12.0.0",
73
73
  "@testing-library/dom": "^10.4.1",
@@ -76,15 +76,15 @@
76
76
  "@types/react": "19.2.14",
77
77
  "@types/react-dom": "19.2.3",
78
78
  "@typescript/native-preview": "7.0.0-dev.20260305.1",
79
- "@vitejs/plugin-react": "4.5.0",
80
- "@vitest/browser-playwright": "4.1.0-beta.5",
81
- "@vitest/coverage-v8": "4.1.0-beta.5",
82
- "@vitest/ui": "4.1.0-beta.5",
79
+ "@vitejs/plugin-react": "5.1.4",
80
+ "@vitest/browser-playwright": "4.1.0-beta.6",
81
+ "@vitest/coverage-v8": "4.1.0-beta.6",
82
+ "@vitest/ui": "4.1.0-beta.6",
83
83
  "babel-plugin-react-compiler": "^0.0.0-experimental-1371fcb-20260304",
84
84
  "concurrently": "9.2.1",
85
85
  "happy-dom": "20.8.3",
86
- "playwright": "1.59.0-alpha-2026-03-05",
87
- "publint": "0.3.0",
86
+ "playwright": "1.59.0-alpha-2026-03-06",
87
+ "publint": "0.3.18",
88
88
  "react": "19.3.0-canary-3bc2d414-20260304",
89
89
  "react-dom": "19.3.0-canary-3bc2d414-20260304",
90
90
  "shiki": "4.0.1",
@@ -92,7 +92,7 @@
92
92
  "size-limit": "12.0.0",
93
93
  "svgo": "4.0.1",
94
94
  "tinybench": "6.0.0",
95
- "tsdown": "0.21.0-beta.5",
95
+ "tsdown": "0.21.0",
96
96
  "tsx": "4.21.0",
97
97
  "typedoc": "0.28.17",
98
98
  "typedoc-plugin-markdown": "4.10.0",
@@ -100,7 +100,7 @@
100
100
  "typescript": "6.0.0-dev.20260305",
101
101
  "vitepress": "2.0.0-alpha.16",
102
102
  "vitepress-plugin-mermaid": "2.0.17",
103
- "vitest": "4.1.0-beta.5"
103
+ "vitest": "4.1.0-beta.6"
104
104
  },
105
105
  "simple-git-hooks": {
106
106
  "pre-commit": "npx @biomejs/biome check --staged || npx @biomejs/biome check --staged 2>&1 | grep -q 'No files were processed'"