@crimson_dev/use-resize-observer 0.2.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 CHANGED
@@ -5,6 +5,53 @@ 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
+
29
+ ## [0.3.0] - 2026-03-05
30
+
31
+ ### Added
32
+ - OKLCH theme hardening: `@property` definitions, `:root:not(.dark)` light mode, `--c-nav-bg` token
33
+ - Typography: `text-wrap: balance` on headings, `text-wrap: pretty` on paragraphs, h5/h6 styles
34
+ - Accessibility: universal `prefers-reduced-motion` reset, `:focus-visible` keyboard navigation style
35
+ - GPU optimization: compositor-only shimmer animation, explicit `will-change` on animated elements
36
+ - Firefox scrollbar styling (`scrollbar-width: thin`)
37
+ - `content-visibility: auto` on footer for off-screen optimization
38
+ - `view-transition-name: sidebar` for page navigation transitions
39
+
40
+ ### Changed
41
+ - All "Sub-300B" size claims corrected to actual 1.04 kB across homepage, README, bundle-size guide, blog, contributing, architecture, and troubleshooting docs
42
+ - `src/shim/wasm-round.ts`: `sumPrecise` param now `readonly number[]`; `normalizeDimension` delegates to `roundToDevicePixel` (removes duplication)
43
+ - `src/pool.ts`: clarifying comment on fire-and-forget `Promise.try()` pattern
44
+ - SVG diagrams optimized via SVGO 4: 24-33% size reduction with `prefers-reduced-motion` preserved
45
+ - All `transition: all` replaced with explicit property lists across theme CSS
46
+ - Nav/sidebar/search backdrop-filter enhanced with `saturate()` for richer visual effect
47
+ - Theme animations: `will-change: background` replaced with `will-change: --crimson-hue` (compositor-friendly)
48
+
49
+ ### Fixed
50
+ - Visualizer demo page: replaced incomplete `<script setup>` stub with proper info block
51
+ - Homepage tagline: "Sub-300B gzip" corrected to "< 1.1kB gzip"
52
+ - 8 documentation pages with stale size claims corrected
53
+ - Light mode theme not applying (was using `@media prefers-color-scheme` instead of VitePress `.dark` class toggle)
54
+
8
55
  ## [0.2.0] - 2026-03-05
9
56
 
10
57
  ### Added
@@ -75,6 +122,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
75
122
  - Stable callback identity via ref pattern (React Compiler safe)
76
123
  - Worker mode: SharedArrayBuffer + Float16Array + Atomics for off-main-thread
77
124
 
125
+ [0.4.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.4.0
126
+ [0.3.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.3.0
78
127
  [0.2.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.2.0
79
128
  [0.1.1]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.1.1
80
129
  [0.1.0]: https://github.com/ABCrimson/use-resize-observer/releases/tag/v0.1.0
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
 
@@ -65,7 +52,7 @@ Most resize hooks create **one `ResizeObserver` per component**. At scale, that
65
52
  ## Highlights
66
53
 
67
54
  <table>
68
- <tr><td>📦</td><td><strong>&lt; 1.1kB</strong> gzip &middot; zero dependencies</td></tr>
55
+ <tr><td>📦</td><td><strong>1.04 kB</strong> gzip &middot; zero dependencies</td></tr>
69
56
  <tr><td>⚡</td><td>Shared <code>ResizeObserver</code> pool &middot; rAF batching &middot; <code>startTransition</code></td></tr>
70
57
  <tr><td>🧵</td><td>Worker mode via <code>SharedArrayBuffer</code> + <code>Float16Array</code></td></tr>
71
58
  <tr><td>🎯</td><td>All 3 box models: <code>content-box</code>, <code>border-box</code>, <code>device-pixel-content-box</code></td></tr>
@@ -166,7 +153,7 @@ observer.observe(element, (entry) => console.log(entry));
166
153
 
167
154
  | | `use-resize-observer@9` | `@crimson_dev/use-resize-observer` |
168
155
  |---|---|---|
169
- | Bundle | ~800B | **< 1.1kB** (pool + scheduler + hook) |
156
+ | Bundle | ~800B | **1.04 kB** (pool + scheduler + hook) |
170
157
  | React | 16.8+ | **19.3+** with Compiler |
171
158
  | Module | CJS + ESM | **ESM only** |
172
159
  | TypeScript | 4.x | **6.0 strict** |
package/dist/index.d.ts CHANGED
@@ -51,7 +51,7 @@ declare const createResizeObserver: (options?: CreateResizeObserverOptions) => R
51
51
  * - `requestAnimationFrame` batching with `startTransition` wrapping
52
52
  * - GC-backed cleanup via `FinalizationRegistry`
53
53
  * - React Compiler-safe (stable callback identity via ref pattern)
54
- * - Sub-300B gzip bundle contribution
54
+ * - Sub-1.1 kB gzip bundle contribution
55
55
  *
56
56
  * @param options - Configuration options.
57
57
  * @returns Ref, width, height, and raw entry.
package/dist/index.js CHANGED
@@ -240,7 +240,7 @@ const createResizeObserver = (options = {}) => {
240
240
  * - `requestAnimationFrame` batching with `startTransition` wrapping
241
241
  * - GC-backed cleanup via `FinalizationRegistry`
242
242
  * - React Compiler-safe (stable callback identity via ref pattern)
243
- * - Sub-300B gzip bundle contribution
243
+ * - Sub-1.1 kB gzip bundle contribution
244
244
  *
245
245
  * @param options - Configuration options.
246
246
  * @returns Ref, width, height, and raw entry.
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 // 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-300B 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 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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"}
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 { width: w, height: h } = readSlot(sharedSab, slotId);
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,
@@ -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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crimson_dev/use-resize-observer",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Zero-dependency, Worker-native, ESNext-first React 19 ResizeObserver hook",
5
5
  "type": "module",
6
6
  "sideEffects": false,