@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,676 @@
1
+ import { useRivetContext } from './chunk-VQYN27OK.js';
2
+ import { useSyncExternalStore, useCallback } from 'react';
3
+
4
+ // src/graph.ts
5
+ function clampChildToParent(position, childSize, parentSize) {
6
+ const maxX = Math.max(0, parentSize.width - childSize.width);
7
+ const maxY = Math.max(0, parentSize.height - childSize.height);
8
+ return {
9
+ x: Math.min(maxX, Math.max(0, position.x)),
10
+ y: Math.min(maxY, Math.max(0, position.y))
11
+ };
12
+ }
13
+ function snapToGrid(position, grid) {
14
+ const [gx, gy] = grid;
15
+ return {
16
+ x: gx > 0 ? Math.round(position.x / gx) * gx : position.x,
17
+ y: gy > 0 ? Math.round(position.y / gy) * gy : position.y
18
+ };
19
+ }
20
+ function boundingRect(rects) {
21
+ if (rects.length === 0) return null;
22
+ let minX = Number.POSITIVE_INFINITY;
23
+ let minY = Number.POSITIVE_INFINITY;
24
+ let maxX = Number.NEGATIVE_INFINITY;
25
+ let maxY = Number.NEGATIVE_INFINITY;
26
+ for (const r of rects) {
27
+ minX = Math.min(minX, r.x);
28
+ minY = Math.min(minY, r.y);
29
+ maxX = Math.max(maxX, r.x + r.width);
30
+ maxY = Math.max(maxY, r.y + r.height);
31
+ }
32
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
33
+ }
34
+ var MAX_PARENT_DEPTH = 50;
35
+ function worldPosition(nodes, id, maxDepth = MAX_PARENT_DEPTH) {
36
+ const node = nodes.get(id);
37
+ if (!node) return { x: 0, y: 0 };
38
+ let x = node.position.x;
39
+ let y = node.position.y;
40
+ let parentId = node.parentId;
41
+ let guard = 0;
42
+ while (parentId && guard++ < maxDepth) {
43
+ const parent = nodes.get(parentId);
44
+ if (!parent) break;
45
+ x += parent.position.x;
46
+ y += parent.position.y;
47
+ parentId = parent.parentId;
48
+ }
49
+ return { x, y };
50
+ }
51
+ function nodeDepth(nodes, id, maxDepth = MAX_PARENT_DEPTH) {
52
+ let depth = 0;
53
+ let parentId = nodes.get(id)?.parentId;
54
+ while (parentId && depth < maxDepth) {
55
+ depth++;
56
+ parentId = nodes.get(parentId)?.parentId;
57
+ }
58
+ return depth;
59
+ }
60
+ function hasAncestorIn(nodes, id, set, maxDepth = MAX_PARENT_DEPTH) {
61
+ let parentId = nodes.get(id)?.parentId;
62
+ let guard = 0;
63
+ while (parentId && guard++ < maxDepth) {
64
+ if (set.has(parentId)) return true;
65
+ parentId = nodes.get(parentId)?.parentId;
66
+ }
67
+ return false;
68
+ }
69
+ function buildChildIndex(nodes) {
70
+ const childrenOf = /* @__PURE__ */ new Map();
71
+ for (const node of nodes.values()) {
72
+ if (!node.parentId) continue;
73
+ const list = childrenOf.get(node.parentId);
74
+ if (list) list.push(node.id);
75
+ else childrenOf.set(node.parentId, [node.id]);
76
+ }
77
+ return childrenOf;
78
+ }
79
+ function collectDescendants(id, childIndex, out) {
80
+ for (const child of childIndex.get(id) ?? []) {
81
+ out.push(child);
82
+ collectDescendants(child, childIndex, out);
83
+ }
84
+ }
85
+ function descendantIds(nodes, id) {
86
+ const out = [];
87
+ collectDescendants(id, buildChildIndex(nodes), out);
88
+ return out;
89
+ }
90
+ function serializeGraph(nodes, edges, viewport) {
91
+ const cleanNodes = [];
92
+ for (const node of nodes) {
93
+ const copy = { ...node, position: { ...node.position } };
94
+ if (node.size) copy.size = { ...node.size };
95
+ copy.hovered = void 0;
96
+ copy.dragging = void 0;
97
+ cleanNodes.push(copy);
98
+ }
99
+ return {
100
+ nodes: cleanNodes,
101
+ edges: [...edges].map((edge) => ({ ...edge })),
102
+ viewport: { ...viewport }
103
+ };
104
+ }
105
+ function nodeControlledEqual(a, b) {
106
+ return a.position.x === b.position.x && a.position.y === b.position.y && Boolean(a.selected) === Boolean(b.selected) && a.data === b.data && a.type === b.type && a.laneId === b.laneId && a.ariaLabel === b.ariaLabel && a.parentId === b.parentId && a.extent === b.extent && a.width === b.width && a.height === b.height && Boolean(a.dragging) === Boolean(b.dragging) && a.size?.width === b.size?.width && a.size?.height === b.size?.height;
107
+ }
108
+
109
+ // src/presence/targets.ts
110
+ var EDGE_PREFIX = "edge:";
111
+ var ANCHOR_PREFIX = "anchor:";
112
+ function presenceTargetKey(target) {
113
+ if (typeof target === "string") return target;
114
+ if (target.kind === "edge") return `${EDGE_PREFIX}${target.id}`;
115
+ if (target.kind === "anchor") return `${ANCHOR_PREFIX}${target.nodeId}:${target.anchorId}`;
116
+ return target.id;
117
+ }
118
+ function edgeTargetKey(id) {
119
+ return `${EDGE_PREFIX}${id}`;
120
+ }
121
+ function anchorTargetKey(nodeId, anchorId) {
122
+ return `${ANCHOR_PREFIX}${nodeId}:${anchorId}`;
123
+ }
124
+ function anchorSplit(rest) {
125
+ const split = rest.indexOf(":");
126
+ return split > 0 && split < rest.length - 1 ? split : -1;
127
+ }
128
+ function isNodeTarget(key) {
129
+ if (key.startsWith(EDGE_PREFIX)) return false;
130
+ if (key.startsWith(ANCHOR_PREFIX)) return anchorSplit(key.slice(ANCHOR_PREFIX.length)) < 0;
131
+ return true;
132
+ }
133
+ function parsePresenceTarget(key) {
134
+ if (key.startsWith(EDGE_PREFIX)) return { kind: "edge", id: key.slice(EDGE_PREFIX.length) };
135
+ if (key.startsWith(ANCHOR_PREFIX)) {
136
+ const rest = key.slice(ANCHOR_PREFIX.length);
137
+ const split = anchorSplit(rest);
138
+ if (split >= 0) {
139
+ return { kind: "anchor", nodeId: rest.slice(0, split), anchorId: rest.slice(split + 1) };
140
+ }
141
+ }
142
+ return { kind: "node", id: key };
143
+ }
144
+ function presenceTargetNodeId(target) {
145
+ const resolved = typeof target === "string" ? parsePresenceTarget(target) : target;
146
+ if (resolved.kind === "node") return resolved.id;
147
+ if (resolved.kind === "anchor") return resolved.nodeId;
148
+ return null;
149
+ }
150
+ function presenceTargetEdgeId(target) {
151
+ if (typeof target === "string") {
152
+ return target.startsWith(EDGE_PREFIX) ? target.slice(EDGE_PREFIX.length) : null;
153
+ }
154
+ return target.kind === "edge" ? target.id : null;
155
+ }
156
+
157
+ // src/locks/registry.ts
158
+ var LOCK_DEFAULT_REFUSED = /* @__PURE__ */ new Set([
159
+ "select",
160
+ "drag",
161
+ "resize",
162
+ "delete",
163
+ "reconnect"
164
+ ]);
165
+ function locksEqual(current, next) {
166
+ const keys = Object.keys(next);
167
+ if (keys.length !== current.size) return false;
168
+ return keys.every((key) => current.get(key) === next[key]);
169
+ }
170
+ var NO_EDGE_LOCKS = /* @__PURE__ */ new Map();
171
+ function indexEdgeLocks(locks) {
172
+ let edges = null;
173
+ for (const [key, holderId] of locks) {
174
+ const edgeId = presenceTargetEdgeId(key);
175
+ if (edgeId === null) continue;
176
+ edges ??= /* @__PURE__ */ new Map();
177
+ edges.set(edgeId, holderId);
178
+ }
179
+ return edges ?? NO_EDGE_LOCKS;
180
+ }
181
+ var Registry = class {
182
+ /**
183
+ * `requestRender` marks the canvas dirty — a lock is paint as well as policy.
184
+ * `getParentId` is how the cascade walks upward; a registry built without one
185
+ * (no graph to ask) governs each node by its own entry alone.
186
+ */
187
+ constructor(requestRender, getParentId = () => void 0) {
188
+ this.requestRender = requestRender;
189
+ this.getParentId = getParentId;
190
+ }
191
+ requestRender;
192
+ getParentId;
193
+ locks = /* @__PURE__ */ new Map();
194
+ /** `edge:` entries of {@link locks}, re-indexed on every table change. */
195
+ edgeLocks = NO_EDGE_LOCKS;
196
+ policy = null;
197
+ version = 0;
198
+ listeners = /* @__PURE__ */ new Set();
199
+ setLocks = (locks) => {
200
+ const next = locks ?? {};
201
+ if (locksEqual(this.locks, next)) return [];
202
+ const opened = [];
203
+ for (const [key, holderId] of Object.entries(next)) {
204
+ if (this.locks.get(key) !== holderId) opened.push(key);
205
+ }
206
+ this.locks = new Map(Object.entries(next));
207
+ this.edgeLocks = indexEdgeLocks(this.locks);
208
+ this.version += 1;
209
+ this.requestRender();
210
+ for (const listener of this.listeners) listener();
211
+ return opened;
212
+ };
213
+ setPolicy = (policy) => {
214
+ this.policy = policy;
215
+ };
216
+ isLocked = (target) => this.getLockSource(target) !== null;
217
+ getHolder = (target) => {
218
+ const source = this.getLockSource(target);
219
+ return source === null ? null : this.locks.get(source) ?? null;
220
+ };
221
+ getLockSource = (target) => {
222
+ if (this.locks.size === 0) return null;
223
+ const key = typeof target === "string" ? target : presenceTargetKey(target);
224
+ if (this.locks.has(key)) return key;
225
+ if (isNodeTarget(key)) return this.ancestorLock(key);
226
+ const owner = presenceTargetNodeId(key);
227
+ if (owner === null) return null;
228
+ return this.locks.has(owner) ? owner : this.ancestorLock(owner);
229
+ };
230
+ /**
231
+ * The nearest locked ancestor of a node, ignoring the node itself. Locking a
232
+ * container claims what's inside it: a lock that stopped at the container
233
+ * would leave its contents draggable out of it, deletable, and rewireable —
234
+ * the box protected and everything it holds is not.
235
+ */
236
+ ancestorLock(nodeId) {
237
+ let parentId = this.getParentId(nodeId);
238
+ let guard = 0;
239
+ while (parentId && guard++ < MAX_PARENT_DEPTH) {
240
+ if (this.locks.has(parentId)) return parentId;
241
+ parentId = this.getParentId(parentId);
242
+ }
243
+ return null;
244
+ }
245
+ getLocks = () => this.locks;
246
+ getLockedEdges = () => this.edgeLocks;
247
+ size = () => this.locks.size;
248
+ allows = (target, intent) => {
249
+ if (this.locks.size === 0) return true;
250
+ const lockedKey = this.getLockSource(target);
251
+ if (lockedKey === null) return true;
252
+ const holderId = this.locks.get(lockedKey);
253
+ if (holderId === void 0) return true;
254
+ if (this.policy) {
255
+ const key = typeof target === "string" ? target : presenceTargetKey(target);
256
+ const resolved = typeof target === "string" ? parsePresenceTarget(key) : target;
257
+ const lockedTarget = parsePresenceTarget(lockedKey);
258
+ return this.policy({
259
+ target: resolved,
260
+ lockedTarget,
261
+ nodeId: presenceTargetNodeId(resolved),
262
+ lockedNodeId: presenceTargetNodeId(lockedTarget),
263
+ holderId,
264
+ intent
265
+ });
266
+ }
267
+ return !LOCK_DEFAULT_REFUSED.has(intent);
268
+ };
269
+ getVersion = () => this.version;
270
+ subscribe = (listener) => {
271
+ this.listeners.add(listener);
272
+ return () => {
273
+ this.listeners.delete(listener);
274
+ };
275
+ };
276
+ };
277
+ function createLockRegistry(requestRender, getParentId) {
278
+ return new Registry(requestRender, getParentId);
279
+ }
280
+
281
+ // src/presence/registry.ts
282
+ var DEFAULT_PRESENCE_OPTIONS = {
283
+ throttleMs: 50,
284
+ renderCursors: true,
285
+ renderOutlines: true
286
+ };
287
+ var PEER_COLORS = [
288
+ "#6366f1",
289
+ "#ec4899",
290
+ "#f59e0b",
291
+ "#10b981",
292
+ "#3b82f6",
293
+ "#8b5cf6",
294
+ "#ef4444",
295
+ "#14b8a6"
296
+ ];
297
+ function peerColor(id) {
298
+ let hash = 0;
299
+ for (let i = 0; i < id.length; i += 1) hash = hash * 31 + id.charCodeAt(i) | 0;
300
+ const index = (hash % PEER_COLORS.length + PEER_COLORS.length) % PEER_COLORS.length;
301
+ return PEER_COLORS[index] ?? PEER_COLORS[0];
302
+ }
303
+ var SMOOTHING_MS = 45;
304
+ var NOMINAL_FRAME_MS = 16;
305
+ var SETTLED_EPSILON = 0.05;
306
+ function approach(from, to, t) {
307
+ const next = from + (to - from) * t;
308
+ return Math.abs(to - next) < SETTLED_EPSILON ? to : next;
309
+ }
310
+ var EMPTY_IDS = [];
311
+ function normalizeTargets(targets) {
312
+ if (!targets || targets.length === 0) return EMPTY_IDS;
313
+ return targets.map(presenceTargetKey);
314
+ }
315
+ var samePoint = (a, b) => a === b || a != null && b != null && a.x === b.x && a.y === b.y;
316
+ var sameConnection = (a, b) => {
317
+ if (a === b) return true;
318
+ if (!a || !b) return false;
319
+ return a.source === b.source && a.sourceHandle === b.sourceHandle && a.sourceType === b.sourceType && a.toPosition === b.toPosition && samePoint(a.from, b.from) && samePoint(a.to, b.to);
320
+ };
321
+ var sameRect = (a, b) => a !== void 0 && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
322
+ var sameIds = (a, b) => {
323
+ const left = a ?? EMPTY_IDS;
324
+ const right = b ?? EMPTY_IDS;
325
+ return left.length === right.length && left.every((id, i) => id === right[i]);
326
+ };
327
+ function identityEqual(a, b) {
328
+ if (a === b) return true;
329
+ if (!a || !b) return false;
330
+ return a.name === b.name && a.color === b.color && a.data === b.data && samePoint(a.cursor, b.cursor) && sameConnection(a.pending, b.pending);
331
+ }
332
+ var Registry2 = class {
333
+ /** Called on every change, roster or hot-path — something has to repaint either way. */
334
+ constructor(requests) {
335
+ this.requests = requests;
336
+ }
337
+ requests;
338
+ peers = /* @__PURE__ */ new Map();
339
+ listeners = /* @__PURE__ */ new Set();
340
+ identityListeners = /* @__PURE__ */ new Set();
341
+ claimsListeners = /* @__PURE__ */ new Set();
342
+ snapshot = [];
343
+ stale = false;
344
+ identitySnapshot = [];
345
+ identityStale = false;
346
+ /** Merged rendered transforms across peers, rebuilt when {@link version} moves. */
347
+ transforms = /* @__PURE__ */ new Map();
348
+ transformsStale = false;
349
+ version = 0;
350
+ /** Timestamp of the last {@link step}, so the ease is frame-rate independent. */
351
+ lastStep = -1;
352
+ touch() {
353
+ this.stale = true;
354
+ this.requests.requestRender();
355
+ }
356
+ /**
357
+ * A change only the cursor layer can see. Same snapshot invalidation, but the
358
+ * frame it asks for repaints one canvas instead of all of them.
359
+ */
360
+ touchCursor() {
361
+ this.stale = true;
362
+ this.requests.requestCursorRender();
363
+ }
364
+ /**
365
+ * A change React needs to know about, as well as the canvas — routed to the
366
+ * half (or halves) it belongs to. The union channel fires once either way: a
367
+ * peer who joins with a selection is one roster change, not two.
368
+ */
369
+ touchRoster(identity, claims) {
370
+ if (!identity && !claims) return;
371
+ this.touch();
372
+ if (identity) {
373
+ this.identityStale = true;
374
+ for (const listener of this.identityListeners) listener();
375
+ }
376
+ if (claims) for (const listener of this.claimsListeners) listener();
377
+ for (const listener of this.listeners) listener();
378
+ }
379
+ peer(id) {
380
+ let state = this.peers.get(id);
381
+ if (!state) {
382
+ state = {
383
+ id,
384
+ declared: null,
385
+ selection: EMPTY_IDS,
386
+ holding: EMPTY_IDS,
387
+ live: void 0,
388
+ renderCursor: null,
389
+ transforms: /* @__PURE__ */ new Map(),
390
+ renderTransforms: /* @__PURE__ */ new Map(),
391
+ livePending: void 0,
392
+ renderPending: null
393
+ };
394
+ this.peers.set(id, state);
395
+ this.identityStale = true;
396
+ }
397
+ return state;
398
+ }
399
+ /** Drop a peer the roster doesn't claim and that has nothing left to draw. */
400
+ prune(state) {
401
+ if (state.declared || state.live || state.transforms.size > 0 || state.livePending) return;
402
+ this.peers.delete(state.id);
403
+ this.identityStale = true;
404
+ this.invalidateTransforms();
405
+ }
406
+ invalidateTransforms() {
407
+ this.transformsStale = true;
408
+ this.version += 1;
409
+ }
410
+ setPeers = (peers) => {
411
+ let identityChanged = false;
412
+ let claimsChanged = false;
413
+ const incoming = /* @__PURE__ */ new Set();
414
+ for (const peer of peers) {
415
+ incoming.add(peer.id);
416
+ const known = this.peers.has(peer.id);
417
+ const state = this.peer(peer.id);
418
+ if (!known || !identityEqual(state.declared, peer)) identityChanged = true;
419
+ const selection = normalizeTargets(peer.selection);
420
+ const holding = normalizeTargets(peer.holding);
421
+ if (!sameIds(state.selection, selection) || !sameIds(state.holding, holding)) {
422
+ state.selection = selection;
423
+ state.holding = holding;
424
+ claimsChanged = true;
425
+ }
426
+ state.declared = peer;
427
+ }
428
+ for (const state of [...this.peers.values()]) {
429
+ if (incoming.has(state.id)) continue;
430
+ if (!state.declared) continue;
431
+ this.peers.delete(state.id);
432
+ identityChanged = true;
433
+ if (state.selection.length > 0 || state.holding.length > 0) claimsChanged = true;
434
+ }
435
+ this.touchRoster(identityChanged, claimsChanged);
436
+ };
437
+ setPeerCursor = (peerId, point) => {
438
+ const state = this.peer(peerId);
439
+ if (state.live !== void 0 && samePoint(state.live, point)) return;
440
+ state.live = point;
441
+ if (point === null) this.prune(state);
442
+ this.touchCursor();
443
+ };
444
+ setPeerNodeTransform = (peerId, nodeId, rect) => {
445
+ const state = this.peer(peerId);
446
+ if (rect === null) {
447
+ const had = state.transforms.delete(nodeId);
448
+ state.renderTransforms.delete(nodeId);
449
+ this.prune(state);
450
+ if (!had) return;
451
+ this.invalidateTransforms();
452
+ this.touch();
453
+ return;
454
+ }
455
+ if (sameRect(state.transforms.get(nodeId), rect)) return;
456
+ state.transforms.set(nodeId, rect);
457
+ if (!state.renderTransforms.has(nodeId)) {
458
+ state.renderTransforms.set(nodeId, rect);
459
+ this.invalidateTransforms();
460
+ }
461
+ this.touch();
462
+ };
463
+ setPeerConnection = (peerId, connection) => {
464
+ const state = this.peer(peerId);
465
+ if (state.livePending !== void 0 && sameConnection(state.livePending, connection)) return;
466
+ state.livePending = connection;
467
+ if (connection === null) this.prune(state);
468
+ this.touch();
469
+ };
470
+ removePeer = (peerId) => {
471
+ const state = this.peers.get(peerId);
472
+ if (!state) return;
473
+ this.peers.delete(peerId);
474
+ this.invalidateTransforms();
475
+ this.touchRoster(true, state.selection.length > 0 || state.holding.length > 0);
476
+ };
477
+ getNodeTransforms = () => {
478
+ if (this.transformsStale) {
479
+ this.transformsStale = false;
480
+ this.transforms = /* @__PURE__ */ new Map();
481
+ for (const state of this.peers.values()) {
482
+ for (const [nodeId, rect] of state.renderTransforms) this.transforms.set(nodeId, rect);
483
+ }
484
+ }
485
+ return this.transforms;
486
+ };
487
+ getTransformVersion = () => this.version;
488
+ step = (now) => {
489
+ const elapsed = this.lastStep < 0 ? NOMINAL_FRAME_MS : Math.max(0, now - this.lastStep);
490
+ this.lastStep = now;
491
+ const t = 1 - Math.exp(-elapsed / SMOOTHING_MS);
492
+ let moving = false;
493
+ let transformsMoved = false;
494
+ for (const state of this.peers.values()) {
495
+ const target = state.live !== void 0 ? state.live : state.declared?.cursor ?? null;
496
+ if (target === null) {
497
+ state.renderCursor = null;
498
+ } else if (!state.renderCursor) {
499
+ state.renderCursor = target;
500
+ } else if (!samePoint(state.renderCursor, target)) {
501
+ state.renderCursor = {
502
+ x: approach(state.renderCursor.x, target.x, t),
503
+ y: approach(state.renderCursor.y, target.y, t)
504
+ };
505
+ moving = true;
506
+ }
507
+ const wire = state.livePending !== void 0 ? state.livePending : state.declared?.pending ?? null;
508
+ if (!wire) {
509
+ state.renderPending = null;
510
+ } else if (!state.renderPending || // A different wire, not the same one moved: placed outright, or the
511
+ // loose end would sweep across the canvas between two drags.
512
+ state.renderPending.source !== wire.source || state.renderPending.sourceHandle !== wire.sourceHandle) {
513
+ state.renderPending = wire;
514
+ } else if (!samePoint(state.renderPending.to, wire.to)) {
515
+ state.renderPending = {
516
+ ...wire,
517
+ to: {
518
+ x: approach(state.renderPending.to.x, wire.to.x, t),
519
+ y: approach(state.renderPending.to.y, wire.to.y, t)
520
+ }
521
+ };
522
+ moving = true;
523
+ }
524
+ for (const [nodeId, rect] of state.transforms) {
525
+ const current = state.renderTransforms.get(nodeId);
526
+ if (!current) {
527
+ state.renderTransforms.set(nodeId, rect);
528
+ transformsMoved = true;
529
+ continue;
530
+ }
531
+ if (sameRect(current, rect)) continue;
532
+ state.renderTransforms.set(nodeId, {
533
+ x: approach(current.x, rect.x, t),
534
+ y: approach(current.y, rect.y, t),
535
+ // Size comes from a resize rather than a drag and rarely differs
536
+ // frame to frame, but easing it keeps a live resize as smooth.
537
+ width: approach(current.width, rect.width, t),
538
+ height: approach(current.height, rect.height, t)
539
+ });
540
+ transformsMoved = true;
541
+ moving = true;
542
+ }
543
+ }
544
+ if (transformsMoved) this.invalidateTransforms();
545
+ if (moving) this.stale = true;
546
+ return moving;
547
+ };
548
+ getPeers = () => {
549
+ if (this.stale) {
550
+ this.stale = false;
551
+ this.snapshot = [...this.peers.values()].map((state) => {
552
+ const declared = state.declared;
553
+ const target = (state.live !== void 0 ? state.live : declared?.cursor) ?? null;
554
+ return {
555
+ id: state.id,
556
+ name: declared?.name,
557
+ color: declared?.color ?? peerColor(state.id),
558
+ data: declared?.data,
559
+ // Rendered, not raw: what {@link step} has eased to. Falls back to the
560
+ // target so a registry nobody steps still paints something sensible.
561
+ cursor: target === null ? null : state.renderCursor ?? target,
562
+ selection: state.selection,
563
+ holding: state.holding,
564
+ transforms: state.renderTransforms,
565
+ // Same fallback as the cursor: a registry nobody steps still reports
566
+ // the wire that arrived rather than nothing.
567
+ pending: state.renderPending ?? (state.livePending !== void 0 ? state.livePending : declared?.pending ?? null)
568
+ };
569
+ });
570
+ }
571
+ return this.snapshot;
572
+ };
573
+ getPeerIdentities = () => {
574
+ if (this.identityStale) {
575
+ this.identityStale = false;
576
+ this.identitySnapshot = [...this.peers.values()].map((state) => ({
577
+ id: state.id,
578
+ name: state.declared?.name,
579
+ color: state.declared?.color ?? peerColor(state.id),
580
+ data: state.declared?.data
581
+ }));
582
+ }
583
+ return this.identitySnapshot;
584
+ };
585
+ isEmpty = () => this.peers.size === 0;
586
+ subscribe = (listener) => {
587
+ this.listeners.add(listener);
588
+ return () => {
589
+ this.listeners.delete(listener);
590
+ };
591
+ };
592
+ subscribeIdentity = (listener) => {
593
+ this.identityListeners.add(listener);
594
+ return () => {
595
+ this.identityListeners.delete(listener);
596
+ };
597
+ };
598
+ subscribeClaims = (listener) => {
599
+ this.claimsListeners.add(listener);
600
+ return () => {
601
+ this.claimsListeners.delete(listener);
602
+ };
603
+ };
604
+ };
605
+ function createPresenceRegistry(requests) {
606
+ return new Registry2(requests);
607
+ }
608
+ function usePeerIdentities() {
609
+ const { store } = useRivetContext();
610
+ return useSyncExternalStore(
611
+ store.presence.subscribeIdentity,
612
+ store.presence.getPeerIdentities,
613
+ store.presence.getPeerIdentities
614
+ );
615
+ }
616
+
617
+ // src/presence/outlines.ts
618
+ var EMPTY_ENTRIES = [];
619
+ function collectPeerOutlines(peers, identities, locks) {
620
+ if (peers.length === 0 && locks.size === 0) return EMPTY_ENTRIES;
621
+ const byId = new Map(identities.map((identity) => [identity.id, identity]));
622
+ const entries = /* @__PURE__ */ new Map();
623
+ const entry = (key) => {
624
+ let found = entries.get(key);
625
+ if (!found) {
626
+ found = { key, target: parsePresenceTarget(key), claims: [], lock: null };
627
+ entries.set(key, found);
628
+ }
629
+ return found;
630
+ };
631
+ const claim = (key, peer, kind) => {
632
+ const target = entry(key);
633
+ if (target.claims.some((existing) => existing.peer.id === peer.id)) return;
634
+ target.claims.push({ peer, kind });
635
+ };
636
+ for (const peer of peers) {
637
+ const identity = byId.get(peer.id) ?? peer;
638
+ for (const key of peer.holding) claim(key, identity, "held");
639
+ for (const nodeId of peer.transforms.keys()) claim(nodeId, identity, "held");
640
+ for (const key of peer.selection) claim(key, identity, "selected");
641
+ }
642
+ for (const [key, holderId] of locks) {
643
+ entry(key).lock = { holderId, peer: byId.get(holderId) };
644
+ }
645
+ return [...entries.values()];
646
+ }
647
+ function peerOutlinesEqual(a, b) {
648
+ if (a === b) return true;
649
+ if (a.length !== b.length) return false;
650
+ return a.every((left, index) => {
651
+ const right = b[index];
652
+ if (!right || left.key !== right.key) return false;
653
+ if (left.lock?.holderId !== right.lock?.holderId) return false;
654
+ if (left.claims.length !== right.claims.length) return false;
655
+ return left.claims.every((claim, i) => {
656
+ const other = right.claims[i];
657
+ return other?.peer === claim.peer && other?.kind === claim.kind;
658
+ });
659
+ });
660
+ }
661
+ var UNLOCKED = { locked: false };
662
+ function useNodeLock(id) {
663
+ const { store } = useRivetContext();
664
+ const getHolder = useCallback(() => store.locks.getHolder(id), [store, id]);
665
+ const holderId = useSyncExternalStore(store.locks.subscribe, getHolder, getHolder);
666
+ return holderId === null ? UNLOCKED : { locked: true, holderId };
667
+ }
668
+ function useNodeLockAllows(id, intent) {
669
+ const { store } = useRivetContext();
670
+ const allows = useCallback(() => store.locks.allows(id, intent), [store, id, intent]);
671
+ return useSyncExternalStore(store.locks.subscribe, allows, allows);
672
+ }
673
+
674
+ export { DEFAULT_PRESENCE_OPTIONS, LOCK_DEFAULT_REFUSED, PEER_COLORS, anchorTargetKey, boundingRect, buildChildIndex, clampChildToParent, collectDescendants, collectPeerOutlines, createLockRegistry, createPresenceRegistry, descendantIds, edgeTargetKey, hasAncestorIn, isNodeTarget, nodeControlledEqual, nodeDepth, parsePresenceTarget, peerColor, peerOutlinesEqual, presenceTargetEdgeId, presenceTargetKey, presenceTargetNodeId, serializeGraph, snapToGrid, useNodeLock, useNodeLockAllows, usePeerIdentities, worldPosition };
675
+ //# sourceMappingURL=chunk-TT2N7KWU.js.map
676
+ //# sourceMappingURL=chunk-TT2N7KWU.js.map