deepspace 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +90 -0
  2. package/dist/cli.js +3590 -1485
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.d.ts +876 -3664
  5. package/dist/index.js +511 -366
  6. package/dist/index.js.map +1 -1
  7. package/dist/server.d.ts +132 -61
  8. package/dist/server.js +149 -110
  9. package/dist/server.js.map +1 -1
  10. package/dist/testing.js.map +1 -1
  11. package/dist/worker.d.ts +128 -59
  12. package/dist/worker.js +144 -99
  13. package/dist/worker.js.map +1 -1
  14. package/features/admin-page/src/AdminPage.tsx +3 -5
  15. package/features/canvas/src/CanvasEditorPage.tsx +0 -2
  16. package/features/canvas/src/components/CanvasView.tsx +1 -1
  17. package/features/docs/src/LibrarySidebar.tsx +7 -1
  18. package/features/docs/src/editor/useDocEditor.ts +5 -1
  19. package/features/docs/src/use-yjs-room-with-awareness.ts +4 -0
  20. package/features/file-attachments/src/AttachmentsPage.tsx +7 -1
  21. package/features/file-attachments/src/useFilePreview.ts +7 -5
  22. package/features/file-manager/src/FileManagerPage.tsx +2 -3
  23. package/features/integration-test/feature.json +10 -3
  24. package/features/integration-test/src/IntegrationTestPage.tsx +15 -12
  25. package/features/landing/src/LandingPage.tsx +1 -1
  26. package/features/landing/src/sections/FeaturesGridSection.tsx +0 -92
  27. package/features/messaging/src/ChatPage.tsx +6 -1
  28. package/features/messaging/src/components/chat/MessageItem.tsx +5 -1
  29. package/features/messaging/src/hooks/useChatChannel.ts +23 -7
  30. package/features/presence-test/feature.json +2 -1
  31. package/features/sidebar/src/AppSidebar.tsx +1 -1
  32. package/features/tasks/src/TasksPage.tsx +1 -1
  33. package/features/testing/feature.json +2 -1
  34. package/features/testing/src/TestPage.tsx +8 -8
  35. package/package.json +8 -4
  36. package/scripts/add-feature.cjs +296 -282
  37. package/dist/testing.d.ts +0 -115
package/dist/index.js CHANGED
@@ -697,6 +697,22 @@ async function signOut(...args) {
697
697
  }
698
698
  }
699
699
 
700
+ // src/client/debug.ts
701
+ function readDebugFlag() {
702
+ try {
703
+ const g = globalThis;
704
+ if (g.DEEPSPACE_DEBUG === true || g.DEEPSPACE_DEBUG === "1") return true;
705
+ const ls = g.localStorage?.getItem("DEEPSPACE_DEBUG");
706
+ return ls === "1" || ls === "true";
707
+ } catch {
708
+ return false;
709
+ }
710
+ }
711
+ var DEBUG = readDebugFlag();
712
+ function debugLog(...args) {
713
+ if (DEBUG) console.log(...args);
714
+ }
715
+
700
716
  // src/client/auth/DeepSpaceAuthProvider.tsx
701
717
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
702
718
  function AuthLogger() {
@@ -707,9 +723,9 @@ function AuthLogger() {
707
723
  if (session.isPending) return;
708
724
  if (prevSignedIn.current !== isSignedIn) {
709
725
  if (isSignedIn) {
710
- console.log(`[ds:auth] signed in as ${session.data?.user?.name ?? session.data?.user?.id}`);
726
+ debugLog(`[ds:auth] signed in as ${session.data?.user?.name ?? session.data?.user?.id}`);
711
727
  } else if (prevSignedIn.current !== null) {
712
- console.log("[ds:auth] signed out");
728
+ debugLog("[ds:auth] signed out");
713
729
  }
714
730
  prevSignedIn.current = isSignedIn;
715
731
  }
@@ -787,7 +803,7 @@ function AuthOverlay({
787
803
  setLoading(false);
788
804
  }
789
805
  } catch (err) {
790
- setError(err.message ?? "Something went wrong");
806
+ setError(err instanceof Error ? err.message : "Something went wrong");
791
807
  setLoading(false);
792
808
  }
793
809
  };
@@ -1128,6 +1144,7 @@ import {
1128
1144
 
1129
1145
  // src/client/storage/store.ts
1130
1146
  var EMPTY_RECORDS = [];
1147
+ var EMPTY_SNAPSHOT = { status: "loading", records: EMPTY_RECORDS };
1131
1148
  var RecordStore = class {
1132
1149
  queries = /* @__PURE__ */ new Map();
1133
1150
  listeners = /* @__PURE__ */ new Map();
@@ -1146,16 +1163,13 @@ var RecordStore = class {
1146
1163
  }
1147
1164
  };
1148
1165
  }
1166
+ /**
1167
+ * Snapshot for `useSyncExternalStore`. Bundles records + status + error so a
1168
+ * transition in any of them re-renders the hook. Returns a stable reference
1169
+ * until the next `commit()`.
1170
+ */
1149
1171
  getSnapshot(queryKey) {
1150
- const state = this.queries.get(queryKey);
1151
- if (!state) return EMPTY_RECORDS;
1152
- return state.records;
1153
- }
1154
- getStatus(queryKey) {
1155
- return this.queries.get(queryKey)?.status ?? "loading";
1156
- }
1157
- getError(queryKey) {
1158
- return this.queries.get(queryKey)?.error;
1172
+ return this.queries.get(queryKey)?.snapshot ?? EMPTY_SNAPSHOT;
1159
1173
  }
1160
1174
  /**
1161
1175
  * Initialize a query subscription.
@@ -1165,7 +1179,10 @@ var RecordStore = class {
1165
1179
  const prevCount = this.refCounts.get(queryKey) ?? 0;
1166
1180
  this.refCounts.set(queryKey, prevCount + 1);
1167
1181
  if (!this.queries.has(queryKey)) {
1168
- this.queries.set(queryKey, { status: "loading", records: [], version: 0 });
1182
+ this.queries.set(queryKey, {
1183
+ version: 0,
1184
+ snapshot: { status: "loading", records: EMPTY_RECORDS }
1185
+ });
1169
1186
  }
1170
1187
  return prevCount === 0;
1171
1188
  }
@@ -1191,46 +1208,49 @@ var RecordStore = class {
1191
1208
  getSubscriptionId(queryKey) {
1192
1209
  return this.activeSubscriptions.get(queryKey);
1193
1210
  }
1211
+ /**
1212
+ * Replace the state for a query with a fresh snapshot and bump the version,
1213
+ * then notify listeners. Single write-path so every mutator mints a new
1214
+ * snapshot reference (which is what drives `useSyncExternalStore` re-renders).
1215
+ */
1216
+ commit(queryKey, snapshot) {
1217
+ const version = (this.queries.get(queryKey)?.version ?? 0) + 1;
1218
+ this.queries.set(queryKey, { version, snapshot });
1219
+ this.notify(queryKey);
1220
+ }
1194
1221
  // Handle QUERY_RESULT from server
1195
1222
  setQueryResult(queryKey, records) {
1196
- const current = this.queries.get(queryKey);
1197
- this.queries.set(queryKey, {
1198
- status: "ready",
1199
- records,
1200
- // New array reference
1201
- version: (current?.version ?? 0) + 1
1202
- });
1203
- this.notify(queryKey);
1223
+ this.commit(queryKey, { status: "ready", records });
1204
1224
  }
1205
1225
  /** Check if a record exists in a query's results */
1206
1226
  hasRecord(queryKey, recordId) {
1207
1227
  const state = this.queries.get(queryKey);
1208
1228
  if (!state) return false;
1209
- return state.records.some((r) => r.recordId === recordId);
1229
+ return state.snapshot.records.some((r) => r.recordId === recordId);
1210
1230
  }
1211
1231
  // Handle RECORD_CHANGE from server
1212
1232
  applyChange(queryKey, record, changeType) {
1213
1233
  const state = this.queries.get(queryKey);
1214
1234
  if (!state) return;
1235
+ const records = state.snapshot.records;
1215
1236
  let newRecords;
1216
1237
  if (changeType === "delete") {
1217
- newRecords = state.records.filter((r) => r.recordId !== record.recordId);
1238
+ newRecords = records.filter((r) => r.recordId !== record.recordId);
1218
1239
  } else if (changeType === "create") {
1219
- const exists = state.records.some((r) => r.recordId === record.recordId);
1240
+ const exists = records.some((r) => r.recordId === record.recordId);
1220
1241
  if (exists) {
1221
- newRecords = state.records.map((r) => r.recordId === record.recordId ? record : r);
1242
+ newRecords = records.map((r) => r.recordId === record.recordId ? record : r);
1222
1243
  } else {
1223
- newRecords = [...state.records, record];
1244
+ newRecords = [...records, record];
1224
1245
  }
1225
1246
  } else {
1226
- newRecords = state.records.map((r) => r.recordId === record.recordId ? record : r);
1247
+ newRecords = records.map((r) => r.recordId === record.recordId ? record : r);
1227
1248
  }
1228
- this.queries.set(queryKey, {
1229
- ...state,
1249
+ this.commit(queryKey, {
1250
+ status: state.snapshot.status,
1230
1251
  records: newRecords,
1231
- version: state.version + 1
1252
+ error: state.snapshot.error
1232
1253
  });
1233
- this.notify(queryKey);
1234
1254
  }
1235
1255
  /**
1236
1256
  * Reset a query to loading state (used on WebSocket reconnect).
@@ -1239,22 +1259,11 @@ var RecordStore = class {
1239
1259
  resetToLoading(queryKey) {
1240
1260
  const current = this.queries.get(queryKey);
1241
1261
  if (!current) return;
1242
- this.queries.set(queryKey, {
1243
- ...current,
1244
- status: "loading",
1245
- version: current.version + 1
1246
- });
1247
- this.notify(queryKey);
1262
+ this.commit(queryKey, { status: "loading", records: current.snapshot.records });
1248
1263
  }
1249
1264
  setError(queryKey, error) {
1250
- const current = this.queries.get(queryKey);
1251
- this.queries.set(queryKey, {
1252
- status: "error",
1253
- records: current?.records ?? [],
1254
- version: (current?.version ?? 0) + 1,
1255
- error
1256
- });
1257
- this.notify(queryKey);
1265
+ const records = this.queries.get(queryKey)?.snapshot.records ?? EMPTY_RECORDS;
1266
+ this.commit(queryKey, { status: "error", records, error });
1258
1267
  }
1259
1268
  // Cleanup when query is no longer needed
1260
1269
  removeQuery(queryKey) {
@@ -1421,7 +1430,6 @@ function recordMatchesWhere(record, where) {
1421
1430
  }
1422
1431
  function RecordProviderCore({
1423
1432
  roomId,
1424
- schemas,
1425
1433
  wsUrl,
1426
1434
  children,
1427
1435
  fetchUser,
@@ -1435,8 +1443,12 @@ function RecordProviderCore({
1435
1443
  const [usersLoaded, setUsersLoaded] = useState3(false);
1436
1444
  const [status, setStatus] = useState3("connecting");
1437
1445
  const [ready, setReady] = useState3(false);
1438
- const onPermissionErrorRef = useRef4(void 0);
1439
- const onValidationErrorRef = useRef4(void 0);
1446
+ const onPermissionErrorRef = useRef4(
1447
+ void 0
1448
+ );
1449
+ const onValidationErrorRef = useRef4(
1450
+ void 0
1451
+ );
1440
1452
  const storeRef = useRef4(new RecordStore());
1441
1453
  const wsRef = useRef4(null);
1442
1454
  const subscriptionMapRef = useRef4(/* @__PURE__ */ new Map());
@@ -1445,6 +1457,7 @@ function RecordProviderCore({
1445
1457
  const pendingRequestsRef = useRef4(/* @__PURE__ */ new Map());
1446
1458
  const reconnectAttemptRef = useRef4(0);
1447
1459
  const reconnectTimeoutRef = useRef4(null);
1460
+ const connectTokenRef = useRef4(0);
1448
1461
  const connectedWithProfileRef = useRef4(false);
1449
1462
  const userProfileRef = useRef4(null);
1450
1463
  userProfileRef.current = userProfile;
@@ -1465,9 +1478,10 @@ function RecordProviderCore({
1465
1478
  if (mounted) setUserProfileLoading(false);
1466
1479
  });
1467
1480
  const interval = setInterval(() => {
1468
- if (mounted) fetchUser().then((p) => {
1469
- if (mounted) setUserProfile(p);
1470
- });
1481
+ if (mounted)
1482
+ fetchUser().then((p) => {
1483
+ if (mounted) setUserProfile(p);
1484
+ });
1471
1485
  }, 3e4);
1472
1486
  return () => {
1473
1487
  mounted = false;
@@ -1485,7 +1499,7 @@ function RecordProviderCore({
1485
1499
  switch (type) {
1486
1500
  case MSG.USER_INFO: {
1487
1501
  const serverUser = payload;
1488
- console.log("[WS] Ready:", { roomId, role: serverUser.role });
1502
+ debugLog("[WS] Ready:", { roomId, role: serverUser.role });
1489
1503
  setRoomRole(serverUser.role);
1490
1504
  setReady(true);
1491
1505
  reconnectAttemptRef.current = 0;
@@ -1511,7 +1525,7 @@ function RecordProviderCore({
1511
1525
  }
1512
1526
  case MSG.RECORD_CHANGE: {
1513
1527
  const { collection, record, changeType } = payload;
1514
- for (const [_subId, queryKey] of subscriptionMapRef.current) {
1528
+ for (const queryKey of subscriptionMapRef.current.values()) {
1515
1529
  try {
1516
1530
  const query = JSON.parse(queryKey);
1517
1531
  if (query.collection !== collection) continue;
@@ -1523,8 +1537,10 @@ function RecordProviderCore({
1523
1537
  if (matches) storeRef.current.applyChange(queryKey, record, "create");
1524
1538
  } else {
1525
1539
  if (matches && exists) storeRef.current.applyChange(queryKey, record, "update");
1526
- else if (matches && !exists) storeRef.current.applyChange(queryKey, record, "create");
1527
- else if (!matches && exists) storeRef.current.applyChange(queryKey, record, "delete");
1540
+ else if (matches && !exists)
1541
+ storeRef.current.applyChange(queryKey, record, "create");
1542
+ else if (!matches && exists)
1543
+ storeRef.current.applyChange(queryKey, record, "delete");
1528
1544
  }
1529
1545
  } catch {
1530
1546
  }
@@ -1585,13 +1601,13 @@ function RecordProviderCore({
1585
1601
  } catch (e) {
1586
1602
  console.error("[RecordProvider] Failed to parse message:", e);
1587
1603
  }
1588
- }, []);
1604
+ }, [roomId]);
1589
1605
  const connect = useCallback2(async () => {
1590
1606
  const profile = userProfileRef.current;
1591
1607
  if (!profile && !allowAnonymous) return;
1592
1608
  if (wsRef.current?.readyState === WebSocket.OPEN) {
1593
1609
  if (profile && !connectedWithProfileRef.current) {
1594
- console.log(`[WS] Reconnecting with profile data`, { roomId });
1610
+ debugLog(`[WS] Reconnecting with profile data`, { roomId });
1595
1611
  wsRef.current.onclose = null;
1596
1612
  wsRef.current.close();
1597
1613
  wsRef.current = null;
@@ -1604,6 +1620,7 @@ function RecordProviderCore({
1604
1620
  clearTimeout(reconnectTimeoutRef.current);
1605
1621
  reconnectTimeoutRef.current = null;
1606
1622
  }
1623
+ const myToken = ++connectTokenRef.current;
1607
1624
  let baseUrl;
1608
1625
  if (wsUrl) {
1609
1626
  baseUrl = wsUrl.replace(/^http/, "ws");
@@ -1618,30 +1635,36 @@ function RecordProviderCore({
1618
1635
  if (token) params.set("token", token);
1619
1636
  } catch {
1620
1637
  }
1638
+ if (connectTokenRef.current !== myToken) return;
1621
1639
  connectedWithProfileRef.current = !!profile;
1622
1640
  const wsPath = `/ws/${roomId}`;
1623
1641
  const url = `${baseUrl}${wsPath}?${params.toString()}`;
1624
- console.log("[WS] Connecting:", { roomId, attempt: reconnectAttemptRef.current });
1642
+ debugLog("[WS] Connecting:", { roomId, attempt: reconnectAttemptRef.current });
1625
1643
  const ws = new WebSocket(url);
1626
1644
  ws.binaryType = "arraybuffer";
1627
1645
  ws.onopen = () => {
1628
- console.log("[WS] Connected:", { roomId });
1646
+ debugLog("[WS] Connected:", { roomId });
1629
1647
  setStatus("connected");
1630
1648
  };
1631
1649
  ws.onmessage = handleMessage;
1632
1650
  ws.onclose = (event) => {
1633
- console.log("[WS] Closed:", { roomId, code: event.code, reason: event.reason, wasClean: event.wasClean });
1651
+ debugLog("[WS] Closed:", {
1652
+ roomId,
1653
+ code: event.code,
1654
+ reason: event.reason,
1655
+ wasClean: event.wasClean
1656
+ });
1634
1657
  setStatus("disconnected");
1635
1658
  setReady(false);
1636
1659
  wsRef.current = null;
1637
- for (const [_id, pending] of pendingRequestsRef.current) {
1660
+ for (const pending of pendingRequestsRef.current.values()) {
1638
1661
  clearTimeout(pending.timer);
1639
1662
  pending.reject(new Error("WebSocket disconnected"));
1640
1663
  }
1641
1664
  pendingRequestsRef.current.clear();
1642
1665
  const attempt = reconnectAttemptRef.current;
1643
1666
  const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
1644
- console.log("[WS] Scheduling reconnect:", { roomId, attempt, delayMs: delay });
1667
+ debugLog("[WS] Scheduling reconnect:", { roomId, attempt, delayMs: delay });
1645
1668
  reconnectAttemptRef.current = attempt + 1;
1646
1669
  reconnectTimeoutRef.current = setTimeout(connect, delay);
1647
1670
  };
@@ -1659,7 +1682,7 @@ function RecordProviderCore({
1659
1682
  if (document.visibilityState === "visible") {
1660
1683
  const ws = wsRef.current;
1661
1684
  if (ws?.readyState !== WebSocket.OPEN && (userProfileRef.current || allowAnonymous)) {
1662
- console.log("[WS] Reconnecting after visibility change");
1685
+ debugLog("[WS] Reconnecting after visibility change");
1663
1686
  reconnectAttemptRef.current = 0;
1664
1687
  connect();
1665
1688
  }
@@ -1667,12 +1690,20 @@ function RecordProviderCore({
1667
1690
  };
1668
1691
  document.addEventListener("visibilitychange", handleVisibilityChange);
1669
1692
  return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
1670
- }, [roomId, connect]);
1693
+ }, [roomId, connect, allowAnonymous]);
1671
1694
  useEffect4(() => {
1672
1695
  return () => {
1673
- console.log("[WS] Unmounting provider:", { roomId });
1696
+ debugLog("[WS] Unmounting provider:", { roomId });
1674
1697
  if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
1675
- wsRef.current?.close();
1698
+ connectTokenRef.current++;
1699
+ const ws = wsRef.current;
1700
+ if (ws) {
1701
+ ws.onclose = null;
1702
+ ws.onmessage = null;
1703
+ ws.onerror = null;
1704
+ ws.close();
1705
+ wsRef.current = null;
1706
+ }
1676
1707
  };
1677
1708
  }, [roomId]);
1678
1709
  const sendMessage = useCallback2((message) => {
@@ -1680,9 +1711,12 @@ function RecordProviderCore({
1680
1711
  if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
1681
1712
  else console.warn("[RecordProvider] Cannot send message, WebSocket not connected");
1682
1713
  }, []);
1683
- const setUserRole = useCallback2((userId, role) => {
1684
- sendMessage({ type: MSG.SET_ROLE, payload: { userId, role } });
1685
- }, [sendMessage]);
1714
+ const setUserRole = useCallback2(
1715
+ (userId, role) => {
1716
+ sendMessage({ type: MSG.SET_ROLE, payload: { userId, role } });
1717
+ },
1718
+ [sendMessage]
1719
+ );
1686
1720
  const requestUserList = useCallback2(() => {
1687
1721
  sendMessage({ type: MSG.USER_LIST, payload: {} });
1688
1722
  }, [sendMessage]);
@@ -1702,70 +1736,109 @@ function RecordProviderCore({
1702
1736
  binaryHandlersRef.current.delete(handler);
1703
1737
  };
1704
1738
  }, []);
1705
- const registerYjsJoinHandler = useCallback2((docKey, handler) => {
1706
- if (!yjsJoinHandlersRef.current.has(docKey)) {
1707
- yjsJoinHandlersRef.current.set(docKey, /* @__PURE__ */ new Set());
1708
- }
1709
- yjsJoinHandlersRef.current.get(docKey).add(handler);
1710
- return () => {
1711
- const handlers = yjsJoinHandlersRef.current.get(docKey);
1712
- if (handlers) {
1713
- handlers.delete(handler);
1714
- if (handlers.size === 0) yjsJoinHandlersRef.current.delete(docKey);
1739
+ const registerYjsJoinHandler = useCallback2(
1740
+ (docKey, handler) => {
1741
+ if (!yjsJoinHandlersRef.current.has(docKey)) {
1742
+ yjsJoinHandlersRef.current.set(docKey, /* @__PURE__ */ new Set());
1715
1743
  }
1716
- };
1717
- }, []);
1718
- const sendConfirmed = useCallback2((message, timeoutMs = 1e4) => {
1719
- const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1720
- const ws = wsRef.current;
1721
- if (!ws || ws.readyState !== WebSocket.OPEN) {
1722
- return Promise.reject(new Error("WebSocket not connected"));
1723
- }
1724
- return new Promise((resolve, reject) => {
1725
- const timer = setTimeout(() => {
1726
- pendingRequestsRef.current.delete(requestId);
1727
- reject(new Error("Mutation confirmation timed out"));
1728
- }, timeoutMs);
1729
- pendingRequestsRef.current.set(requestId, { resolve, reject, timer });
1730
- ws.send(JSON.stringify({ ...message, payload: { ...message.payload, requestId } }));
1731
- });
1732
- }, []);
1733
- const value = useMemo2(() => ({
1734
- store: storeRef.current,
1735
- roomId,
1736
- userProfile,
1737
- userProfileLoading,
1738
- refetchUserProfile,
1739
- roomRole,
1740
- allUsers,
1741
- usersLoaded,
1742
- status,
1743
- ready,
1744
- setUserRole,
1745
- requestUserList,
1746
- registerSubscription,
1747
- unregisterSubscription,
1748
- sendMessage,
1749
- sendBinary,
1750
- onBinaryMessage,
1751
- registerYjsJoinHandler,
1752
- sendConfirmed
1753
- }), [roomId, userProfile, userProfileLoading, refetchUserProfile, roomRole, allUsers, usersLoaded, status, ready, setUserRole, requestUserList, registerSubscription, unregisterSubscription, sendMessage, sendBinary, onBinaryMessage, registerYjsJoinHandler, sendConfirmed]);
1744
+ yjsJoinHandlersRef.current.get(docKey).add(handler);
1745
+ return () => {
1746
+ const handlers = yjsJoinHandlersRef.current.get(docKey);
1747
+ if (handlers) {
1748
+ handlers.delete(handler);
1749
+ if (handlers.size === 0) yjsJoinHandlersRef.current.delete(docKey);
1750
+ }
1751
+ };
1752
+ },
1753
+ []
1754
+ );
1755
+ const sendConfirmed = useCallback2(
1756
+ (message, timeoutMs = 1e4) => {
1757
+ const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1758
+ const ws = wsRef.current;
1759
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
1760
+ return Promise.reject(new Error("WebSocket not connected"));
1761
+ }
1762
+ return new Promise((resolve, reject) => {
1763
+ const timer = setTimeout(() => {
1764
+ pendingRequestsRef.current.delete(requestId);
1765
+ reject(new Error("Mutation confirmation timed out"));
1766
+ }, timeoutMs);
1767
+ pendingRequestsRef.current.set(requestId, { resolve, reject, timer });
1768
+ ws.send(JSON.stringify({ ...message, payload: { ...message.payload, requestId } }));
1769
+ });
1770
+ },
1771
+ []
1772
+ );
1773
+ const value = useMemo2(
1774
+ () => ({
1775
+ store: storeRef.current,
1776
+ roomId,
1777
+ userProfile,
1778
+ userProfileLoading,
1779
+ refetchUserProfile,
1780
+ roomRole,
1781
+ allUsers,
1782
+ usersLoaded,
1783
+ status,
1784
+ ready,
1785
+ setUserRole,
1786
+ requestUserList,
1787
+ registerSubscription,
1788
+ unregisterSubscription,
1789
+ sendMessage,
1790
+ sendBinary,
1791
+ onBinaryMessage,
1792
+ registerYjsJoinHandler,
1793
+ sendConfirmed
1794
+ }),
1795
+ [
1796
+ roomId,
1797
+ userProfile,
1798
+ userProfileLoading,
1799
+ refetchUserProfile,
1800
+ roomRole,
1801
+ allUsers,
1802
+ usersLoaded,
1803
+ status,
1804
+ ready,
1805
+ setUserRole,
1806
+ requestUserList,
1807
+ registerSubscription,
1808
+ unregisterSubscription,
1809
+ sendMessage,
1810
+ sendBinary,
1811
+ onBinaryMessage,
1812
+ registerYjsJoinHandler,
1813
+ sendConfirmed
1814
+ ]
1815
+ );
1754
1816
  const onPermissionError = useCallback2((title, detail) => {
1755
1817
  onPermissionErrorRef.current?.(title, detail);
1756
1818
  }, []);
1757
1819
  const onValidationError = useCallback2((title, detail) => {
1758
1820
  onValidationErrorRef.current?.(title, detail);
1759
1821
  }, []);
1760
- const authValue = useMemo2(() => ({
1761
- userProfile,
1762
- userProfileLoading,
1763
- refetchUserProfile,
1764
- allowAnonymous,
1765
- getAuthToken: getAuthTokenProp,
1766
- onPermissionError,
1767
- onValidationError
1768
- }), [userProfile, userProfileLoading, refetchUserProfile, allowAnonymous, getAuthTokenProp, onPermissionError, onValidationError]);
1822
+ const authValue = useMemo2(
1823
+ () => ({
1824
+ userProfile,
1825
+ userProfileLoading,
1826
+ refetchUserProfile,
1827
+ allowAnonymous,
1828
+ getAuthToken: getAuthTokenProp,
1829
+ onPermissionError,
1830
+ onValidationError
1831
+ }),
1832
+ [
1833
+ userProfile,
1834
+ userProfileLoading,
1835
+ refetchUserProfile,
1836
+ allowAnonymous,
1837
+ getAuthTokenProp,
1838
+ onPermissionError,
1839
+ onValidationError
1840
+ ]
1841
+ );
1769
1842
  return /* @__PURE__ */ jsx6(RecordAuthContext.Provider, { value: authValue, children: /* @__PURE__ */ jsx6(ScopeRegistryProvider, { children: /* @__PURE__ */ jsx6(RecordContext.Provider, { value, children }) }) });
1770
1843
  }
1771
1844
  function RecordProviderAuthOnly({
@@ -1804,18 +1877,20 @@ function RecordProviderAuthOnly({
1804
1877
  clearInterval(interval);
1805
1878
  };
1806
1879
  }, [fetchUser]);
1807
- const authValue = useMemo2(() => ({
1808
- userProfile,
1809
- userProfileLoading,
1810
- refetchUserProfile,
1811
- allowAnonymous,
1812
- getAuthToken: getAuthTokenProp
1813
- }), [userProfile, userProfileLoading, refetchUserProfile, allowAnonymous, getAuthTokenProp]);
1880
+ const authValue = useMemo2(
1881
+ () => ({
1882
+ userProfile,
1883
+ userProfileLoading,
1884
+ refetchUserProfile,
1885
+ allowAnonymous,
1886
+ getAuthToken: getAuthTokenProp
1887
+ }),
1888
+ [userProfile, userProfileLoading, refetchUserProfile, allowAnonymous, getAuthTokenProp]
1889
+ );
1814
1890
  return /* @__PURE__ */ jsx6(RecordAuthContext.Provider, { value: authValue, children: /* @__PURE__ */ jsx6(ScopeRegistryProvider, { children }) });
1815
1891
  }
1816
1892
  function RecordProvider({
1817
1893
  roomId,
1818
- schemas = [],
1819
1894
  wsUrl,
1820
1895
  children,
1821
1896
  allowAnonymous = false,
@@ -1855,7 +1930,6 @@ function RecordProvider({
1855
1930
  RecordProviderCore,
1856
1931
  {
1857
1932
  roomId,
1858
- schemas,
1859
1933
  wsUrl,
1860
1934
  fetchUser,
1861
1935
  allowAnonymous,
@@ -1876,20 +1950,14 @@ function RecordProvider({
1876
1950
  }
1877
1951
 
1878
1952
  // src/client/storage/RecordScope.tsx
1879
- import {
1880
- useEffect as useEffect5,
1881
- useState as useState4,
1882
- useCallback as useCallback3,
1883
- useRef as useRef5,
1884
- useMemo as useMemo3
1885
- } from "react";
1953
+ import { useEffect as useEffect5, useState as useState4, useCallback as useCallback3, useRef as useRef5, useMemo as useMemo3 } from "react";
1886
1954
 
1887
1955
  // src/client/storage/ws-log.ts
1888
1956
  var activeCount = 0;
1889
1957
  function wsLog(event, label) {
1890
1958
  if (event === "connected") activeCount++;
1891
1959
  if (event === "closing") activeCount--;
1892
- console.log(`[ds:ws] ${event} \u2192 ${label} (${activeCount} active)`);
1960
+ debugLog(`[ds:ws] ${event} \u2192 ${label} (${activeCount} active)`);
1893
1961
  }
1894
1962
 
1895
1963
  // src/client/storage/RecordScope.tsx
@@ -1906,7 +1974,13 @@ function recordMatchesWhere2(record, where) {
1906
1974
  return false;
1907
1975
  });
1908
1976
  }
1909
- function HeadlessScope({ roomId, schemas, appId, wsUrl, wsPathPrefix }) {
1977
+ function HeadlessScope({
1978
+ roomId,
1979
+ schemas,
1980
+ appId,
1981
+ wsUrl,
1982
+ wsPathPrefix
1983
+ }) {
1910
1984
  return /* @__PURE__ */ jsx7(
1911
1985
  ScopeConnection,
1912
1986
  {
@@ -1944,6 +2018,7 @@ function ScopeConnection({
1944
2018
  const pendingRequestsRef = useRef5(/* @__PURE__ */ new Map());
1945
2019
  const reconnectAttemptRef = useRef5(0);
1946
2020
  const reconnectTimeoutRef = useRef5(null);
2021
+ const connectTokenRef = useRef5(0);
1947
2022
  const userProfileRef = useRef5(auth?.userProfile ?? null);
1948
2023
  userProfileRef.current = auth?.userProfile ?? null;
1949
2024
  const getAuthTokenRef = useRef5(auth?.getAuthToken ?? null);
@@ -1955,131 +2030,145 @@ function ScopeConnection({
1955
2030
  authCallbacksRef.current.onPermissionError = auth?.onPermissionError;
1956
2031
  authCallbacksRef.current.onValidationError = auth?.onValidationError;
1957
2032
  const allowAnonymous = auth?.allowAnonymous ?? false;
1958
- const handleMessage = useCallback3((event) => {
1959
- if (event.data instanceof ArrayBuffer) {
1960
- binaryHandlersRef.current.forEach((h) => h(event.data));
1961
- return;
1962
- }
1963
- let msg;
1964
- try {
1965
- msg = JSON.parse(event.data);
1966
- } catch {
1967
- console.error("[RecordScope] Failed to parse message");
1968
- return;
1969
- }
1970
- const { type, payload } = msg;
1971
- switch (type) {
1972
- case MSG.USER_INFO: {
1973
- const { role } = payload;
1974
- setRoomRole(role);
1975
- setReady(true);
1976
- reconnectAttemptRef.current = 0;
1977
- const ws = wsRef.current;
1978
- if (ws?.readyState === WebSocket.OPEN) {
1979
- ws.send(JSON.stringify({ type: MSG.USER_LIST, payload: {} }));
1980
- }
1981
- break;
2033
+ const handleMessage = useCallback3(
2034
+ (event) => {
2035
+ if (event.data instanceof ArrayBuffer) {
2036
+ binaryHandlersRef.current.forEach((h) => h(event.data));
2037
+ return;
1982
2038
  }
1983
- case MSG.USER_LIST:
1984
- setAllUsers(payload.users);
1985
- setUsersLoaded(true);
1986
- break;
1987
- case MSG.QUERY_RESULT: {
1988
- const { subscriptionId, records } = payload;
1989
- for (const [subId, queryKey] of subscriptionMapRef.current) {
1990
- if (subId === subscriptionId) {
1991
- storeRef.current.setQueryResult(queryKey, records);
1992
- break;
1993
- }
1994
- }
1995
- break;
2039
+ let msg;
2040
+ try {
2041
+ msg = JSON.parse(event.data);
2042
+ } catch {
2043
+ console.error("[RecordScope] Failed to parse message");
2044
+ return;
1996
2045
  }
1997
- case MSG.RECORD_CHANGE: {
1998
- const { collection, record, changeType } = payload;
1999
- for (const [, queryKey] of subscriptionMapRef.current) {
2000
- try {
2001
- const query = JSON.parse(queryKey);
2002
- if (query.collection !== collection) continue;
2003
- const matches = recordMatchesWhere2(record, query.where);
2004
- const exists = storeRef.current.hasRecord(queryKey, record.recordId);
2005
- if (changeType === "delete") {
2006
- if (exists) storeRef.current.applyChange(queryKey, record, "delete");
2007
- } else if (changeType === "create") {
2008
- if (matches) storeRef.current.applyChange(queryKey, record, "create");
2009
- } else {
2010
- if (matches && exists) storeRef.current.applyChange(queryKey, record, "update");
2011
- else if (matches && !exists) storeRef.current.applyChange(queryKey, record, "create");
2012
- else if (!matches && exists) storeRef.current.applyChange(queryKey, record, "delete");
2013
- }
2014
- } catch {
2046
+ const { type, payload } = msg;
2047
+ switch (type) {
2048
+ case MSG.USER_INFO: {
2049
+ const { role } = payload;
2050
+ setRoomRole(role);
2051
+ setReady(true);
2052
+ reconnectAttemptRef.current = 0;
2053
+ const ws = wsRef.current;
2054
+ if (ws?.readyState === WebSocket.OPEN) {
2055
+ ws.send(JSON.stringify({ type: MSG.USER_LIST, payload: {} }));
2015
2056
  }
2057
+ break;
2016
2058
  }
2017
- break;
2018
- }
2019
- case MSG.ERROR: {
2020
- const { subscriptionId, error } = payload;
2021
- if (subscriptionId) {
2059
+ case MSG.USER_LIST:
2060
+ setAllUsers(payload.users);
2061
+ setUsersLoaded(true);
2062
+ break;
2063
+ case MSG.QUERY_RESULT: {
2064
+ const { subscriptionId, records } = payload;
2022
2065
  for (const [subId, queryKey] of subscriptionMapRef.current) {
2023
2066
  if (subId === subscriptionId) {
2024
- storeRef.current.setError(queryKey, error);
2067
+ storeRef.current.setQueryResult(queryKey, records);
2025
2068
  break;
2026
2069
  }
2027
2070
  }
2028
- } else {
2029
- const parsed = parseServerError(error);
2030
- if (parsed.isPermissionError) {
2031
- authCallbacksRef.current.onPermissionError?.(parsed.title, parsed.detail);
2032
- } else {
2033
- authCallbacksRef.current.onValidationError?.(parsed.title, parsed.detail);
2034
- }
2035
- }
2036
- break;
2037
- }
2038
- case MSG.YJS_JOIN: {
2039
- const { collection, recordId, fieldName, canWrite } = payload;
2040
- const docKey = `${collection}:${recordId}:${fieldName}`;
2041
- yjsJoinHandlersRef.current.get(docKey)?.forEach((h) => h(canWrite));
2042
- break;
2043
- }
2044
- case MSG.ACK: {
2045
- const { requestId, success, error, ...rest } = payload;
2046
- const pending = pendingRequestsRef.current.get(requestId);
2047
- if (pending) {
2048
- clearTimeout(pending.timer);
2049
- pendingRequestsRef.current.delete(requestId);
2050
- success ? pending.resolve(rest) : pending.reject(new Error(error || "Mutation rejected"));
2071
+ break;
2051
2072
  }
2052
- break;
2053
- }
2054
- case MSG.LIST_SCHEMAS: {
2055
- const { schemas: list } = payload;
2056
- setDiscoveredSchemas(list ?? []);
2057
- break;
2058
- }
2059
- case MSG.RESUBSCRIBE: {
2060
- const ws = wsRef.current;
2061
- if (ws?.readyState === WebSocket.OPEN) {
2062
- for (const [subscriptionId, queryKey] of subscriptionMapRef.current) {
2073
+ case MSG.RECORD_CHANGE: {
2074
+ const { collection, record, changeType } = payload;
2075
+ for (const [, queryKey] of subscriptionMapRef.current) {
2063
2076
  try {
2064
2077
  const query = JSON.parse(queryKey);
2065
- ws.send(JSON.stringify({ type: MSG.SUBSCRIBE, payload: { subscriptionId, query } }));
2078
+ if (query.collection !== collection) continue;
2079
+ const matches = recordMatchesWhere2(record, query.where);
2080
+ const exists = storeRef.current.hasRecord(queryKey, record.recordId);
2081
+ if (changeType === "delete") {
2082
+ if (exists) storeRef.current.applyChange(queryKey, record, "delete");
2083
+ } else if (changeType === "create") {
2084
+ if (matches) storeRef.current.applyChange(queryKey, record, "create");
2085
+ } else {
2086
+ if (matches && exists) storeRef.current.applyChange(queryKey, record, "update");
2087
+ else if (matches && !exists)
2088
+ storeRef.current.applyChange(queryKey, record, "create");
2089
+ else if (!matches && exists)
2090
+ storeRef.current.applyChange(queryKey, record, "delete");
2091
+ }
2066
2092
  } catch {
2067
2093
  }
2068
2094
  }
2095
+ break;
2096
+ }
2097
+ case MSG.ERROR: {
2098
+ const { subscriptionId, error } = payload;
2099
+ if (subscriptionId) {
2100
+ for (const [subId, queryKey] of subscriptionMapRef.current) {
2101
+ if (subId === subscriptionId) {
2102
+ storeRef.current.setError(queryKey, error);
2103
+ break;
2104
+ }
2105
+ }
2106
+ } else {
2107
+ const parsed = parseServerError(error);
2108
+ if (parsed.isPermissionError) {
2109
+ authCallbacksRef.current.onPermissionError?.(parsed.title, parsed.detail);
2110
+ } else {
2111
+ authCallbacksRef.current.onValidationError?.(parsed.title, parsed.detail);
2112
+ }
2113
+ }
2114
+ break;
2115
+ }
2116
+ case MSG.YJS_JOIN: {
2117
+ const { collection, recordId, fieldName, canWrite } = payload;
2118
+ const docKey = `${collection}:${recordId}:${fieldName}`;
2119
+ yjsJoinHandlersRef.current.get(docKey)?.forEach((h) => h(canWrite));
2120
+ break;
2121
+ }
2122
+ case MSG.ACK: {
2123
+ const { requestId, success, error, ...rest } = payload;
2124
+ const pending = pendingRequestsRef.current.get(requestId);
2125
+ if (pending) {
2126
+ clearTimeout(pending.timer);
2127
+ pendingRequestsRef.current.delete(requestId);
2128
+ if (success) {
2129
+ pending.resolve(rest);
2130
+ } else {
2131
+ pending.reject(new Error(error || "Mutation rejected"));
2132
+ }
2133
+ }
2134
+ break;
2135
+ }
2136
+ case MSG.LIST_SCHEMAS: {
2137
+ const { schemas: list } = payload;
2138
+ setDiscoveredSchemas(list ?? []);
2139
+ break;
2140
+ }
2141
+ case MSG.RESUBSCRIBE: {
2142
+ const ws = wsRef.current;
2143
+ if (ws?.readyState === WebSocket.OPEN) {
2144
+ for (const [subscriptionId, queryKey] of subscriptionMapRef.current) {
2145
+ try {
2146
+ const query = JSON.parse(queryKey);
2147
+ ws.send(JSON.stringify({ type: MSG.SUBSCRIBE, payload: { subscriptionId, query } }));
2148
+ } catch {
2149
+ }
2150
+ }
2151
+ }
2152
+ break;
2069
2153
  }
2070
- break;
2071
2154
  }
2072
- }
2073
- }, [roomId]);
2155
+ },
2156
+ // Body reads only refs/setters, never roomId; keeping deps empty makes the
2157
+ // handler stable and avoids needlessly re-triggering the connect effect
2158
+ // (which lists roomId itself).
2159
+ []
2160
+ );
2074
2161
  const connect = useCallback3(async () => {
2075
2162
  const profile = userProfileRef.current;
2076
2163
  const hasTokenProvider = !!getAuthTokenRef.current;
2077
2164
  if (!profile && !hasTokenProvider && !allowAnonymous) return;
2078
- if (wsRef.current?.readyState === WebSocket.OPEN || wsRef.current?.readyState === WebSocket.CONNECTING) return;
2165
+ if (wsRef.current?.readyState === WebSocket.OPEN || wsRef.current?.readyState === WebSocket.CONNECTING)
2166
+ return;
2079
2167
  if (reconnectTimeoutRef.current) {
2080
2168
  clearTimeout(reconnectTimeoutRef.current);
2081
2169
  reconnectTimeoutRef.current = null;
2082
2170
  }
2171
+ const myToken = ++connectTokenRef.current;
2083
2172
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
2084
2173
  const baseUrl = wsUrl?.replace(/^http/, "ws") ?? `${protocol}//${window.location.host}`;
2085
2174
  const params = new URLSearchParams();
@@ -2089,6 +2178,7 @@ function ScopeConnection({
2089
2178
  if (token) params.set("token", token);
2090
2179
  } catch {
2091
2180
  }
2181
+ if (connectTokenRef.current !== myToken) return;
2092
2182
  params.set("appId", appId);
2093
2183
  const url = `${baseUrl}${wsPathPrefix}/${roomId}?${params.toString()}`;
2094
2184
  wsLog("connecting", roomId);
@@ -2112,7 +2202,8 @@ function ScopeConnection({
2112
2202
  setStatus("disconnected");
2113
2203
  setReady(false);
2114
2204
  wsRef.current = null;
2115
- for (const [, queryKey] of subscriptionMapRef.current) storeRef.current.resetToLoading(queryKey);
2205
+ for (const [, queryKey] of subscriptionMapRef.current)
2206
+ storeRef.current.resetToLoading(queryKey);
2116
2207
  for (const [, pending] of pendingRequestsRef.current) {
2117
2208
  clearTimeout(pending.timer);
2118
2209
  pending.reject(new Error("WebSocket disconnected"));
@@ -2127,6 +2218,7 @@ function ScopeConnection({
2127
2218
  };
2128
2219
  }, [roomId, wsUrl, wsPathPrefix, handleMessage, allowAnonymous, appId]);
2129
2220
  const disconnect = useCallback3(() => {
2221
+ connectTokenRef.current++;
2130
2222
  if (reconnectTimeoutRef.current) {
2131
2223
  clearTimeout(reconnectTimeoutRef.current);
2132
2224
  reconnectTimeoutRef.current = null;
@@ -2178,33 +2270,43 @@ function ScopeConnection({
2178
2270
  binaryHandlersRef.current.delete(handler);
2179
2271
  };
2180
2272
  }, []);
2181
- const registerYjsJoinHandler = useCallback3((docKey, handler) => {
2182
- if (!yjsJoinHandlersRef.current.has(docKey)) yjsJoinHandlersRef.current.set(docKey, /* @__PURE__ */ new Set());
2183
- yjsJoinHandlersRef.current.get(docKey).add(handler);
2184
- return () => {
2185
- const handlers = yjsJoinHandlersRef.current.get(docKey);
2186
- if (handlers) {
2187
- handlers.delete(handler);
2188
- if (handlers.size === 0) yjsJoinHandlersRef.current.delete(docKey);
2189
- }
2190
- };
2191
- }, []);
2192
- const sendConfirmed = useCallback3((message, timeoutMs = 1e4) => {
2193
- const ws = wsRef.current;
2194
- if (!ws || ws.readyState !== WebSocket.OPEN) return Promise.reject(new Error("WebSocket not connected"));
2195
- const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2196
- return new Promise((resolve, reject) => {
2197
- const timer = setTimeout(() => {
2198
- pendingRequestsRef.current.delete(requestId);
2199
- reject(new Error("Mutation confirmation timed out"));
2200
- }, timeoutMs);
2201
- pendingRequestsRef.current.set(requestId, { resolve, reject, timer });
2202
- ws.send(JSON.stringify({ ...message, payload: { ...message.payload, requestId } }));
2203
- });
2204
- }, []);
2205
- const setUserRole = useCallback3((userId, role) => {
2206
- sendMessage({ type: MSG.SET_ROLE, payload: { userId, role } });
2207
- }, [sendMessage]);
2273
+ const registerYjsJoinHandler = useCallback3(
2274
+ (docKey, handler) => {
2275
+ if (!yjsJoinHandlersRef.current.has(docKey)) yjsJoinHandlersRef.current.set(docKey, /* @__PURE__ */ new Set());
2276
+ yjsJoinHandlersRef.current.get(docKey).add(handler);
2277
+ return () => {
2278
+ const handlers = yjsJoinHandlersRef.current.get(docKey);
2279
+ if (handlers) {
2280
+ handlers.delete(handler);
2281
+ if (handlers.size === 0) yjsJoinHandlersRef.current.delete(docKey);
2282
+ }
2283
+ };
2284
+ },
2285
+ []
2286
+ );
2287
+ const sendConfirmed = useCallback3(
2288
+ (message, timeoutMs = 1e4) => {
2289
+ const ws = wsRef.current;
2290
+ if (!ws || ws.readyState !== WebSocket.OPEN)
2291
+ return Promise.reject(new Error("WebSocket not connected"));
2292
+ const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
2293
+ return new Promise((resolve, reject) => {
2294
+ const timer = setTimeout(() => {
2295
+ pendingRequestsRef.current.delete(requestId);
2296
+ reject(new Error("Mutation confirmation timed out"));
2297
+ }, timeoutMs);
2298
+ pendingRequestsRef.current.set(requestId, { resolve, reject, timer });
2299
+ ws.send(JSON.stringify({ ...message, payload: { ...message.payload, requestId } }));
2300
+ });
2301
+ },
2302
+ []
2303
+ );
2304
+ const setUserRole = useCallback3(
2305
+ (userId, role) => {
2306
+ sendMessage({ type: MSG.SET_ROLE, payload: { userId, role } });
2307
+ },
2308
+ [sendMessage]
2309
+ );
2208
2310
  const requestUserList = useCallback3(() => {
2209
2311
  sendMessage({ type: MSG.USER_LIST, payload: {} });
2210
2312
  }, [sendMessage]);
@@ -2237,7 +2339,10 @@ function ScopeConnection({
2237
2339
  const registeredSchemasRef = useRef5(null);
2238
2340
  if (!isolated && registry && schemas !== registeredSchemasRef.current) {
2239
2341
  if (registeredSchemasRef.current) {
2240
- registry.unregister(scopeIdRef.current, registeredSchemasRef.current.map((s) => s.name));
2342
+ registry.unregister(
2343
+ scopeIdRef.current,
2344
+ registeredSchemasRef.current.map((s) => s.name)
2345
+ );
2241
2346
  }
2242
2347
  const names = schemas.map((s) => s.name);
2243
2348
  if (names.length > 0) registry.register(scopeIdRef.current, names, scopeEntryRef.current);
@@ -2245,57 +2350,61 @@ function ScopeConnection({
2245
2350
  }
2246
2351
  useEffect5(() => {
2247
2352
  if (isolated || !registry) return;
2353
+ const scopeId = scopeIdRef.current;
2248
2354
  return () => {
2249
2355
  const names = (registeredSchemasRef.current ?? []).map((s) => s.name);
2250
- if (names.length > 0) registry.unregister(scopeIdRef.current, names);
2356
+ if (names.length > 0) registry.unregister(scopeId, names);
2251
2357
  };
2252
2358
  }, [isolated, registry, roomId]);
2253
2359
  const registeredCollections = useMemo3(() => new Set(schemas.map((s) => s.name)), [schemas]);
2254
- const value = useMemo3(() => ({
2255
- store: storeRef.current,
2256
- roomId,
2257
- registeredCollections,
2258
- userProfile: auth?.userProfile ?? null,
2259
- userProfileLoading: auth?.userProfileLoading ?? false,
2260
- refetchUserProfile: auth?.refetchUserProfile ?? (async () => {
2360
+ const value = useMemo3(
2361
+ () => ({
2362
+ store: storeRef.current,
2363
+ roomId,
2364
+ registeredCollections,
2365
+ userProfile: auth?.userProfile ?? null,
2366
+ userProfileLoading: auth?.userProfileLoading ?? false,
2367
+ refetchUserProfile: auth?.refetchUserProfile ?? (async () => {
2368
+ }),
2369
+ roomRole,
2370
+ allUsers,
2371
+ usersLoaded,
2372
+ status,
2373
+ ready,
2374
+ discoveredSchemas,
2375
+ setUserRole,
2376
+ requestUserList,
2377
+ registerSubscription,
2378
+ unregisterSubscription,
2379
+ sendMessage,
2380
+ sendBinary,
2381
+ onBinaryMessage,
2382
+ registerYjsJoinHandler,
2383
+ sendConfirmed
2261
2384
  }),
2262
- roomRole,
2263
- allUsers,
2264
- usersLoaded,
2265
- status,
2266
- ready,
2267
- discoveredSchemas,
2268
- setUserRole,
2269
- requestUserList,
2270
- registerSubscription,
2271
- unregisterSubscription,
2272
- sendMessage,
2273
- sendBinary,
2274
- onBinaryMessage,
2275
- registerYjsJoinHandler,
2276
- sendConfirmed
2277
- }), [
2278
- roomId,
2279
- registeredCollections,
2280
- auth?.userProfile,
2281
- auth?.userProfileLoading,
2282
- auth?.refetchUserProfile,
2283
- roomRole,
2284
- allUsers,
2285
- usersLoaded,
2286
- status,
2287
- ready,
2288
- discoveredSchemas,
2289
- setUserRole,
2290
- requestUserList,
2291
- registerSubscription,
2292
- unregisterSubscription,
2293
- sendMessage,
2294
- sendBinary,
2295
- onBinaryMessage,
2296
- registerYjsJoinHandler,
2297
- sendConfirmed
2298
- ]);
2385
+ [
2386
+ roomId,
2387
+ registeredCollections,
2388
+ auth?.userProfile,
2389
+ auth?.userProfileLoading,
2390
+ auth?.refetchUserProfile,
2391
+ roomRole,
2392
+ allUsers,
2393
+ usersLoaded,
2394
+ status,
2395
+ ready,
2396
+ discoveredSchemas,
2397
+ setUserRole,
2398
+ requestUserList,
2399
+ registerSubscription,
2400
+ unregisterSubscription,
2401
+ sendMessage,
2402
+ sendBinary,
2403
+ onBinaryMessage,
2404
+ registerYjsJoinHandler,
2405
+ sendConfirmed
2406
+ ]
2407
+ );
2299
2408
  if (!children) return null;
2300
2409
  return /* @__PURE__ */ jsx7(RecordContext.Provider, { value, children });
2301
2410
  }
@@ -2375,8 +2484,11 @@ function useQuery(collection, options) {
2375
2484
  orderBy: options?.orderBy,
2376
2485
  orderDir: options?.orderDir,
2377
2486
  limit: options?.limit
2378
- // eslint-disable-next-line react-hooks/exhaustive-deps
2487
+ // Keyed by the serialized optionsKey rather than the raw options object
2488
+ // (a fresh object each render) so the query — and its WS subscription —
2489
+ // is only rebuilt when the options actually change.
2379
2490
  }),
2491
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on serialized optionsKey, not raw options.where/orderBy/orderDir/limit objects, to avoid rebuilding the WS subscription every render
2380
2492
  [collection, optionsKey]
2381
2493
  );
2382
2494
  const queryKey = useMemo5(() => JSON.stringify(query), [query]);
@@ -2404,13 +2516,10 @@ function useQuery(collection, options) {
2404
2516
  (onStoreChange) => store.subscribe(queryKey, onStoreChange),
2405
2517
  [store, queryKey]
2406
2518
  );
2407
- const getSnapshot = useCallback4(
2408
- () => store.getSnapshot(queryKey),
2409
- [store, queryKey]
2410
- );
2411
- const records = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
2412
- const status = store.getStatus(queryKey);
2413
- const error = store.getError(queryKey);
2519
+ const getSnapshot = useCallback4(() => store.getSnapshot(queryKey), [store, queryKey]);
2520
+ const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
2521
+ const records = snapshot.records;
2522
+ const { status, error } = snapshot;
2414
2523
  return useMemo5(() => ({ records, status, error }), [records, status, error]);
2415
2524
  }
2416
2525
 
@@ -2560,7 +2669,11 @@ function useYjsField(collection, recordId, fieldName) {
2560
2669
  }, [doc, awareness, docKey, sendBinary, onBinaryMessage]);
2561
2670
  useEffect8(() => {
2562
2671
  if (!ready) return;
2563
- const handleAwarenessUpdate = ({ added, updated, removed }) => {
2672
+ const handleAwarenessUpdate = ({
2673
+ added,
2674
+ updated,
2675
+ removed
2676
+ }) => {
2564
2677
  const changedClients = added.concat(updated).concat(removed);
2565
2678
  if (changedClients.length === 0) return;
2566
2679
  const encoder = createEncoder();
@@ -2576,16 +2689,16 @@ function useYjsField(collection, recordId, fieldName) {
2576
2689
  }, [awareness, docKey, sendBinary, ready]);
2577
2690
  useEffect8(() => {
2578
2691
  if (!ready) {
2579
- console.log("[YJS] Not ready, resetting:", docKey);
2692
+ debugLog("[YJS] Not ready, resetting:", docKey);
2580
2693
  setSynced(false);
2581
2694
  setCanWrite(false);
2582
2695
  return;
2583
2696
  }
2584
- console.log("[YJS] Joining:", docKey);
2697
+ debugLog("[YJS] Joining:", docKey);
2585
2698
  sendMessage({ type: MSG.YJS_JOIN, payload: { collection, recordId, fieldName } });
2586
2699
  return () => {
2587
2700
  awareness.setLocalState(null);
2588
- console.log("[YJS] Leaving:", docKey);
2701
+ debugLog("[YJS] Leaving:", docKey);
2589
2702
  sendMessage({ type: MSG.YJS_LEAVE, payload: { collection, recordId, fieldName } });
2590
2703
  };
2591
2704
  }, [ready, collection, recordId, fieldName, sendMessage, docKey, awareness]);
@@ -2621,17 +2734,20 @@ function useYjsText(collection, recordId, fieldName) {
2621
2734
  }
2622
2735
  isLocalUpdateRef.current = false;
2623
2736
  }, [synced, updateCount]);
2624
- const setText = useCallback6((value) => {
2625
- setTextState(value);
2626
- if (yTextRef.current && canWrite) {
2627
- isLocalUpdateRef.current = true;
2628
- doc.transact(() => {
2629
- const yText = yTextRef.current;
2630
- yText.delete(0, yText.length);
2631
- yText.insert(0, value);
2632
- });
2633
- }
2634
- }, [doc, canWrite]);
2737
+ const setText = useCallback6(
2738
+ (value) => {
2739
+ setTextState(value);
2740
+ if (yTextRef.current && canWrite) {
2741
+ isLocalUpdateRef.current = true;
2742
+ doc.transact(() => {
2743
+ const yText = yTextRef.current;
2744
+ yText.delete(0, yText.length);
2745
+ yText.insert(0, value);
2746
+ });
2747
+ }
2748
+ },
2749
+ [doc, canWrite]
2750
+ );
2635
2751
  return { text, setText, synced, canWrite };
2636
2752
  }
2637
2753
 
@@ -2753,6 +2869,7 @@ function useYjsRoom(docId, fieldName) {
2753
2869
  const connect = async () => {
2754
2870
  if (!alive) return;
2755
2871
  const token = await getAuthToken();
2872
+ if (!alive) return;
2756
2873
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
2757
2874
  const baseUrl = `${protocol}//${window.location.host}`;
2758
2875
  const url = new URL(`/ws/yjs/${encodeURIComponent(docId)}`, baseUrl);
@@ -2837,7 +2954,11 @@ function useYjsRoom(docId, fieldName) {
2837
2954
  };
2838
2955
  }, [doc, awareness, docId, yText]);
2839
2956
  useEffect10(() => {
2840
- const handler = ({ added, updated, removed }) => {
2957
+ const handler = ({
2958
+ added,
2959
+ updated,
2960
+ removed
2961
+ }) => {
2841
2962
  if (applyingRemoteAwarenessRef.current) return;
2842
2963
  const changedClients = added.concat(updated).concat(removed);
2843
2964
  if (changedClients.length === 0) return;
@@ -2867,16 +2988,19 @@ function useYjsRoom(docId, fieldName) {
2867
2988
  doc.off("update", handler);
2868
2989
  };
2869
2990
  }, [doc]);
2870
- const setText = useCallback9((value) => {
2871
- setTextState(value);
2872
- if (!canWrite) return;
2873
- isLocalRef.current = true;
2874
- doc.transact(() => {
2875
- yText.delete(0, yText.length);
2876
- yText.insert(0, value);
2877
- });
2878
- isLocalRef.current = false;
2879
- }, [doc, yText, canWrite]);
2991
+ const setText = useCallback9(
2992
+ (value) => {
2993
+ setTextState(value);
2994
+ if (!canWrite) return;
2995
+ isLocalRef.current = true;
2996
+ doc.transact(() => {
2997
+ yText.delete(0, yText.length);
2998
+ yText.insert(0, value);
2999
+ });
3000
+ isLocalRef.current = false;
3001
+ },
3002
+ [doc, yText, canWrite]
3003
+ );
2880
3004
  return { doc, awareness, text, setText, synced, canWrite };
2881
3005
  }
2882
3006
 
@@ -3609,7 +3733,7 @@ function useMessages(channelId, options) {
3609
3733
  if (parentMessageId) data.parentMessageId = parentMessageId;
3610
3734
  return create(data);
3611
3735
  },
3612
- [channelId, create]
3736
+ [channelId, user, create]
3613
3737
  );
3614
3738
  const edit = useCallback16(
3615
3739
  (messageId, newContent) => {
@@ -3629,7 +3753,15 @@ function useMessages(channelId, options) {
3629
3753
  },
3630
3754
  [put]
3631
3755
  );
3632
- return { messages: records, status, error, send, edit, remove, softDelete };
3756
+ return {
3757
+ messages: records,
3758
+ status,
3759
+ error,
3760
+ send,
3761
+ edit,
3762
+ remove,
3763
+ softDelete
3764
+ };
3633
3765
  }
3634
3766
 
3635
3767
  // src/client/messaging/useChannels.ts
@@ -3651,7 +3783,7 @@ function useChannels() {
3651
3783
  archived: false
3652
3784
  });
3653
3785
  },
3654
- [createMutation]
3786
+ [user, createMutation]
3655
3787
  );
3656
3788
  const archive = useCallback17(
3657
3789
  (channelId) => {
@@ -3665,7 +3797,15 @@ function useChannels() {
3665
3797
  },
3666
3798
  [put]
3667
3799
  );
3668
- return { channels: records, status, error, create, archive, update, remove };
3800
+ return {
3801
+ channels: records,
3802
+ status,
3803
+ error,
3804
+ create,
3805
+ archive,
3806
+ update,
3807
+ remove
3808
+ };
3669
3809
  }
3670
3810
 
3671
3811
  // src/client/messaging/useReactions.ts
@@ -3788,6 +3928,7 @@ function useConversation(opts) {
3788
3928
  });
3789
3929
  opts?.onMessageSent?.(content, parentMessageId);
3790
3930
  },
3931
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- depend on the precise opts?.onMessageSent callback, not the whole opts object, to avoid recreating send when unrelated opts fields change
3791
3932
  [messageMut, user?.id, opts?.onMessageSent]
3792
3933
  );
3793
3934
  const edit = useCallback21(
@@ -3886,7 +4027,7 @@ function formatFullTimestamp(dateStr) {
3886
4027
 
3887
4028
  // src/client/messaging/conversation-utils.ts
3888
4029
  function getConversationDisplayName(conversation, currentUserId, getUser) {
3889
- const { Type, Name, ParticipantIds } = conversation.data;
4030
+ const { Type, Name } = conversation.data;
3890
4031
  if (Type !== "dm" && Type !== "group") {
3891
4032
  return Name || "Untitled";
3892
4033
  }
@@ -3945,6 +4086,13 @@ function parseMessageMetadata(raw) {
3945
4086
 
3946
4087
  // src/client/directory/useConversations.ts
3947
4088
  import { useCallback as useCallback22, useMemo as useMemo13, useRef as useRef14 } from "react";
4089
+ var defaultConvFields = {
4090
+ Visibility: "public",
4091
+ Status: "active",
4092
+ AssigneeId: "",
4093
+ LinkedRef: "",
4094
+ MessageCount: 0
4095
+ };
3948
4096
  function useConversations() {
3949
4097
  const { records: conversations, status } = useQuery("conversations");
3950
4098
  const { create, put } = useMutations("conversations");
@@ -3980,13 +4128,6 @@ function useConversations() {
3980
4128
  },
3981
4129
  [stateRecords]
3982
4130
  );
3983
- const defaultConvFields = {
3984
- Visibility: "public",
3985
- Status: "active",
3986
- AssigneeId: "",
3987
- LinkedRef: "",
3988
- MessageCount: 0
3989
- };
3990
4131
  const createChannel = useCallback22(
3991
4132
  async (name, description) => {
3992
4133
  const existing = conversations.find((r) => r.data.Name === name && r.data.Type !== "dm");
@@ -4748,10 +4889,13 @@ function PlatformProvider({ children }) {
4748
4889
  }
4749
4890
  };
4750
4891
  ws.onclose = () => {
4892
+ clearInterval(pingInterval);
4893
+ pingInterval = void 0;
4751
4894
  if (mounted) {
4752
4895
  retryTimer = setTimeout(connect, 3e3);
4753
4896
  }
4754
4897
  };
4898
+ clearInterval(pingInterval);
4755
4899
  pingInterval = setInterval(() => {
4756
4900
  if (ws?.readyState === WebSocket.OPEN) ws.send("ping");
4757
4901
  }, 3e4);
@@ -4915,7 +5059,8 @@ async function request(method, endpoint, body, options) {
4915
5059
  if ("data" in payload) {
4916
5060
  return { success: true, data: payload.data };
4917
5061
  }
4918
- const { success: _success, ...rest } = payload;
5062
+ const rest = { ...payload };
5063
+ delete rest.success;
4919
5064
  return { success: true, data: rest };
4920
5065
  } catch (err) {
4921
5066
  if (err instanceof DOMException && err.name === "AbortError") {
@@ -5556,7 +5701,7 @@ function usePagedResource(fetchPage, deps, options = {}) {
5556
5701
  controllerRef.current = null;
5557
5702
  }
5558
5703
  }
5559
- }, [autoRetryOnError, clearAutoRetryTimer, enabled, fetchPage, keepPreviousData, pageSize, resetCooldown, setFailureCooldown]);
5704
+ }, [autoRetryOnError, clearAutoRetryTimer, enabled, fetchPage, keepPreviousData, maxItemsPerPage, pageSize, resetCooldown, setFailureCooldown]);
5560
5705
  const loadMore = useCallback27(() => {
5561
5706
  if (!state.hasMore || state.isLoadingInitial || state.isLoadingMore || state.isRefreshing) return;
5562
5707
  void startLoad(nextPageRef.current, "auto");