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.
Files changed (44) hide show
  1. package/README.md +118 -34
  2. package/README.zh-CN.md +128 -44
  3. package/dist/index.d.ts +4 -16
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -14
  6. package/dist/measure-loop.d.ts +9 -28
  7. package/dist/measure-loop.d.ts.map +1 -1
  8. package/dist/measure-loop.js +37 -16
  9. package/dist/protocol-publisher.d.ts +41 -0
  10. package/dist/protocol-publisher.d.ts.map +1 -0
  11. package/dist/protocol-publisher.js +191 -0
  12. package/dist/protocol-types.d.ts +36 -0
  13. package/dist/protocol-types.d.ts.map +1 -0
  14. package/dist/protocol-types.js +10 -0
  15. package/dist/protocol.d.ts +35 -0
  16. package/dist/protocol.d.ts.map +1 -0
  17. package/dist/protocol.js +131 -0
  18. package/dist/react.d.ts +18 -30
  19. package/dist/react.d.ts.map +1 -1
  20. package/dist/react.js +125 -122
  21. package/dist/size-advertiser.d.ts +9 -14
  22. package/dist/size-advertiser.d.ts.map +1 -1
  23. package/dist/size-advertiser.js +20 -28
  24. package/dist/types.d.ts +29 -73
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/types.js +1 -15
  27. package/dist/view-anchor.d.ts +36 -77
  28. package/dist/view-anchor.d.ts.map +1 -1
  29. package/dist/view-anchor.js +206 -174
  30. package/docs/bidirectional-design.md +64 -96
  31. package/docs/{anchor-3d.html → index.html} +215 -73
  32. package/docs/mechanism.mdx +55 -49
  33. package/docs/performance-report.md +63 -0
  34. package/docs/protocol.md +79 -0
  35. package/package.json +30 -4
  36. package/src/index.ts +6 -15
  37. package/src/measure-loop.ts +36 -41
  38. package/src/protocol-publisher.ts +236 -0
  39. package/src/protocol-types.ts +43 -0
  40. package/src/protocol.ts +193 -0
  41. package/src/react.ts +186 -141
  42. package/src/size-advertiser.ts +24 -31
  43. package/src/types.ts +34 -79
  44. package/src/view-anchor.ts +228 -212
@@ -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
- last = value;
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"}
@@ -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
- * Non-DOM dependencies that move the target's rect and must force a
12
- * re-publish (layout signature, project path, a tab toggle's
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
- export type ViewAnchorRef = (el: HTMLElement | null) => void;
20
- /**
21
- * Bind a native view's bounds to whichever DOM element the returned ref
22
- * callback is attached to. On attach → `createViewAnchor(el, opts)`; on
23
- * detach (`null`) publish ZERO then `dispose()`; on `opts`/`deps` change
24
- * `update`; on unmount → publish ZERO then `dispose`.
25
- *
26
- * Why ZERO on disappearance: the anchor's follower is a *main-process*
27
- * `WebContentsView`, not a DOM node. When the anchored element vanishes, core
28
- * `dispose()` only stops observing it deliberately never publishes again
29
- * (its Contract 6/7). But the host only collapses the native view when it
30
- * receives `{0,0,0,0}` (isHidden). In production the debug cell is *unmounted*
31
- * (not `display:none`) when hidden, so the ref goes to `null` and the native
32
- * view would otherwise stay frozen at its last bounds, floating on
33
- * top and occluding content. So the adapter (not core) must emit one ZERO via
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
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAoB,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE7E;;;;;;GAMG;AACH,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;AAE5D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,aAAa,CAgIvE;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,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"}