view-anchor 0.2.0 → 0.2.2-alpha.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/README.md CHANGED
@@ -1,6 +1,8 @@
1
- # view-anchor
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/lbb00/view-anchor/main/assets/banner.svg" alt="view-anchor — keep anything outside the DOM aligned to a DOM element" width="820">
3
+ </p>
2
4
 
3
- > A high-performance geometry bridge that keeps anything living outside the DOM aligned to a DOM element: an Electron `WebContentsView`, a native webview in another desktop shell, a cross-origin iframe, or any surface you position from a rectangle. Every move and resize is published synchronously with no duplicate frames, and the whole package is about 2.6 KB gzipped.
5
+ > A high-performance geometry bridge that keeps anything living outside the DOM aligned to a DOM element: an Electron `WebContentsView`, a native webview in another desktop shell, a cross-origin iframe, or any surface you position from a rectangle. Every move and resize is published synchronously with no duplicate frames.
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/view-anchor)](https://www.npmjs.com/package/view-anchor)
6
8
  [![npm downloads](https://img.shields.io/npm/dm/view-anchor)](https://www.npmjs.com/package/view-anchor)
@@ -21,33 +23,17 @@ The core has no dependency on Electron, a browser shell, React, or any layout li
21
23
 
22
24
  ## Built for the hot path
23
25
 
24
- Geometry updates fire on every resize and, when following a drag, on every animation frame. The library is written for that path and the numbers are measured, not assumed:
26
+ Geometry updates fire on every resize and, when following a drag, on every animation frame. The library keeps that work predictable:
25
27
 
26
28
  - **Synchronous delivery.** Measurement and publish happen inside the same `ResizeObserver` callback. No timers, no extra frame of lag.
27
29
  - **Dedupe before allocate.** A rectangle identical to the last accepted one is rejected by comparing four numbers, before any object is created.
28
30
  - **Frame following only when needed.** `followGeometry` polls `requestAnimationFrame` during a scroll burst, a splitter drag, or an explicit `pulse()`, then closes itself once the rectangle settles. Idle cost is zero, and hidden or invalid targets are capped at 30 frames.
29
31
  - **O(1) generation changes.** In the protocol layer, moving an anchor to a new generation or clearing it does not touch other anchors.
30
- - **Latest-wins batching.** Messages queued in the same task are merged in a microtask and only the newest geometry per anchor is sent.
31
- - **Small, tree-shakeable output.** Every function is a separate export with `sideEffects: false`. If you only need `createViewAnchor`, you pay for 528 bytes gzipped.
32
+ - **Latest-wins batching.** Messages queued in the same task are merged in a microtask. The newest placement and size for each anchor are sent separately.
33
+ - **Release on disposal.** Disposed handles stop observing and release their target and callback references, even when the caller keeps the handle.
34
+ - **Compact, tree-shakeable core.** Functions are separate exports with `sideEffects: false`; the complete core export is under 3 KB gzipped.
32
35
 
33
- Numbers from `pnpm benchmark` on Node.js 24, Apple M4, median of three fresh processes:
34
-
35
- | Operation | Volume | Time |
36
- | --- | ---: | ---: |
37
- | `measurePlacement` | 1,000,000 calls | 8.9 ms |
38
- | Publish a placement message | 1,000,000 calls | 7.3 ms |
39
- | Decode a valid batch | 100,000 messages | 3.1 ms |
40
- | Move all anchors to a new generation | 10,000 anchors | 1.7 ms |
41
- | Flush one message with 100,000 anchors already tracked | 1 message | 0.008 ms |
42
-
43
- | Entry | Gzipped |
44
- | --- | ---: |
45
- | `view-anchor` (everything) | 2.6 KB |
46
- | `createViewAnchor` alone | 528 B |
47
- | `view-anchor/protocol` | 1.4 KB |
48
- | `view-anchor/react` | 2.0 KB |
49
-
50
- These are same-machine Node.js microbenchmarks. They do not include DOM layout, Electron IPC, or structured clone, so measure those in your own app. Methodology, memory figures, and V8 traces are in [docs/performance-report.md](./docs/performance-report.md).
36
+ The [performance report](./docs/performance-report.md) contains the reproducible measurements and their environment. They are useful for comparing changes on the same machine, not for predicting DOM layout, Electron IPC, structured clone, or your app's workload.
51
37
 
52
38
  ## Installation
53
39
 
@@ -152,46 +138,50 @@ const publish = createPlacementMessagePublisher(
152
138
 
153
139
  // receiving side (main process, host page, ...)
154
140
  const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
155
- if (decoded.ok) { /* check the sender, then apply only newer messages */ }
141
+ if (decoded.ok) {
142
+ /* check the sender, then apply only newer messages */
143
+ }
156
144
  ```
157
145
 
158
146
  Two rules keep the ordering correct:
159
147
 
160
- - **Keep one publisher per `{ anchorId, generation }`.** The batcher and `createGeometrySequenceGuard` remember the highest sequence number seen for each anchor. A publisher rebuilt for the same address restarts at sequence 1 and its messages are dropped as stale. In React, hold it in `useMemo` or `useRef`. Bump `generation` when you really want a fresh start.
148
+ - **Keep one publisher per `{ anchorId, generation }`.** The batcher and `createGeometrySequenceGuard` remember the highest sequence number for each message kind per anchor. A publisher rebuilt for the same address restarts at sequence 1 and its messages are dropped as stale. In React, hold it in `useMemo` or `useRef`. Bump `generation` when you really want a fresh start.
161
149
  - **A synchronous publisher returns `false` to say "not accepted".** The core then retries the same geometry on the next trigger. A batching publisher returns `true` once queued and owns any later retries.
162
150
 
163
151
  The full contract is in [docs/protocol.md](./docs/protocol.md).
164
152
 
165
153
  ## API
166
154
 
167
- | Export | Kind | Purpose |
168
- |---|---|---|
169
- | `createViewAnchor(target, opts)` | function | Measure a DOM element and publish live bounds. A zero rect means collapsed. |
170
- | `createPlacementAnchor(target, opts)` | function | Same core with explicit `Placement` visibility, opt-in `followScroll` / `followGeometry` / `guardDisplayNone`, and `pulse()`. |
171
- | `measurePlacement(target)` | function | Pure measurement: wraps the target rect as `{ visible: true, bounds }`. |
172
- | `createSizeAdvertiser(target, opts)` | function | Reverse direction: report the view's own content size to the host. |
173
- | `useViewAnchor(opts)` from `view-anchor/react` | hook | Returns a ref callback for a placeholder element. |
174
- | `usePlacementAnchor(opts)` from `view-anchor/react` | hook | React adapter for the `Placement` API, including `followScroll` and `followGeometry`. |
175
- | `Bounds` | type | `{ x, y, width, height }` in CSS pixels. |
176
- | `Placement` | type | `{ visible: true; bounds } \| { visible: false }`. |
177
- | `ViewAnchorOptions` / `ViewAnchorHandle` | type | Options and handle for `createViewAnchor`. |
178
- | `PlacementAnchorOptions` / `PlacementAnchorHandle` | type | Options and handle for `createPlacementAnchor`. |
179
- | `UseViewAnchorOptions` / `ViewAnchorRef` from `view-anchor/react` | type | Options and ref shape for `useViewAnchor`. |
180
- | `UsePlacementAnchorOptions` / `PlacementAnchorRef` from `view-anchor/react` | type | Options and ref shape for `usePlacementAnchor`. |
181
- | `AdvertisedAxis` / `AdvertisedSize` | type | Axis and payload types for the reverse direction. |
182
- | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | type | Options and handle for `createSizeAdvertiser`. |
183
- | `view-anchor/protocol` | functions + types | Versioned messages, strict decoding, sequence guards, message publishers, and microtask batching. |
155
+ | Export | Kind | Purpose |
156
+ | --------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
157
+ | `createViewAnchor(target, opts)` | function | Measure a DOM element and publish live bounds. A zero rect means collapsed. |
158
+ | `createPlacementAnchor(target, opts)` | function | Same core with explicit `Placement` visibility, opt-in `followScroll` / `followGeometry` / `guardDisplayNone`, and `pulse()`. |
159
+ | `measurePlacement(target)` | function | Pure measurement: wraps the target rect as `{ visible: true, bounds }`. |
160
+ | `createSizeAdvertiser(target, opts)` | function | Reverse direction: report the view's own content size to the host. |
161
+ | `useViewAnchor(opts)` from `view-anchor/react` | hook | Returns a ref callback for a placeholder element. |
162
+ | `usePlacementAnchor(opts)` from `view-anchor/react` | hook | React adapter for the `Placement` API, including `followScroll` and `followGeometry`. |
163
+ | `Bounds` | type | `{ x, y, width, height }` in CSS pixels. |
164
+ | `Placement` | type | `{ visible: true; bounds } \| { visible: false }`. |
165
+ | `ViewAnchorOptions` / `ViewAnchorHandle` | type | Options and handle for `createViewAnchor`. |
166
+ | `PlacementAnchorOptions` / `PlacementAnchorHandle` | type | Options and handle for `createPlacementAnchor`. |
167
+ | `UseViewAnchorOptions` / `ViewAnchorRef` from `view-anchor/react` | type | Options and ref shape for `useViewAnchor`. |
168
+ | `UsePlacementAnchorOptions` / `PlacementAnchorRef` from `view-anchor/react` | type | Options and ref shape for `usePlacementAnchor`. |
169
+ | `AdvertisedAxis` / `AdvertisedSize` | type | Axis and payload types for the reverse direction. |
170
+ | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | type | Options and handle for `createSizeAdvertiser`. |
171
+ | `view-anchor/protocol` | functions + types | Versioned messages, strict decoding, sequence guards, message publishers, and microtask batching. |
184
172
 
185
173
  ## Documentation
186
174
 
187
- - [docs/mechanism.mdx](./docs/mechanism.mdx): how the forward direction works. Synchronous publishing, stale-frame safety, the `present` / zero-rect / unmount contract, StrictMode behaviour. Includes the interactive 3D demo at [docs/index.html](./docs/index.html).
175
+ - [docs/mechanism.md](./docs/mechanism.md): how the forward direction works. Synchronous publishing, stale-frame safety, the `present` / zero-rect / unmount contract, StrictMode behaviour. Includes the interactive 3D demo at [docs/index.html](./docs/index.html).
188
176
  - [docs/bidirectional-design.md](./docs/bidirectional-design.md): running both directions at once. Why the forward path is synchronous while the reverse path uses animation frames, single-axis ownership, and where the trust boundary sits.
189
177
  - [docs/protocol.md](./docs/protocol.md): message envelopes, validation, ordering, batching, and what happens on failure.
190
178
  - [docs/performance-report.md](./docs/performance-report.md): reproducible CPU, heap, RSS, extreme-case, V8, and export-size measurements.
191
179
 
192
180
  ## Contributing
193
181
 
194
- Issues and pull requests are welcome. Before submitting, run `pnpm lint`, `pnpm check-types`, `pnpm test`, and `pnpm build`. `pnpm benchmark` regenerates the performance report.
182
+ Issues and pull requests are welcome. Before submitting, run `pnpm lint`, `pnpm format:check`, `pnpm check-types`, `pnpm test`, and `pnpm build`. `pnpm benchmark` prints the data used to update the performance report.
183
+
184
+ Changes that affect a published version must include a Changeset. Run `pnpm changeset`, select the version bump, and describe the user-visible change. Merging it into `main` opens a version PR; merging that PR publishes the package.
195
185
 
196
186
  ## License
197
187
 
package/README.zh-CN.md CHANGED
@@ -1,6 +1,8 @@
1
- # view-anchor
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/lbb00/view-anchor/main/assets/banner.svg" alt="view-anchor — 让 DOM 之外的画面贴合 DOM 元素" width="820">
3
+ </p>
2
4
 
3
- > 高性能几何桥接库,让任何"不在 DOM 里的东西"始终贴合某个 DOM 元素:Electron 的 `WebContentsView`、其他桌面壳里的原生 webview、跨域 iframe,或者任何你能用一个矩形来定位的画面。每次移动和缩放都同步发布、不会重复发帧,整个包 gzip 后约 2.6 KB。
5
+ > 高性能几何桥接库,让任何"不在 DOM 里的东西"始终贴合某个 DOM 元素:Electron 的 `WebContentsView`、其他桌面壳里的原生 webview、跨域 iframe,或者任何你能用一个矩形来定位的画面。每次移动和缩放都同步发布、不会重复发帧。
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/view-anchor)](https://www.npmjs.com/package/view-anchor)
6
8
  [![npm downloads](https://img.shields.io/npm/dm/view-anchor)](https://www.npmjs.com/package/view-anchor)
@@ -21,33 +23,17 @@
21
23
 
22
24
  ## 为热路径而写
23
25
 
24
- 几何更新在每次缩放时都会触发,跟随拖拽时更是每一帧都触发。这个库就是为这条路径写的,下面的数字是实测,不是推断:
26
+ 几何更新在每次缩放时都会触发,跟随拖拽时更是每一帧都触发。这个库让这条路径保持可控:
25
27
 
26
28
  - **同步发布。** 测量和发布在同一个 `ResizeObserver` 回调里完成。没有定时器,不多等一帧。
27
29
  - **先去重,再分配。** 和上一次已接受的矩形完全相同的结果,只比较四个数字就会被丢弃,不会创建任何对象。
28
30
  - **只在需要时逐帧跟随。** `followGeometry` 只在滚动、拖动分栏或显式调用 `pulse()` 时才开始按 `requestAnimationFrame` 轮询,矩形稳定后自动停止。空闲时开销为零,隐藏或无效目标最多跟 30 帧。
29
31
  - **generation 切换是 O(1)。** 协议层里,把某个锚点换到新的 generation 或清除它,不会碰到其他锚点。
30
- - **合并批量,只发最新。** 同一个任务里排队的消息在一个微任务里合并,每个锚点只发送最新的几何数据。
31
- - **体积小,可摇树。** 每个函数都是独立导出,并声明了 `sideEffects: false`。只用 `createViewAnchor` 的话,你只为 gzip 后 528 字节买单。
32
+ - **合并批量,只发最新。** 同一个任务里排队的消息在一个微任务里合并,每个锚点分别发送最新的位置和尺寸。
33
+ - **销毁后释放引用。** 句柄 `dispose()` 后会停止观察,并释放目标元素和回调;调用方继续持有句柄也不会保留它们。
34
+ - **核心紧凑,可摇树。** 每个函数都是独立导出,并声明了 `sideEffects: false`;完整核心导出 gzip 后小于 3 KB。
32
35
 
33
- 以下数字来自 `pnpm benchmark`,环境是 Node.js 24Apple M4,取三个全新进程的中位数:
34
-
35
- | 操作 | 数据量 | 耗时 |
36
- | --- | ---: | ---: |
37
- | `measurePlacement` | 1,000,000 次 | 8.9 ms |
38
- | 发布一条 placement 消息 | 1,000,000 次 | 7.3 ms |
39
- | 解码合法 batch | 100,000 条 | 3.1 ms |
40
- | 所有锚点切换到新 generation | 10,000 个锚点 | 1.7 ms |
41
- | 已有 100,000 个锚点时只 flush 一条 | 1 条 | 0.008 ms |
42
-
43
- | 入口 | gzip 后 |
44
- | --- | ---: |
45
- | `view-anchor`(全部) | 2.6 KB |
46
- | 单独 `createViewAnchor` | 528 B |
47
- | `view-anchor/protocol` | 1.4 KB |
48
- | `view-anchor/react` | 2.0 KB |
49
-
50
- 这些是同一台机器上的 Node.js 微基准,不包含 DOM layout、Electron IPC 和 structured clone,这几项请在你自己的应用里测。方法、内存数据和 V8 trace 见 [docs/performance-report.md](./docs/performance-report.md)。
36
+ [性能报告](./docs/performance-report.md) 保留可复现的测量数据和环境。它适合比较同一台机器上的改动,不用于预测 DOM layout、Electron IPCstructured clone 或你的应用负载。
51
37
 
52
38
  ## 安装
53
39
 
@@ -152,46 +138,48 @@ const publish = createPlacementMessagePublisher(
152
138
 
153
139
  // 接收方(主进程、宿主页面等)
154
140
  const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
155
- if (decoded.ok) { /* 先校验发送方,再只应用更新的消息 */ }
141
+ if (decoded.ok) {
142
+ /* 先校验发送方,再只应用更新的消息 */
143
+ }
156
144
  ```
157
145
 
158
146
  两条规则保证顺序正确:
159
147
 
160
- - **每个 `{ anchorId, generation }` 只保留一个 publisher。** batcher 和 `createGeometrySequenceGuard` 会记住每个锚点见过的最大序号。针对同一地址重建 publisher 会让序号从 1 重新开始,它发的消息会被当成过期而丢弃。在 React 里用 `useMemo` 或 `useRef` 持有它。确实需要重新开始时,把 `generation` 加一。
148
+ - **每个 `{ anchorId, generation }` 只保留一个 publisher。** batcher 和 `createGeometrySequenceGuard` 会分别记住每个锚点的位置与尺寸消息的最大序号。针对同一地址重建 publisher 会让序号从 1 重新开始,它发的消息会被当成过期而丢弃。在 React 里用 `useMemo` 或 `useRef` 持有它。确实需要重新开始时,把 `generation` 加一。
161
149
  - **同步 publisher 返回 `false` 表示"未接受"。** 核心会在下一次触发时重试同一份几何数据。批处理 publisher 入队后就返回 `true`,之后的重试由它自己负责。
162
150
 
163
151
  完整约定见 [docs/protocol.md](./docs/protocol.md)。
164
152
 
165
153
  ## API
166
154
 
167
- | 导出 | 类型 | 用途 |
168
- |---|---|---|
169
- | `createViewAnchor(target, opts)` | 函数 | 测量 DOM 元素并发布实时 bounds。零矩形表示已收起。 |
170
- | `createPlacementAnchor(target, opts)` | 函数 | 同一个核心,带显式 `Placement` 可见性、可选的 `followScroll` / `followGeometry` / `guardDisplayNone`,以及 `pulse()`。 |
171
- | `measurePlacement(target)` | 函数 | 纯测量:把目标矩形包成 `{ visible: true, bounds }`。 |
172
- | `createSizeAdvertiser(target, opts)` | 函数 | 反向:把视图自身的内容尺寸报告给宿主。 |
173
- | `useViewAnchor(opts)`,来自 `view-anchor/react` | Hook | 返回用于占位元素的 ref 回调。 |
174
- | `usePlacementAnchor(opts)`,来自 `view-anchor/react` | Hook | `Placement` API 的 React 适配,包含 `followScroll` 和 `followGeometry`。 |
175
- | `Bounds` | 类型 | CSS 像素单位的 `{ x, y, width, height }`。 |
176
- | `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }`。 |
177
- | `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | `createViewAnchor` 的选项和句柄。 |
178
- | `PlacementAnchorOptions` / `PlacementAnchorHandle` | 类型 | `createPlacementAnchor` 的选项和句柄。 |
179
- | `UseViewAnchorOptions` / `ViewAnchorRef`,来自 `view-anchor/react` | 类型 | `useViewAnchor` 的选项和 ref 形状。 |
180
- | `UsePlacementAnchorOptions` / `PlacementAnchorRef`,来自 `view-anchor/react` | 类型 | `usePlacementAnchor` 的选项和 ref 形状。 |
181
- | `AdvertisedAxis` / `AdvertisedSize` | 类型 | 反向方向的轴和负载类型。 |
182
- | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | 类型 | `createSizeAdvertiser` 的选项和句柄。 |
183
- | `view-anchor/protocol` | 函数 + 类型 | 带版本的消息、严格解码、序列守卫、消息 publisher 和微任务批处理。 |
155
+ | 导出 | 类型 | 用途 |
156
+ | ---------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
157
+ | `createViewAnchor(target, opts)` | 函数 | 测量 DOM 元素并发布实时 bounds。零矩形表示已收起。 |
158
+ | `createPlacementAnchor(target, opts)` | 函数 | 同一个核心,带显式 `Placement` 可见性、可选的 `followScroll` / `followGeometry` / `guardDisplayNone`,以及 `pulse()`。 |
159
+ | `measurePlacement(target)` | 函数 | 纯测量:把目标矩形包成 `{ visible: true, bounds }`。 |
160
+ | `createSizeAdvertiser(target, opts)` | 函数 | 反向:把视图自身的内容尺寸报告给宿主。 |
161
+ | `useViewAnchor(opts)`,来自 `view-anchor/react` | Hook | 返回用于占位元素的 ref 回调。 |
162
+ | `usePlacementAnchor(opts)`,来自 `view-anchor/react` | Hook | `Placement` API 的 React 适配,包含 `followScroll` 和 `followGeometry`。 |
163
+ | `Bounds` | 类型 | CSS 像素单位的 `{ x, y, width, height }`。 |
164
+ | `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }`。 |
165
+ | `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | `createViewAnchor` 的选项和句柄。 |
166
+ | `PlacementAnchorOptions` / `PlacementAnchorHandle` | 类型 | `createPlacementAnchor` 的选项和句柄。 |
167
+ | `UseViewAnchorOptions` / `ViewAnchorRef`,来自 `view-anchor/react` | 类型 | `useViewAnchor` 的选项和 ref 形状。 |
168
+ | `UsePlacementAnchorOptions` / `PlacementAnchorRef`,来自 `view-anchor/react` | 类型 | `usePlacementAnchor` 的选项和 ref 形状。 |
169
+ | `AdvertisedAxis` / `AdvertisedSize` | 类型 | 反向方向的轴和负载类型。 |
170
+ | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | 类型 | `createSizeAdvertiser` 的选项和句柄。 |
171
+ | `view-anchor/protocol` | 函数 + 类型 | 带版本的消息、严格解码、序列守卫、消息 publisher 和微任务批处理。 |
184
172
 
185
173
  ## 文档
186
174
 
187
- - [docs/mechanism.mdx](./docs/mechanism.mdx):正向方向的原理。同步发布、防止过期帧、`present` / 零矩形 / 卸载的约定、StrictMode 下的行为。内含 [docs/index.html](./docs/index.html) 的 3D 交互演示。
175
+ - [docs/mechanism.md](./docs/mechanism.md):正向方向的原理。同步发布、防止过期帧、`present` / 零矩形 / 卸载的约定、StrictMode 下的行为。内含 [docs/index.html](./docs/index.html) 的 3D 交互演示。
188
176
  - [docs/bidirectional-design.md](./docs/bidirectional-design.md):两个方向同时运行时的设计。为什么正向是同步的而反向走动画帧、单轴归属,以及信任边界在哪。
189
177
  - [docs/protocol.md](./docs/protocol.md):消息信封、校验、排序、批处理,以及失败时会发生什么。
190
178
  - [docs/performance-report.md](./docs/performance-report.md):可复现的 CPU、堆、RSS、极端场景、V8 和导出体积测量。
191
179
 
192
180
  ## 参与贡献
193
181
 
194
- 欢迎提 issue 和 pull request。提交前请运行 `pnpm lint`、`pnpm check-types`、`pnpm test` 和 `pnpm build`。`pnpm benchmark` 会重新生成性能报告。
182
+ 欢迎提 issue 和 pull request。提交前请运行 `pnpm lint`、`pnpm format:check`、`pnpm check-types`、`pnpm test` 和 `pnpm build`。`pnpm benchmark` 会输出用于更新性能报告的数据。
195
183
 
196
184
  ## 许可证
197
185
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * view-anchor: keeps an external surface aligned with a DOM element's geometry.
3
3
  */
4
- export { createViewAnchor, measurePlacement, createPlacementAnchor, } from './view-anchor.js';
5
- export type { PlacementAnchorOptions, PlacementAnchorHandle, } from './view-anchor.js';
4
+ export { createViewAnchor, measurePlacement, createPlacementAnchor } from './view-anchor.js';
5
+ export type { PlacementAnchorOptions, PlacementAnchorHandle } from './view-anchor.js';
6
6
  export type { Bounds, Placement, Publisher, PublishResult, ViewAnchorOptions, ViewAnchorHandle, } from './types.js';
7
7
  export { createSizeAdvertiser } from './size-advertiser.js';
8
8
  export { useViewAnchor } from './react.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,MAAM,EACN,SAAS,EACT,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC1C,YAAY,EACV,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAC5F,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AACrF,YAAY,EACV,MAAM,EACN,SAAS,EACT,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC1C,YAAY,EACV,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * view-anchor: keeps an external surface aligned with a DOM element's geometry.
3
3
  */
4
- export { createViewAnchor, measurePlacement, createPlacementAnchor, } from './view-anchor.js';
4
+ export { createViewAnchor, measurePlacement, createPlacementAnchor } from './view-anchor.js';
5
5
  export { createSizeAdvertiser } from './size-advertiser.js';
6
6
  export { useViewAnchor } from './react.js';
@@ -1 +1 @@
1
- {"version":3,"file":"measure-loop.d.ts","sourceRoot":"","sources":["../src/measure-loop.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,QAAQ,IAAI,IAAI,CAAA;IAChB,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,MAAM,IAAI,IAAI,CAAA;IACd,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE;IACxC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;IACvB,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;IAC7B,IAAI,EAAE,OAAO,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;CACxC,GAAG,WAAW,CAAC,CAAC,CAAC,CAyDjB"}
1
+ {"version":3,"file":"measure-loop.d.ts","sourceRoot":"","sources":["../src/measure-loop.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,QAAQ,IAAI,IAAI,CAAA;IAChB,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,MAAM,IAAI,IAAI,CAAA;IACd,OAAO,IAAI,IAAI,CAAA;CAChB;AASD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE;IACxC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;IACvB,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;IAC7B,IAAI,EAAE,OAAO,YAAY,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;CACxC,GAAG,WAAW,CAAC,CAAC,CAAC,CAqEjB"}
@@ -5,25 +5,40 @@
5
5
  * Coalesces resize triggers into a single requestAnimationFrame, drops
6
6
  * duplicate measurements, and manages disposal.
7
7
  */
8
+ // Terminal-state stand-ins for cfg.produce/same/sink so a retained handle's
9
+ // dispose()d loop does not keep the original closures (or what they
10
+ // captured) alive. Guarded call sites never actually reach these.
11
+ const NOOP_PRODUCE = () => null;
12
+ const NOOP_SAME = () => true;
13
+ const NOOP_SINK = () => false;
8
14
  export function createMeasureLoop(cfg) {
9
- const { produce, same, sink } = cfg;
15
+ let produce = cfg.produce;
16
+ let same = cfg.same;
17
+ let sink = cfg.sink;
10
18
  let rafId = null;
11
19
  let active = false;
12
20
  let disposed = false;
13
21
  let last = null;
14
22
  let publicationRevision = 0;
15
23
  const deliver = (value) => {
24
+ // A reentrant dispose() from produce()/same() (invoked by frame() just
25
+ // before this call) already cleared `last`; do not let this delivery
26
+ // attempt write over that terminal state.
27
+ if (disposed)
28
+ return false;
16
29
  const previous = last;
17
30
  const attempt = ++publicationRevision;
18
31
  last = value;
19
32
  try {
20
33
  const accepted = sink(value) !== false;
21
- if (!accepted && publicationRevision === attempt)
34
+ // A reentrant dispose() during sink() already cleared `last`; do not
35
+ // resurrect the pre-dispose value over that terminal state.
36
+ if (!accepted && publicationRevision === attempt && !disposed)
22
37
  last = previous;
23
38
  return accepted;
24
39
  }
25
40
  catch (error) {
26
- if (publicationRevision === attempt)
41
+ if (publicationRevision === attempt && !disposed)
27
42
  last = previous;
28
43
  throw error;
29
44
  }
@@ -65,6 +80,10 @@ export function createMeasureLoop(cfg) {
65
80
  return;
66
81
  disposed = true;
67
82
  cancel();
83
+ produce = NOOP_PRODUCE;
84
+ same = NOOP_SAME;
85
+ sink = NOOP_SINK;
86
+ last = null;
68
87
  },
69
88
  };
70
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"protocol-publisher.d.ts","sourceRoot":"","sources":["../src/protocol-publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EAGrB,MAAM,qBAAqB,CAAA;AAE5B,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,eAAe,CAAC,CAAA;AACrD,MAAM,MAAM,iBAAiB,GAAG,SAAS,CAAC,aAAa,CAAC,CAAA;AAExD,MAAM,WAAW,sBAAsB;IACrC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAA;IAC1C,4FAA4F;IAC5F,KAAK,IAAI,OAAO,CAAA;IAChB,gEAAgE;IAChE,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,uEAAuE;IACvE,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,YAAY,GACjB,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,CAcnC;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,YAAY,GACjB,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAcnC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE,sBAA2B,GACnC,eAAe,CAkJjB"}
1
+ {"version":3,"file":"protocol-publisher.d.ts","sourceRoot":"","sources":["../src/protocol-publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EAGrB,MAAM,qBAAqB,CAAA;AAE5B,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,eAAe,CAAC,CAAA;AACrD,MAAM,MAAM,iBAAiB,GAAG,SAAS,CAAC,aAAa,CAAC,CAAA;AAExD,MAAM,WAAW,sBAAsB;IACrC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAA;IAC1C,4FAA4F;IAC5F,KAAK,IAAI,OAAO,CAAA;IAChB,gEAAgE;IAChE,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,uEAAuE;IACvE,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,YAAY,GACjB,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,CAcnC;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,YAAY,GACjB,CAAC,IAAI,EAAE,cAAc,KAAK,OAAO,CAcnC;AAMD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,iBAAiB,EACvB,OAAO,GAAE,sBAA2B,GACnC,eAAe,CAgKjB"}
@@ -41,6 +41,9 @@ export function createSizeMessagePublisher(address, send) {
41
41
  return send(message) !== false;
42
42
  };
43
43
  }
44
+ // Terminal-state stand-in for `send` so a retained, disposed batcher does not
45
+ // keep the caller's transport closure (and whatever it captured) alive.
46
+ const NOOP_SEND = () => false;
44
47
  /**
45
48
  * Coalesces same-task messages without adding a rendering-frame delay. It owns
46
49
  * no authorization policy: callers must associate addresses with trusted IPC
@@ -52,9 +55,12 @@ export function createGeometryBatcher(send, options = {}) {
52
55
  let disposed = false;
53
56
  let scheduled = false;
54
57
  let flushing = false;
55
- function report(error) {
58
+ // `activeOptions` lets a caller keep reporting to the options object that
59
+ // was live when its flush() call started, even if a reentrant dispose()
60
+ // during send() has since cleared the instance-level `options` reference.
61
+ function report(activeOptions, error) {
56
62
  try {
57
- options.onError?.(error);
63
+ activeOptions.onError?.(error);
58
64
  }
59
65
  catch {
60
66
  // Error reporting must not turn scheduled delivery into an unhandled error.
@@ -82,6 +88,9 @@ export function createGeometryBatcher(send, options = {}) {
82
88
  kind: 'batch',
83
89
  messages,
84
90
  };
91
+ // Captured before send() runs so a reentrant dispose() (which clears the
92
+ // instance-level `options`) cannot blind this call's own error report.
93
+ const activeOptions = options;
85
94
  flushing = true;
86
95
  try {
87
96
  let accepted;
@@ -89,7 +98,7 @@ export function createGeometryBatcher(send, options = {}) {
89
98
  accepted = send(batch) !== false;
90
99
  }
91
100
  catch (error) {
92
- report(error);
101
+ report(activeOptions, error);
93
102
  return false;
94
103
  }
95
104
  if (!accepted)
@@ -186,6 +195,13 @@ export function createGeometryBatcher(send, options = {}) {
186
195
  disposed = true;
187
196
  anchors.clear();
188
197
  pendingAnchors.clear();
198
+ // Callers may still mutate the original options object after
199
+ // construction (e.g. reassigning onError); flush() captures its own
200
+ // reference before send() runs, so an in-flight error report keeps
201
+ // reading that object even though dispose() drops the instance's
202
+ // long-lived reference here.
203
+ options = {};
204
+ send = NOOP_SEND;
189
205
  },
190
206
  };
191
207
  }
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,yBAAyB,EAAE,CAAA;AACpC,YAAY,EACV,eAAe,EACf,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,GACZ,CAAA;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhC,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAA;CACpB;AAkED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CA0BtB;AAED,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAA;IACzC,kEAAkE;IAClE,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AASD;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,qBAAqB,CA+BnE;AAED,OAAO,EACL,qBAAqB,EACrB,+BAA+B,EAC/B,0BAA0B,GAC3B,MAAM,yBAAyB,CAAA;AAChC,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,iBAAiB,EACjB,YAAY,GACb,MAAM,yBAAyB,CAAA"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AACA,OAAO,EACL,yBAAyB,EACzB,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAAE,yBAAyB,EAAE,CAAA;AACpC,YAAY,EACV,eAAe,EACf,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,WAAW,GACZ,CAAA;AAED,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,GACtC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEhC,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAA;CACpB;AA6DD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CA0BtB;AAED,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,MAAM,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAA;IACzC,kEAAkE;IAClE,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAKD;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,qBAAqB,CA4BnE;AAED,OAAO,EACL,qBAAqB,EACrB,+BAA+B,EAC/B,0BAA0B,GAC3B,MAAM,yBAAyB,CAAA;AAChC,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,iBAAiB,EACjB,YAAY,GACb,MAAM,yBAAyB,CAAA"}
package/dist/protocol.js CHANGED
@@ -36,8 +36,7 @@ function decodeMessage(value) {
36
36
  return undefined;
37
37
  if (typeof value.anchorId !== 'string' || value.anchorId.length === 0)
38
38
  return undefined;
39
- if (!isNonNegativeSafeInteger(value.generation) ||
40
- !isNonNegativeSafeInteger(value.seq)) {
39
+ if (!isNonNegativeSafeInteger(value.generation) || !isNonNegativeSafeInteger(value.seq)) {
41
40
  return undefined;
42
41
  }
43
42
  const address = {
@@ -48,9 +47,7 @@ function decodeMessage(value) {
48
47
  };
49
48
  if (value.kind === 'placement') {
50
49
  const placement = decodePlacement(value.placement);
51
- return placement === undefined
52
- ? undefined
53
- : { ...address, kind: 'placement', placement };
50
+ return placement === undefined ? undefined : { ...address, kind: 'placement', placement };
54
51
  }
55
52
  if (value.kind === 'size') {
56
53
  const size = decodeSize(value.size);
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EACV,MAAM,EAEN,iBAAiB,EAClB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAA;AAE3E,MAAM,WAAW,yBAA0B,SAAQ,sBAAsB;IACvE;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,8DAA8D;AAC9D,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAA;AAoI9C,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAM1E;AAyBD,qEAAqE;AACrE,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,yBAAyB,GACjC,kBAAkB,CAapB;AAED,YAAY,EAAE,MAAM,EAAE,CAAA"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,MAAM,EAAoB,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE7E,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAA;AAE3E,MAAM,WAAW,yBAA0B,SAAQ,sBAAsB;IACvE;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,8DAA8D;AAC9D,MAAM,MAAM,kBAAkB,GAAG,aAAa,CAAA;AA+H9C,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAM1E;AAsBD,qEAAqE;AACrE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CAazF;AAED,YAAY,EAAE,MAAM,EAAE,CAAA"}
package/dist/react.js CHANGED
@@ -8,14 +8,11 @@ function useAnchorRef(options, applied, adapter) {
8
8
  const handleRef = useRef(null);
9
9
  const elementRef = useRef(null);
10
10
  const optionsRef = useRef(options);
11
- // eslint-disable-next-line react-hooks/refs
12
11
  optionsRef.current = options;
13
12
  const adapterRef = useRef(adapter);
14
- // eslint-disable-next-line react-hooks/refs
15
13
  adapterRef.current = adapter;
16
14
  const appliedRef = useRef(applied);
17
15
  const currentAppliedRef = useRef(applied);
18
- // eslint-disable-next-line react-hooks/refs
19
16
  currentAppliedRef.current = applied;
20
17
  // Options handed to the adapter on the last create/update call.
21
18
  // Tracks applied state across renders where the deps array reference changes.
@@ -73,7 +70,6 @@ function useAnchorRef(options, applied, adapter) {
73
70
  return () => deferDetach(element);
74
71
  }
75
72
  return undefined;
76
- // eslint-disable-next-line react-hooks/exhaustive-deps -- helpers only read stable refs
77
73
  }, []);
78
74
  useEffect(() => {
79
75
  const previous = appliedRef.current;
@@ -87,7 +83,7 @@ function useAnchorRef(options, applied, adapter) {
87
83
  adapterRef.current.update(handle, optionsRef.current);
88
84
  lastAppliedOptionsRef.current = optionsRef.current;
89
85
  }
90
- // eslint-disable-next-line react-hooks/exhaustive-deps
86
+ // oxlint-disable-next-line react/exhaustive-deps
91
87
  }, applied);
92
88
  useEffect(() => {
93
89
  cancelPendingDetach();
@@ -96,7 +92,6 @@ function useAnchorRef(options, applied, adapter) {
96
92
  if (element)
97
93
  deferDetach(element);
98
94
  };
99
- // eslint-disable-next-line react-hooks/exhaustive-deps -- helpers only read stable refs
100
95
  }, []);
101
96
  return ref;
102
97
  }
@@ -1 +1 @@
1
- {"version":3,"file":"size-advertiser.d.ts","sourceRoot":"","sources":["../src/size-advertiser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAA;AAGnB;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,qBAAqB,GAC1B,oBAAoB,CA+DtB"}
1
+ {"version":3,"file":"size-advertiser.d.ts","sourceRoot":"","sources":["../src/size-advertiser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAA;AAOnB;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,qBAAqB,GAC1B,oBAAoB,CAoEtB"}
@@ -1,4 +1,7 @@
1
1
  import { createMeasureLoop } from './measure-loop.js';
2
+ // Replaces a disposed instance's publish callback so a retained handle does
3
+ // not keep the caller's original callback (and whatever it captured) alive.
4
+ const NOOP_PUBLISH = () => false;
2
5
  /**
3
6
  * Report content size for a single axis back to the host.
4
7
  *
@@ -33,6 +36,10 @@ export function createSizeAdvertiser(target, opts) {
33
36
  sink: (extent) => publish({ axis, extent }),
34
37
  });
35
38
  const onResize = (entries) => {
39
+ // A callback queued before disconnect() can still fire once more; do not
40
+ // let it write `latest` after dispose() has already cleared it.
41
+ if (disposed)
42
+ return;
36
43
  const entry = entries[entries.length - 1];
37
44
  if (entry) {
38
45
  latest = entry.borderBoxSize?.[0] ?? entry.contentBoxSize?.[0] ?? latest;
@@ -70,6 +77,8 @@ export function createSizeAdvertiser(target, opts) {
70
77
  observer = null;
71
78
  }
72
79
  loop.dispose();
80
+ publish = NOOP_PUBLISH;
81
+ latest = null;
73
82
  },
74
83
  };
75
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,8CAA8C;AAC9C,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,iEAAiE;AACjE,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,OAAO,CAAA;AAE1C;;;GAGG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,aAAa,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GACjB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,CAAA;AAEtB,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,MAAM,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,6FAA6F;IAC7F,OAAO,IAAI,IAAI,CAAA;CAChB;AAOD,gFAAgF;AAChF,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE/C,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,IAAI,EAAE,cAAc,CAAA;IACpB,qCAAqC;IACrC,OAAO,EAAE,SAAS,CAAC,cAAc,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,cAAc,CAAC,GAAG,IAAI,CAAA;IAChD,6DAA6D;IAC7D,OAAO,IAAI,IAAI,CAAA;CAChB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,8CAA8C;AAC9C,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,iEAAiE;AACjE,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,OAAO,CAAA;AAE1C;;;GAGG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,aAAa,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,CAAA;AAE9E,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,MAAM,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,6FAA6F;IAC7F,OAAO,IAAI,IAAI,CAAA;CAChB;AAOD,gFAAgF;AAChF,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE/C,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,IAAI,EAAE,cAAc,CAAA;IACpB,qCAAqC;IACrC,OAAO,EAAE,SAAS,CAAC,cAAc,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,cAAc,CAAC,GAAG,IAAI,CAAA;IAChD,6DAA6D;IAC7D,OAAO,IAAI,IAAI,CAAA;CAChB"}
@@ -1 +1 @@
1
- {"version":3,"file":"view-anchor.d.ts","sourceRoot":"","sources":["../src/view-anchor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,gBAAgB,EACjB,MAAM,YAAY,CAAA;AAmBnB;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,iBAAiB,GACtB,gBAAgB,CA6FlB;AAID,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,wCAAwC;IACxC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IAC7B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,sBAAsB,GAAG,IAAI,CAAA;IAC1C,2CAA2C;IAC3C,OAAO,IAAI,IAAI,CAAA;IACf;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAM/D;AAeD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,sBAAsB,GAC3B,qBAAqB,CA2SvB"}
1
+ {"version":3,"file":"view-anchor.d.ts","sourceRoot":"","sources":["../src/view-anchor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,SAAS,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAkBnG;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,iBAAiB,GAAG,gBAAgB,CAsG/F;AAID,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,wCAAwC;IACxC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IAC7B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,sBAAsB,GAAG,IAAI,CAAA;IAC1C,2CAA2C;IAC3C,OAAO,IAAI,IAAI,CAAA;IACf;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAM/D;AAeD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,sBAAsB,GAC3B,qBAAqB,CAmTvB"}