@crimson_dev/use-resize-observer 0.1.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 +52 -0
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/core.d.ts +38 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +58 -0
- package/dist/core.js.map +1 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +386 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +32 -0
- package/dist/server.js.map +1 -0
- package/dist/shim.d.ts +38 -0
- package/dist/shim.d.ts.map +1 -0
- package/dist/shim.js +108 -0
- package/dist/shim.js.map +1 -0
- package/dist/types-ASPFw2w_.d.ts +49 -0
- package/dist/types-ASPFw2w_.d.ts.map +1 -0
- package/dist/worker.d.ts +103 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +222 -0
- package/dist/worker.js.map +1 -0
- package/package.json +129 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
|
|
2
|
+
import { RefObject } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
/** Callback invoked with a ResizeObserverEntry on each resize. */
|
|
6
|
+
type ResizeCallback = (entry: ResizeObserverEntry) => void;
|
|
7
|
+
/** Box model options for ResizeObserver observation. */
|
|
8
|
+
type ResizeObserverBoxOptions = "border-box" | "content-box" | "device-pixel-content-box";
|
|
9
|
+
/** Options for the `useResizeObserver` hook. */
|
|
10
|
+
interface UseResizeObserverOptions<T extends Element = Element> {
|
|
11
|
+
/** Pre-existing ref to observe. If omitted, an internal ref is created. */
|
|
12
|
+
ref?: RefObject<T | null>;
|
|
13
|
+
/** Which box model to report. @default 'content-box' */
|
|
14
|
+
box?: ResizeObserverBoxOptions;
|
|
15
|
+
/** Document or ShadowRoot scoping the pool. @default target.ownerDocument */
|
|
16
|
+
root?: Document | ShadowRoot;
|
|
17
|
+
/**
|
|
18
|
+
* Called on every resize event. Identity is stable across renders
|
|
19
|
+
* (powered by useEffectEvent) — do NOT wrap in useCallback.
|
|
20
|
+
*/
|
|
21
|
+
onResize?: (entry: ResizeObserverEntry) => void;
|
|
22
|
+
}
|
|
23
|
+
/** Return value of the `useResizeObserver` hook. */
|
|
24
|
+
interface UseResizeObserverResult<T extends Element = Element> {
|
|
25
|
+
/** Attach this ref to the element you want to observe. */
|
|
26
|
+
ref: RefObject<T | null>;
|
|
27
|
+
/** Inline size of the observed box. undefined until first observation. */
|
|
28
|
+
width: number | undefined;
|
|
29
|
+
/** Block size of the observed box. undefined until first observation. */
|
|
30
|
+
height: number | undefined;
|
|
31
|
+
/** The raw ResizeObserverEntry. undefined until first observation. */
|
|
32
|
+
entry: ResizeObserverEntry | undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Options for the `createResizeObserver` factory. */
|
|
35
|
+
interface CreateResizeObserverOptions {
|
|
36
|
+
/** Which box model to report. @default 'content-box' */
|
|
37
|
+
box?: ResizeObserverBoxOptions;
|
|
38
|
+
/** Document or ShadowRoot scoping the pool. @default document */
|
|
39
|
+
root?: Document | ShadowRoot;
|
|
40
|
+
}
|
|
41
|
+
/** Return type for the `createResizeObserver` factory. */
|
|
42
|
+
interface ResizeObserverFactory {
|
|
43
|
+
observe(target: Element, callback: ResizeCallback): void;
|
|
44
|
+
unobserve(target: Element, callback: ResizeCallback): void;
|
|
45
|
+
disconnect(): void;
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { UseResizeObserverOptions as a, ResizeObserverFactory as i, ResizeCallback as n, UseResizeObserverResult as o, ResizeObserverBoxOptions as r, CreateResizeObserverOptions as t };
|
|
49
|
+
//# sourceMappingURL=types-ASPFw2w_.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-ASPFw2w_.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAGY,cAAA,IAAkB,KAAA,EAAO,mBAAA;AAArC;AAAA,KAGY,wBAAA;;UAGK,wBAAA,WAAmC,OAAA,GAAU,OAAA;EANzB;EAQnC,GAAA,GAAM,SAAA,CAAU,CAAA;EALN;EAOV,GAAA,GAAM,wBAAA;EAPI;EASV,IAAA,GAAO,QAAA,GAAW,UAAA;EANpB;;;;EAWE,QAAA,IAAY,KAAA,EAAO,mBAAA;AAAA;;UAIJ,uBAAA,WAAkC,OAAA,GAAU,OAAA;;EAE3D,GAAA,EAAK,SAAA,CAAU,CAAA;;EAEf,KAAA;EARmB;EAUnB,MAAA;;EAEA,KAAA,EAAO,mBAAA;AAAA;;UAIQ,2BAAA;;EAEf,GAAA,GAAM,wBAAA;;EAEN,IAAA,GAAO,QAAA,GAAW,UAAA;AAAA;;UAIH,qBAAA;EACf,OAAA,CAAQ,MAAA,EAAQ,OAAA,EAAS,QAAA,EAAU,cAAA;EACnC,SAAA,CAAU,MAAA,EAAQ,OAAA,EAAS,QAAA,EAAU,cAAA;EACrC,UAAA;AAAA"}
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
import { o as UseResizeObserverResult, r as ResizeObserverBoxOptions } from "./types-ASPFw2w_.js";
|
|
3
|
+
import { RefObject } from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/worker/hook.d.ts
|
|
6
|
+
/** Options for the Worker-based resize observer hook. */
|
|
7
|
+
interface UseResizeObserverWorkerOptions<T extends Element = Element> {
|
|
8
|
+
/** Pre-existing ref to observe. If omitted, an internal ref is created. */
|
|
9
|
+
ref?: RefObject<T | null>;
|
|
10
|
+
/** Which box model to report. @default 'content-box' */
|
|
11
|
+
box?: ResizeObserverBoxOptions;
|
|
12
|
+
/**
|
|
13
|
+
* Called on every resize event. Identity is stable across renders
|
|
14
|
+
* (powered by ref pattern) — do NOT wrap in useCallback.
|
|
15
|
+
*/
|
|
16
|
+
onResize?: (dimensions: {
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
}) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Worker-based resize observer hook.
|
|
23
|
+
*
|
|
24
|
+
* Moves all `ResizeObserver` measurement off the main thread using
|
|
25
|
+
* `SharedArrayBuffer` + `Float16Array` + `Atomics`.
|
|
26
|
+
*
|
|
27
|
+
* Requires `crossOriginIsolated === true` (COOP/COEP headers).
|
|
28
|
+
*
|
|
29
|
+
* @param options - Configuration options.
|
|
30
|
+
* @returns Ref, width, height, and raw entry (entry is `undefined` in Worker mode).
|
|
31
|
+
*/
|
|
32
|
+
declare const useResizeObserverWorker: <T extends Element = Element>(options?: UseResizeObserverWorkerOptions<T>) => UseResizeObserverResult<T>;
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/worker/protocol.d.ts
|
|
35
|
+
/**
|
|
36
|
+
* SharedArrayBuffer protocol for Worker-based resize observations.
|
|
37
|
+
*
|
|
38
|
+
* Layout:
|
|
39
|
+
* - 4 Float16 values per element slot (2 bytes each = 8 bytes per slot)
|
|
40
|
+
* - Int32Array overlay for `Atomics.notify()` / `Atomics.waitAsync()` synchronization
|
|
41
|
+
* - Supports up to 256 simultaneous element observations
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
/** Bytes per observation slot: 4 x Float16 (2 bytes each) = 8 bytes. */
|
|
46
|
+
declare const SLOT_BYTES: number;
|
|
47
|
+
/** Maximum number of simultaneously observable elements. */
|
|
48
|
+
declare const MAX_ELEMENTS: number;
|
|
49
|
+
/** Total SharedArrayBuffer size in bytes. */
|
|
50
|
+
declare const SAB_SIZE: number;
|
|
51
|
+
/** Offsets within a single Float16Array slot. */
|
|
52
|
+
declare const SlotOffset: {
|
|
53
|
+
readonly InlineSize: 0;
|
|
54
|
+
readonly BlockSize: 1;
|
|
55
|
+
readonly BorderInline: 2;
|
|
56
|
+
readonly BorderBlock: 3;
|
|
57
|
+
};
|
|
58
|
+
type SlotOffsetKey = keyof typeof SlotOffset;
|
|
59
|
+
/** Discriminated union of all Worker protocol messages. */
|
|
60
|
+
type WorkerMessage = {
|
|
61
|
+
readonly op: "init";
|
|
62
|
+
readonly sab: SharedArrayBuffer;
|
|
63
|
+
} | {
|
|
64
|
+
readonly op: "observe";
|
|
65
|
+
readonly slotId: number;
|
|
66
|
+
readonly elementId: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly op: "unobserve";
|
|
69
|
+
readonly slotId: number;
|
|
70
|
+
} | {
|
|
71
|
+
readonly op: "terminate";
|
|
72
|
+
} | {
|
|
73
|
+
readonly op: "ready";
|
|
74
|
+
} | {
|
|
75
|
+
readonly op: "error";
|
|
76
|
+
readonly message: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Write resize measurements into a SharedArrayBuffer slot.
|
|
80
|
+
* Uses `Float16Array` (ES2026) for compact storage and
|
|
81
|
+
* `Atomics.notify()` for cross-thread signaling.
|
|
82
|
+
*
|
|
83
|
+
* @param sab - SharedArrayBuffer backing the measurement protocol.
|
|
84
|
+
* @param slotId - Zero-based slot index for this element.
|
|
85
|
+
* @param entry - ResizeObserverEntry from the Worker's observer.
|
|
86
|
+
*/
|
|
87
|
+
declare const writeSlot: (sab: SharedArrayBuffer, slotId: number, entry: ResizeObserverEntry) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Read resize measurements from a SharedArrayBuffer slot.
|
|
90
|
+
*
|
|
91
|
+
* @param sab - SharedArrayBuffer backing the measurement protocol.
|
|
92
|
+
* @param slotId - Zero-based slot index for this element.
|
|
93
|
+
* @returns Measurement object with width, height, and border dimensions.
|
|
94
|
+
*/
|
|
95
|
+
declare const readSlot: (sab: SharedArrayBuffer, slotId: number) => {
|
|
96
|
+
readonly width: number;
|
|
97
|
+
readonly height: number;
|
|
98
|
+
readonly borderWidth: number;
|
|
99
|
+
readonly borderHeight: number;
|
|
100
|
+
};
|
|
101
|
+
//#endregion
|
|
102
|
+
export { MAX_ELEMENTS, SAB_SIZE, SLOT_BYTES, type SlotOffsetKey, type UseResizeObserverWorkerOptions, type WorkerMessage, readSlot, useResizeObserverWorker, writeSlot };
|
|
103
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","names":[],"sources":["../src/worker/hook.ts","../src/worker/protocol.ts"],"mappings":";;;;;;UAUiB,8BAAA,WAAyC,OAAA,GAAU,OAAA;EAApE;EAEE,GAAA,GAAM,SAAA,CAAU,CAAA;;EAEhB,GAAA,GAAM,wBAAA;;;;;EAKN,QAAA,IAAY,UAAA;IAAA,SAAuB,KAAA;IAAA,SAAwB,MAAA;EAAA;AAAA;;;;;;;;;;;;cAsFhD,uBAAA,aAAqC,OAAA,GAAU,OAAA,EAC1D,OAAA,GAAS,8BAAA,CAA+B,CAAA,MACvC,uBAAA,CAAwB,CAAA;;;;;;;AAjG3B;;;;;;;cCEa,UAAA;;cAGA,YAAA;;cAGA,QAAA;;cAGA,UAAA;EAAA,SACX,UAAA;EAAA,SACA,SAAA;EAAA,SACA,YAAA;EAAA,SACA,WAAA;AAAA;AAAA,KAGU,aAAA,gBAA6B,UAAA;;KAG7B,aAAA;EAAA,SACG,EAAA;EAAA,SAAqB,GAAA,EAAK,iBAAA;AAAA;EAAA,SAC1B,EAAA;EAAA,SAAwB,MAAA;EAAA,SAAyB,SAAA;AAAA;EAAA,SACjD,EAAA;EAAA,SAA0B,MAAA;AAAA;EAAA,SAC1B,EAAA;AAAA;EAAA,SACA,EAAA;AAAA;EAAA,SACA,EAAA;EAAA,SAAsB,OAAA;AAAA;;;;;;;;;;cAWxB,SAAA,GACX,GAAA,EAAK,iBAAA,EACL,MAAA,UACA,KAAA,EAAO,mBAAA;;;;;AApCT;;;cAwDa,QAAA,GACX,GAAA,EAAK,iBAAA,EACL,MAAA;EAAA,SAES,KAAA;EAAA,SACA,MAAA;EAAA,SACA,WAAA;EAAA,SACA,YAAA;AAAA"}
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
|
|
6
|
+
//#region src/worker/protocol.ts
|
|
7
|
+
/**
|
|
8
|
+
* SharedArrayBuffer protocol for Worker-based resize observations.
|
|
9
|
+
*
|
|
10
|
+
* Layout:
|
|
11
|
+
* - 4 Float16 values per element slot (2 bytes each = 8 bytes per slot)
|
|
12
|
+
* - Int32Array overlay for `Atomics.notify()` / `Atomics.waitAsync()` synchronization
|
|
13
|
+
* - Supports up to 256 simultaneous element observations
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
/** Bytes per observation slot: 4 x Float16 (2 bytes each) = 8 bytes. */
|
|
18
|
+
const SLOT_BYTES = 8;
|
|
19
|
+
/** Maximum number of simultaneously observable elements. */
|
|
20
|
+
const MAX_ELEMENTS = 256;
|
|
21
|
+
/** Total SharedArrayBuffer size in bytes. */
|
|
22
|
+
const SAB_SIZE = SLOT_BYTES * MAX_ELEMENTS;
|
|
23
|
+
/** Offsets within a single Float16Array slot. */
|
|
24
|
+
const SlotOffset = {
|
|
25
|
+
InlineSize: 0,
|
|
26
|
+
BlockSize: 1,
|
|
27
|
+
BorderInline: 2,
|
|
28
|
+
BorderBlock: 3
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Write resize measurements into a SharedArrayBuffer slot.
|
|
32
|
+
* Uses `Float16Array` (ES2026) for compact storage and
|
|
33
|
+
* `Atomics.notify()` for cross-thread signaling.
|
|
34
|
+
*
|
|
35
|
+
* @param sab - SharedArrayBuffer backing the measurement protocol.
|
|
36
|
+
* @param slotId - Zero-based slot index for this element.
|
|
37
|
+
* @param entry - ResizeObserverEntry from the Worker's observer.
|
|
38
|
+
*/
|
|
39
|
+
const writeSlot = (sab, slotId, entry) => {
|
|
40
|
+
const view = new Float16Array(sab, slotId * SLOT_BYTES, 4);
|
|
41
|
+
const cs = entry.contentBoxSize[0];
|
|
42
|
+
const bs = entry.borderBoxSize[0];
|
|
43
|
+
view[SlotOffset.InlineSize] = cs?.inlineSize ?? 0;
|
|
44
|
+
view[SlotOffset.BlockSize] = cs?.blockSize ?? 0;
|
|
45
|
+
view[SlotOffset.BorderInline] = bs?.inlineSize ?? 0;
|
|
46
|
+
view[SlotOffset.BorderBlock] = bs?.blockSize ?? 0;
|
|
47
|
+
Atomics.notify(new Int32Array(sab), slotId, 1);
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Read resize measurements from a SharedArrayBuffer slot.
|
|
51
|
+
*
|
|
52
|
+
* @param sab - SharedArrayBuffer backing the measurement protocol.
|
|
53
|
+
* @param slotId - Zero-based slot index for this element.
|
|
54
|
+
* @returns Measurement object with width, height, and border dimensions.
|
|
55
|
+
*/
|
|
56
|
+
const readSlot = (sab, slotId) => {
|
|
57
|
+
const view = new Float16Array(sab, slotId * SLOT_BYTES, 4);
|
|
58
|
+
return {
|
|
59
|
+
width: view[SlotOffset.InlineSize] ?? 0,
|
|
60
|
+
height: view[SlotOffset.BlockSize] ?? 0,
|
|
61
|
+
borderWidth: view[SlotOffset.BorderInline] ?? 0,
|
|
62
|
+
borderHeight: view[SlotOffset.BorderBlock] ?? 0
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Allocate a slot from the bitmap tracker.
|
|
67
|
+
* Scans for the first unallocated slot in O(n) worst case.
|
|
68
|
+
*
|
|
69
|
+
* @param bitmap - Int32Array tracking allocated slots (1 = in use).
|
|
70
|
+
* @returns The allocated slot index, or -1 if all slots are in use.
|
|
71
|
+
*/
|
|
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;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Release a slot back to the bitmap tracker.
|
|
81
|
+
*
|
|
82
|
+
* @param bitmap - Int32Array tracking allocated slots.
|
|
83
|
+
* @param slotId - The slot index to release.
|
|
84
|
+
*/
|
|
85
|
+
const releaseSlot = (bitmap, slotId) => {
|
|
86
|
+
if (slotId >= 0 && slotId < MAX_ELEMENTS) bitmap[slotId] = 0;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/worker/hook.ts
|
|
91
|
+
/** Shared Worker instance — lazy-initialized, lives until last observer unmounts. */
|
|
92
|
+
let sharedWorker = null;
|
|
93
|
+
let sharedSab = null;
|
|
94
|
+
const slotBitmap = new Int32Array(MAX_ELEMENTS);
|
|
95
|
+
let activeObserverCount = 0;
|
|
96
|
+
let workerReady = false;
|
|
97
|
+
/** Promise that resolves when the Worker is initialized and ready. */
|
|
98
|
+
let initPromise = null;
|
|
99
|
+
/**
|
|
100
|
+
* Lazily initialize the shared Worker with `Promise.withResolvers()` (ES2024+).
|
|
101
|
+
* Uses `Error.isError()` (ES2026) for robust error discrimination.
|
|
102
|
+
*/
|
|
103
|
+
const ensureWorker = () => {
|
|
104
|
+
if (initPromise) return initPromise;
|
|
105
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
106
|
+
initPromise = promise;
|
|
107
|
+
Promise.try(() => {
|
|
108
|
+
if (!globalThis.crossOriginIsolated) throw new Error("[@crimson_dev/use-resize-observer/worker] crossOriginIsolated is false. Worker mode requires COOP/COEP headers. See: https://developer.mozilla.org/en-US/docs/Web/API/crossOriginIsolated");
|
|
109
|
+
sharedSab = new SharedArrayBuffer(SAB_SIZE);
|
|
110
|
+
const workerUrl = new URL("./worker.js", import.meta.url);
|
|
111
|
+
sharedWorker = new Worker(workerUrl, { type: "module" });
|
|
112
|
+
sharedWorker.addEventListener("message", (event) => {
|
|
113
|
+
if (event.data.op === "ready") {
|
|
114
|
+
workerReady = true;
|
|
115
|
+
resolve();
|
|
116
|
+
} else if (event.data.op === "error") reject(new Error(event.data.message));
|
|
117
|
+
});
|
|
118
|
+
sharedWorker.addEventListener("error", (event) => {
|
|
119
|
+
const errorMessage = event instanceof ErrorEvent ? event.message : "Worker error";
|
|
120
|
+
reject(new Error(errorMessage));
|
|
121
|
+
sharedWorker = null;
|
|
122
|
+
initPromise = null;
|
|
123
|
+
workerReady = false;
|
|
124
|
+
});
|
|
125
|
+
sharedWorker.postMessage({
|
|
126
|
+
op: "init",
|
|
127
|
+
sab: sharedSab
|
|
128
|
+
});
|
|
129
|
+
}).catch((error) => {
|
|
130
|
+
reject(Error.isError(error) ? error : new Error(String(error)));
|
|
131
|
+
});
|
|
132
|
+
return promise;
|
|
133
|
+
};
|
|
134
|
+
const terminateWorkerIfIdle = () => {
|
|
135
|
+
if (activeObserverCount === 0 && sharedWorker) {
|
|
136
|
+
sharedWorker.postMessage({ op: "terminate" });
|
|
137
|
+
sharedWorker.terminate();
|
|
138
|
+
sharedWorker = null;
|
|
139
|
+
sharedSab = null;
|
|
140
|
+
initPromise = null;
|
|
141
|
+
workerReady = false;
|
|
142
|
+
slotBitmap.fill(0);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Worker-based resize observer hook.
|
|
147
|
+
*
|
|
148
|
+
* Moves all `ResizeObserver` measurement off the main thread using
|
|
149
|
+
* `SharedArrayBuffer` + `Float16Array` + `Atomics`.
|
|
150
|
+
*
|
|
151
|
+
* Requires `crossOriginIsolated === true` (COOP/COEP headers).
|
|
152
|
+
*
|
|
153
|
+
* @param options - Configuration options.
|
|
154
|
+
* @returns Ref, width, height, and raw entry (entry is `undefined` in Worker mode).
|
|
155
|
+
*/
|
|
156
|
+
const useResizeObserverWorker = (options = {}) => {
|
|
157
|
+
const { ref: externalRef, onResize } = options;
|
|
158
|
+
const internalRef = useRef(null);
|
|
159
|
+
const targetRef = externalRef ?? internalRef;
|
|
160
|
+
const [width, setWidth] = useState(void 0);
|
|
161
|
+
const [height, setHeight] = useState(void 0);
|
|
162
|
+
const onResizeRef = useRef(onResize);
|
|
163
|
+
onResizeRef.current = onResize;
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const element = targetRef.current;
|
|
166
|
+
if (!element) return;
|
|
167
|
+
const slotId = allocateSlot(slotBitmap);
|
|
168
|
+
if (slotId === -1) {
|
|
169
|
+
console.error(`[@crimson_dev/use-resize-observer/worker] Maximum ${String(MAX_ELEMENTS)} simultaneous observations exceeded.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
activeObserverCount++;
|
|
173
|
+
let cancelled = false;
|
|
174
|
+
let rafId = null;
|
|
175
|
+
const startPolling = () => {
|
|
176
|
+
const poll = () => {
|
|
177
|
+
if (cancelled || !sharedSab) return;
|
|
178
|
+
const { width: w, height: h } = readSlot(sharedSab, slotId);
|
|
179
|
+
setWidth(w);
|
|
180
|
+
setHeight(h);
|
|
181
|
+
onResizeRef.current?.({
|
|
182
|
+
width: w,
|
|
183
|
+
height: h
|
|
184
|
+
});
|
|
185
|
+
rafId = requestAnimationFrame(poll);
|
|
186
|
+
};
|
|
187
|
+
rafId = requestAnimationFrame(poll);
|
|
188
|
+
};
|
|
189
|
+
ensureWorker().then(() => {
|
|
190
|
+
if (cancelled) return;
|
|
191
|
+
sharedWorker?.postMessage({
|
|
192
|
+
op: "observe",
|
|
193
|
+
slotId,
|
|
194
|
+
elementId: element.id || `slot-${String(slotId)}`
|
|
195
|
+
});
|
|
196
|
+
startPolling();
|
|
197
|
+
}).catch((error) => {
|
|
198
|
+
console.error("[@crimson_dev/use-resize-observer/worker] Init failed:", error);
|
|
199
|
+
});
|
|
200
|
+
return () => {
|
|
201
|
+
cancelled = true;
|
|
202
|
+
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
203
|
+
if (workerReady && sharedWorker) sharedWorker.postMessage({
|
|
204
|
+
op: "unobserve",
|
|
205
|
+
slotId
|
|
206
|
+
});
|
|
207
|
+
releaseSlot(slotBitmap, slotId);
|
|
208
|
+
activeObserverCount--;
|
|
209
|
+
terminateWorkerIfIdle();
|
|
210
|
+
};
|
|
211
|
+
}, [targetRef]);
|
|
212
|
+
return {
|
|
213
|
+
ref: targetRef,
|
|
214
|
+
width,
|
|
215
|
+
height,
|
|
216
|
+
entry: void 0
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
//#endregion
|
|
221
|
+
export { MAX_ELEMENTS, SAB_SIZE, SLOT_BYTES, readSlot, useResizeObserverWorker, writeSlot };
|
|
222
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +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('[@crimson_dev/use-resize-observer/worker] Init failed:', error);\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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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crimson_dev/use-resize-observer",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Zero-dependency, Worker-native, ESNext-first React 19 ResizeObserver hook",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Crimson Dev",
|
|
10
|
+
"url": "https://github.com/ABCrimson"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/ABCrimson/use-resize-observer.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react",
|
|
18
|
+
"hook",
|
|
19
|
+
"resize",
|
|
20
|
+
"ResizeObserver",
|
|
21
|
+
"typescript",
|
|
22
|
+
"esm",
|
|
23
|
+
"worker",
|
|
24
|
+
"performance"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./worker": {
|
|
32
|
+
"types": "./dist/worker.d.ts",
|
|
33
|
+
"import": "./dist/worker.js"
|
|
34
|
+
},
|
|
35
|
+
"./shim": {
|
|
36
|
+
"types": "./dist/shim.d.ts",
|
|
37
|
+
"import": "./dist/shim.js"
|
|
38
|
+
},
|
|
39
|
+
"./server": {
|
|
40
|
+
"types": "./dist/server.d.ts",
|
|
41
|
+
"import": "./dist/server.js"
|
|
42
|
+
},
|
|
43
|
+
"./core": {
|
|
44
|
+
"types": "./dist/core.d.ts",
|
|
45
|
+
"import": "./dist/core.js"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"main": "./dist/index.js",
|
|
50
|
+
"module": "./dist/index.js",
|
|
51
|
+
"types": "./dist/index.d.ts",
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"README.md",
|
|
55
|
+
"LICENSE",
|
|
56
|
+
"CHANGELOG.md"
|
|
57
|
+
],
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=25.0.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"react": ">=19.3.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"react": {
|
|
66
|
+
"optional": false
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@biomejs/biome": "2.4.5",
|
|
71
|
+
"@changesets/cli": "2.30.0",
|
|
72
|
+
"@size-limit/preset-small-lib": "12.0.0",
|
|
73
|
+
"@testing-library/dom": "^10.4.1",
|
|
74
|
+
"@testing-library/react": "16.3.2",
|
|
75
|
+
"@testing-library/user-event": "14.6.1",
|
|
76
|
+
"@types/react": "19.2.14",
|
|
77
|
+
"@types/react-dom": "19.2.3",
|
|
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",
|
|
83
|
+
"concurrently": "9.2.1",
|
|
84
|
+
"happy-dom": "20.8.3",
|
|
85
|
+
"playwright": "1.59.0-alpha-2026-03-05",
|
|
86
|
+
"publint": "0.3.0",
|
|
87
|
+
"react": "19.3.0-canary-3bc2d414-20260304",
|
|
88
|
+
"react-dom": "19.3.0-canary-3bc2d414-20260304",
|
|
89
|
+
"shiki": "4.0.1",
|
|
90
|
+
"simple-git-hooks": "2.13.1",
|
|
91
|
+
"size-limit": "12.0.0",
|
|
92
|
+
"svgo": "4.0.1",
|
|
93
|
+
"tinybench": "6.0.0",
|
|
94
|
+
"tsdown": "0.21.0-beta.5",
|
|
95
|
+
"tsx": "4.21.0",
|
|
96
|
+
"typedoc": "0.28.17",
|
|
97
|
+
"typedoc-plugin-markdown": "4.10.0",
|
|
98
|
+
"typedoc-vitepress-theme": "1.1.2",
|
|
99
|
+
"typescript": "6.0.0-dev.20260305",
|
|
100
|
+
"vitepress": "2.0.0-alpha.16",
|
|
101
|
+
"vitepress-plugin-mermaid": "2.0.17",
|
|
102
|
+
"vitest": "4.1.0-beta.5"
|
|
103
|
+
},
|
|
104
|
+
"simple-git-hooks": {
|
|
105
|
+
"pre-commit": "npx @biomejs/biome check --staged"
|
|
106
|
+
},
|
|
107
|
+
"scripts": {
|
|
108
|
+
"build": "tsdown",
|
|
109
|
+
"dev": "tsdown --watch",
|
|
110
|
+
"lint": "biome check ./src ./tests ./bench",
|
|
111
|
+
"format": "biome check --write ./src ./tests ./bench",
|
|
112
|
+
"typecheck": "tsc --noEmit",
|
|
113
|
+
"typecheck:ts7": "tsgo --noEmit",
|
|
114
|
+
"test": "vitest run",
|
|
115
|
+
"test:watch": "vitest",
|
|
116
|
+
"test:browser": "vitest run --project browser:chromium --project browser:firefox --project browser:webkit",
|
|
117
|
+
"test:coverage": "vitest run --coverage",
|
|
118
|
+
"bench": "tsx bench/pool.ts && tsx bench/scheduler.ts && tsx bench/hook.ts && tsx bench/worker.ts && tsx bench/memory.ts",
|
|
119
|
+
"publint": "publint",
|
|
120
|
+
"size": "size-limit",
|
|
121
|
+
"docs:dev": "vitepress dev docs",
|
|
122
|
+
"docs:build": "typedoc && vitepress build docs",
|
|
123
|
+
"docs:preview": "vitepress preview docs",
|
|
124
|
+
"release": "changeset publish --provenance",
|
|
125
|
+
"version": "changeset version",
|
|
126
|
+
"optimize-svgs": "tsx scripts/optimize-svgs.ts",
|
|
127
|
+
"prepare": "simple-git-hooks"
|
|
128
|
+
}
|
|
129
|
+
}
|