view-anchor 0.2.1 → 0.2.2
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 +57 -25
- package/README.zh-CN.md +90 -77
- package/dist/abort.d.ts +3 -0
- package/dist/abort.d.ts.map +1 -0
- package/dist/abort.js +9 -0
- package/dist/protocol-publisher.d.ts +4 -2
- package/dist/protocol-publisher.d.ts.map +1 -1
- package/dist/protocol-publisher.js +25 -14
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +1 -6
- package/dist/size-advertiser.d.ts.map +1 -1
- package/dist/size-advertiser.js +26 -16
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/view-anchor.d.ts +4 -2
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +42 -23
- package/docs/bidirectional-design.md +21 -32
- package/docs/index.html +54 -28
- package/docs/mechanism.md +22 -22
- package/docs/performance-report.md +2 -2
- package/docs/protocol.md +7 -7
- package/package.json +10 -16
- package/src/abort.ts +9 -0
- package/src/protocol-publisher.ts +26 -14
- package/src/react.ts +1 -6
- package/src/size-advertiser.ts +26 -15
- package/src/types.ts +4 -0
- package/src/view-anchor.ts +44 -21
package/docs/protocol.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 通信协议
|
|
2
2
|
|
|
3
|
-
`view-anchor` 核心模块只负责测量并同步调用 `publish
|
|
3
|
+
`view-anchor` 核心模块只负责测量并同步调用 `publish`。如果应用要把几何数据交给异步或不完全可信的通道,可以使用可选的 `view-anchor/protocol` 模块。它提供带版本的消息结构、输入校验、消息防乱序和微任务批处理,不绑定具体的传递方式。
|
|
4
4
|
|
|
5
5
|
一条消息从锚点走到宿主要经过的环节:
|
|
6
6
|
|
|
@@ -16,12 +16,12 @@ sequenceDiagram
|
|
|
16
16
|
A->>P: publish(placement)
|
|
17
17
|
P->>B: 带 anchorId、generation、seq 的消息
|
|
18
18
|
Note over B: 同一微任务内合并,<br/>每个 anchorId 的 placement 和 size<br/>各自只留最新一条
|
|
19
|
-
B->>D:
|
|
19
|
+
B->>D: 应用传递原始数据
|
|
20
20
|
D->>D: 校验版本、类型、整数范围、批次上限
|
|
21
21
|
alt 校验不通过
|
|
22
22
|
D-->>H: 返回 ok: false,不抛异常
|
|
23
23
|
else 校验通过
|
|
24
|
-
Note over D,G:
|
|
24
|
+
Note over D,G: 接收方先验证来源身份
|
|
25
25
|
D->>G: 逐条交给 guard
|
|
26
26
|
alt generation 更旧,或同类型 seq 不大于已见最大值
|
|
27
27
|
G-->>H: 丢弃
|
|
@@ -42,7 +42,7 @@ sequenceDiagram
|
|
|
42
42
|
- `seq`:同一 publisher 内单调递增的序列号。
|
|
43
43
|
- `placement` 或 `size`:具体的位置或尺寸数据。
|
|
44
44
|
|
|
45
|
-
`generation` 作用于同一个 `anchorId`:一旦接收到新一代的消息,所有该锚点的旧代消息都会被直接丢弃。`generation` 与 `seq`
|
|
45
|
+
`generation` 作用于同一个 `anchorId`:一旦接收到新一代的消息,所有该锚点的旧代消息都会被直接丢弃。`generation` 与 `seq` 只负责保证消息顺序,不作为鉴权凭据。接收端仍应在分发前验证来源身份。
|
|
46
46
|
|
|
47
47
|
## 发送端
|
|
48
48
|
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
} from 'view-anchor/protocol'
|
|
54
54
|
|
|
55
55
|
const batcher = createGeometryBatcher(
|
|
56
|
-
|
|
56
|
+
sendGeometryBatch,
|
|
57
57
|
{ onError: (err) => console.error(err) },
|
|
58
58
|
)
|
|
59
59
|
|
|
@@ -88,7 +88,7 @@ if (result.ok) {
|
|
|
88
88
|
|
|
89
89
|
for (const message of messages) {
|
|
90
90
|
if (
|
|
91
|
-
isAuthorized(
|
|
91
|
+
isAuthorized(message.anchorId) &&
|
|
92
92
|
guard.accept(message)
|
|
93
93
|
) {
|
|
94
94
|
applyGeometry(message)
|
|
@@ -105,4 +105,4 @@ if (result.ok) {
|
|
|
105
105
|
- 返回 `true` 或 `void`:表示数据已成功接收或已加入发送队列。
|
|
106
106
|
- 返回 `false`:表示当前未接收。核心会在下一次测量触发时重新尝试该值。
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
如果底层传递是异步的,应当先在同步回调中将消息放入本地队列并返回 `true`,后续的重试由发送队列自行管理。
|
package/package.json
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "view-anchor",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "High-performance geometry bridge that
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "High-performance geometry bridge that measures a DOM element and synchronously gives application code its current bounds: deduplicated updates, optional versioned message batching, React adapter, framework-free core.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"electron",
|
|
7
|
-
"webcontentsview",
|
|
8
|
-
"browserview",
|
|
9
|
-
"webview",
|
|
10
|
-
"native view",
|
|
11
|
-
"iframe",
|
|
12
|
-
"postmessage",
|
|
13
|
-
"ipc",
|
|
14
6
|
"dom",
|
|
15
7
|
"geometry",
|
|
16
8
|
"bounds",
|
|
@@ -73,6 +65,7 @@
|
|
|
73
65
|
},
|
|
74
66
|
"devDependencies": {
|
|
75
67
|
"@testing-library/react": "^16.3.2",
|
|
68
|
+
"@changesets/cli": "^3.0.2",
|
|
76
69
|
"@types/node": "^26.5.1",
|
|
77
70
|
"@types/react": "^18.3.12",
|
|
78
71
|
"@vitejs/plugin-react": "^6.0.1",
|
|
@@ -91,16 +84,17 @@
|
|
|
91
84
|
},
|
|
92
85
|
"scripts": {
|
|
93
86
|
"build": "tsc -p tsconfig.build.json && pnpm run build:docs",
|
|
94
|
-
"build:docs": "node scripts/build-docs.
|
|
95
|
-
"check-types": "tsc --noEmit",
|
|
96
|
-
"lint": "oxlint . --deny-warnings",
|
|
87
|
+
"build:docs": "node scripts/build-docs.js",
|
|
88
|
+
"check-types": "tsc --noEmit -p tsconfig.test.json",
|
|
89
|
+
"lint": "oxlint . --deny-warnings --report-unused-disable-directives",
|
|
97
90
|
"format": "oxfmt --write .",
|
|
98
91
|
"format:check": "oxfmt --check .",
|
|
99
92
|
"test": "vitest run --reporter=default --reporter=json --outputFile.json=test-report.json --coverage.enabled --coverage.reporter=json-summary --coverage.reportsDirectory=coverage",
|
|
100
93
|
"test:dev": "vitest",
|
|
101
94
|
"test:coverage": "vitest run --coverage",
|
|
102
|
-
"benchmark": "node --expose-gc scripts/performance-report.
|
|
103
|
-
"benchmark:v8": "node --trace-opt --trace-deopt --trace-turbo-inlining --expose-gc scripts/performance-report.
|
|
104
|
-
"check-package": "node scripts/check-package.
|
|
95
|
+
"benchmark": "node --expose-gc scripts/performance-report.js",
|
|
96
|
+
"benchmark:v8": "node --trace-opt --trace-deopt --trace-turbo-inlining --expose-gc scripts/performance-report.js",
|
|
97
|
+
"check-package": "node scripts/check-package.js",
|
|
98
|
+
"release": "pnpm run build && pnpm run test && pnpm run check-package && changeset publish"
|
|
105
99
|
}
|
|
106
100
|
}
|
package/src/abort.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const NOOP = (): void => {}
|
|
2
|
+
|
|
3
|
+
/** Attach a one-shot abort listener and return a function that detaches it. */
|
|
4
|
+
export function watchAbort(signal: AbortSignal | undefined, dispose: () => void): () => void {
|
|
5
|
+
if (!signal) return NOOP
|
|
6
|
+
const onAbort = (): void => dispose()
|
|
7
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
8
|
+
return () => signal.removeEventListener('abort', onAbort)
|
|
9
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AdvertisedSize, Placement, Publisher } from './types.js'
|
|
2
|
+
import { watchAbort } from './abort.js'
|
|
2
3
|
import {
|
|
3
4
|
GEOMETRY_PROTOCOL_VERSION,
|
|
4
5
|
type GeometryAddress,
|
|
@@ -14,6 +15,8 @@ export type GeometryBatchSend = Publisher<GeometryBatch>
|
|
|
14
15
|
export interface GeometryBatcherOptions {
|
|
15
16
|
/** Observes every batch-delivery error, including explicit flushes; it must not throw. */
|
|
16
17
|
onError?: (error: unknown) => void
|
|
18
|
+
/** Stops this batcher when aborted. An already-aborted signal starts no work. */
|
|
19
|
+
signal?: AbortSignal
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export interface GeometryBatcher {
|
|
@@ -85,8 +88,8 @@ const NOOP_SEND: GeometryBatchSend = () => false
|
|
|
85
88
|
|
|
86
89
|
/**
|
|
87
90
|
* Coalesces same-task messages without adding a rendering-frame delay. It owns
|
|
88
|
-
* no authorization policy: callers must associate addresses with trusted
|
|
89
|
-
*
|
|
91
|
+
* no authorization policy: callers must associate addresses with trusted
|
|
92
|
+
* sources before accepting a delivered batch.
|
|
90
93
|
*/
|
|
91
94
|
export function createGeometryBatcher(
|
|
92
95
|
send: GeometryBatchSend,
|
|
@@ -107,6 +110,7 @@ export function createGeometryBatcher(
|
|
|
107
110
|
let disposed = false
|
|
108
111
|
let scheduled = false
|
|
109
112
|
let flushing = false
|
|
113
|
+
let removeAbortListener = (): void => {}
|
|
110
114
|
|
|
111
115
|
// `activeOptions` lets a caller keep reporting to the options object that
|
|
112
116
|
// was live when its flush() call started, even if a reentrant dispose()
|
|
@@ -192,6 +196,25 @@ export function createGeometryBatcher(
|
|
|
192
196
|
})
|
|
193
197
|
}
|
|
194
198
|
|
|
199
|
+
const dispose = (): void => {
|
|
200
|
+
if (disposed) return
|
|
201
|
+
disposed = true
|
|
202
|
+
removeAbortListener()
|
|
203
|
+
removeAbortListener = (): void => {}
|
|
204
|
+
anchors.clear()
|
|
205
|
+
pendingAnchors.clear()
|
|
206
|
+
// Callers may still mutate the original options object after
|
|
207
|
+
// construction (e.g. reassigning onError); flush() captures its own
|
|
208
|
+
// reference before send() runs, so an in-flight error report keeps
|
|
209
|
+
// reading that object even though dispose() drops the instance's
|
|
210
|
+
// long-lived reference here.
|
|
211
|
+
options = {}
|
|
212
|
+
send = NOOP_SEND
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (options.signal?.aborted) dispose()
|
|
216
|
+
else removeAbortListener = watchAbort(options.signal, dispose)
|
|
217
|
+
|
|
195
218
|
return {
|
|
196
219
|
publish(message) {
|
|
197
220
|
if (disposed) return false
|
|
@@ -238,17 +261,6 @@ export function createGeometryBatcher(
|
|
|
238
261
|
if (state !== undefined) pendingAnchors.delete(state)
|
|
239
262
|
anchors.delete(anchorId)
|
|
240
263
|
},
|
|
241
|
-
dispose
|
|
242
|
-
disposed = true
|
|
243
|
-
anchors.clear()
|
|
244
|
-
pendingAnchors.clear()
|
|
245
|
-
// Callers may still mutate the original options object after
|
|
246
|
-
// construction (e.g. reassigning onError); flush() captures its own
|
|
247
|
-
// reference before send() runs, so an in-flight error report keeps
|
|
248
|
-
// reading that object even though dispose() drops the instance's
|
|
249
|
-
// long-lived reference here.
|
|
250
|
-
options = {}
|
|
251
|
-
send = NOOP_SEND
|
|
252
|
-
},
|
|
264
|
+
dispose,
|
|
253
265
|
}
|
|
254
266
|
}
|
package/src/react.ts
CHANGED
|
@@ -50,14 +50,11 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
50
50
|
const handleRef = useRef<Handle | null>(null)
|
|
51
51
|
const elementRef = useRef<HTMLElement | null>(null)
|
|
52
52
|
const optionsRef = useRef(options)
|
|
53
|
-
// eslint-disable-next-line react-hooks/refs
|
|
54
53
|
optionsRef.current = options
|
|
55
54
|
const adapterRef = useRef(adapter)
|
|
56
|
-
// eslint-disable-next-line react-hooks/refs
|
|
57
55
|
adapterRef.current = adapter
|
|
58
56
|
const appliedRef = useRef(applied)
|
|
59
57
|
const currentAppliedRef = useRef(applied)
|
|
60
|
-
// eslint-disable-next-line react-hooks/refs
|
|
61
58
|
currentAppliedRef.current = applied
|
|
62
59
|
// Options handed to the adapter on the last create/update call.
|
|
63
60
|
// Tracks applied state across renders where the deps array reference changes.
|
|
@@ -116,7 +113,6 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
116
113
|
return () => deferDetach(element)
|
|
117
114
|
}
|
|
118
115
|
return undefined
|
|
119
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- helpers only read stable refs
|
|
120
116
|
}, [])
|
|
121
117
|
|
|
122
118
|
useEffect(() => {
|
|
@@ -131,7 +127,7 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
131
127
|
adapterRef.current.update(handle, optionsRef.current)
|
|
132
128
|
lastAppliedOptionsRef.current = optionsRef.current
|
|
133
129
|
}
|
|
134
|
-
//
|
|
130
|
+
// oxlint-disable-next-line react/exhaustive-deps
|
|
135
131
|
}, applied)
|
|
136
132
|
|
|
137
133
|
useEffect(() => {
|
|
@@ -140,7 +136,6 @@ function useAnchorRef<Options, Handle extends AnchorHandle>(
|
|
|
140
136
|
const element = elementRef.current
|
|
141
137
|
if (element) deferDetach(element)
|
|
142
138
|
}
|
|
143
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- helpers only read stable refs
|
|
144
139
|
}, [])
|
|
145
140
|
|
|
146
141
|
return ref
|
package/src/size-advertiser.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
SizeAdvertiserOptions,
|
|
5
5
|
SizeAdvertiserHandle,
|
|
6
6
|
} from './types.js'
|
|
7
|
+
import { watchAbort } from './abort.js'
|
|
7
8
|
import { createMeasureLoop } from './measure-loop.js'
|
|
8
9
|
|
|
9
10
|
// Replaces a disposed instance's publish callback so a retained handle does
|
|
@@ -68,9 +69,30 @@ export function createSizeAdvertiser(
|
|
|
68
69
|
)
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
let removeAbortListener = (): void => {}
|
|
73
|
+
|
|
74
|
+
const dispose = (): void => {
|
|
75
|
+
if (disposed) return
|
|
76
|
+
disposed = true
|
|
77
|
+
removeAbortListener()
|
|
78
|
+
removeAbortListener = (): void => {}
|
|
79
|
+
loop.cancel()
|
|
80
|
+
if (observer) {
|
|
81
|
+
observer.disconnect()
|
|
82
|
+
observer = null
|
|
83
|
+
}
|
|
84
|
+
loop.dispose()
|
|
85
|
+
publish = NOOP_PUBLISH
|
|
86
|
+
latest = null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (opts.signal?.aborted) dispose()
|
|
90
|
+
else {
|
|
91
|
+
removeAbortListener = watchAbort(opts.signal, dispose)
|
|
92
|
+
loop.setActive(true)
|
|
93
|
+
observer = new ResizeObserver(onResize)
|
|
94
|
+
observer.observe(target)
|
|
95
|
+
}
|
|
74
96
|
|
|
75
97
|
return {
|
|
76
98
|
update(nextPublish: Publisher<AdvertisedSize>): void {
|
|
@@ -81,17 +103,6 @@ export function createSizeAdvertiser(
|
|
|
81
103
|
const cur = produce()
|
|
82
104
|
if (cur !== null) loop.emitNow(cur)
|
|
83
105
|
},
|
|
84
|
-
dispose
|
|
85
|
-
if (disposed) return
|
|
86
|
-
disposed = true
|
|
87
|
-
loop.cancel()
|
|
88
|
-
if (observer) {
|
|
89
|
-
observer.disconnect()
|
|
90
|
-
observer = null
|
|
91
|
-
}
|
|
92
|
-
loop.dispose()
|
|
93
|
-
publish = NOOP_PUBLISH
|
|
94
|
-
latest = null
|
|
95
|
-
},
|
|
106
|
+
dispose,
|
|
96
107
|
}
|
|
97
108
|
}
|
package/src/types.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface ViewAnchorOptions {
|
|
|
36
36
|
present: boolean
|
|
37
37
|
/** Receives the live rect, or zero bounds when detached. */
|
|
38
38
|
publish: Publisher<Bounds>
|
|
39
|
+
/** Stops this anchor when aborted. An already-aborted signal starts no work. */
|
|
40
|
+
signal?: AbortSignal
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export interface ViewAnchorHandle {
|
|
@@ -66,6 +68,8 @@ export interface SizeAdvertiserOptions {
|
|
|
66
68
|
axis: AdvertisedAxis
|
|
67
69
|
/** Receives each advertised size. */
|
|
68
70
|
publish: Publisher<AdvertisedSize>
|
|
71
|
+
/** Stops this advertiser when aborted. An already-aborted signal starts no work. */
|
|
72
|
+
signal?: AbortSignal
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
export interface SizeAdvertiserHandle {
|
package/src/view-anchor.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Bounds, Placement, Publisher, ViewAnchorOptions, ViewAnchorHandle } from './types.js'
|
|
2
|
+
import { watchAbort } from './abort.js'
|
|
2
3
|
|
|
3
4
|
const ZERO: Bounds = { x: 0, y: 0, width: 0, height: 0 }
|
|
4
5
|
|
|
@@ -27,8 +28,8 @@ const clampRect = (r: { x: number; y: number; width: number; height: number }):
|
|
|
27
28
|
* - `dispose()`: stops observing and prevents any further publishes.
|
|
28
29
|
*
|
|
29
30
|
* Synchronous publishing: measurement and publishing occur directly in the
|
|
30
|
-
* observer tick.
|
|
31
|
-
* adding requestAnimationFrame would add
|
|
31
|
+
* observer tick. Applying geometry outside the DOM may already be delayed;
|
|
32
|
+
* adding requestAnimationFrame would add another frame of visual lag during drag
|
|
32
33
|
* operations. High-frequency updates are deduplicated against the last accepted rect.
|
|
33
34
|
*/
|
|
34
35
|
export function createViewAnchor(target: HTMLElement, opts: ViewAnchorOptions): ViewAnchorHandle {
|
|
@@ -46,7 +47,7 @@ export function createViewAnchor(target: HTMLElement, opts: ViewAnchorOptions):
|
|
|
46
47
|
|
|
47
48
|
const measure = (): Bounds | null => {
|
|
48
49
|
const r = targetRef!.getBoundingClientRect()
|
|
49
|
-
// Drop ticks with non-finite values (NaN / Infinity
|
|
50
|
+
// Drop ticks with non-finite values (NaN / Infinity are not usable geometry).
|
|
50
51
|
if (
|
|
51
52
|
!Number.isFinite(r.left) ||
|
|
52
53
|
!Number.isFinite(r.top) ||
|
|
@@ -115,7 +116,24 @@ export function createViewAnchor(target: HTMLElement, opts: ViewAnchorOptions):
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
let removeAbortListener = (): void => {}
|
|
120
|
+
|
|
121
|
+
const dispose = (): void => {
|
|
122
|
+
if (disposed) return
|
|
123
|
+
disposed = true
|
|
124
|
+
removeAbortListener()
|
|
125
|
+
removeAbortListener = (): void => {}
|
|
126
|
+
stopObserving()
|
|
127
|
+
targetRef = null
|
|
128
|
+
publish = NOOP_PUBLISH
|
|
129
|
+
lastPublished = null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (opts.signal?.aborted) dispose()
|
|
133
|
+
else {
|
|
134
|
+
removeAbortListener = watchAbort(opts.signal, dispose)
|
|
135
|
+
apply()
|
|
136
|
+
}
|
|
119
137
|
|
|
120
138
|
return {
|
|
121
139
|
update(next: ViewAnchorOptions): void {
|
|
@@ -124,14 +142,7 @@ export function createViewAnchor(target: HTMLElement, opts: ViewAnchorOptions):
|
|
|
124
142
|
present = next.present
|
|
125
143
|
apply()
|
|
126
144
|
},
|
|
127
|
-
dispose
|
|
128
|
-
if (disposed) return
|
|
129
|
-
disposed = true
|
|
130
|
-
stopObserving()
|
|
131
|
-
targetRef = null
|
|
132
|
-
publish = NOOP_PUBLISH
|
|
133
|
-
lastPublished = null
|
|
134
|
-
},
|
|
145
|
+
dispose,
|
|
135
146
|
}
|
|
136
147
|
}
|
|
137
148
|
|
|
@@ -145,6 +156,8 @@ export interface PlacementAnchorOptions {
|
|
|
145
156
|
visible: boolean
|
|
146
157
|
/** Receives each explicit Placement. */
|
|
147
158
|
publish: Publisher<Placement>
|
|
159
|
+
/** Stops this anchor when aborted. An already-aborted signal starts no work. */
|
|
160
|
+
signal?: AbortSignal
|
|
148
161
|
/**
|
|
149
162
|
* When true, targets with zero area (such as display: none or unmounted elements)
|
|
150
163
|
* publish { visible: false } instead of { visible: true, bounds: 0x0 }, and an
|
|
@@ -485,7 +498,24 @@ export function createPlacementAnchor(
|
|
|
485
498
|
}
|
|
486
499
|
}
|
|
487
500
|
|
|
488
|
-
|
|
501
|
+
let removeAbortListener = (): void => {}
|
|
502
|
+
|
|
503
|
+
const dispose = (): void => {
|
|
504
|
+
if (disposed) return
|
|
505
|
+
disposed = true
|
|
506
|
+
removeAbortListener()
|
|
507
|
+
removeAbortListener = (): void => {}
|
|
508
|
+
stopObserving()
|
|
509
|
+
targetRef = null
|
|
510
|
+
publish = NOOP_PUBLISH
|
|
511
|
+
lastPublished = null
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (opts.signal?.aborted) dispose()
|
|
515
|
+
else {
|
|
516
|
+
removeAbortListener = watchAbort(opts.signal, dispose)
|
|
517
|
+
apply()
|
|
518
|
+
}
|
|
489
519
|
|
|
490
520
|
return {
|
|
491
521
|
update(next: PlacementAnchorOptions): void {
|
|
@@ -503,14 +533,7 @@ export function createPlacementAnchor(
|
|
|
503
533
|
}
|
|
504
534
|
apply()
|
|
505
535
|
},
|
|
506
|
-
dispose
|
|
507
|
-
if (disposed) return
|
|
508
|
-
disposed = true
|
|
509
|
-
stopObserving()
|
|
510
|
-
targetRef = null
|
|
511
|
-
publish = NOOP_PUBLISH
|
|
512
|
-
lastPublished = null
|
|
513
|
-
},
|
|
536
|
+
dispose,
|
|
514
537
|
pulse(durationMs?: number): void {
|
|
515
538
|
if (disposed || !followGeometry) return
|
|
516
539
|
if (durationMs !== undefined && durationMs > 0) {
|