@crimson_dev/use-resize-observer 0.3.0 → 0.4.0
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 +22 -0
- package/README.md +6 -19
- package/dist/worker.js +8 -3
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ 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.0] - 2026-03-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Benchmark suite modernization: JSON results output to `bench/results/`, 500-element scheduler tier, writeSlot/readSlot roundtrip latency benchmark, heap delta tracking
|
|
12
|
+
- Concurrency stress tests: 1000 elements in same rAF, rapid observe/unobserve cycling, concurrent callbacks, 1000-element scheduler flush
|
|
13
|
+
- Memory pressure tests: 10k cycle leak detection, mass observe/unobserve verification, repeated pool disposal, worker slot bitmap full recycling
|
|
14
|
+
- CI benchmark workflow (`.github/workflows/bench.yml`): auto-runs on push/PR, uploads artifacts, posts PR comments with results
|
|
15
|
+
- Accessibility: ARIA live regions (`aria-live="polite"`, `role="status"`) on visualizer dimension readout, `role="img"` on bar chart
|
|
16
|
+
- Accessibility documentation section in visualizer demo page
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Worker hook now respects `box` option (previously ignored — always read content-box dimensions)
|
|
20
|
+
- Worker hook effect dependency array includes `box` for proper re-subscription on box model change
|
|
21
|
+
- Deep modernization audit of all 18 source files and 14 test files — all confirmed ES2026-compliant
|
|
22
|
+
- V8 optimization audit: all hot paths (`observe`, `unobserve`, `schedule`, `writeSlot`, `readSlot`) confirmed monomorphic with no deoptimizations
|
|
23
|
+
- Performance guide updated with V8 audit results and actual benchmark numbers
|
|
24
|
+
- Test suite expanded: 102 tests across 14 suites (up from 94 tests in 12 suites)
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Worker hook `box` option was declared but not used — border-box measurements now correctly read from `borderWidth`/`borderHeight` slot values
|
|
28
|
+
|
|
8
29
|
## [0.3.0] - 2026-03-05
|
|
9
30
|
|
|
10
31
|
### Added
|
|
@@ -101,6 +122,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
101
122
|
- Stable callback identity via ref pattern (React Compiler safe)
|
|
102
123
|
- Worker mode: SharedArrayBuffer + Float16Array + Atomics for off-main-thread
|
|
103
124
|
|
|
125
|
+
[0.4.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.4.0
|
|
104
126
|
[0.3.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.3.0
|
|
105
127
|
[0.2.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.2.0
|
|
106
128
|
[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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
package/dist/worker.js
CHANGED
|
@@ -154,13 +154,15 @@ const terminateWorkerIfIdle = () => {
|
|
|
154
154
|
* @returns Ref, width, height, and raw entry (entry is `undefined` in Worker mode).
|
|
155
155
|
*/
|
|
156
156
|
const useResizeObserverWorker = (options = {}) => {
|
|
157
|
-
const { ref: externalRef, onResize } = options;
|
|
157
|
+
const { ref: externalRef, box = "content-box", onResize } = options;
|
|
158
158
|
const internalRef = useRef(null);
|
|
159
159
|
const targetRef = externalRef ?? internalRef;
|
|
160
160
|
const [width, setWidth] = useState(void 0);
|
|
161
161
|
const [height, setHeight] = useState(void 0);
|
|
162
162
|
const onResizeRef = useRef(onResize);
|
|
163
163
|
onResizeRef.current = onResize;
|
|
164
|
+
const boxRef = useRef(box);
|
|
165
|
+
boxRef.current = box;
|
|
164
166
|
useEffect(() => {
|
|
165
167
|
const element = targetRef.current;
|
|
166
168
|
if (!element) return;
|
|
@@ -175,7 +177,10 @@ const useResizeObserverWorker = (options = {}) => {
|
|
|
175
177
|
const startPolling = () => {
|
|
176
178
|
const poll = () => {
|
|
177
179
|
if (cancelled || !sharedSab) return;
|
|
178
|
-
const
|
|
180
|
+
const slot = readSlot(sharedSab, slotId);
|
|
181
|
+
const useBorder = boxRef.current === "border-box";
|
|
182
|
+
const w = useBorder ? slot.borderWidth : slot.width;
|
|
183
|
+
const h = useBorder ? slot.borderHeight : slot.height;
|
|
179
184
|
setWidth(w);
|
|
180
185
|
setHeight(h);
|
|
181
186
|
onResizeRef.current?.({
|
|
@@ -208,7 +213,7 @@ const useResizeObserverWorker = (options = {}) => {
|
|
|
208
213
|
activeObserverCount--;
|
|
209
214
|
terminateWorkerIfIdle();
|
|
210
215
|
};
|
|
211
|
-
}, [targetRef]);
|
|
216
|
+
}, [targetRef, box]);
|
|
212
217
|
return {
|
|
213
218
|
ref: targetRef,
|
|
214
219
|
width,
|
package/dist/worker.js.map
CHANGED
|
@@ -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 * 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, box = 'content-box', 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 const boxRef = useRef(box);\n boxRef.current = box;\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 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 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, box]);\n\n return { ref: targetRef, width, height, entry: undefined };\n};\n"],"mappings":"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"}
|