@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
package/dist/index.cjs
CHANGED
|
@@ -770,6 +770,11 @@ __export(live_collab_exports, {
|
|
|
770
770
|
LiveCollab: () => LiveCollab,
|
|
771
771
|
roomIdFor: () => roomIdFor
|
|
772
772
|
});
|
|
773
|
+
function generateConnId() {
|
|
774
|
+
const a = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
775
|
+
const b = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
776
|
+
return `${a}${b}`;
|
|
777
|
+
}
|
|
773
778
|
function fnv1a32Hex(input) {
|
|
774
779
|
let h = 2166136261;
|
|
775
780
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -839,29 +844,27 @@ function labelForStatus(status) {
|
|
|
839
844
|
return "";
|
|
840
845
|
}
|
|
841
846
|
}
|
|
842
|
-
var
|
|
847
|
+
var PUSH_INTERVAL_MS, PULL_INTERVAL_MS, INACTIVITY_FADE_MS, CURSOR_TRANSITION_MS, Z_INDEX3, FOLLOW_SCROLL_THRESHOLD_PX, RECONNECT_AFTER_ERRORS, DISCONNECT_AFTER_ERRORS, LiveCollab, ConnectionIndicator, FollowPill;
|
|
843
848
|
var init_live_collab = __esm({
|
|
844
849
|
"src/live-collab.ts"() {
|
|
845
850
|
"use strict";
|
|
846
|
-
import_client = require("@liveblocks/client");
|
|
847
851
|
init_presence_stack();
|
|
848
852
|
init_selection_overlay();
|
|
849
853
|
init_cursor_colors();
|
|
850
|
-
|
|
851
|
-
|
|
854
|
+
PUSH_INTERVAL_MS = 100;
|
|
855
|
+
PULL_INTERVAL_MS = 200;
|
|
852
856
|
INACTIVITY_FADE_MS = 5e3;
|
|
853
|
-
CURSOR_TRANSITION_MS =
|
|
857
|
+
CURSOR_TRANSITION_MS = 220;
|
|
854
858
|
Z_INDEX3 = 2147483641;
|
|
855
859
|
FOLLOW_SCROLL_THRESHOLD_PX = 8;
|
|
860
|
+
RECONNECT_AFTER_ERRORS = 2;
|
|
861
|
+
DISCONNECT_AFTER_ERRORS = 5;
|
|
856
862
|
LiveCollab = class {
|
|
857
863
|
constructor(opts) {
|
|
858
864
|
this.opts = opts;
|
|
859
|
-
this.client = null;
|
|
860
|
-
this.room = null;
|
|
861
|
-
this.leave = null;
|
|
862
865
|
this.container = null;
|
|
863
866
|
this.cursors = /* @__PURE__ */ new Map();
|
|
864
|
-
// keyed by connectionId
|
|
867
|
+
// keyed by connId now (was numeric connectionId)
|
|
865
868
|
this.presenceStack = null;
|
|
866
869
|
this.followPill = null;
|
|
867
870
|
this.selectionOverlay = null;
|
|
@@ -870,50 +873,36 @@ var init_live_collab = __esm({
|
|
|
870
873
|
this.visibilityListener = null;
|
|
871
874
|
this.keydownListener = null;
|
|
872
875
|
this.statusIndicator = null;
|
|
873
|
-
this.
|
|
874
|
-
this.
|
|
875
|
-
this.pendingCursor = null;
|
|
876
|
-
this.lastBroadcast = 0;
|
|
877
|
-
this.lastScrollBroadcast = 0;
|
|
878
|
-
this.unsubscribers = [];
|
|
876
|
+
this.pushTimer = null;
|
|
877
|
+
this.pullTimer = null;
|
|
879
878
|
this.destroyed = false;
|
|
879
|
+
/** Full room id, built once in start() from workspace + pageUrl. */
|
|
880
|
+
this.roomId = null;
|
|
881
|
+
/** Latest snapshot of other connections from /api/live/[room]/others. */
|
|
882
|
+
this.latestOthers = [];
|
|
883
|
+
/** Local presence — mouse + scroll + selection update this in place; pushTimer ships it. */
|
|
884
|
+
this.currentPresence = { cursor: null, scrollY: 0, selecting: null };
|
|
885
|
+
/** True when something changed since the last push — skip the POST otherwise. */
|
|
886
|
+
this.presenceDirty = false;
|
|
887
|
+
/** Connection-health tracker for the indicator. */
|
|
888
|
+
this.status = "initial";
|
|
889
|
+
this.consecutiveErrors = 0;
|
|
880
890
|
// Follow mode state
|
|
881
891
|
/** Clerk user id we're currently following, or null. */
|
|
882
892
|
this.followingUserId = null;
|
|
883
893
|
/** Last scrollY we received from the followee — for delta gating. */
|
|
884
894
|
this.lastFolloweeScrollY = null;
|
|
895
|
+
this.connId = generateConnId();
|
|
896
|
+
if (typeof window !== "undefined") {
|
|
897
|
+
this.currentPresence.scrollY = window.scrollY;
|
|
898
|
+
}
|
|
885
899
|
}
|
|
886
900
|
async start() {
|
|
887
901
|
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
888
902
|
if (this.destroyed) return;
|
|
889
903
|
try {
|
|
890
|
-
void this.opts.publicKey;
|
|
891
|
-
this.client = (0, import_client.createClient)({
|
|
892
|
-
authEndpoint: async (room2) => {
|
|
893
|
-
const res = await fetch(this.opts.authEndpoint, {
|
|
894
|
-
method: "POST",
|
|
895
|
-
headers: { "content-type": "application/json" },
|
|
896
|
-
credentials: "include",
|
|
897
|
-
body: JSON.stringify({ room: room2 })
|
|
898
|
-
});
|
|
899
|
-
if (!res.ok) {
|
|
900
|
-
throw new Error(`vizu-liveblocks-auth: HTTP ${res.status}`);
|
|
901
|
-
}
|
|
902
|
-
return res.json();
|
|
903
|
-
}
|
|
904
|
-
});
|
|
905
904
|
const pageUrl = this.opts.pageUrl ?? window.location.origin + window.location.pathname;
|
|
906
|
-
|
|
907
|
-
if (!room) return;
|
|
908
|
-
const result = this.client.enterRoom(room, {
|
|
909
|
-
initialPresence: {
|
|
910
|
-
cursor: null,
|
|
911
|
-
scrollY: typeof window !== "undefined" ? window.scrollY : 0,
|
|
912
|
-
selecting: null
|
|
913
|
-
}
|
|
914
|
-
});
|
|
915
|
-
this.room = result.room;
|
|
916
|
-
this.leave = result.leave;
|
|
905
|
+
this.roomId = roomIdFor(this.opts.workspaceSlug, pageUrl);
|
|
917
906
|
this.mountContainer();
|
|
918
907
|
this.presenceStack = new PresenceStack({
|
|
919
908
|
onAvatarClick: (userId) => this.setFollowing(userId)
|
|
@@ -926,12 +915,16 @@ var init_live_collab = __esm({
|
|
|
926
915
|
this.selectionOverlay.mount();
|
|
927
916
|
this.statusIndicator = new ConnectionIndicator();
|
|
928
917
|
this.statusIndicator.mount();
|
|
929
|
-
this.
|
|
930
|
-
this.subscribeStatus();
|
|
918
|
+
this.setStatus("connecting");
|
|
931
919
|
this.attachMouseListener();
|
|
932
920
|
this.attachScrollListener();
|
|
933
921
|
this.attachVisibilityListener();
|
|
934
922
|
this.attachKeyboardListener();
|
|
923
|
+
this.pushTimer = window.setInterval(() => void this.push(), PUSH_INTERVAL_MS);
|
|
924
|
+
this.pullTimer = window.setInterval(() => void this.pull(), PULL_INTERVAL_MS);
|
|
925
|
+
this.presenceDirty = true;
|
|
926
|
+
void this.push();
|
|
927
|
+
void this.pull();
|
|
935
928
|
} catch (err) {
|
|
936
929
|
if (typeof console !== "undefined") {
|
|
937
930
|
console.warn("[vizu/live-collab] start failed; live cursors disabled:", err);
|
|
@@ -945,6 +938,7 @@ var init_live_collab = __esm({
|
|
|
945
938
|
}
|
|
946
939
|
/* ────────────────── private ────────────────── */
|
|
947
940
|
cleanup() {
|
|
941
|
+
void this.clearRemotePresence();
|
|
948
942
|
if (this.mouseListener) {
|
|
949
943
|
window.removeEventListener("mousemove", this.mouseListener);
|
|
950
944
|
this.mouseListener = null;
|
|
@@ -961,16 +955,14 @@ var init_live_collab = __esm({
|
|
|
961
955
|
document.removeEventListener("keydown", this.keydownListener);
|
|
962
956
|
this.keydownListener = null;
|
|
963
957
|
}
|
|
964
|
-
if (this.
|
|
965
|
-
window.
|
|
966
|
-
this.
|
|
958
|
+
if (this.pushTimer != null) {
|
|
959
|
+
window.clearInterval(this.pushTimer);
|
|
960
|
+
this.pushTimer = null;
|
|
967
961
|
}
|
|
968
|
-
if (this.
|
|
969
|
-
window.
|
|
970
|
-
this.
|
|
962
|
+
if (this.pullTimer != null) {
|
|
963
|
+
window.clearInterval(this.pullTimer);
|
|
964
|
+
this.pullTimer = null;
|
|
971
965
|
}
|
|
972
|
-
for (const unsub of this.unsubscribers) unsub();
|
|
973
|
-
this.unsubscribers = [];
|
|
974
966
|
for (const c of this.cursors.values()) {
|
|
975
967
|
if (c.fadeTimer != null) window.clearTimeout(c.fadeTimer);
|
|
976
968
|
c.el.remove();
|
|
@@ -988,10 +980,8 @@ var init_live_collab = __esm({
|
|
|
988
980
|
this.lastFolloweeScrollY = null;
|
|
989
981
|
this.container?.remove();
|
|
990
982
|
this.container = null;
|
|
991
|
-
this.
|
|
992
|
-
this.
|
|
993
|
-
this.room = null;
|
|
994
|
-
this.client = null;
|
|
983
|
+
this.roomId = null;
|
|
984
|
+
this.latestOthers = [];
|
|
995
985
|
}
|
|
996
986
|
/**
|
|
997
987
|
* Public API: broadcast the local user's currently-hovered element
|
|
@@ -1000,10 +990,8 @@ var init_live_collab = __esm({
|
|
|
1000
990
|
* Idempotent on no-op transitions. Slice 5 of live-collab.md.
|
|
1001
991
|
*/
|
|
1002
992
|
setLocalSelection(fingerprint2) {
|
|
1003
|
-
|
|
1004
|
-
this.
|
|
1005
|
-
selecting: fingerprint2 ? { fingerprintJson: JSON.stringify(fingerprint2) } : null
|
|
1006
|
-
});
|
|
993
|
+
this.currentPresence.selecting = fingerprint2 ? { fingerprintJson: JSON.stringify(fingerprint2) } : null;
|
|
994
|
+
this.presenceDirty = true;
|
|
1007
995
|
}
|
|
1008
996
|
/**
|
|
1009
997
|
* Public API: start/stop following a user by Clerk id. Null clears.
|
|
@@ -1019,30 +1007,24 @@ var init_live_collab = __esm({
|
|
|
1019
1007
|
if (userId) this.scrollToFolloweeIfKnown();
|
|
1020
1008
|
}
|
|
1021
1009
|
lookupUserForPill(userId) {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
for (const o of others) {
|
|
1025
|
-
const meta = o;
|
|
1026
|
-
if ((meta.id ?? `c${o.connectionId}`) === userId) {
|
|
1010
|
+
for (const o of this.latestOthers) {
|
|
1011
|
+
if (o.user.userId === userId) {
|
|
1027
1012
|
return {
|
|
1028
1013
|
id: userId,
|
|
1029
|
-
name:
|
|
1014
|
+
name: o.user.name ?? "Anonymous",
|
|
1030
1015
|
color: colorForUser(userId),
|
|
1031
|
-
avatar:
|
|
1016
|
+
avatar: o.user.avatar
|
|
1032
1017
|
};
|
|
1033
1018
|
}
|
|
1034
1019
|
}
|
|
1035
1020
|
return null;
|
|
1036
1021
|
}
|
|
1037
1022
|
scrollToFolloweeIfKnown() {
|
|
1038
|
-
if (!this.followingUserId
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const presence = o.presence;
|
|
1044
|
-
if (typeof presence.scrollY === "number") {
|
|
1045
|
-
this.followScrollTo(presence.scrollY);
|
|
1023
|
+
if (!this.followingUserId) return;
|
|
1024
|
+
for (const o of this.latestOthers) {
|
|
1025
|
+
if (o.user.userId === this.followingUserId) {
|
|
1026
|
+
if (typeof o.presence.scrollY === "number") {
|
|
1027
|
+
this.followScrollTo(o.presence.scrollY);
|
|
1046
1028
|
}
|
|
1047
1029
|
return;
|
|
1048
1030
|
}
|
|
@@ -1068,39 +1050,27 @@ var init_live_collab = __esm({
|
|
|
1068
1050
|
}
|
|
1069
1051
|
attachMouseListener() {
|
|
1070
1052
|
this.mouseListener = (e) => {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1053
|
+
this.currentPresence.cursor = {
|
|
1054
|
+
xPct: e.clientX / window.innerWidth * 100,
|
|
1055
|
+
yPct: e.clientY / window.innerHeight * 100
|
|
1056
|
+
};
|
|
1057
|
+
this.presenceDirty = true;
|
|
1075
1058
|
};
|
|
1076
1059
|
window.addEventListener("mousemove", this.mouseListener, { passive: true });
|
|
1077
1060
|
}
|
|
1078
1061
|
attachScrollListener() {
|
|
1079
1062
|
this.scrollListener = () => {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
if (elapsed >= SCROLL_THROTTLE_MS) {
|
|
1083
|
-
this.flushScroll();
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
if (this.scrollThrottleTimer == null) {
|
|
1087
|
-
this.scrollThrottleTimer = window.setTimeout(() => {
|
|
1088
|
-
this.scrollThrottleTimer = null;
|
|
1089
|
-
this.flushScroll();
|
|
1090
|
-
}, SCROLL_THROTTLE_MS - elapsed);
|
|
1091
|
-
}
|
|
1063
|
+
this.currentPresence.scrollY = window.scrollY;
|
|
1064
|
+
this.presenceDirty = true;
|
|
1092
1065
|
};
|
|
1093
1066
|
window.addEventListener("scroll", this.scrollListener, { passive: true });
|
|
1094
1067
|
}
|
|
1095
|
-
flushScroll() {
|
|
1096
|
-
if (!this.room) return;
|
|
1097
|
-
this.room.updatePresence({ scrollY: window.scrollY });
|
|
1098
|
-
this.lastScrollBroadcast = performance.now();
|
|
1099
|
-
}
|
|
1100
1068
|
attachVisibilityListener() {
|
|
1101
1069
|
this.visibilityListener = () => {
|
|
1102
|
-
if (document.hidden
|
|
1103
|
-
this.
|
|
1070
|
+
if (document.hidden) {
|
|
1071
|
+
void this.clearRemotePresence();
|
|
1072
|
+
} else {
|
|
1073
|
+
this.presenceDirty = true;
|
|
1104
1074
|
}
|
|
1105
1075
|
};
|
|
1106
1076
|
document.addEventListener("visibilitychange", this.visibilityListener);
|
|
@@ -1113,90 +1083,187 @@ var init_live_collab = __esm({
|
|
|
1113
1083
|
};
|
|
1114
1084
|
document.addEventListener("keydown", this.keydownListener);
|
|
1115
1085
|
}
|
|
1116
|
-
|
|
1117
|
-
if (
|
|
1118
|
-
this.
|
|
1119
|
-
|
|
1120
|
-
this.statusIndicator?.setStatus(status);
|
|
1121
|
-
});
|
|
1122
|
-
this.unsubscribers.push(unsub);
|
|
1086
|
+
setStatus(next) {
|
|
1087
|
+
if (this.status === next) return;
|
|
1088
|
+
this.status = next;
|
|
1089
|
+
this.statusIndicator?.setStatus(next);
|
|
1123
1090
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
if (this.
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1091
|
+
/**
|
|
1092
|
+
* Push tick — POST current presence to /api/live/[room]/me. Skips
|
|
1093
|
+
* the request when nothing changed since last push, so an idle tab
|
|
1094
|
+
* generates near-zero traffic.
|
|
1095
|
+
*/
|
|
1096
|
+
async push() {
|
|
1097
|
+
if (this.destroyed || !this.roomId) return;
|
|
1098
|
+
if (!this.presenceDirty) return;
|
|
1099
|
+
if (typeof document !== "undefined" && document.hidden) return;
|
|
1100
|
+
this.presenceDirty = false;
|
|
1101
|
+
try {
|
|
1102
|
+
const res = await this.fetchLive(`/api/live/${encodeURIComponent(this.roomId)}/me`, {
|
|
1103
|
+
method: "POST",
|
|
1104
|
+
body: JSON.stringify({
|
|
1105
|
+
connId: this.connId,
|
|
1106
|
+
presence: this.currentPresence
|
|
1107
|
+
})
|
|
1108
|
+
});
|
|
1109
|
+
if (!res.ok) {
|
|
1110
|
+
if (res.status === 402 || res.status === 401 || res.status === 503) {
|
|
1111
|
+
this.setStatus("disconnected");
|
|
1112
|
+
if (this.pushTimer != null) {
|
|
1113
|
+
window.clearInterval(this.pushTimer);
|
|
1114
|
+
this.pushTimer = null;
|
|
1115
|
+
}
|
|
1116
|
+
if (this.pullTimer != null) {
|
|
1117
|
+
window.clearInterval(this.pullTimer);
|
|
1118
|
+
this.pullTimer = null;
|
|
1119
|
+
}
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
throw new Error(`push HTTP ${res.status}`);
|
|
1123
|
+
}
|
|
1124
|
+
this.noteSuccess();
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
this.noteError(err);
|
|
1127
|
+
this.presenceDirty = true;
|
|
1136
1128
|
}
|
|
1137
1129
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const name = meta.info?.name ?? "Anonymous";
|
|
1157
|
-
const color = colorForUser(userId);
|
|
1158
|
-
if (!stackUsers.has(userId)) {
|
|
1159
|
-
stackUsers.set(userId, {
|
|
1160
|
-
id: userId,
|
|
1161
|
-
name,
|
|
1162
|
-
color,
|
|
1163
|
-
avatar: meta.info?.avatar
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
const presence = other.presence;
|
|
1167
|
-
if (this.followingUserId === userId && followeeScrollY === null) {
|
|
1168
|
-
followeeStillPresent = true;
|
|
1169
|
-
if (typeof presence.scrollY === "number") {
|
|
1170
|
-
followeeScrollY = presence.scrollY;
|
|
1130
|
+
/**
|
|
1131
|
+
* Pull tick — GET /api/live/[room]/others and drive cursor + stack +
|
|
1132
|
+
* selection + follow updates. Same handler shape as the Liveblocks-era
|
|
1133
|
+
* subscribeOthers callback; only the data source changed.
|
|
1134
|
+
*/
|
|
1135
|
+
async pull() {
|
|
1136
|
+
if (this.destroyed || !this.roomId) return;
|
|
1137
|
+
try {
|
|
1138
|
+
const res = await this.fetchLive(
|
|
1139
|
+
`/api/live/${encodeURIComponent(this.roomId)}/others?me=${encodeURIComponent(this.connId)}`,
|
|
1140
|
+
{ method: "GET" }
|
|
1141
|
+
);
|
|
1142
|
+
if (!res.ok) {
|
|
1143
|
+
if (res.status === 503 || res.status === 401) {
|
|
1144
|
+
this.setStatus("disconnected");
|
|
1145
|
+
if (this.pullTimer != null) {
|
|
1146
|
+
window.clearInterval(this.pullTimer);
|
|
1147
|
+
this.pullTimer = null;
|
|
1171
1148
|
}
|
|
1149
|
+
return;
|
|
1172
1150
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1151
|
+
throw new Error(`pull HTTP ${res.status}`);
|
|
1152
|
+
}
|
|
1153
|
+
const body = await res.json();
|
|
1154
|
+
const others = (body.others ?? []).map((o) => ({
|
|
1155
|
+
connId: o.connId,
|
|
1156
|
+
presence: o.payload.presence,
|
|
1157
|
+
user: o.payload.user
|
|
1158
|
+
}));
|
|
1159
|
+
this.latestOthers = others;
|
|
1160
|
+
this.renderOthers(others);
|
|
1161
|
+
this.noteSuccess();
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
this.noteError(err);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
noteSuccess() {
|
|
1167
|
+
this.consecutiveErrors = 0;
|
|
1168
|
+
if (this.status !== "connected") this.setStatus("connected");
|
|
1169
|
+
}
|
|
1170
|
+
noteError(err) {
|
|
1171
|
+
this.consecutiveErrors++;
|
|
1172
|
+
if (this.consecutiveErrors >= DISCONNECT_AFTER_ERRORS) {
|
|
1173
|
+
this.setStatus("disconnected");
|
|
1174
|
+
} else if (this.consecutiveErrors >= RECONNECT_AFTER_ERRORS) {
|
|
1175
|
+
this.setStatus("reconnecting");
|
|
1176
|
+
}
|
|
1177
|
+
if (typeof console !== "undefined" && this.consecutiveErrors === 1) {
|
|
1178
|
+
console.warn("[vizu/live-collab] transport error", err);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Drive the cursor / stack / selection / follow layers from a fresh
|
|
1183
|
+
* `others` snapshot. Mirrors the body of the Liveblocks-era
|
|
1184
|
+
* subscribeOthers callback so DOM behavior is unchanged.
|
|
1185
|
+
*/
|
|
1186
|
+
renderOthers(others) {
|
|
1187
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1188
|
+
const stackUsers = /* @__PURE__ */ new Map();
|
|
1189
|
+
const selections = [];
|
|
1190
|
+
let followeeScrollY = null;
|
|
1191
|
+
let followeeStillPresent = false;
|
|
1192
|
+
for (const other of others) {
|
|
1193
|
+
seen.add(other.connId);
|
|
1194
|
+
const userId = other.user.userId;
|
|
1195
|
+
const name = other.user.name ?? "Anonymous";
|
|
1196
|
+
const color = colorForUser(userId);
|
|
1197
|
+
if (!stackUsers.has(userId)) {
|
|
1198
|
+
stackUsers.set(userId, { id: userId, name, color, avatar: other.user.avatar });
|
|
1199
|
+
}
|
|
1200
|
+
if (this.followingUserId === userId && followeeScrollY === null) {
|
|
1201
|
+
followeeStillPresent = true;
|
|
1202
|
+
if (typeof other.presence.scrollY === "number") {
|
|
1203
|
+
followeeScrollY = other.presence.scrollY;
|
|
1179
1204
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1205
|
+
}
|
|
1206
|
+
if (other.presence.selecting && !selections.find((s) => s.id === userId)) {
|
|
1207
|
+
try {
|
|
1208
|
+
const fp = JSON.parse(other.presence.selecting.fingerprintJson);
|
|
1209
|
+
selections.push({ id: userId, name, color, fingerprint: fp });
|
|
1210
|
+
} catch {
|
|
1183
1211
|
}
|
|
1184
|
-
this.upsertCursor(other.connectionId, userId, name, presence.cursor);
|
|
1185
1212
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1213
|
+
if (!other.presence.cursor) {
|
|
1214
|
+
this.removeCursor(other.connId);
|
|
1215
|
+
continue;
|
|
1188
1216
|
}
|
|
1189
|
-
this.
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1217
|
+
this.upsertCursor(other.connId, userId, name, other.presence.cursor);
|
|
1218
|
+
}
|
|
1219
|
+
for (const id of Array.from(this.cursors.keys())) {
|
|
1220
|
+
if (!seen.has(id)) this.removeCursor(id);
|
|
1221
|
+
}
|
|
1222
|
+
this.presenceStack?.setOthers(Array.from(stackUsers.values()));
|
|
1223
|
+
this.selectionOverlay?.setSelections(selections);
|
|
1224
|
+
if (this.followingUserId) {
|
|
1225
|
+
if (!followeeStillPresent) {
|
|
1226
|
+
this.setFollowing(null);
|
|
1227
|
+
} else if (followeeScrollY !== null) {
|
|
1228
|
+
this.followScrollTo(followeeScrollY);
|
|
1197
1229
|
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Async wrapper around fetch that injects the optional auth header
|
|
1234
|
+
* resolved from opts.getAuthHeader. Same-origin requests rely on
|
|
1235
|
+
* cookies via `credentials: 'include'`; cross-origin gets the
|
|
1236
|
+
* bearer token returned by the host.
|
|
1237
|
+
*/
|
|
1238
|
+
async fetchLive(path, init) {
|
|
1239
|
+
const headers = new Headers(init.headers);
|
|
1240
|
+
headers.set("content-type", "application/json");
|
|
1241
|
+
if (this.opts.getAuthHeader) {
|
|
1242
|
+
const extra = await this.opts.getAuthHeader();
|
|
1243
|
+
if (extra) {
|
|
1244
|
+
new Headers(extra).forEach((v, k) => headers.set(k, v));
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return fetch(this.opts.apiUrl + path, {
|
|
1248
|
+
...init,
|
|
1249
|
+
headers,
|
|
1250
|
+
credentials: "include"
|
|
1198
1251
|
});
|
|
1199
|
-
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Fire-and-forget clearOnly POST so other clients see us drop within
|
|
1255
|
+
* one pull cycle (~200ms) instead of waiting for the 5s presence TTL.
|
|
1256
|
+
* Errors swallowed — the TTL is the safety net.
|
|
1257
|
+
*/
|
|
1258
|
+
async clearRemotePresence() {
|
|
1259
|
+
if (!this.roomId) return;
|
|
1260
|
+
try {
|
|
1261
|
+
await this.fetchLive(`/api/live/${encodeURIComponent(this.roomId)}/me`, {
|
|
1262
|
+
method: "POST",
|
|
1263
|
+
body: JSON.stringify({ connId: this.connId, clearOnly: true })
|
|
1264
|
+
});
|
|
1265
|
+
} catch {
|
|
1266
|
+
}
|
|
1200
1267
|
}
|
|
1201
1268
|
upsertCursor(connectionId, userId, name, cursor) {
|
|
1202
1269
|
if (!this.container) return;
|
|
@@ -1747,6 +1814,18 @@ var CloudStorageAdapter = class {
|
|
|
1747
1814
|
async preflightAuth() {
|
|
1748
1815
|
await this.resolveToken();
|
|
1749
1816
|
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Synchronous accessor for the currently cached bearer token, used
|
|
1819
|
+
* by sibling modules that need to authenticate against the same
|
|
1820
|
+
* backend (live-collab cross-origin polling). Returns null when no
|
|
1821
|
+
* valid token is cached — callers fall back to credentials:'include'
|
|
1822
|
+
* for same-origin or just disable themselves.
|
|
1823
|
+
*/
|
|
1824
|
+
getCachedAuthToken() {
|
|
1825
|
+
if (!this.cachedToken) return null;
|
|
1826
|
+
if (this.isExpired(this.cachedToken)) return null;
|
|
1827
|
+
return this.cachedToken.token;
|
|
1828
|
+
}
|
|
1750
1829
|
/* ─── StorageAdapter v2 ───────────────────────────────────────────── */
|
|
1751
1830
|
async load(_namespace) {
|
|
1752
1831
|
const out = [];
|
|
@@ -1919,7 +1998,7 @@ var CloudStorageAdapter = class {
|
|
|
1919
1998
|
}
|
|
1920
1999
|
}
|
|
1921
2000
|
sessionKey() {
|
|
1922
|
-
return `vizu:cloud-token:${this.workspace}`;
|
|
2001
|
+
return `vizu:cloud-token:v2:${this.workspace}`;
|
|
1923
2002
|
}
|
|
1924
2003
|
/**
|
|
1925
2004
|
* Resolve a valid token, opening the popup if needed.
|
|
@@ -4233,14 +4312,27 @@ var Vizu = class {
|
|
|
4233
4312
|
if (options.actions) this.actions = [...options.actions];
|
|
4234
4313
|
if (options.onCommentAdded) this.bus.on("comment:added", (p) => options.onCommentAdded(p.comment));
|
|
4235
4314
|
if (options.onCommentRemoved) this.bus.on("comment:removed", (p) => options.onCommentRemoved(p.id));
|
|
4236
|
-
if (options.
|
|
4237
|
-
const live = options.liveblocks;
|
|
4315
|
+
if (options.cloud) {
|
|
4238
4316
|
const cloud = options.cloud;
|
|
4317
|
+
const apiUrl = (cloud.apiUrl ?? "https://vizu.unhingged.com").replace(/\/$/, "");
|
|
4239
4318
|
void Promise.resolve().then(() => (init_live_collab(), live_collab_exports)).then(({ LiveCollab: LiveCollab2 }) => {
|
|
4240
4319
|
this.liveCollab = new LiveCollab2({
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4320
|
+
workspaceSlug: cloud.workspace,
|
|
4321
|
+
apiUrl,
|
|
4322
|
+
getAuthHeader: async () => {
|
|
4323
|
+
if (typeof window === "undefined") return null;
|
|
4324
|
+
try {
|
|
4325
|
+
if (new URL(apiUrl).origin === window.location.origin) return null;
|
|
4326
|
+
} catch {
|
|
4327
|
+
return null;
|
|
4328
|
+
}
|
|
4329
|
+
const adapter = this.storage;
|
|
4330
|
+
if (adapter instanceof CloudStorageAdapter) {
|
|
4331
|
+
const token = adapter.getCachedAuthToken();
|
|
4332
|
+
if (token) return { Authorization: `Bearer ${token}` };
|
|
4333
|
+
}
|
|
4334
|
+
return null;
|
|
4335
|
+
}
|
|
4244
4336
|
});
|
|
4245
4337
|
void this.liveCollab.start();
|
|
4246
4338
|
});
|