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
package/dist/measure-loop.js
CHANGED
|
@@ -1,39 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helper: animation-frame scheduling and deduplication loop for
|
|
3
|
+
* `createSizeAdvertiser`.
|
|
4
|
+
*
|
|
5
|
+
* Coalesces resize triggers into a single requestAnimationFrame, drops
|
|
6
|
+
* duplicate measurements, and manages disposal.
|
|
7
|
+
*/
|
|
1
8
|
export function createMeasureLoop(cfg) {
|
|
2
9
|
const { produce, same, sink } = cfg;
|
|
3
10
|
let rafId = null;
|
|
4
11
|
let active = false;
|
|
5
12
|
let disposed = false;
|
|
6
13
|
let last = null;
|
|
14
|
+
let publicationRevision = 0;
|
|
15
|
+
const deliver = (value) => {
|
|
16
|
+
const previous = last;
|
|
17
|
+
const attempt = ++publicationRevision;
|
|
18
|
+
last = value;
|
|
19
|
+
try {
|
|
20
|
+
const accepted = sink(value) !== false;
|
|
21
|
+
if (!accepted && publicationRevision === attempt)
|
|
22
|
+
last = previous;
|
|
23
|
+
return accepted;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (publicationRevision === attempt)
|
|
27
|
+
last = previous;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
7
31
|
const cancel = () => {
|
|
8
32
|
if (rafId !== null) {
|
|
9
33
|
cancelAnimationFrame(rafId);
|
|
10
34
|
rafId = null;
|
|
11
35
|
}
|
|
12
36
|
};
|
|
37
|
+
const frame = () => {
|
|
38
|
+
rafId = null;
|
|
39
|
+
if (disposed || !active)
|
|
40
|
+
return;
|
|
41
|
+
const value = produce();
|
|
42
|
+
if (value === null)
|
|
43
|
+
return;
|
|
44
|
+
if (last !== null && same(value, last))
|
|
45
|
+
return;
|
|
46
|
+
deliver(value);
|
|
47
|
+
};
|
|
13
48
|
return {
|
|
14
49
|
schedule() {
|
|
15
50
|
if (disposed || !active || rafId !== null)
|
|
16
51
|
return;
|
|
17
|
-
rafId = requestAnimationFrame(
|
|
18
|
-
rafId = null;
|
|
19
|
-
if (disposed || !active)
|
|
20
|
-
return;
|
|
21
|
-
const value = produce();
|
|
22
|
-
if (value === null)
|
|
23
|
-
return; // producer declined this frame
|
|
24
|
-
// last-value dedupe: a frame whose produced value equals the last one
|
|
25
|
-
// we emitted costs nothing (no IPC / setBounds).
|
|
26
|
-
if (last !== null && same(value, last))
|
|
27
|
-
return;
|
|
28
|
-
last = value;
|
|
29
|
-
sink(value);
|
|
30
|
-
});
|
|
52
|
+
rafId = requestAnimationFrame(frame);
|
|
31
53
|
},
|
|
32
54
|
emitNow(value) {
|
|
33
55
|
if (disposed)
|
|
34
56
|
return;
|
|
35
|
-
|
|
36
|
-
sink(value);
|
|
57
|
+
deliver(value);
|
|
37
58
|
},
|
|
38
59
|
setActive(on) {
|
|
39
60
|
active = on;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AdvertisedSize, Placement, Publisher } from './types.js';
|
|
2
|
+
import { type GeometryAddress, type GeometryBatch, type GeometryMessage } from './protocol-types.js';
|
|
3
|
+
export type GeometrySend = Publisher<GeometryMessage>;
|
|
4
|
+
export type GeometryBatchSend = Publisher<GeometryBatch>;
|
|
5
|
+
export interface GeometryBatcherOptions {
|
|
6
|
+
/** Observes every batch-delivery error, including explicit flushes; it must not throw. */
|
|
7
|
+
onError?: (error: unknown) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface GeometryBatcher {
|
|
10
|
+
/** Queues one message. Returns false after disposal. */
|
|
11
|
+
publish(message: GeometryMessage): boolean;
|
|
12
|
+
/** Attempts delivery of the current latest-value snapshot; delivery errors return false. */
|
|
13
|
+
flush(): boolean;
|
|
14
|
+
/** Forgets one anchor's state, or every anchor when omitted. */
|
|
15
|
+
clear(anchorId?: string): void;
|
|
16
|
+
/** Clears pending state; already scheduled microtasks become inert. */
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Wraps placement updates in a versioned protocol message. Sequence numbers
|
|
21
|
+
* start at 1 and increment with each attempted delivery.
|
|
22
|
+
*
|
|
23
|
+
* Keep one publisher per `{ anchorId, generation }` (e.g. via `useMemo` or `useRef`).
|
|
24
|
+
* Batchers and sequence guards drop messages with older sequence numbers, so
|
|
25
|
+
* recreating a publisher for the same address causes its messages to be dropped.
|
|
26
|
+
* Increment `generation` when intentionally resetting the publisher.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createPlacementMessagePublisher(address: GeometryAddress, send: GeometrySend): (placement: Placement) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Wraps size updates in a versioned protocol message.
|
|
31
|
+
* Follows the same stability rule: keep one publisher per `{ anchorId, generation }`,
|
|
32
|
+
* and increment `generation` when resetting.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createSizeMessagePublisher(address: GeometryAddress, send: GeometrySend): (size: AdvertisedSize) => boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Coalesces same-task messages without adding a rendering-frame delay. It owns
|
|
37
|
+
* no authorization policy: callers must associate addresses with trusted IPC
|
|
38
|
+
* senders before accepting a delivered batch.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createGeometryBatcher(send: GeometryBatchSend, options?: GeometryBatcherOptions): GeometryBatcher;
|
|
41
|
+
//# sourceMappingURL=protocol-publisher.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { GEOMETRY_PROTOCOL_VERSION, } from './protocol-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps placement updates in a versioned protocol message. Sequence numbers
|
|
4
|
+
* start at 1 and increment with each attempted delivery.
|
|
5
|
+
*
|
|
6
|
+
* Keep one publisher per `{ anchorId, generation }` (e.g. via `useMemo` or `useRef`).
|
|
7
|
+
* Batchers and sequence guards drop messages with older sequence numbers, so
|
|
8
|
+
* recreating a publisher for the same address causes its messages to be dropped.
|
|
9
|
+
* Increment `generation` when intentionally resetting the publisher.
|
|
10
|
+
*/
|
|
11
|
+
export function createPlacementMessagePublisher(address, send) {
|
|
12
|
+
let seq = 0;
|
|
13
|
+
return (placement) => {
|
|
14
|
+
const message = {
|
|
15
|
+
v: GEOMETRY_PROTOCOL_VERSION,
|
|
16
|
+
kind: 'placement',
|
|
17
|
+
anchorId: address.anchorId,
|
|
18
|
+
generation: address.generation,
|
|
19
|
+
seq: ++seq,
|
|
20
|
+
placement,
|
|
21
|
+
};
|
|
22
|
+
return send(message) !== false;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Wraps size updates in a versioned protocol message.
|
|
27
|
+
* Follows the same stability rule: keep one publisher per `{ anchorId, generation }`,
|
|
28
|
+
* and increment `generation` when resetting.
|
|
29
|
+
*/
|
|
30
|
+
export function createSizeMessagePublisher(address, send) {
|
|
31
|
+
let seq = 0;
|
|
32
|
+
return (size) => {
|
|
33
|
+
const message = {
|
|
34
|
+
v: GEOMETRY_PROTOCOL_VERSION,
|
|
35
|
+
kind: 'size',
|
|
36
|
+
anchorId: address.anchorId,
|
|
37
|
+
generation: address.generation,
|
|
38
|
+
seq: ++seq,
|
|
39
|
+
size,
|
|
40
|
+
};
|
|
41
|
+
return send(message) !== false;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Coalesces same-task messages without adding a rendering-frame delay. It owns
|
|
46
|
+
* no authorization policy: callers must associate addresses with trusted IPC
|
|
47
|
+
* senders before accepting a delivered batch.
|
|
48
|
+
*/
|
|
49
|
+
export function createGeometryBatcher(send, options = {}) {
|
|
50
|
+
const anchors = new Map();
|
|
51
|
+
const pendingAnchors = new Set();
|
|
52
|
+
let disposed = false;
|
|
53
|
+
let scheduled = false;
|
|
54
|
+
let flushing = false;
|
|
55
|
+
function report(error) {
|
|
56
|
+
try {
|
|
57
|
+
options.onError?.(error);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Error reporting must not turn scheduled delivery into an unhandled error.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function flush() {
|
|
64
|
+
if (disposed || flushing)
|
|
65
|
+
return false;
|
|
66
|
+
const messages = [];
|
|
67
|
+
const snapshotStates = [];
|
|
68
|
+
for (const state of pendingAnchors) {
|
|
69
|
+
if (state.p !== undefined) {
|
|
70
|
+
messages.push(state.p);
|
|
71
|
+
snapshotStates.push(state);
|
|
72
|
+
}
|
|
73
|
+
if (state.s !== undefined) {
|
|
74
|
+
messages.push(state.s);
|
|
75
|
+
snapshotStates.push(state);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (messages.length === 0)
|
|
79
|
+
return false;
|
|
80
|
+
const batch = {
|
|
81
|
+
v: GEOMETRY_PROTOCOL_VERSION,
|
|
82
|
+
kind: 'batch',
|
|
83
|
+
messages,
|
|
84
|
+
};
|
|
85
|
+
flushing = true;
|
|
86
|
+
try {
|
|
87
|
+
let accepted;
|
|
88
|
+
try {
|
|
89
|
+
accepted = send(batch) !== false;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
report(error);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (!accepted)
|
|
96
|
+
return false;
|
|
97
|
+
for (let index = 0; index < messages.length; index++) {
|
|
98
|
+
const message = messages[index];
|
|
99
|
+
const state = anchors.get(message.anchorId);
|
|
100
|
+
// A reentrant clear or generation upgrade replaces the state object.
|
|
101
|
+
if (state === undefined || state !== snapshotStates[index])
|
|
102
|
+
continue;
|
|
103
|
+
if (message.kind === 'placement') {
|
|
104
|
+
if (state.pSeq === undefined || message.seq > state.pSeq) {
|
|
105
|
+
state.pSeq = message.seq;
|
|
106
|
+
}
|
|
107
|
+
// Reentrant publishing may have replaced this message while send ran.
|
|
108
|
+
if (state.p === message)
|
|
109
|
+
state.p = undefined;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
if (state.sSeq === undefined || message.seq > state.sSeq) {
|
|
113
|
+
state.sSeq = message.seq;
|
|
114
|
+
}
|
|
115
|
+
if (state.s === message)
|
|
116
|
+
state.s = undefined;
|
|
117
|
+
}
|
|
118
|
+
if (state.p === undefined && state.s === undefined) {
|
|
119
|
+
pendingAnchors.delete(state);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
flushing = false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function schedule() {
|
|
129
|
+
if (scheduled || disposed)
|
|
130
|
+
return;
|
|
131
|
+
scheduled = true;
|
|
132
|
+
queueMicrotask(() => {
|
|
133
|
+
scheduled = false;
|
|
134
|
+
if (!disposed)
|
|
135
|
+
flush();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
publish(message) {
|
|
140
|
+
if (disposed)
|
|
141
|
+
return false;
|
|
142
|
+
let state = anchors.get(message.anchorId);
|
|
143
|
+
if (state !== undefined && message.generation < state.g)
|
|
144
|
+
return true;
|
|
145
|
+
if (state === undefined || message.generation > state.g) {
|
|
146
|
+
if (state !== undefined)
|
|
147
|
+
pendingAnchors.delete(state);
|
|
148
|
+
state = {
|
|
149
|
+
g: message.generation,
|
|
150
|
+
p: undefined,
|
|
151
|
+
s: undefined,
|
|
152
|
+
pSeq: undefined,
|
|
153
|
+
sSeq: undefined,
|
|
154
|
+
};
|
|
155
|
+
anchors.set(message.anchorId, state);
|
|
156
|
+
}
|
|
157
|
+
if (message.kind === 'placement') {
|
|
158
|
+
if ((state.p === undefined || message.seq > state.p.seq) &&
|
|
159
|
+
(state.pSeq === undefined || message.seq > state.pSeq)) {
|
|
160
|
+
state.p = message;
|
|
161
|
+
pendingAnchors.add(state);
|
|
162
|
+
schedule();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if ((state.s === undefined || message.seq > state.s.seq) &&
|
|
166
|
+
(state.sSeq === undefined || message.seq > state.sSeq)) {
|
|
167
|
+
state.s = message;
|
|
168
|
+
pendingAnchors.add(state);
|
|
169
|
+
schedule();
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
},
|
|
173
|
+
flush,
|
|
174
|
+
clear(anchorId) {
|
|
175
|
+
if (anchorId === undefined) {
|
|
176
|
+
anchors.clear();
|
|
177
|
+
pendingAnchors.clear();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const state = anchors.get(anchorId);
|
|
181
|
+
if (state !== undefined)
|
|
182
|
+
pendingAnchors.delete(state);
|
|
183
|
+
anchors.delete(anchorId);
|
|
184
|
+
},
|
|
185
|
+
dispose() {
|
|
186
|
+
disposed = true;
|
|
187
|
+
anchors.clear();
|
|
188
|
+
pendingAnchors.clear();
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AdvertisedSize, Placement } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Leaf module for the wire-format shapes shared by `protocol.ts` (decode/guard)
|
|
4
|
+
* and `protocol-publisher.ts` (encode/batch). Keeping them here — rather than
|
|
5
|
+
* in either of those two — avoids a value import cycle: `protocol.ts`
|
|
6
|
+
* re-exports `protocol-publisher.ts`'s functions, and those functions need
|
|
7
|
+
* `GEOMETRY_PROTOCOL_VERSION`, so neither of those two files can be the
|
|
8
|
+
* source of it without the other importing back from it.
|
|
9
|
+
*/
|
|
10
|
+
/** Current wire format version for geometry messages. */
|
|
11
|
+
export declare const GEOMETRY_PROTOCOL_VERSION: 1;
|
|
12
|
+
/** Identifies one logical anchor instance within a transport session. */
|
|
13
|
+
export interface GeometryAddress {
|
|
14
|
+
anchorId: string;
|
|
15
|
+
generation: number;
|
|
16
|
+
}
|
|
17
|
+
export interface PlacementMessage extends GeometryAddress {
|
|
18
|
+
v: typeof GEOMETRY_PROTOCOL_VERSION;
|
|
19
|
+
kind: 'placement';
|
|
20
|
+
seq: number;
|
|
21
|
+
placement: Placement;
|
|
22
|
+
}
|
|
23
|
+
export interface SizeMessage extends GeometryAddress {
|
|
24
|
+
v: typeof GEOMETRY_PROTOCOL_VERSION;
|
|
25
|
+
kind: 'size';
|
|
26
|
+
seq: number;
|
|
27
|
+
size: AdvertisedSize;
|
|
28
|
+
}
|
|
29
|
+
export type GeometryMessage = PlacementMessage | SizeMessage;
|
|
30
|
+
export interface GeometryBatch {
|
|
31
|
+
v: typeof GEOMETRY_PROTOCOL_VERSION;
|
|
32
|
+
kind: 'batch';
|
|
33
|
+
messages: readonly GeometryMessage[];
|
|
34
|
+
}
|
|
35
|
+
export type GeometryWireValue = GeometryMessage | GeometryBatch;
|
|
36
|
+
//# sourceMappingURL=protocol-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-types.d.ts","sourceRoot":"","sources":["../src/protocol-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3D;;;;;;;GAOG;AAEH,yDAAyD;AACzD,eAAO,MAAM,yBAAyB,EAAG,CAAU,CAAA;AAEnD,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,CAAC,EAAE,OAAO,yBAAyB,CAAA;IACnC,IAAI,EAAE,WAAW,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,SAAS,CAAA;CACrB;AAED,MAAM,WAAW,WAAY,SAAQ,eAAe;IAClD,CAAC,EAAE,OAAO,yBAAyB,CAAA;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,WAAW,CAAA;AAE5D,MAAM,WAAW,aAAa;IAC5B,CAAC,EAAE,OAAO,yBAAyB,CAAA;IACnC,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAA;CACrC;AAED,MAAM,MAAM,iBAAiB,GAAG,eAAe,GAAG,aAAa,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leaf module for the wire-format shapes shared by `protocol.ts` (decode/guard)
|
|
3
|
+
* and `protocol-publisher.ts` (encode/batch). Keeping them here — rather than
|
|
4
|
+
* in either of those two — avoids a value import cycle: `protocol.ts`
|
|
5
|
+
* re-exports `protocol-publisher.ts`'s functions, and those functions need
|
|
6
|
+
* `GEOMETRY_PROTOCOL_VERSION`, so neither of those two files can be the
|
|
7
|
+
* source of it without the other importing back from it.
|
|
8
|
+
*/
|
|
9
|
+
/** Current wire format version for geometry messages. */
|
|
10
|
+
export const GEOMETRY_PROTOCOL_VERSION = 1;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { GEOMETRY_PROTOCOL_VERSION, type GeometryAddress, type GeometryBatch, type GeometryMessage, type GeometryWireValue, type PlacementMessage, type SizeMessage } from './protocol-types.js';
|
|
2
|
+
export { GEOMETRY_PROTOCOL_VERSION };
|
|
3
|
+
export type { GeometryAddress, GeometryBatch, GeometryMessage, GeometryWireValue, PlacementMessage, SizeMessage, };
|
|
4
|
+
export type GeometryDecodeResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
value: GeometryWireValue;
|
|
7
|
+
} | {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: string;
|
|
10
|
+
};
|
|
11
|
+
export interface GeometryDecodeOptions {
|
|
12
|
+
/** Required caller-owned resource limit for a received batch. */
|
|
13
|
+
maxMessages: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Decodes untrusted transport data without throwing. `maxMessages` is required
|
|
17
|
+
* so each receiver, rather than this library, chooses its own batch limit.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decodeGeometryWireValue(value: unknown, options: GeometryDecodeOptions): GeometryDecodeResult;
|
|
20
|
+
export interface GeometrySequenceGuard {
|
|
21
|
+
/** Returns whether this message is newer than the last accepted equivalent. */
|
|
22
|
+
accept(message: GeometryMessage): boolean;
|
|
23
|
+
/** Forgets state for one anchor, or every anchor when omitted. */
|
|
24
|
+
clear(anchorId?: string): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Keeps one generation floor per anchor and a sequence high-water mark per
|
|
28
|
+
* message kind within that generation. Ordering metadata is not authority:
|
|
29
|
+
* receivers must still authorize anchor IDs from their trusted transport
|
|
30
|
+
* context before passing a message here.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createGeometrySequenceGuard(): GeometrySequenceGuard;
|
|
33
|
+
export { createGeometryBatcher, createPlacementMessagePublisher, createSizeMessagePublisher, } from './protocol-publisher.js';
|
|
34
|
+
export type { GeometryBatcher, GeometryBatcherOptions, GeometryBatchSend, GeometrySend, } from './protocol-publisher.js';
|
|
35
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { GEOMETRY_PROTOCOL_VERSION, } from './protocol-types.js';
|
|
2
|
+
export { GEOMETRY_PROTOCOL_VERSION };
|
|
3
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
4
|
+
const isSafeInteger = (value) => typeof value === 'number' && Number.isSafeInteger(value);
|
|
5
|
+
const isNonNegativeSafeInteger = (value) => isSafeInteger(value) && value >= 0;
|
|
6
|
+
function error(message) {
|
|
7
|
+
return { ok: false, error: message };
|
|
8
|
+
}
|
|
9
|
+
function decodePlacement(value) {
|
|
10
|
+
if (!isRecord(value) || typeof value.visible !== 'boolean')
|
|
11
|
+
return undefined;
|
|
12
|
+
if (!value.visible)
|
|
13
|
+
return { visible: false };
|
|
14
|
+
if (!isRecord(value.bounds))
|
|
15
|
+
return undefined;
|
|
16
|
+
const { x, y, width, height } = value.bounds;
|
|
17
|
+
if (!isSafeInteger(x) ||
|
|
18
|
+
!isSafeInteger(y) ||
|
|
19
|
+
!isNonNegativeSafeInteger(width) ||
|
|
20
|
+
!isNonNegativeSafeInteger(height)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return { visible: true, bounds: { x, y, width, height } };
|
|
24
|
+
}
|
|
25
|
+
function decodeSize(value) {
|
|
26
|
+
if (!isRecord(value))
|
|
27
|
+
return undefined;
|
|
28
|
+
if (value.axis !== 'block' && value.axis !== 'inline')
|
|
29
|
+
return undefined;
|
|
30
|
+
if (!isNonNegativeSafeInteger(value.extent))
|
|
31
|
+
return undefined;
|
|
32
|
+
return { axis: value.axis, extent: value.extent };
|
|
33
|
+
}
|
|
34
|
+
function decodeMessage(value) {
|
|
35
|
+
if (!isRecord(value) || value.v !== GEOMETRY_PROTOCOL_VERSION)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (typeof value.anchorId !== 'string' || value.anchorId.length === 0)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (!isNonNegativeSafeInteger(value.generation) ||
|
|
40
|
+
!isNonNegativeSafeInteger(value.seq)) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const address = {
|
|
44
|
+
v: GEOMETRY_PROTOCOL_VERSION,
|
|
45
|
+
anchorId: value.anchorId,
|
|
46
|
+
generation: value.generation,
|
|
47
|
+
seq: value.seq,
|
|
48
|
+
};
|
|
49
|
+
if (value.kind === 'placement') {
|
|
50
|
+
const placement = decodePlacement(value.placement);
|
|
51
|
+
return placement === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: { ...address, kind: 'placement', placement };
|
|
54
|
+
}
|
|
55
|
+
if (value.kind === 'size') {
|
|
56
|
+
const size = decodeSize(value.size);
|
|
57
|
+
return size === undefined ? undefined : { ...address, kind: 'size', size };
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decodes untrusted transport data without throwing. `maxMessages` is required
|
|
63
|
+
* so each receiver, rather than this library, chooses its own batch limit.
|
|
64
|
+
*/
|
|
65
|
+
export function decodeGeometryWireValue(value, options) {
|
|
66
|
+
try {
|
|
67
|
+
if (!isNonNegativeSafeInteger(options?.maxMessages) || options.maxMessages === 0) {
|
|
68
|
+
return error('maxMessages must be a positive safe integer');
|
|
69
|
+
}
|
|
70
|
+
const message = decodeMessage(value);
|
|
71
|
+
if (message !== undefined)
|
|
72
|
+
return { ok: true, value: message };
|
|
73
|
+
if (!isRecord(value) || value.v !== GEOMETRY_PROTOCOL_VERSION || value.kind !== 'batch') {
|
|
74
|
+
return error('invalid geometry message');
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(value.messages) || value.messages.length > options.maxMessages) {
|
|
77
|
+
return error('invalid geometry batch');
|
|
78
|
+
}
|
|
79
|
+
const messages = [];
|
|
80
|
+
for (const item of value.messages) {
|
|
81
|
+
const decoded = decodeMessage(item);
|
|
82
|
+
if (decoded === undefined)
|
|
83
|
+
return error('invalid geometry batch member');
|
|
84
|
+
messages.push(decoded);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
value: { v: GEOMETRY_PROTOCOL_VERSION, kind: 'batch', messages },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return error('invalid geometry message');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Keeps one generation floor per anchor and a sequence high-water mark per
|
|
97
|
+
* message kind within that generation. Ordering metadata is not authority:
|
|
98
|
+
* receivers must still authorize anchor IDs from their trusted transport
|
|
99
|
+
* context before passing a message here.
|
|
100
|
+
*/
|
|
101
|
+
export function createGeometrySequenceGuard() {
|
|
102
|
+
const latest = new Map();
|
|
103
|
+
return {
|
|
104
|
+
accept(message) {
|
|
105
|
+
const current = latest.get(message.anchorId);
|
|
106
|
+
if (current === undefined || message.generation > current[0]) {
|
|
107
|
+
latest.set(message.anchorId, [
|
|
108
|
+
message.generation,
|
|
109
|
+
message.kind === 'placement' ? message.seq : -1,
|
|
110
|
+
message.kind === 'size' ? message.seq : -1,
|
|
111
|
+
]);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (message.generation < current[0])
|
|
115
|
+
return false;
|
|
116
|
+
const sequenceIndex = message.kind === 'placement' ? 1 : 2;
|
|
117
|
+
if (message.seq <= current[sequenceIndex])
|
|
118
|
+
return false;
|
|
119
|
+
current[sequenceIndex] = message.seq;
|
|
120
|
+
return true;
|
|
121
|
+
},
|
|
122
|
+
clear(anchorId) {
|
|
123
|
+
if (anchorId === undefined) {
|
|
124
|
+
latest.clear();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
latest.delete(anchorId);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export { createGeometryBatcher, createPlacementMessagePublisher, createSizeMessagePublisher, } from './protocol-publisher.js';
|
package/dist/react.d.ts
CHANGED
|
@@ -1,38 +1,26 @@
|
|
|
1
|
+
import { type PlacementAnchorOptions } from './view-anchor.js';
|
|
1
2
|
import type { Bounds, ViewAnchorOptions } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* React adapter over the imperative `createViewAnchor` core.
|
|
4
|
-
*
|
|
5
|
-
* (React lint forces the `use` prefix on any hook returning a ref
|
|
6
|
-
* callback; the library's identity is still the `ViewAnchor` core — this is
|
|
7
|
-
* just the React binding.)
|
|
8
|
-
*/
|
|
9
3
|
export interface UseViewAnchorOptions extends ViewAnchorOptions {
|
|
10
4
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `display:none`, …). A `ResizeObserver` covers pure geometry; `deps`
|
|
14
|
-
* covers state it cannot see. Keep the array length stable across
|
|
15
|
-
* renders (React effect-deps rule).
|
|
5
|
+
* Values that re-apply the anchor when changed. Keep this array's length
|
|
6
|
+
* stable across renders.
|
|
16
7
|
*/
|
|
17
8
|
deps?: ReadonlyArray<unknown>;
|
|
18
9
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* the already-tested `update({ present:false })` path before disposing.
|
|
35
|
-
*/
|
|
36
|
-
export declare function useViewAnchor(opts: UseViewAnchorOptions): ViewAnchorRef;
|
|
10
|
+
/** Compatible with React 18's null callback and React 19's ref cleanup. */
|
|
11
|
+
export type ViewAnchorRef = (el: HTMLElement | null) => void | (() => void);
|
|
12
|
+
export interface UsePlacementAnchorOptions extends PlacementAnchorOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Values that re-apply the anchor when changed. Keep this array's length
|
|
15
|
+
* stable across renders.
|
|
16
|
+
*/
|
|
17
|
+
deps?: ReadonlyArray<unknown>;
|
|
18
|
+
}
|
|
19
|
+
/** Callback ref for the explicit-visibility Placement API. */
|
|
20
|
+
export type PlacementAnchorRef = ViewAnchorRef;
|
|
21
|
+
/** Bind zero-bounds visibility to a DOM element callback ref. */
|
|
22
|
+
export declare function useViewAnchor(options: UseViewAnchorOptions): ViewAnchorRef;
|
|
23
|
+
/** Bind the explicit Placement API to a DOM element callback ref. */
|
|
24
|
+
export declare function usePlacementAnchor(options: UsePlacementAnchorOptions): PlacementAnchorRef;
|
|
37
25
|
export type { Bounds };
|
|
38
26
|
//# sourceMappingURL=react.d.ts.map
|
package/dist/react.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"
|
|
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"}
|