@kolosal-ai/rivet 0.2.0 → 0.4.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.
@@ -0,0 +1,716 @@
1
+ import { N as NodeId, E as EdgeId, V as Vec2, b as HandleType, H as HandlePosition, R as Rect } from './types-D3aldmsO.js';
2
+ import { ComponentType } from 'react';
3
+
4
+ /**
5
+ * Presence targets — the things a peer can have a claim on.
6
+ *
7
+ * Presence started node-shaped: `Peer.selection` and `Peer.holding` were
8
+ * `NodeId[]`. The graph addresses more than nodes, though, and the rest of it
9
+ * is just as selectable — an edge is a row with its own id, and an anchor is an
10
+ * addressable element inside a node that can be clicked and wired. A target
11
+ * says *which kind of thing* an id refers to, so presence can describe any of
12
+ * them without three parallel fields.
13
+ *
14
+ * Strings stay the wire format, the same arrangement {@link EdgeEndpoint} uses
15
+ * for edge ends: a bare string is a node id, which is exactly what a 0.2.0
16
+ * transport already sends, so nothing has to be migrated to adopt the rest of
17
+ * this. The struct sits on top as a codec.
18
+ *
19
+ * | structured | key |
20
+ * |-------------------------------------|----------------------------|
21
+ * | `{ kind: "node", id }` | `<nodeId>` |
22
+ * | `{ kind: "edge", id }` | `edge:<edgeId>` |
23
+ * | `{ kind: "anchor", nodeId, anchorId }` | `anchor:<nodeId>:<anchorId>` |
24
+ *
25
+ * `edge:` and `anchor:` are therefore reserved prefixes on a node id, and a
26
+ * node id inside an anchor key can't contain `:` (the anchor id may — the split
27
+ * takes the first separator). Anything that doesn't parse reads as a node id,
28
+ * which is the safe direction: an unknown node is skipped everywhere, where an
29
+ * unknown *kind* would need a decision at every call site.
30
+ */
31
+
32
+ /** Something a peer can select, hold or lock. */
33
+ type PresenceTarget = {
34
+ kind: "node";
35
+ id: NodeId;
36
+ } | {
37
+ kind: "edge";
38
+ id: EdgeId;
39
+ }
40
+ /** An {@link Anchor} inside a node — addressed by both, since anchor ids are per-node. */
41
+ | {
42
+ kind: "anchor";
43
+ nodeId: NodeId;
44
+ anchorId: string;
45
+ };
46
+ /**
47
+ * A target in wire form — what travels, and what `PeerRecord.selection` and
48
+ * `PeerRecord.holding` hold. A plain node id is its own key.
49
+ */
50
+ type PresenceTargetKey = string;
51
+ /** The wire key for a target. A bare node id passes through unchanged. */
52
+ declare function presenceTargetKey(target: PresenceTarget | NodeId): PresenceTargetKey;
53
+ /**
54
+ * The key for an edge, without building the struct first. Sugar over
55
+ * {@link presenceTargetKey} for the call sites that ask a key-shaped question
56
+ * every frame — a lock check on the hovered edge, say — where the intermediate
57
+ * object is the only allocation in the path.
58
+ */
59
+ declare function edgeTargetKey(id: EdgeId): PresenceTargetKey;
60
+ /** The key for an anchor, without building the struct first. */
61
+ declare function anchorTargetKey(nodeId: NodeId, anchorId: string): PresenceTargetKey;
62
+ /**
63
+ * Whether a key addresses a node itself — in which case the key *is* the node
64
+ * id, and no parse is needed. The render loop's question, asked once per
65
+ * claimed target per peer per frame, so it allocates nothing.
66
+ */
67
+ declare function isNodeTarget(key: PresenceTargetKey): boolean;
68
+ /** Read a wire key back into a target. Anything malformed comes back as a node. */
69
+ declare function parsePresenceTarget(key: PresenceTargetKey): PresenceTarget;
70
+ /**
71
+ * The node a target belongs to: itself for a node, its owner for an anchor,
72
+ * `null` for an edge (which has two, and rivet holds neither as the edge's own).
73
+ * How a node-keyed question — geometry, a parent chain, a lock — is asked of a
74
+ * target that isn't a node.
75
+ */
76
+ declare function presenceTargetNodeId(target: PresenceTarget | PresenceTargetKey): NodeId | null;
77
+ /**
78
+ * The edge a target names, or `null` for anything else. The mirror of
79
+ * {@link presenceTargetNodeId}, and deliberately parse-free: an `edge:` key is
80
+ * recognisable by its prefix alone, so the frame loop that asks this of every
81
+ * claim allocates nothing on the answers it discards.
82
+ */
83
+ declare function presenceTargetEdgeId(target: PresenceTarget | PresenceTargetKey): EdgeId | null;
84
+
85
+ /**
86
+ * Locks — what a peer has claimed, and what this client may still do to it.
87
+ *
88
+ * A lock is policy where presence is description. `Peer.holding` says "they
89
+ * have their pointer on this"; a lock says "hands off", and rivet acts on it.
90
+ * The two render identically on purpose — to whoever is looking, they mean the
91
+ * same thing — but only a lock refuses anything.
92
+ *
93
+ * Locks come in whole, as the `lockedNodes` prop, because the transport already
94
+ * has to hold them: a lock outlives the tab that took it (that's the point of
95
+ * taking one), so the server owns the lifetime and rivet only reads it. Nothing
96
+ * here enters the graph, the change stream, or history — a lock is no more a
97
+ * fact about the document than a cursor is.
98
+ *
99
+ * What a lock refuses is a default, not a rule. {@link LOCK_DEFAULT_REFUSED} is
100
+ * rivet's opinion; `canInteractWithLocked` is the consumer's, and it wins on
101
+ * every intent it's asked about.
102
+ *
103
+ * A lock reaches down the parent chain: locking a container makes everything
104
+ * inside it read-only, because a claim on a box that leaves its contents free
105
+ * isn't a claim on anything. The table itself stays exactly as declared — the
106
+ * cascade is resolved per question, not stored — so paint and policy can differ,
107
+ * and they should: one outline on the container says what a stroke around every
108
+ * descendant would only repeat.
109
+ *
110
+ * The table is keyed by {@link PresenceTargetKey}, so an edge and an anchor are
111
+ * lockable on the same terms a node is. A bare node id is its own key, which is
112
+ * what every 0.2.0 table already contains — widening the key space cost nothing
113
+ * and migrates nothing. What each kind inherits differs, because containment
114
+ * does:
115
+ *
116
+ * - a **node** inherits from its ancestors;
117
+ * - an **anchor** inherits from the node that owns it, and so from that node's
118
+ * ancestors — a locked container reaches its contents' anchors too;
119
+ * - an **edge** inherits from nothing. Locking a node claims the node, not the
120
+ * graph around it (the same reasoning that keeps `connect` out of
121
+ * {@link LOCK_DEFAULT_REFUSED}), so an edge is governed by its own entry
122
+ * alone even when both its endpoints are locked.
123
+ */
124
+
125
+ /**
126
+ * A thing this client might try to do to a locked node.
127
+ *
128
+ * Granular because the answer genuinely differs per app: a design tool wants a
129
+ * locked node inspectable, a form builder wants it untouchable, and a pipeline
130
+ * editor usually wants to keep wiring around one while a colleague renames it.
131
+ */
132
+ type LockIntent =
133
+ /** Click it, or sweep it up in a marquee. */
134
+ "select"
135
+ /** Move it — pointer drag, group drag, keyboard grab or arrow nudge. */
136
+ | "drag"
137
+ /** Change its box through {@link NodeResizer}. */
138
+ | "resize"
139
+ /** Remove it as part of deleting the selection. */
140
+ | "delete"
141
+ /** Start or land an edge on one of its handles. */
142
+ | "connect"
143
+ /**
144
+ * Drag one end of an existing edge onto a different handle. Asked of the
145
+ * *edge*, never of the nodes it runs between — reconnection is the one thing
146
+ * a lock on an edge is for, and asking its endpoints instead would make
147
+ * `connect`'s deliberate permissiveness meaningless.
148
+ */
149
+ | "reconnect";
150
+ /** What {@link CanInteractWithLocked} is asked about. */
151
+ type LockInteraction = {
152
+ /** The thing this client is trying to act on. */
153
+ target: PresenceTarget;
154
+ /**
155
+ * The target actually carrying the lock: {@link target} itself for a direct
156
+ * lock, an ancestor node when it's inherited from a container, the owning
157
+ * node when an anchor inherits from it. Compare the two to treat a locked
158
+ * container's contents differently from the container.
159
+ */
160
+ lockedTarget: PresenceTarget;
161
+ /** The peer holding the lock — the `lockedNodes` value for {@link lockedTarget}. */
162
+ holderId: string;
163
+ /**
164
+ * The node {@link target} belongs to: itself for a node, its owner for an
165
+ * anchor, and `null` for an edge — an edge runs between two nodes and rivet
166
+ * holds neither as its own. The node-shaped view of {@link target}, kept
167
+ * because every policy written before targets existed reads it.
168
+ */
169
+ nodeId: NodeId | null;
170
+ /** The node-shaped view of {@link lockedTarget}, `null` for an edge lock. */
171
+ lockedNodeId: NodeId | null;
172
+ intent: LockIntent;
173
+ };
174
+ /**
175
+ * Consumer override for rivet's default refusals, consulted once per attempt.
176
+ * Return `true` to allow the intent, `false` to refuse it — the default is
177
+ * replaced, not consulted, so a handler owns the whole policy.
178
+ */
179
+ type CanInteractWithLocked = (event: LockInteraction) => boolean;
180
+ /** A node's lock as {@link useNodeLock} reports it. */
181
+ type NodeLock = {
182
+ locked: boolean;
183
+ /** The peer holding it, or `undefined` when the node isn't locked. */
184
+ holderId?: string;
185
+ };
186
+ /**
187
+ * The intents rivet refuses on a locked node when no policy is given.
188
+ *
189
+ * `connect` is deliberately absent. A lock claims the node, not the graph
190
+ * around it: an edge is its own row with its own id, and refusing to wire one
191
+ * would make a locked node an island for as long as somebody holds it — a far
192
+ * heavier lock than "someone is editing this" warrants. Consumers who want that
193
+ * can say so through {@link CanInteractWithLocked}.
194
+ *
195
+ * `reconnect` is present for the same reason `connect` isn't. It's asked of the
196
+ * edge being re-pointed, so refusing it withholds exactly the edge somebody has
197
+ * claimed and leaves every other wire on those nodes free.
198
+ */
199
+ declare const LOCK_DEFAULT_REFUSED: ReadonlySet<LockIntent>;
200
+ /**
201
+ * What the registry can be asked about — a target struct, or its wire key.
202
+ * A node's key *is* its id, so every call site written against the node-only
203
+ * registry still passes a valid target.
204
+ */
205
+ type LockTarget = PresenceTarget | PresenceTargetKey;
206
+ /**
207
+ * The lock registry — one per graph, reachable as `store.locks`.
208
+ *
209
+ * Reads are hot: every marquee frame and every mover loop asks
210
+ * {@link LockRegistry.allows}, so it stays a map lookup plus at most one
211
+ * consumer call, with no allocation on the common (nothing locked) path.
212
+ */
213
+ type LockRegistry = {
214
+ /**
215
+ * Replace the lock table (the `lockedNodes` prop). Returns the targets that
216
+ * weren't locked before and are now — the store scans those for conflicts
217
+ * with a live local gesture.
218
+ */
219
+ setLocks: (locks: Record<PresenceTargetKey, string> | undefined) => PresenceTargetKey[];
220
+ /** Install the consumer's policy, or `null` to fall back to the defaults. */
221
+ setPolicy: (policy: CanInteractWithLocked | null) => void;
222
+ /** Whether a lock governs this target — its own, or one it inherits. */
223
+ isLocked: (target: LockTarget) => boolean;
224
+ /**
225
+ * Who holds the lock governing this target, or `null` when none does. Reports
226
+ * the inherited holder for a node inside a locked container, so consumer
227
+ * chrome inside the contents reads the same as chrome on the container.
228
+ */
229
+ getHolder: (target: LockTarget) => string | null;
230
+ /**
231
+ * The target whose lock governs `target` — itself when directly locked, the
232
+ * nearest locked ancestor (or owning node, for an anchor) when inherited,
233
+ * `null` when nothing governs it.
234
+ */
235
+ getLockSource: (target: LockTarget) => PresenceTargetKey | null;
236
+ /**
237
+ * The declared table, exactly as the prop gave it — containers only, never
238
+ * the contents they govern. The renderer walks it once a frame and outlines
239
+ * what it finds; expanding it to every descendant would stroke a box around
240
+ * each child of a locked container, which reads as many claims rather than
241
+ * one.
242
+ *
243
+ * Keyed by {@link PresenceTargetKey}, so a walk that only wants nodes has to
244
+ * say so — `isNodeTarget` is the cheap filter.
245
+ */
246
+ getLocks: () => ReadonlyMap<PresenceTargetKey, string>;
247
+ /**
248
+ * Just the edge locks, `edgeId -> holderId`, indexed when the table is set.
249
+ * The edge renderer wants them keyed by edge id and nothing else, once a
250
+ * frame; deriving that from {@link getLocks} each time would allocate a map
251
+ * per frame to answer a question that changes at human rate.
252
+ */
253
+ getLockedEdges: () => ReadonlyMap<EdgeId, string>;
254
+ /** How many targets are directly locked — the render loop's early-out. */
255
+ size: () => number;
256
+ /**
257
+ * Whether this client may act on a target. Always `true` for a target no lock
258
+ * governs, so callers can ask unconditionally.
259
+ */
260
+ allows: (target: LockTarget, intent: LockIntent) => boolean;
261
+ /** Bumped on every table change, for `useSyncExternalStore`. */
262
+ getVersion: () => number;
263
+ /** Subscribe to the table changing — locks move at human rate, so this is React-safe. */
264
+ subscribe: (listener: () => void) => () => void;
265
+ };
266
+
267
+ /**
268
+ * Presence — who else is in the document, and where they are.
269
+ *
270
+ * Presence is ephemeral by construction. Nothing here enters the graph, the
271
+ * change stream, or history: a peer's cursor is not a fact about the document,
272
+ * it's a fact about a person, and it stops being true the moment they move. So
273
+ * the registry is imperative, painted straight from the render loop, and thrown
274
+ * away when the tab closes.
275
+ *
276
+ * Two doors write to it, at deliberately different speeds:
277
+ *
278
+ * - the `peers` prop — the roster. Identity (name, colour, `data`) and the
279
+ * slow fields (selection, holding). React-rate, and notified as two
280
+ * channels: people change far less often than what they're pointing at.
281
+ * - {@link PresenceRegistry.setPeerCursor} / `setPeerNodeTransform` — the hot
282
+ * path off your ephemeral channel, called as fast as frames arrive. These
283
+ * never touch React: they mark the canvas dirty and nothing else. A cursor
284
+ * dirties only the cursor layer; a transform moves a real node, so it
285
+ * dirties the frame.
286
+ *
287
+ * A peer the roster has never mentioned is still legitimate — a cursor frame
288
+ * can beat the join event, and dropping it would blink the cursor. Such a peer
289
+ * lives until its cursor clears (see {@link PresenceRegistry.setPeerCursor}),
290
+ * and its colour is derived from its id, so the record can be rebuilt any
291
+ * number of times without the colour ever flickering.
292
+ */
293
+
294
+ /**
295
+ * A wire somebody is pulling out of a handle, right now — the peer-side view of
296
+ * a `PendingConnection`.
297
+ *
298
+ * Both ends are in **world** coordinates for the reason cursors are: peers pan
299
+ * and zoom independently. `from` travels rather than being recomputed from the
300
+ * handle, because the node it belongs to may be culled on this client, and a
301
+ * wire that vanishes when you scroll away from its source is worse than one
302
+ * drawn from a point.
303
+ *
304
+ * Deliberately not `PendingConnection` itself: that type carries `reconnecting`,
305
+ * the local drag's note that an edge is hidden while its preview stands in for
306
+ * it. A peer's uncommitted gesture must not hide an edge from this client — the
307
+ * edge is still in the document until their change lands — so the field stops
308
+ * at the publishing boundary.
309
+ */
310
+ type PeerConnection = {
311
+ source: NodeId;
312
+ sourceHandle: string;
313
+ sourceType: HandleType;
314
+ /** The anchored end, at the handle it was pulled from. */
315
+ from: Vec2;
316
+ /** The loose end, following their pointer. Interpolated, like a cursor. */
317
+ to: Vec2;
318
+ /** Side the loose end enters from while they're snapped onto a handle. */
319
+ toPosition?: HandlePosition;
320
+ };
321
+ /**
322
+ * A participant other than this client, as your transport describes them.
323
+ *
324
+ * Everything is optional but `id`: presence degrades field by field, and a peer
325
+ * you know nothing about except that they exist is still worth an avatar.
326
+ */
327
+ type Peer = {
328
+ id: string;
329
+ /** Display name, drawn beside their cursor. Unnamed peers get no label. */
330
+ name?: string;
331
+ /** Any CSS colour. Defaults to a stable colour derived from {@link Peer.id}. */
332
+ color?: string;
333
+ /**
334
+ * Cursor in **world** coordinates — peers pan and zoom independently, so a
335
+ * screen point means nothing to anyone else. `null` when their pointer is
336
+ * off the canvas. Only a seed: once {@link PresenceRegistry.setPeerCursor}
337
+ * has been called for a peer, the imperative value owns the cursor.
338
+ */
339
+ cursor?: Vec2 | null;
340
+ /**
341
+ * Anything else your app knows about them — an avatar URL, a role, what
342
+ * they're doing. rivet never reads it; it rides along to {@link PeerRecord}
343
+ * so participant chrome doesn't need a side map keyed by peer id.
344
+ */
345
+ data?: unknown;
346
+ /**
347
+ * What they have selected. Bare strings are node ids — the 0.2.0 shape, still
348
+ * exactly right — and a {@link PresenceTarget} names an edge or an anchor.
349
+ */
350
+ selection?: readonly (NodeId | PresenceTarget)[];
351
+ /**
352
+ * What's under one of their live gestures (their `onNodeDragStart`). Nodes in
353
+ * practice: an edge or an anchor is selected, never dragged.
354
+ */
355
+ holding?: readonly (NodeId | PresenceTarget)[];
356
+ /**
357
+ * A wire they're pulling out of a handle, or `null` when they aren't. Like
358
+ * {@link Peer.cursor} this is only a seed: once
359
+ * {@link PresenceRegistry.setPeerConnection} has been called for a peer, the
360
+ * imperative value owns it.
361
+ */
362
+ pending?: PeerConnection | null;
363
+ };
364
+ /**
365
+ * Who a peer is, with nothing about what they're doing.
366
+ *
367
+ * Its own type because it's its own subscription: identity changes when someone
368
+ * joins, leaves, is renamed or recoloured — human-rate events — while claims
369
+ * change every time anybody clicks anything. Chrome that only shows *who is
370
+ * here* subscribes to this and sits out the rest (see `usePeerIdentities`).
371
+ */
372
+ type PeerIdentity = {
373
+ id: string;
374
+ name?: string;
375
+ /** Always set — {@link peerColor} fills in for a peer that declared none. */
376
+ color: string;
377
+ /** Whatever the roster attached to them, untouched. */
378
+ data?: unknown;
379
+ };
380
+ /**
381
+ * A peer as the registry holds it: identity resolved, live overlays merged in.
382
+ * What the renderer and {@link usePeers} both read.
383
+ */
384
+ type PeerRecord = PeerIdentity & {
385
+ cursor: Vec2 | null;
386
+ /**
387
+ * Their claims, in wire form: a bare node id, or an `edge:`/`anchor:` key.
388
+ * {@link isNodeTarget} is the cheap question, `parsePresenceTarget` the full
389
+ * one.
390
+ */
391
+ selection: readonly PresenceTargetKey[];
392
+ holding: readonly PresenceTargetKey[];
393
+ /**
394
+ * Where this peer currently has each node they're dragging, in world space —
395
+ * the frame-rate overlay from `setPeerNodeTransform`. The node renders there
396
+ * until they let go; the graph keeps its own position until their commit
397
+ * arrives as a real change.
398
+ */
399
+ transforms: ReadonlyMap<NodeId, Rect>;
400
+ /**
401
+ * The wire they're pulling right now, `null` when they aren't. The loose end
402
+ * is the interpolated one — what {@link step} has eased to, as with cursors.
403
+ */
404
+ pending: PeerConnection | null;
405
+ };
406
+ /**
407
+ * What a {@link PresenceOptions.cursorComponent} is handed: who the cursor
408
+ * belongs to, and nothing about where it is.
409
+ *
410
+ * Position is deliberately absent. A cursor moves at pointer rate for every
411
+ * peer in the room, and passing it as a prop would re-render the component that
412
+ * often — the cost the cursor layer exists to avoid. rivet writes the position
413
+ * onto the wrapper element around your component instead, as a `transform`, so
414
+ * your component re-renders only when the *person* changes.
415
+ *
416
+ * {@link PeerIdentity} rather than {@link PeerRecord} for the same reason: a
417
+ * record's `cursor`, `selection` and `transforms` all move faster than this
418
+ * component is re-rendered, so they would be stale in your hands. Anything else
419
+ * you want to show — an avatar, a role, what they're doing — rides on
420
+ * {@link PeerIdentity.data}.
421
+ */
422
+ type PeerCursorProps = {
423
+ peer: PeerIdentity;
424
+ };
425
+ /** One peer's claim on a target, as {@link PeerOutlineProps} reports it. */
426
+ type PeerClaim = {
427
+ peer: PeerIdentity;
428
+ /**
429
+ * `"held"` when the target is under one of their live gestures, `"selected"`
430
+ * when they merely have it selected. A peer who is doing both appears once,
431
+ * as `"held"` — the more current fact, and the one the canvas paint picks too.
432
+ */
433
+ kind: "selected" | "held";
434
+ };
435
+ /**
436
+ * What a {@link PresenceOptions.outlineComponent} is handed: one claimed
437
+ * target, everybody who has claimed it, and whether it's locked.
438
+ *
439
+ * One wrapper per **target**, not per peer-per-target. A node can be in three
440
+ * peers' selections while a fourth holds it and a lock sits on top; the canvas
441
+ * paint resolves that itself, because two strokes on one box read as a heavier
442
+ * border rather than as two facts. A component can do better than a stroke —
443
+ * stack avatars, badge a count, truncate at three — so it gets the whole list
444
+ * instead of rivet's answer to a question it didn't ask.
445
+ *
446
+ * Like {@link PeerCursorProps}, peers arrive as {@link PeerIdentity}: this
447
+ * re-renders when the *claims* change, so a record's `cursor` and `transforms`
448
+ * would be stale in your hands. Geometry isn't a prop for the same reason —
449
+ * rivet sizes and positions the wrapper element imperatively every frame.
450
+ */
451
+ type PeerOutlineProps = {
452
+ target: PresenceTarget;
453
+ claims: readonly PeerClaim[];
454
+ /**
455
+ * The lock on this target, or `null` when it isn't locked. `peer` is absent
456
+ * when the holder isn't in the roster — a lock outlives the tab that took it,
457
+ * which is the point of taking one.
458
+ */
459
+ lock: {
460
+ holderId: string;
461
+ peer?: PeerIdentity;
462
+ } | null;
463
+ };
464
+ /** Presence policy, set via the `presenceOptions` prop. */
465
+ type PresenceOptions = {
466
+ /**
467
+ * Smallest gap between outbound `onLocalPresence` snapshots, in ms. Cursor
468
+ * movement is coalesced to this rate; selection and gesture changes ignore it
469
+ * and go out at once (a lock signal that waits is a lock signal that races).
470
+ * Default `50`.
471
+ */
472
+ throttleMs?: number;
473
+ /**
474
+ * Draw peer cursors. Turn off to render them entirely yourself off
475
+ * `subscribeCursorFrame`; neither layer below is created. Node outlines and
476
+ * peers' live boxes are unaffected — they live with the geometry they
477
+ * annotate. Default `true`.
478
+ */
479
+ renderCursors?: boolean;
480
+ /**
481
+ * Render each peer's cursor as your own React component instead of the
482
+ * built-in canvas arrow: an avatar, a follow button, a status chip — anything
483
+ * a canvas glyph can't be.
484
+ *
485
+ * One wrapper per peer, mounted when they join and unmounted when they leave.
486
+ * rivet positions the wrapper imperatively every cursor frame, so nothing
487
+ * re-renders at pointer rate; see {@link PeerCursorProps}.
488
+ *
489
+ * Supplying this means the cursor *canvas* is never created. Setting
490
+ * `renderCursors: false` beats it: the boolean is "no cursors from rivet at
491
+ * all", which is a different statement from "cursors, drawn my way".
492
+ */
493
+ cursorComponent?: ComponentType<PeerCursorProps>;
494
+ /**
495
+ * Outline what peers have claimed — their selections, whatever they're
496
+ * holding, and locked targets — in the claimant's colour. Turn off to show
497
+ * claims entirely yourself, or not at all: no outlines are stroked around
498
+ * nodes and anchors, and no halo is drawn under a claimed edge. Cursors are
499
+ * unaffected. Default `true`.
500
+ */
501
+ renderOutlines?: boolean;
502
+ /**
503
+ * Render each claimed target as your own React component instead of the
504
+ * built-in outline: an avatar stack, a "3 others" badge, a name chip — again,
505
+ * anything a stroked box can't be.
506
+ *
507
+ * One wrapper per claimed target, mounted when the first claim on it arrives
508
+ * and unmounted with the last. rivet positions and sizes the wrapper
509
+ * imperatively every frame, so nothing re-renders as peers drag things
510
+ * around; see {@link PeerOutlineProps}.
511
+ *
512
+ * Supplying this replaces the outlines rivet strokes around **nodes and
513
+ * anchors**. The halo under a claimed *edge* stays, because a DOM overlay
514
+ * cannot follow an edge's path — your component still gets `edge` targets,
515
+ * positioned at the midpoint of the drawn path, for chrome that sits on one.
516
+ * `renderOutlines: false` is the switch that turns off all of it.
517
+ */
518
+ outlineComponent?: ComponentType<PeerOutlineProps>;
519
+ };
520
+ /**
521
+ * Presence options with every default filled in. The two components stay
522
+ * optional — there is no default component, and "no component" is the state
523
+ * that selects the canvas.
524
+ */
525
+ type ResolvedPresenceOptions = Required<Omit<PresenceOptions, "cursorComponent" | "outlineComponent">> & Pick<PresenceOptions, "cursorComponent" | "outlineComponent">;
526
+ declare const DEFAULT_PRESENCE_OPTIONS: ResolvedPresenceOptions;
527
+ /**
528
+ * Default peer colours. Spaced around the wheel and mid-toned, so they read on
529
+ * a light or dark canvas and stay distinguishable side by side.
530
+ */
531
+ declare const PEER_COLORS: readonly ["#6366f1", "#ec4899", "#f59e0b", "#10b981", "#3b82f6", "#8b5cf6", "#ef4444", "#14b8a6"];
532
+ /**
533
+ * A stable colour for a peer id. Deterministic, so the same person is the same
534
+ * colour on every client and across a record being pruned and rebuilt — nobody
535
+ * coordinates a palette, and nobody's cursor changes colour mid-session.
536
+ */
537
+ declare function peerColor(id: string): string;
538
+ /**
539
+ * The presence registry — one per graph, reachable as `store.presence`.
540
+ *
541
+ * Reads are snapshot-based: {@link getPeers} returns a cached array rebuilt
542
+ * only when something actually changed, so the render loop can call it every
543
+ * frame and `useSyncExternalStore` can compare identities.
544
+ */
545
+ type PresenceRegistry = {
546
+ /**
547
+ * Replace the roster (the `peers` prop). Peers that were in the previous
548
+ * roster and aren't in this one are dropped outright — leaving the roster is
549
+ * how a peer leaves. Peers this registry only knows imperatively are kept.
550
+ */
551
+ setPeers: (peers: Peer[]) => void;
552
+ /**
553
+ * Move a peer's cursor, in world coordinates. The hot path: call it as fast
554
+ * as your channel delivers frames — it dirties the cursor layer and nothing
555
+ * else, so the cost doesn't scale into the rest of the canvas.
556
+ *
557
+ * `null` parks the cursor. For a peer the roster never declared, that leaves
558
+ * nothing to draw, so the record is dropped — the next frame recreates it
559
+ * with the same colour.
560
+ */
561
+ setPeerCursor: (peerId: string, point: Vec2 | null) => void;
562
+ /**
563
+ * Where a peer currently has a node, in world space — their drag, mid-flight.
564
+ * The node itself moves there: it's painted, hit-tested and wired at the peer's
565
+ * live box, while the graph underneath is untouched until their commit lands.
566
+ * `null` releases it, which is what their drop should do just before their
567
+ * real change arrives.
568
+ */
569
+ setPeerNodeTransform: (peerId: string, nodeId: NodeId, rect: Rect | null) => void;
570
+ /**
571
+ * The wire a peer is pulling out of a handle, or `null` when they let go.
572
+ *
573
+ * Hot path like the cursor, but it takes a **full** frame rather than the
574
+ * cursor layer's narrow one: the wire is painted on the shared foreground,
575
+ * which is also where the local pending connection, the alignment guides and
576
+ * the peer outlines live. Unlike a pointer, which is always moving, this only
577
+ * arrives while somebody is actually dragging a connection.
578
+ */
579
+ setPeerConnection: (peerId: string, connection: PeerConnection | null) => void;
580
+ /**
581
+ * Every node a peer currently has under a gesture, at its *rendered* position
582
+ * — the interpolated one, not the last frame that arrived. What the store
583
+ * folds into world positions.
584
+ */
585
+ getNodeTransforms: () => ReadonlyMap<NodeId, Rect>;
586
+ /**
587
+ * Bumped whenever a rendered transform moves. Lets the store cache world
588
+ * positions across frames where peers are idle.
589
+ */
590
+ getTransformVersion: () => number;
591
+ /**
592
+ * Advance the interpolation toward the values that arrived, and report
593
+ * whether anything is still in motion (i.e. whether another frame is owed).
594
+ *
595
+ * Frames arrive at whatever rate a transport manages — 20Hz is normal, and
596
+ * stepping straight to each one makes a cursor stutter and a dragged node
597
+ * jump. Easing between them costs one lerp per peer and buys motion that
598
+ * reads as somebody moving rather than as packets landing.
599
+ */
600
+ step: (now: number) => boolean;
601
+ /** Forget a peer entirely — cursor, held nodes and all. Use it when they disconnect. */
602
+ removePeer: (peerId: string) => void;
603
+ /** Every known peer. A cached snapshot; identity only changes when the data does. */
604
+ getPeers: () => readonly PeerRecord[];
605
+ /**
606
+ * Every known peer, identity only — no cursor, no claims. Its own cached
607
+ * snapshot, so it keeps its identity across every frame and every click that
608
+ * leaves the roster's *people* unchanged.
609
+ */
610
+ getPeerIdentities: () => readonly PeerIdentity[];
611
+ /** True when nobody else is here — the render loop's early-out. */
612
+ isEmpty: () => boolean;
613
+ /**
614
+ * Subscribe to **roster** changes: peers arriving or leaving, and their
615
+ * identity, selection or holding changing. Cursor and transform frames
616
+ * deliberately don't notify — they'd re-render React at pointer rate, and
617
+ * they're already painted on the canvas. Read them from {@link getPeers} on
618
+ * the frame channel if you need them in the DOM.
619
+ */
620
+ subscribe: (listener: () => void) => () => void;
621
+ /**
622
+ * The half of {@link subscribe} that's about *people*: joins, leaves, names,
623
+ * colours and `data`. Silent when a peer only changes what they have
624
+ * selected or held.
625
+ *
626
+ * The two halves move at completely different rates. At 15–100 participants,
627
+ * somebody is always clicking something, so a roster subscription that counts
628
+ * claims re-renders continuously — while the set of people in the room barely
629
+ * moves. An avatar stack wants this one.
630
+ */
631
+ subscribeIdentity: (listener: () => void) => () => void;
632
+ /**
633
+ * The other half: what peers have selected or are holding. Also fires when a
634
+ * peer joins or leaves carrying claims, since that changes who claims what.
635
+ */
636
+ subscribeClaims: (listener: () => void) => () => void;
637
+ };
638
+
639
+ /**
640
+ * The outbound half of presence: what this client tells everyone else.
641
+ *
642
+ * It publishes a *snapshot*, never an event stream. A snapshot is idempotent —
643
+ * a dropped one costs nothing, a duplicate costs nothing, and a client that
644
+ * reconnects mid-session is immediately correct rather than replaying history.
645
+ * That's also how presence channels themselves model state, so this maps onto
646
+ * any transport without a translation layer.
647
+ *
648
+ * Cursor movement is coalesced to `throttleMs`; selection and gesture changes
649
+ * jump the queue, because they're the signals a peer acts on (a lock request
650
+ * that waits 50ms is a lock request that can lose a race it should have won).
651
+ */
652
+
653
+ /** This client's presence, as handed to `onLocalPresence`. */
654
+ type LocalPresence = {
655
+ /**
656
+ * Pointer position in **world** coordinates, or `null` when the pointer is
657
+ * off the canvas. World, not screen: peers have their own viewports, and a
658
+ * screen point means nothing on the other side.
659
+ */
660
+ cursor: Vec2 | null;
661
+ /**
662
+ * What this client has selected, as {@link PresenceTargetKey}s: node ids bare,
663
+ * the selected edge as `edge:<id>`, selected anchors as `anchor:<node>:<id>`.
664
+ * Feed it straight back to a peer's `selection` on the other side.
665
+ *
666
+ * Only nodes existed here in 0.2.0 and node ids are still bare strings, so a
667
+ * transport that treats these as node ids keeps working — it just carries a
668
+ * couple of keys it doesn't recognise once someone clicks an edge.
669
+ */
670
+ selection: PresenceTargetKey[];
671
+ /** Nodes under a live local gesture — the same set as `onNodeDragStart`. */
672
+ holding: NodeId[];
673
+ /**
674
+ * The wire this client is pulling out of a handle, or `null`. Both ends in
675
+ * world coordinates; feed it straight back to a peer's `pending`.
676
+ *
677
+ * The local drag's `reconnecting` id is deliberately not here. It means "the
678
+ * edge this preview stands in for is hidden", which is true for the client
679
+ * doing the dragging and must not be true for anybody else — the edge is
680
+ * still in the document until their change lands.
681
+ */
682
+ pending: PeerConnection | null;
683
+ };
684
+
685
+ /**
686
+ * Whether a node is locked, and by whom — for the chrome rivet can't draw for
687
+ * you: a padlock in the node's header, a "Ada is editing" line, a disabled
688
+ * form inside the node body.
689
+ *
690
+ * rivet already refuses the interactions it owns (see the `lockedNodes` prop),
691
+ * so this is for *your* controls. Re-renders only when this node changes hands,
692
+ * not when any lock in the document moves.
693
+ *
694
+ * Answers for the lock that *governs* the node: a node inside a locked container
695
+ * reports the container's holder, so a disabled input inside the contents reads
696
+ * the same as one on the container itself.
697
+ *
698
+ * ```tsx
699
+ * function MyNode({ id }: NodeProps) {
700
+ * const { locked, holderId } = useNodeLock(id)
701
+ * return <input disabled={locked} title={locked ? `${holderId} is editing` : ""} />
702
+ * }
703
+ * ```
704
+ */
705
+ declare function useNodeLock(id: NodeId): NodeLock;
706
+ /**
707
+ * Whether this client may do `intent` to a node right now — the same question
708
+ * every rivet input path asks, answered through the same policy.
709
+ *
710
+ * Use it to keep your own affordances honest: hide a delete button on a node a
711
+ * peer has locked, rather than letting it be pressed and refused. Always `true`
712
+ * for an unlocked node.
713
+ */
714
+ declare function useNodeLockAllows(id: NodeId, intent: LockIntent): boolean;
715
+
716
+ export { type CanInteractWithLocked as C, DEFAULT_PRESENCE_OPTIONS as D, LOCK_DEFAULT_REFUSED as L, type NodeLock as N, type PeerIdentity as P, type ResolvedPresenceOptions as R, type PeerRecord as a, type PresenceTargetKey as b, type PresenceTarget as c, type PeerClaim as d, type LocalPresence as e, type LockIntent as f, type LockInteraction as g, type LockRegistry as h, type LockTarget as i, PEER_COLORS as j, type Peer as k, type PeerConnection as l, type PeerCursorProps as m, type PeerOutlineProps as n, type PresenceOptions as o, type PresenceRegistry as p, anchorTargetKey as q, edgeTargetKey as r, isNodeTarget as s, parsePresenceTarget as t, peerColor as u, presenceTargetEdgeId as v, presenceTargetKey as w, presenceTargetNodeId as x, useNodeLock as y, useNodeLockAllows as z };