@unhingged/vizu-core 0.1.1 → 0.1.2
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/dist/auto.cjs +268 -176
- package/dist/auto.cjs.map +1 -1
- package/dist/auto.js +32 -7
- package/dist/auto.js.map +1 -1
- package/dist/index.cjs +268 -176
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +32 -7
- package/dist/index.js.map +1 -1
- package/dist/{live-collab-IRUNFAPE.js → live-collab-BVLNJ5QI.js} +237 -172
- package/dist/live-collab-BVLNJ5QI.js.map +1 -0
- package/dist/vizu.min.js +107 -109
- package/dist/vizu.min.js.map +1 -1
- package/package.json +24 -27
- package/dist/live-collab-IRUNFAPE.js.map +0 -1
|
@@ -2,9 +2,6 @@ import {
|
|
|
2
2
|
findByFingerprint
|
|
3
3
|
} from "./chunk-OMIFOSQ2.js";
|
|
4
4
|
|
|
5
|
-
// src/live-collab.ts
|
|
6
|
-
import { createClient } from "@liveblocks/client";
|
|
7
|
-
|
|
8
5
|
// src/cursor-colors.ts
|
|
9
6
|
var CURSOR_COLORS = [
|
|
10
7
|
"#4F46E5",
|
|
@@ -436,12 +433,19 @@ function createOverlay(color, name) {
|
|
|
436
433
|
}
|
|
437
434
|
|
|
438
435
|
// src/live-collab.ts
|
|
439
|
-
|
|
440
|
-
|
|
436
|
+
function generateConnId() {
|
|
437
|
+
const a = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
438
|
+
const b = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
439
|
+
return `${a}${b}`;
|
|
440
|
+
}
|
|
441
|
+
var PUSH_INTERVAL_MS = 100;
|
|
442
|
+
var PULL_INTERVAL_MS = 200;
|
|
441
443
|
var INACTIVITY_FADE_MS = 5e3;
|
|
442
|
-
var CURSOR_TRANSITION_MS =
|
|
444
|
+
var CURSOR_TRANSITION_MS = 220;
|
|
443
445
|
var Z_INDEX3 = 2147483641;
|
|
444
446
|
var FOLLOW_SCROLL_THRESHOLD_PX = 8;
|
|
447
|
+
var RECONNECT_AFTER_ERRORS = 2;
|
|
448
|
+
var DISCONNECT_AFTER_ERRORS = 5;
|
|
445
449
|
function fnv1a32Hex(input) {
|
|
446
450
|
let h = 2166136261;
|
|
447
451
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -456,12 +460,9 @@ function roomIdFor(workspaceSlug, pageUrl) {
|
|
|
456
460
|
var LiveCollab = class {
|
|
457
461
|
constructor(opts) {
|
|
458
462
|
this.opts = opts;
|
|
459
|
-
this.client = null;
|
|
460
|
-
this.room = null;
|
|
461
|
-
this.leave = null;
|
|
462
463
|
this.container = null;
|
|
463
464
|
this.cursors = /* @__PURE__ */ new Map();
|
|
464
|
-
// keyed by connectionId
|
|
465
|
+
// keyed by connId now (was numeric connectionId)
|
|
465
466
|
this.presenceStack = null;
|
|
466
467
|
this.followPill = null;
|
|
467
468
|
this.selectionOverlay = null;
|
|
@@ -470,50 +471,36 @@ var LiveCollab = class {
|
|
|
470
471
|
this.visibilityListener = null;
|
|
471
472
|
this.keydownListener = null;
|
|
472
473
|
this.statusIndicator = null;
|
|
473
|
-
this.
|
|
474
|
-
this.
|
|
475
|
-
this.pendingCursor = null;
|
|
476
|
-
this.lastBroadcast = 0;
|
|
477
|
-
this.lastScrollBroadcast = 0;
|
|
478
|
-
this.unsubscribers = [];
|
|
474
|
+
this.pushTimer = null;
|
|
475
|
+
this.pullTimer = null;
|
|
479
476
|
this.destroyed = false;
|
|
477
|
+
/** Full room id, built once in start() from workspace + pageUrl. */
|
|
478
|
+
this.roomId = null;
|
|
479
|
+
/** Latest snapshot of other connections from /api/live/[room]/others. */
|
|
480
|
+
this.latestOthers = [];
|
|
481
|
+
/** Local presence — mouse + scroll + selection update this in place; pushTimer ships it. */
|
|
482
|
+
this.currentPresence = { cursor: null, scrollY: 0, selecting: null };
|
|
483
|
+
/** True when something changed since the last push — skip the POST otherwise. */
|
|
484
|
+
this.presenceDirty = false;
|
|
485
|
+
/** Connection-health tracker for the indicator. */
|
|
486
|
+
this.status = "initial";
|
|
487
|
+
this.consecutiveErrors = 0;
|
|
480
488
|
// Follow mode state
|
|
481
489
|
/** Clerk user id we're currently following, or null. */
|
|
482
490
|
this.followingUserId = null;
|
|
483
491
|
/** Last scrollY we received from the followee — for delta gating. */
|
|
484
492
|
this.lastFolloweeScrollY = null;
|
|
493
|
+
this.connId = generateConnId();
|
|
494
|
+
if (typeof window !== "undefined") {
|
|
495
|
+
this.currentPresence.scrollY = window.scrollY;
|
|
496
|
+
}
|
|
485
497
|
}
|
|
486
498
|
async start() {
|
|
487
499
|
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
488
500
|
if (this.destroyed) return;
|
|
489
501
|
try {
|
|
490
|
-
void this.opts.publicKey;
|
|
491
|
-
this.client = createClient({
|
|
492
|
-
authEndpoint: async (room2) => {
|
|
493
|
-
const res = await fetch(this.opts.authEndpoint, {
|
|
494
|
-
method: "POST",
|
|
495
|
-
headers: { "content-type": "application/json" },
|
|
496
|
-
credentials: "include",
|
|
497
|
-
body: JSON.stringify({ room: room2 })
|
|
498
|
-
});
|
|
499
|
-
if (!res.ok) {
|
|
500
|
-
throw new Error(`vizu-liveblocks-auth: HTTP ${res.status}`);
|
|
501
|
-
}
|
|
502
|
-
return res.json();
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
502
|
const pageUrl = this.opts.pageUrl ?? window.location.origin + window.location.pathname;
|
|
506
|
-
|
|
507
|
-
if (!room) return;
|
|
508
|
-
const result = this.client.enterRoom(room, {
|
|
509
|
-
initialPresence: {
|
|
510
|
-
cursor: null,
|
|
511
|
-
scrollY: typeof window !== "undefined" ? window.scrollY : 0,
|
|
512
|
-
selecting: null
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
this.room = result.room;
|
|
516
|
-
this.leave = result.leave;
|
|
503
|
+
this.roomId = roomIdFor(this.opts.workspaceSlug, pageUrl);
|
|
517
504
|
this.mountContainer();
|
|
518
505
|
this.presenceStack = new PresenceStack({
|
|
519
506
|
onAvatarClick: (userId) => this.setFollowing(userId)
|
|
@@ -526,12 +513,16 @@ var LiveCollab = class {
|
|
|
526
513
|
this.selectionOverlay.mount();
|
|
527
514
|
this.statusIndicator = new ConnectionIndicator();
|
|
528
515
|
this.statusIndicator.mount();
|
|
529
|
-
this.
|
|
530
|
-
this.subscribeStatus();
|
|
516
|
+
this.setStatus("connecting");
|
|
531
517
|
this.attachMouseListener();
|
|
532
518
|
this.attachScrollListener();
|
|
533
519
|
this.attachVisibilityListener();
|
|
534
520
|
this.attachKeyboardListener();
|
|
521
|
+
this.pushTimer = window.setInterval(() => void this.push(), PUSH_INTERVAL_MS);
|
|
522
|
+
this.pullTimer = window.setInterval(() => void this.pull(), PULL_INTERVAL_MS);
|
|
523
|
+
this.presenceDirty = true;
|
|
524
|
+
void this.push();
|
|
525
|
+
void this.pull();
|
|
535
526
|
} catch (err) {
|
|
536
527
|
if (typeof console !== "undefined") {
|
|
537
528
|
console.warn("[vizu/live-collab] start failed; live cursors disabled:", err);
|
|
@@ -545,6 +536,7 @@ var LiveCollab = class {
|
|
|
545
536
|
}
|
|
546
537
|
/* ────────────────── private ────────────────── */
|
|
547
538
|
cleanup() {
|
|
539
|
+
void this.clearRemotePresence();
|
|
548
540
|
if (this.mouseListener) {
|
|
549
541
|
window.removeEventListener("mousemove", this.mouseListener);
|
|
550
542
|
this.mouseListener = null;
|
|
@@ -561,16 +553,14 @@ var LiveCollab = class {
|
|
|
561
553
|
document.removeEventListener("keydown", this.keydownListener);
|
|
562
554
|
this.keydownListener = null;
|
|
563
555
|
}
|
|
564
|
-
if (this.
|
|
565
|
-
window.
|
|
566
|
-
this.
|
|
556
|
+
if (this.pushTimer != null) {
|
|
557
|
+
window.clearInterval(this.pushTimer);
|
|
558
|
+
this.pushTimer = null;
|
|
567
559
|
}
|
|
568
|
-
if (this.
|
|
569
|
-
window.
|
|
570
|
-
this.
|
|
560
|
+
if (this.pullTimer != null) {
|
|
561
|
+
window.clearInterval(this.pullTimer);
|
|
562
|
+
this.pullTimer = null;
|
|
571
563
|
}
|
|
572
|
-
for (const unsub of this.unsubscribers) unsub();
|
|
573
|
-
this.unsubscribers = [];
|
|
574
564
|
for (const c of this.cursors.values()) {
|
|
575
565
|
if (c.fadeTimer != null) window.clearTimeout(c.fadeTimer);
|
|
576
566
|
c.el.remove();
|
|
@@ -588,10 +578,8 @@ var LiveCollab = class {
|
|
|
588
578
|
this.lastFolloweeScrollY = null;
|
|
589
579
|
this.container?.remove();
|
|
590
580
|
this.container = null;
|
|
591
|
-
this.
|
|
592
|
-
this.
|
|
593
|
-
this.room = null;
|
|
594
|
-
this.client = null;
|
|
581
|
+
this.roomId = null;
|
|
582
|
+
this.latestOthers = [];
|
|
595
583
|
}
|
|
596
584
|
/**
|
|
597
585
|
* Public API: broadcast the local user's currently-hovered element
|
|
@@ -600,10 +588,8 @@ var LiveCollab = class {
|
|
|
600
588
|
* Idempotent on no-op transitions. Slice 5 of live-collab.md.
|
|
601
589
|
*/
|
|
602
590
|
setLocalSelection(fingerprint) {
|
|
603
|
-
|
|
604
|
-
this.
|
|
605
|
-
selecting: fingerprint ? { fingerprintJson: JSON.stringify(fingerprint) } : null
|
|
606
|
-
});
|
|
591
|
+
this.currentPresence.selecting = fingerprint ? { fingerprintJson: JSON.stringify(fingerprint) } : null;
|
|
592
|
+
this.presenceDirty = true;
|
|
607
593
|
}
|
|
608
594
|
/**
|
|
609
595
|
* Public API: start/stop following a user by Clerk id. Null clears.
|
|
@@ -619,30 +605,24 @@ var LiveCollab = class {
|
|
|
619
605
|
if (userId) this.scrollToFolloweeIfKnown();
|
|
620
606
|
}
|
|
621
607
|
lookupUserForPill(userId) {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
for (const o of others) {
|
|
625
|
-
const meta = o;
|
|
626
|
-
if ((meta.id ?? `c${o.connectionId}`) === userId) {
|
|
608
|
+
for (const o of this.latestOthers) {
|
|
609
|
+
if (o.user.userId === userId) {
|
|
627
610
|
return {
|
|
628
611
|
id: userId,
|
|
629
|
-
name:
|
|
612
|
+
name: o.user.name ?? "Anonymous",
|
|
630
613
|
color: colorForUser(userId),
|
|
631
|
-
avatar:
|
|
614
|
+
avatar: o.user.avatar
|
|
632
615
|
};
|
|
633
616
|
}
|
|
634
617
|
}
|
|
635
618
|
return null;
|
|
636
619
|
}
|
|
637
620
|
scrollToFolloweeIfKnown() {
|
|
638
|
-
if (!this.followingUserId
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const presence = o.presence;
|
|
644
|
-
if (typeof presence.scrollY === "number") {
|
|
645
|
-
this.followScrollTo(presence.scrollY);
|
|
621
|
+
if (!this.followingUserId) return;
|
|
622
|
+
for (const o of this.latestOthers) {
|
|
623
|
+
if (o.user.userId === this.followingUserId) {
|
|
624
|
+
if (typeof o.presence.scrollY === "number") {
|
|
625
|
+
this.followScrollTo(o.presence.scrollY);
|
|
646
626
|
}
|
|
647
627
|
return;
|
|
648
628
|
}
|
|
@@ -668,39 +648,27 @@ var LiveCollab = class {
|
|
|
668
648
|
}
|
|
669
649
|
attachMouseListener() {
|
|
670
650
|
this.mouseListener = (e) => {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
651
|
+
this.currentPresence.cursor = {
|
|
652
|
+
xPct: e.clientX / window.innerWidth * 100,
|
|
653
|
+
yPct: e.clientY / window.innerHeight * 100
|
|
654
|
+
};
|
|
655
|
+
this.presenceDirty = true;
|
|
675
656
|
};
|
|
676
657
|
window.addEventListener("mousemove", this.mouseListener, { passive: true });
|
|
677
658
|
}
|
|
678
659
|
attachScrollListener() {
|
|
679
660
|
this.scrollListener = () => {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (elapsed >= SCROLL_THROTTLE_MS) {
|
|
683
|
-
this.flushScroll();
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
if (this.scrollThrottleTimer == null) {
|
|
687
|
-
this.scrollThrottleTimer = window.setTimeout(() => {
|
|
688
|
-
this.scrollThrottleTimer = null;
|
|
689
|
-
this.flushScroll();
|
|
690
|
-
}, SCROLL_THROTTLE_MS - elapsed);
|
|
691
|
-
}
|
|
661
|
+
this.currentPresence.scrollY = window.scrollY;
|
|
662
|
+
this.presenceDirty = true;
|
|
692
663
|
};
|
|
693
664
|
window.addEventListener("scroll", this.scrollListener, { passive: true });
|
|
694
665
|
}
|
|
695
|
-
flushScroll() {
|
|
696
|
-
if (!this.room) return;
|
|
697
|
-
this.room.updatePresence({ scrollY: window.scrollY });
|
|
698
|
-
this.lastScrollBroadcast = performance.now();
|
|
699
|
-
}
|
|
700
666
|
attachVisibilityListener() {
|
|
701
667
|
this.visibilityListener = () => {
|
|
702
|
-
if (document.hidden
|
|
703
|
-
this.
|
|
668
|
+
if (document.hidden) {
|
|
669
|
+
void this.clearRemotePresence();
|
|
670
|
+
} else {
|
|
671
|
+
this.presenceDirty = true;
|
|
704
672
|
}
|
|
705
673
|
};
|
|
706
674
|
document.addEventListener("visibilitychange", this.visibilityListener);
|
|
@@ -713,90 +681,187 @@ var LiveCollab = class {
|
|
|
713
681
|
};
|
|
714
682
|
document.addEventListener("keydown", this.keydownListener);
|
|
715
683
|
}
|
|
716
|
-
|
|
717
|
-
if (
|
|
718
|
-
this.
|
|
719
|
-
|
|
720
|
-
this.statusIndicator?.setStatus(status);
|
|
721
|
-
});
|
|
722
|
-
this.unsubscribers.push(unsub);
|
|
684
|
+
setStatus(next) {
|
|
685
|
+
if (this.status === next) return;
|
|
686
|
+
this.status = next;
|
|
687
|
+
this.statusIndicator?.setStatus(next);
|
|
723
688
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (this.
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Push tick — POST current presence to /api/live/[room]/me. Skips
|
|
691
|
+
* the request when nothing changed since last push, so an idle tab
|
|
692
|
+
* generates near-zero traffic.
|
|
693
|
+
*/
|
|
694
|
+
async push() {
|
|
695
|
+
if (this.destroyed || !this.roomId) return;
|
|
696
|
+
if (!this.presenceDirty) return;
|
|
697
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
698
|
+
this.presenceDirty = false;
|
|
699
|
+
try {
|
|
700
|
+
const res = await this.fetchLive(`/api/live/${encodeURIComponent(this.roomId)}/me`, {
|
|
701
|
+
method: "POST",
|
|
702
|
+
body: JSON.stringify({
|
|
703
|
+
connId: this.connId,
|
|
704
|
+
presence: this.currentPresence
|
|
705
|
+
})
|
|
706
|
+
});
|
|
707
|
+
if (!res.ok) {
|
|
708
|
+
if (res.status === 402 || res.status === 401 || res.status === 503) {
|
|
709
|
+
this.setStatus("disconnected");
|
|
710
|
+
if (this.pushTimer != null) {
|
|
711
|
+
window.clearInterval(this.pushTimer);
|
|
712
|
+
this.pushTimer = null;
|
|
713
|
+
}
|
|
714
|
+
if (this.pullTimer != null) {
|
|
715
|
+
window.clearInterval(this.pullTimer);
|
|
716
|
+
this.pullTimer = null;
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
throw new Error(`push HTTP ${res.status}`);
|
|
721
|
+
}
|
|
722
|
+
this.noteSuccess();
|
|
723
|
+
} catch (err) {
|
|
724
|
+
this.noteError(err);
|
|
725
|
+
this.presenceDirty = true;
|
|
736
726
|
}
|
|
737
727
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const name = meta.info?.name ?? "Anonymous";
|
|
757
|
-
const color = colorForUser(userId);
|
|
758
|
-
if (!stackUsers.has(userId)) {
|
|
759
|
-
stackUsers.set(userId, {
|
|
760
|
-
id: userId,
|
|
761
|
-
name,
|
|
762
|
-
color,
|
|
763
|
-
avatar: meta.info?.avatar
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
const presence = other.presence;
|
|
767
|
-
if (this.followingUserId === userId && followeeScrollY === null) {
|
|
768
|
-
followeeStillPresent = true;
|
|
769
|
-
if (typeof presence.scrollY === "number") {
|
|
770
|
-
followeeScrollY = presence.scrollY;
|
|
728
|
+
/**
|
|
729
|
+
* Pull tick — GET /api/live/[room]/others and drive cursor + stack +
|
|
730
|
+
* selection + follow updates. Same handler shape as the Liveblocks-era
|
|
731
|
+
* subscribeOthers callback; only the data source changed.
|
|
732
|
+
*/
|
|
733
|
+
async pull() {
|
|
734
|
+
if (this.destroyed || !this.roomId) return;
|
|
735
|
+
try {
|
|
736
|
+
const res = await this.fetchLive(
|
|
737
|
+
`/api/live/${encodeURIComponent(this.roomId)}/others?me=${encodeURIComponent(this.connId)}`,
|
|
738
|
+
{ method: "GET" }
|
|
739
|
+
);
|
|
740
|
+
if (!res.ok) {
|
|
741
|
+
if (res.status === 503 || res.status === 401) {
|
|
742
|
+
this.setStatus("disconnected");
|
|
743
|
+
if (this.pullTimer != null) {
|
|
744
|
+
window.clearInterval(this.pullTimer);
|
|
745
|
+
this.pullTimer = null;
|
|
771
746
|
}
|
|
747
|
+
return;
|
|
772
748
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
749
|
+
throw new Error(`pull HTTP ${res.status}`);
|
|
750
|
+
}
|
|
751
|
+
const body = await res.json();
|
|
752
|
+
const others = (body.others ?? []).map((o) => ({
|
|
753
|
+
connId: o.connId,
|
|
754
|
+
presence: o.payload.presence,
|
|
755
|
+
user: o.payload.user
|
|
756
|
+
}));
|
|
757
|
+
this.latestOthers = others;
|
|
758
|
+
this.renderOthers(others);
|
|
759
|
+
this.noteSuccess();
|
|
760
|
+
} catch (err) {
|
|
761
|
+
this.noteError(err);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
noteSuccess() {
|
|
765
|
+
this.consecutiveErrors = 0;
|
|
766
|
+
if (this.status !== "connected") this.setStatus("connected");
|
|
767
|
+
}
|
|
768
|
+
noteError(err) {
|
|
769
|
+
this.consecutiveErrors++;
|
|
770
|
+
if (this.consecutiveErrors >= DISCONNECT_AFTER_ERRORS) {
|
|
771
|
+
this.setStatus("disconnected");
|
|
772
|
+
} else if (this.consecutiveErrors >= RECONNECT_AFTER_ERRORS) {
|
|
773
|
+
this.setStatus("reconnecting");
|
|
774
|
+
}
|
|
775
|
+
if (typeof console !== "undefined" && this.consecutiveErrors === 1) {
|
|
776
|
+
console.warn("[vizu/live-collab] transport error", err);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Drive the cursor / stack / selection / follow layers from a fresh
|
|
781
|
+
* `others` snapshot. Mirrors the body of the Liveblocks-era
|
|
782
|
+
* subscribeOthers callback so DOM behavior is unchanged.
|
|
783
|
+
*/
|
|
784
|
+
renderOthers(others) {
|
|
785
|
+
const seen = /* @__PURE__ */ new Set();
|
|
786
|
+
const stackUsers = /* @__PURE__ */ new Map();
|
|
787
|
+
const selections = [];
|
|
788
|
+
let followeeScrollY = null;
|
|
789
|
+
let followeeStillPresent = false;
|
|
790
|
+
for (const other of others) {
|
|
791
|
+
seen.add(other.connId);
|
|
792
|
+
const userId = other.user.userId;
|
|
793
|
+
const name = other.user.name ?? "Anonymous";
|
|
794
|
+
const color = colorForUser(userId);
|
|
795
|
+
if (!stackUsers.has(userId)) {
|
|
796
|
+
stackUsers.set(userId, { id: userId, name, color, avatar: other.user.avatar });
|
|
797
|
+
}
|
|
798
|
+
if (this.followingUserId === userId && followeeScrollY === null) {
|
|
799
|
+
followeeStillPresent = true;
|
|
800
|
+
if (typeof other.presence.scrollY === "number") {
|
|
801
|
+
followeeScrollY = other.presence.scrollY;
|
|
779
802
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
803
|
+
}
|
|
804
|
+
if (other.presence.selecting && !selections.find((s) => s.id === userId)) {
|
|
805
|
+
try {
|
|
806
|
+
const fp = JSON.parse(other.presence.selecting.fingerprintJson);
|
|
807
|
+
selections.push({ id: userId, name, color, fingerprint: fp });
|
|
808
|
+
} catch {
|
|
783
809
|
}
|
|
784
|
-
this.upsertCursor(other.connectionId, userId, name, presence.cursor);
|
|
785
810
|
}
|
|
786
|
-
|
|
787
|
-
|
|
811
|
+
if (!other.presence.cursor) {
|
|
812
|
+
this.removeCursor(other.connId);
|
|
813
|
+
continue;
|
|
788
814
|
}
|
|
789
|
-
this.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
815
|
+
this.upsertCursor(other.connId, userId, name, other.presence.cursor);
|
|
816
|
+
}
|
|
817
|
+
for (const id of Array.from(this.cursors.keys())) {
|
|
818
|
+
if (!seen.has(id)) this.removeCursor(id);
|
|
819
|
+
}
|
|
820
|
+
this.presenceStack?.setOthers(Array.from(stackUsers.values()));
|
|
821
|
+
this.selectionOverlay?.setSelections(selections);
|
|
822
|
+
if (this.followingUserId) {
|
|
823
|
+
if (!followeeStillPresent) {
|
|
824
|
+
this.setFollowing(null);
|
|
825
|
+
} else if (followeeScrollY !== null) {
|
|
826
|
+
this.followScrollTo(followeeScrollY);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Async wrapper around fetch that injects the optional auth header
|
|
832
|
+
* resolved from opts.getAuthHeader. Same-origin requests rely on
|
|
833
|
+
* cookies via `credentials: 'include'`; cross-origin gets the
|
|
834
|
+
* bearer token returned by the host.
|
|
835
|
+
*/
|
|
836
|
+
async fetchLive(path, init) {
|
|
837
|
+
const headers = new Headers(init.headers);
|
|
838
|
+
headers.set("content-type", "application/json");
|
|
839
|
+
if (this.opts.getAuthHeader) {
|
|
840
|
+
const extra = await this.opts.getAuthHeader();
|
|
841
|
+
if (extra) {
|
|
842
|
+
new Headers(extra).forEach((v, k) => headers.set(k, v));
|
|
797
843
|
}
|
|
844
|
+
}
|
|
845
|
+
return fetch(this.opts.apiUrl + path, {
|
|
846
|
+
...init,
|
|
847
|
+
headers,
|
|
848
|
+
credentials: "include"
|
|
798
849
|
});
|
|
799
|
-
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Fire-and-forget clearOnly POST so other clients see us drop within
|
|
853
|
+
* one pull cycle (~200ms) instead of waiting for the 5s presence TTL.
|
|
854
|
+
* Errors swallowed — the TTL is the safety net.
|
|
855
|
+
*/
|
|
856
|
+
async clearRemotePresence() {
|
|
857
|
+
if (!this.roomId) return;
|
|
858
|
+
try {
|
|
859
|
+
await this.fetchLive(`/api/live/${encodeURIComponent(this.roomId)}/me`, {
|
|
860
|
+
method: "POST",
|
|
861
|
+
body: JSON.stringify({ connId: this.connId, clearOnly: true })
|
|
862
|
+
});
|
|
863
|
+
} catch {
|
|
864
|
+
}
|
|
800
865
|
}
|
|
801
866
|
upsertCursor(connectionId, userId, name, cursor) {
|
|
802
867
|
if (!this.container) return;
|
|
@@ -1030,4 +1095,4 @@ export {
|
|
|
1030
1095
|
LiveCollab,
|
|
1031
1096
|
roomIdFor
|
|
1032
1097
|
};
|
|
1033
|
-
//# sourceMappingURL=live-collab-
|
|
1098
|
+
//# sourceMappingURL=live-collab-BVLNJ5QI.js.map
|