view-anchor 0.1.2 → 0.2.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 +118 -34
- package/README.zh-CN.md +128 -44
- package/dist/index.d.ts +4 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -14
- package/dist/measure-loop.d.ts +9 -28
- package/dist/measure-loop.d.ts.map +1 -1
- package/dist/measure-loop.js +37 -16
- package/dist/protocol-publisher.d.ts +41 -0
- package/dist/protocol-publisher.d.ts.map +1 -0
- package/dist/protocol-publisher.js +191 -0
- package/dist/protocol-types.d.ts +36 -0
- package/dist/protocol-types.d.ts.map +1 -0
- package/dist/protocol-types.js +10 -0
- package/dist/protocol.d.ts +35 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +131 -0
- package/dist/react.d.ts +18 -30
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +125 -122
- package/dist/size-advertiser.d.ts +9 -14
- package/dist/size-advertiser.d.ts.map +1 -1
- package/dist/size-advertiser.js +20 -28
- package/dist/types.d.ts +29 -73
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -15
- package/dist/view-anchor.d.ts +36 -77
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +206 -174
- package/docs/bidirectional-design.md +64 -96
- package/docs/{anchor-3d.html → index.html} +215 -73
- package/docs/mechanism.mdx +55 -49
- package/docs/performance-report.md +63 -0
- package/docs/protocol.md +79 -0
- package/package.json +30 -4
- package/src/index.ts +6 -15
- package/src/measure-loop.ts +36 -41
- package/src/protocol-publisher.ts +236 -0
- package/src/protocol-types.ts +43 -0
- package/src/protocol.ts +193 -0
- package/src/react.ts +186 -141
- package/src/size-advertiser.ts +24 -31
- package/src/types.ts +34 -79
- package/src/view-anchor.ts +228 -212
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { AdvertisedSize, Placement, Publisher } from './types.js'
|
|
2
|
+
import {
|
|
3
|
+
GEOMETRY_PROTOCOL_VERSION,
|
|
4
|
+
type GeometryAddress,
|
|
5
|
+
type GeometryBatch,
|
|
6
|
+
type GeometryMessage,
|
|
7
|
+
type PlacementMessage,
|
|
8
|
+
type SizeMessage,
|
|
9
|
+
} from './protocol-types.js'
|
|
10
|
+
|
|
11
|
+
export type GeometrySend = Publisher<GeometryMessage>
|
|
12
|
+
export type GeometryBatchSend = Publisher<GeometryBatch>
|
|
13
|
+
|
|
14
|
+
export interface GeometryBatcherOptions {
|
|
15
|
+
/** Observes every batch-delivery error, including explicit flushes; it must not throw. */
|
|
16
|
+
onError?: (error: unknown) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GeometryBatcher {
|
|
20
|
+
/** Queues one message. Returns false after disposal. */
|
|
21
|
+
publish(message: GeometryMessage): boolean
|
|
22
|
+
/** Attempts delivery of the current latest-value snapshot; delivery errors return false. */
|
|
23
|
+
flush(): boolean
|
|
24
|
+
/** Forgets one anchor's state, or every anchor when omitted. */
|
|
25
|
+
clear(anchorId?: string): void
|
|
26
|
+
/** Clears pending state; already scheduled microtasks become inert. */
|
|
27
|
+
dispose(): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Wraps placement updates in a versioned protocol message. Sequence numbers
|
|
32
|
+
* start at 1 and increment with each attempted delivery.
|
|
33
|
+
*
|
|
34
|
+
* Keep one publisher per `{ anchorId, generation }` (e.g. via `useMemo` or `useRef`).
|
|
35
|
+
* Batchers and sequence guards drop messages with older sequence numbers, so
|
|
36
|
+
* recreating a publisher for the same address causes its messages to be dropped.
|
|
37
|
+
* Increment `generation` when intentionally resetting the publisher.
|
|
38
|
+
*/
|
|
39
|
+
export function createPlacementMessagePublisher(
|
|
40
|
+
address: GeometryAddress,
|
|
41
|
+
send: GeometrySend,
|
|
42
|
+
): (placement: Placement) => boolean {
|
|
43
|
+
let seq = 0
|
|
44
|
+
|
|
45
|
+
return (placement) => {
|
|
46
|
+
const message: PlacementMessage = {
|
|
47
|
+
v: GEOMETRY_PROTOCOL_VERSION,
|
|
48
|
+
kind: 'placement',
|
|
49
|
+
anchorId: address.anchorId,
|
|
50
|
+
generation: address.generation,
|
|
51
|
+
seq: ++seq,
|
|
52
|
+
placement,
|
|
53
|
+
}
|
|
54
|
+
return send(message) !== false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wraps size updates in a versioned protocol message.
|
|
60
|
+
* Follows the same stability rule: keep one publisher per `{ anchorId, generation }`,
|
|
61
|
+
* and increment `generation` when resetting.
|
|
62
|
+
*/
|
|
63
|
+
export function createSizeMessagePublisher(
|
|
64
|
+
address: GeometryAddress,
|
|
65
|
+
send: GeometrySend,
|
|
66
|
+
): (size: AdvertisedSize) => boolean {
|
|
67
|
+
let seq = 0
|
|
68
|
+
|
|
69
|
+
return (size) => {
|
|
70
|
+
const message: SizeMessage = {
|
|
71
|
+
v: GEOMETRY_PROTOCOL_VERSION,
|
|
72
|
+
kind: 'size',
|
|
73
|
+
anchorId: address.anchorId,
|
|
74
|
+
generation: address.generation,
|
|
75
|
+
seq: ++seq,
|
|
76
|
+
size,
|
|
77
|
+
}
|
|
78
|
+
return send(message) !== false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Coalesces same-task messages without adding a rendering-frame delay. It owns
|
|
84
|
+
* no authorization policy: callers must associate addresses with trusted IPC
|
|
85
|
+
* senders before accepting a delivered batch.
|
|
86
|
+
*/
|
|
87
|
+
export function createGeometryBatcher(
|
|
88
|
+
send: GeometryBatchSend,
|
|
89
|
+
options: GeometryBatcherOptions = {},
|
|
90
|
+
): GeometryBatcher {
|
|
91
|
+
/** State is indexed by anchor so upgrades and clear(anchor) are O(1). */
|
|
92
|
+
interface AnchorState {
|
|
93
|
+
/** Generation, pending placement/size, and their accepted sequence marks. */
|
|
94
|
+
g: number
|
|
95
|
+
p?: PlacementMessage
|
|
96
|
+
s?: SizeMessage
|
|
97
|
+
pSeq?: number
|
|
98
|
+
sSeq?: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const anchors = new Map<string, AnchorState>()
|
|
102
|
+
const pendingAnchors = new Set<AnchorState>()
|
|
103
|
+
let disposed = false
|
|
104
|
+
let scheduled = false
|
|
105
|
+
let flushing = false
|
|
106
|
+
|
|
107
|
+
function report(error: unknown): void {
|
|
108
|
+
try {
|
|
109
|
+
options.onError?.(error)
|
|
110
|
+
} catch {
|
|
111
|
+
// Error reporting must not turn scheduled delivery into an unhandled error.
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function flush(): boolean {
|
|
116
|
+
if (disposed || flushing) return false
|
|
117
|
+
const messages: GeometryMessage[] = []
|
|
118
|
+
const snapshotStates: AnchorState[] = []
|
|
119
|
+
for (const state of pendingAnchors) {
|
|
120
|
+
if (state.p !== undefined) {
|
|
121
|
+
messages.push(state.p)
|
|
122
|
+
snapshotStates.push(state)
|
|
123
|
+
}
|
|
124
|
+
if (state.s !== undefined) {
|
|
125
|
+
messages.push(state.s)
|
|
126
|
+
snapshotStates.push(state)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (messages.length === 0) return false
|
|
130
|
+
|
|
131
|
+
const batch: GeometryBatch = {
|
|
132
|
+
v: GEOMETRY_PROTOCOL_VERSION,
|
|
133
|
+
kind: 'batch',
|
|
134
|
+
messages,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
flushing = true
|
|
138
|
+
try {
|
|
139
|
+
let accepted: boolean
|
|
140
|
+
try {
|
|
141
|
+
accepted = send(batch) !== false
|
|
142
|
+
} catch (error) {
|
|
143
|
+
report(error)
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
if (!accepted) return false
|
|
147
|
+
|
|
148
|
+
for (let index = 0; index < messages.length; index++) {
|
|
149
|
+
const message = messages[index]!
|
|
150
|
+
const state = anchors.get(message.anchorId)
|
|
151
|
+
// A reentrant clear or generation upgrade replaces the state object.
|
|
152
|
+
if (state === undefined || state !== snapshotStates[index]) continue
|
|
153
|
+
if (message.kind === 'placement') {
|
|
154
|
+
if (state.pSeq === undefined || message.seq > state.pSeq) {
|
|
155
|
+
state.pSeq = message.seq
|
|
156
|
+
}
|
|
157
|
+
// Reentrant publishing may have replaced this message while send ran.
|
|
158
|
+
if (state.p === message) state.p = undefined
|
|
159
|
+
} else {
|
|
160
|
+
if (state.sSeq === undefined || message.seq > state.sSeq) {
|
|
161
|
+
state.sSeq = message.seq
|
|
162
|
+
}
|
|
163
|
+
if (state.s === message) state.s = undefined
|
|
164
|
+
}
|
|
165
|
+
if (state.p === undefined && state.s === undefined) {
|
|
166
|
+
pendingAnchors.delete(state)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return true
|
|
170
|
+
} finally {
|
|
171
|
+
flushing = false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function schedule(): void {
|
|
176
|
+
if (scheduled || disposed) return
|
|
177
|
+
scheduled = true
|
|
178
|
+
queueMicrotask(() => {
|
|
179
|
+
scheduled = false
|
|
180
|
+
if (!disposed) flush()
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
publish(message) {
|
|
186
|
+
if (disposed) return false
|
|
187
|
+
let state = anchors.get(message.anchorId)
|
|
188
|
+
if (state !== undefined && message.generation < state.g) return true
|
|
189
|
+
if (state === undefined || message.generation > state.g) {
|
|
190
|
+
if (state !== undefined) pendingAnchors.delete(state)
|
|
191
|
+
state = {
|
|
192
|
+
g: message.generation,
|
|
193
|
+
p: undefined,
|
|
194
|
+
s: undefined,
|
|
195
|
+
pSeq: undefined,
|
|
196
|
+
sSeq: undefined,
|
|
197
|
+
}
|
|
198
|
+
anchors.set(message.anchorId, state)
|
|
199
|
+
}
|
|
200
|
+
if (message.kind === 'placement') {
|
|
201
|
+
if (
|
|
202
|
+
(state.p === undefined || message.seq > state.p.seq) &&
|
|
203
|
+
(state.pSeq === undefined || message.seq > state.pSeq)
|
|
204
|
+
) {
|
|
205
|
+
state.p = message
|
|
206
|
+
pendingAnchors.add(state)
|
|
207
|
+
schedule()
|
|
208
|
+
}
|
|
209
|
+
} else if (
|
|
210
|
+
(state.s === undefined || message.seq > state.s.seq) &&
|
|
211
|
+
(state.sSeq === undefined || message.seq > state.sSeq)
|
|
212
|
+
) {
|
|
213
|
+
state.s = message
|
|
214
|
+
pendingAnchors.add(state)
|
|
215
|
+
schedule()
|
|
216
|
+
}
|
|
217
|
+
return true
|
|
218
|
+
},
|
|
219
|
+
flush,
|
|
220
|
+
clear(anchorId) {
|
|
221
|
+
if (anchorId === undefined) {
|
|
222
|
+
anchors.clear()
|
|
223
|
+
pendingAnchors.clear()
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
const state = anchors.get(anchorId)
|
|
227
|
+
if (state !== undefined) pendingAnchors.delete(state)
|
|
228
|
+
anchors.delete(anchorId)
|
|
229
|
+
},
|
|
230
|
+
dispose() {
|
|
231
|
+
disposed = true
|
|
232
|
+
anchors.clear()
|
|
233
|
+
pendingAnchors.clear()
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AdvertisedSize, Placement } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Leaf module for the wire-format shapes shared by `protocol.ts` (decode/guard)
|
|
5
|
+
* and `protocol-publisher.ts` (encode/batch). Keeping them here — rather than
|
|
6
|
+
* in either of those two — avoids a value import cycle: `protocol.ts`
|
|
7
|
+
* re-exports `protocol-publisher.ts`'s functions, and those functions need
|
|
8
|
+
* `GEOMETRY_PROTOCOL_VERSION`, so neither of those two files can be the
|
|
9
|
+
* source of it without the other importing back from it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Current wire format version for geometry messages. */
|
|
13
|
+
export const GEOMETRY_PROTOCOL_VERSION = 1 as const
|
|
14
|
+
|
|
15
|
+
/** Identifies one logical anchor instance within a transport session. */
|
|
16
|
+
export interface GeometryAddress {
|
|
17
|
+
anchorId: string
|
|
18
|
+
generation: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PlacementMessage extends GeometryAddress {
|
|
22
|
+
v: typeof GEOMETRY_PROTOCOL_VERSION
|
|
23
|
+
kind: 'placement'
|
|
24
|
+
seq: number
|
|
25
|
+
placement: Placement
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SizeMessage extends GeometryAddress {
|
|
29
|
+
v: typeof GEOMETRY_PROTOCOL_VERSION
|
|
30
|
+
kind: 'size'
|
|
31
|
+
seq: number
|
|
32
|
+
size: AdvertisedSize
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type GeometryMessage = PlacementMessage | SizeMessage
|
|
36
|
+
|
|
37
|
+
export interface GeometryBatch {
|
|
38
|
+
v: typeof GEOMETRY_PROTOCOL_VERSION
|
|
39
|
+
kind: 'batch'
|
|
40
|
+
messages: readonly GeometryMessage[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type GeometryWireValue = GeometryMessage | GeometryBatch
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { AdvertisedSize, Placement } from './types.js'
|
|
2
|
+
import {
|
|
3
|
+
GEOMETRY_PROTOCOL_VERSION,
|
|
4
|
+
type GeometryAddress,
|
|
5
|
+
type GeometryBatch,
|
|
6
|
+
type GeometryMessage,
|
|
7
|
+
type GeometryWireValue,
|
|
8
|
+
type PlacementMessage,
|
|
9
|
+
type SizeMessage,
|
|
10
|
+
} from './protocol-types.js'
|
|
11
|
+
|
|
12
|
+
export { GEOMETRY_PROTOCOL_VERSION }
|
|
13
|
+
export type {
|
|
14
|
+
GeometryAddress,
|
|
15
|
+
GeometryBatch,
|
|
16
|
+
GeometryMessage,
|
|
17
|
+
GeometryWireValue,
|
|
18
|
+
PlacementMessage,
|
|
19
|
+
SizeMessage,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type GeometryDecodeResult =
|
|
23
|
+
| { ok: true; value: GeometryWireValue }
|
|
24
|
+
| { ok: false; error: string }
|
|
25
|
+
|
|
26
|
+
export interface GeometryDecodeOptions {
|
|
27
|
+
/** Required caller-owned resource limit for a received batch. */
|
|
28
|
+
maxMessages: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
32
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
33
|
+
|
|
34
|
+
const isSafeInteger = (value: unknown): value is number =>
|
|
35
|
+
typeof value === 'number' && Number.isSafeInteger(value)
|
|
36
|
+
|
|
37
|
+
const isNonNegativeSafeInteger = (value: unknown): value is number =>
|
|
38
|
+
isSafeInteger(value) && value >= 0
|
|
39
|
+
|
|
40
|
+
function error(message: string): GeometryDecodeResult {
|
|
41
|
+
return { ok: false, error: message }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function decodePlacement(value: unknown): Placement | undefined {
|
|
45
|
+
if (!isRecord(value) || typeof value.visible !== 'boolean') return undefined
|
|
46
|
+
if (!value.visible) return { visible: false }
|
|
47
|
+
if (!isRecord(value.bounds)) return undefined
|
|
48
|
+
const { x, y, width, height } = value.bounds
|
|
49
|
+
if (
|
|
50
|
+
!isSafeInteger(x) ||
|
|
51
|
+
!isSafeInteger(y) ||
|
|
52
|
+
!isNonNegativeSafeInteger(width) ||
|
|
53
|
+
!isNonNegativeSafeInteger(height)
|
|
54
|
+
) {
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
return { visible: true, bounds: { x, y, width, height } }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function decodeSize(value: unknown): AdvertisedSize | undefined {
|
|
61
|
+
if (!isRecord(value)) return undefined
|
|
62
|
+
if (value.axis !== 'block' && value.axis !== 'inline') return undefined
|
|
63
|
+
if (!isNonNegativeSafeInteger(value.extent)) return undefined
|
|
64
|
+
return { axis: value.axis, extent: value.extent }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function decodeMessage(value: unknown): GeometryMessage | undefined {
|
|
68
|
+
if (!isRecord(value) || value.v !== GEOMETRY_PROTOCOL_VERSION) return undefined
|
|
69
|
+
if (typeof value.anchorId !== 'string' || value.anchorId.length === 0) return undefined
|
|
70
|
+
if (
|
|
71
|
+
!isNonNegativeSafeInteger(value.generation) ||
|
|
72
|
+
!isNonNegativeSafeInteger(value.seq)
|
|
73
|
+
) {
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
const address = {
|
|
77
|
+
v: GEOMETRY_PROTOCOL_VERSION,
|
|
78
|
+
anchorId: value.anchorId,
|
|
79
|
+
generation: value.generation,
|
|
80
|
+
seq: value.seq,
|
|
81
|
+
}
|
|
82
|
+
if (value.kind === 'placement') {
|
|
83
|
+
const placement = decodePlacement(value.placement)
|
|
84
|
+
return placement === undefined
|
|
85
|
+
? undefined
|
|
86
|
+
: { ...address, kind: 'placement', placement }
|
|
87
|
+
}
|
|
88
|
+
if (value.kind === 'size') {
|
|
89
|
+
const size = decodeSize(value.size)
|
|
90
|
+
return size === undefined ? undefined : { ...address, kind: 'size', size }
|
|
91
|
+
}
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Decodes untrusted transport data without throwing. `maxMessages` is required
|
|
97
|
+
* so each receiver, rather than this library, chooses its own batch limit.
|
|
98
|
+
*/
|
|
99
|
+
export function decodeGeometryWireValue(
|
|
100
|
+
value: unknown,
|
|
101
|
+
options: GeometryDecodeOptions,
|
|
102
|
+
): GeometryDecodeResult {
|
|
103
|
+
try {
|
|
104
|
+
if (!isNonNegativeSafeInteger(options?.maxMessages) || options.maxMessages === 0) {
|
|
105
|
+
return error('maxMessages must be a positive safe integer')
|
|
106
|
+
}
|
|
107
|
+
const message = decodeMessage(value)
|
|
108
|
+
if (message !== undefined) return { ok: true, value: message }
|
|
109
|
+
if (!isRecord(value) || value.v !== GEOMETRY_PROTOCOL_VERSION || value.kind !== 'batch') {
|
|
110
|
+
return error('invalid geometry message')
|
|
111
|
+
}
|
|
112
|
+
if (!Array.isArray(value.messages) || value.messages.length > options.maxMessages) {
|
|
113
|
+
return error('invalid geometry batch')
|
|
114
|
+
}
|
|
115
|
+
const messages: GeometryMessage[] = []
|
|
116
|
+
for (const item of value.messages) {
|
|
117
|
+
const decoded = decodeMessage(item)
|
|
118
|
+
if (decoded === undefined) return error('invalid geometry batch member')
|
|
119
|
+
messages.push(decoded)
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
value: { v: GEOMETRY_PROTOCOL_VERSION, kind: 'batch', messages },
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
return error('invalid geometry message')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface GeometrySequenceGuard {
|
|
131
|
+
/** Returns whether this message is newer than the last accepted equivalent. */
|
|
132
|
+
accept(message: GeometryMessage): boolean
|
|
133
|
+
/** Forgets state for one anchor, or every anchor when omitted. */
|
|
134
|
+
clear(anchorId?: string): void
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// -1 means that this generation has not seen the corresponding message kind.
|
|
138
|
+
type GeometrySequenceState = [
|
|
139
|
+
generation: number,
|
|
140
|
+
placementSeq: number,
|
|
141
|
+
sizeSeq: number,
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Keeps one generation floor per anchor and a sequence high-water mark per
|
|
146
|
+
* message kind within that generation. Ordering metadata is not authority:
|
|
147
|
+
* receivers must still authorize anchor IDs from their trusted transport
|
|
148
|
+
* context before passing a message here.
|
|
149
|
+
*/
|
|
150
|
+
export function createGeometrySequenceGuard(): GeometrySequenceGuard {
|
|
151
|
+
const latest = new Map<string, GeometrySequenceState>()
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
accept(message) {
|
|
155
|
+
const current = latest.get(message.anchorId)
|
|
156
|
+
if (current === undefined || message.generation > current[0]) {
|
|
157
|
+
latest.set(
|
|
158
|
+
message.anchorId,
|
|
159
|
+
[
|
|
160
|
+
message.generation,
|
|
161
|
+
message.kind === 'placement' ? message.seq : -1,
|
|
162
|
+
message.kind === 'size' ? message.seq : -1,
|
|
163
|
+
],
|
|
164
|
+
)
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
if (message.generation < current[0]) return false
|
|
168
|
+
const sequenceIndex = message.kind === 'placement' ? 1 : 2
|
|
169
|
+
if (message.seq <= current[sequenceIndex]) return false
|
|
170
|
+
current[sequenceIndex] = message.seq
|
|
171
|
+
return true
|
|
172
|
+
},
|
|
173
|
+
clear(anchorId) {
|
|
174
|
+
if (anchorId === undefined) {
|
|
175
|
+
latest.clear()
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
latest.delete(anchorId)
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
createGeometryBatcher,
|
|
185
|
+
createPlacementMessagePublisher,
|
|
186
|
+
createSizeMessagePublisher,
|
|
187
|
+
} from './protocol-publisher.js'
|
|
188
|
+
export type {
|
|
189
|
+
GeometryBatcher,
|
|
190
|
+
GeometryBatcherOptions,
|
|
191
|
+
GeometryBatchSend,
|
|
192
|
+
GeometrySend,
|
|
193
|
+
} from './protocol-publisher.js'
|