deepspace 0.7.0 → 0.8.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 (63) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +22 -8
  3. package/dist/cli.js +9812 -4674
  4. package/dist/cli.js.map +1 -1
  5. package/dist/feature-installer.js +638 -0
  6. package/dist/feature-installer.js.map +1 -0
  7. package/dist/index.d.ts +107 -117
  8. package/dist/index.js +610 -779
  9. package/dist/index.js.map +1 -1
  10. package/dist/server.d.ts +403 -167
  11. package/dist/server.js +498 -367
  12. package/dist/server.js.map +1 -1
  13. package/dist/testing.d.ts +115 -0
  14. package/dist/testing.js +3 -3
  15. package/dist/testing.js.map +1 -1
  16. package/dist/worker.d.ts +397 -160
  17. package/dist/worker.js +506 -367
  18. package/dist/worker.js.map +1 -1
  19. package/features/ai-chat/feature.json +2 -0
  20. package/features/ai-chat/src/AiChatPage.tsx +2 -3
  21. package/features/ai-chat/src/ChatPanel.messages.tsx +344 -0
  22. package/features/ai-chat/src/ChatPanel.stream.ts +397 -0
  23. package/features/ai-chat/src/ChatPanel.tsx +158 -862
  24. package/features/ai-chat/src/__tests__/ChatPanel.stream.test.ts +163 -0
  25. package/features/canvas/src/CanvasListPage.tsx +1 -1
  26. package/features/docs/feature.json +4 -8
  27. package/features/docs/src/DocsEditorPage.tsx +33 -326
  28. package/features/docs/src/DocsListPage.tsx +33 -29
  29. package/features/docs/src/InviteDialog.tsx +11 -40
  30. package/features/docs/src/__tests__/docs-presence-access.test.ts +71 -0
  31. package/features/docs/src/docs-library-types.ts +28 -3
  32. package/features/docs/src/docs-ui.css +1 -4
  33. package/features/docs/src/use-docs-presence-access.ts +330 -0
  34. package/features/file-attachments/feature.json +2 -2
  35. package/features/file-attachments/src/AttachmentPreview.tsx +428 -0
  36. package/features/file-attachments/src/AttachmentsPage.tsx +100 -462
  37. package/features/file-attachments/src/useFilePreview.ts +54 -68
  38. package/features/file-manager/src/FileManagerPage.tsx +1 -1
  39. package/features/integration-test/src/integration.spec.ts +1 -1
  40. package/features/items/feature.json +1 -5
  41. package/features/items/src/ItemsPage.tsx +207 -135
  42. package/features/landing/feature.json +3 -0
  43. package/features/landing/src/LandingPage.tsx +4 -18
  44. package/features/leaderboard/feature.json +1 -5
  45. package/features/leaderboard/src/LeaderboardPage.tsx +333 -198
  46. package/features/messaging/feature.json +0 -4
  47. package/features/messaging/src/ChatMultiPage.tsx +1 -3
  48. package/features/messaging/src/ChatPage.tsx +1 -3
  49. package/features/messaging/src/components/chat/MessageActionSheet.tsx +40 -16
  50. package/features/search-bar/feature.json +8 -3
  51. package/features/search-bar/src/SearchOverlay.tsx +108 -252
  52. package/features/search-bar/src/__tests__/search-model.test.ts +40 -0
  53. package/features/search-bar/src/search-model.ts +223 -0
  54. package/features/tasks/src/TasksPage.tsx +3 -3
  55. package/features/topbar/src/Topbar.tsx +0 -17
  56. package/package.json +2 -3
  57. package/features/docs/src/docs-favorites.ts +0 -18
  58. package/features/docs/src/use-yjs-room-with-awareness.ts +0 -289
  59. package/features/file-attachments/src/attachments-constants.ts +0 -9
  60. package/features/items/src/items-constants.ts +0 -10
  61. package/features/leaderboard/src/leaderboard-constants.ts +0 -21
  62. package/features/messaging/src/components/chat/ReactionPicker.tsx +0 -2
  63. package/scripts/add-feature.cjs +0 -807
package/dist/index.js CHANGED
@@ -9,6 +9,16 @@ var DEV_CONFIG = {
9
9
  mainAppUrl: "http://localhost:5173",
10
10
  dashboardUrl: "http://localhost:5174"
11
11
  };
12
+ var STAGING_CONFIG = {
13
+ name: "staging",
14
+ apiUrl: "https://api.deepspacesites.com",
15
+ platformWorkerUrl: "https://platform.deepspacesites.com",
16
+ authUrl: "https://auth.deepspacesites.com",
17
+ authSignInUrl: "https://auth.deepspacesites.com/login/social",
18
+ authSignUpUrl: "https://auth.deepspacesites.com/login/social",
19
+ mainAppUrl: "https://spacestest.com",
20
+ dashboardUrl: "https://dashboard.deepspacesites.com"
21
+ };
12
22
  var PROD_CONFIG = {
13
23
  name: "prod",
14
24
  apiUrl: "https://api-worker.deep.space",
@@ -24,6 +34,7 @@ function parseEnv(value) {
24
34
  if (!value) return null;
25
35
  const v = value.toLowerCase();
26
36
  if (v === "dev" || v === "development") return "dev";
37
+ if (v === "staging") return "staging";
27
38
  if (v === "prod" || v === "production") return "prod";
28
39
  return null;
29
40
  }
@@ -64,6 +75,10 @@ function detectEnvironment() {
64
75
  cachedEnvironment = "dev";
65
76
  return "dev";
66
77
  }
78
+ if (hostname === "spacestest.com" || hostname.endsWith(".spacestest.com") || hostname === "deepspacesites.com" || hostname.endsWith(".deepspacesites.com")) {
79
+ cachedEnvironment = "staging";
80
+ return "staging";
81
+ }
67
82
  if (hostname === "app.space" || hostname.endsWith(".app.space") || hostname === "deep.space" || hostname.endsWith(".deep.space")) {
68
83
  cachedEnvironment = "prod";
69
84
  return "prod";
@@ -74,7 +89,7 @@ function detectEnvironment() {
74
89
  }
75
90
  function getEnvironmentConfig() {
76
91
  const env = detectEnvironment();
77
- return env === "dev" ? DEV_CONFIG : PROD_CONFIG;
92
+ return env === "dev" ? DEV_CONFIG : env === "staging" ? STAGING_CONFIG : PROD_CONFIG;
78
93
  }
79
94
  function getApiUrl() {
80
95
  return getEnvironmentConfig().apiUrl;
@@ -371,6 +386,14 @@ var Awareness = class {
371
386
  this.emit({ added: [], updated: [], removed });
372
387
  }
373
388
  }
389
+ /** Release all state and listeners owned by this awareness instance. */
390
+ destroy() {
391
+ this.setLocalState(null);
392
+ this.states.clear();
393
+ this.meta.clear();
394
+ this.updateListeners.clear();
395
+ this.changeListeners.clear();
396
+ }
374
397
  };
375
398
  function encodeAwarenessMessage(awareness, clientIds) {
376
399
  const encoder = createEncoder();
@@ -1156,6 +1179,229 @@ import {
1156
1179
  useMemo as useMemo2
1157
1180
  } from "react";
1158
1181
 
1182
+ // src/client/storage/ScopeRegistry.tsx
1183
+ import { createContext, useContext, useCallback, useRef as useRef3, useMemo } from "react";
1184
+ import { jsx as jsx5 } from "react/jsx-runtime";
1185
+ var ScopeRegistryContext = createContext(null);
1186
+ function useScopeRegistry() {
1187
+ return useContext(ScopeRegistryContext);
1188
+ }
1189
+ function ScopeRegistryProvider({ children }) {
1190
+ const mapRef = useRef3(/* @__PURE__ */ new Map());
1191
+ const register = useCallback((scopeId, collections, entry) => {
1192
+ for (const name of collections) {
1193
+ mapRef.current.set(name, { scopeId, entry });
1194
+ }
1195
+ }, []);
1196
+ const unregister = useCallback((scopeId, collections) => {
1197
+ for (const name of collections) {
1198
+ const existing = mapRef.current.get(name);
1199
+ if (existing?.scopeId === scopeId) {
1200
+ mapRef.current.delete(name);
1201
+ }
1202
+ }
1203
+ }, []);
1204
+ const resolve = useCallback((collection) => {
1205
+ return mapRef.current.get(collection)?.entry ?? null;
1206
+ }, []);
1207
+ const value = useMemo(() => ({ register, unregister, resolve }), [register, unregister, resolve]);
1208
+ return /* @__PURE__ */ jsx5(ScopeRegistryContext.Provider, { value, children });
1209
+ }
1210
+
1211
+ // src/client/storage/context.tsx
1212
+ import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1213
+ var RecordContext = createContext2(null);
1214
+ function useRecordContext() {
1215
+ const ctx = useContext2(RecordContext);
1216
+ if (!ctx) {
1217
+ throw new Error("useRecordContext must be used within a RecordScope");
1218
+ }
1219
+ return ctx;
1220
+ }
1221
+ var RecordAuthContext = createContext2(null);
1222
+ function useRecordAuth() {
1223
+ return useContext2(RecordAuthContext);
1224
+ }
1225
+ function RecordProviderState({
1226
+ children,
1227
+ fetchUser,
1228
+ allowAnonymous = false,
1229
+ getAuthToken: getAuthTokenProp,
1230
+ onPermissionError: onPermissionErrorProp,
1231
+ onValidationError: onValidationErrorProp
1232
+ }) {
1233
+ const [userProfile, setUserProfile] = useState3(null);
1234
+ const [userProfileLoading, setUserProfileLoading] = useState3(true);
1235
+ const onPermissionErrorRef = useRef4(onPermissionErrorProp);
1236
+ onPermissionErrorRef.current = onPermissionErrorProp;
1237
+ const onValidationErrorRef = useRef4(onValidationErrorProp);
1238
+ onValidationErrorRef.current = onValidationErrorProp;
1239
+ const onPermissionError = useCallback2((title, detail) => {
1240
+ onPermissionErrorRef.current?.(title, detail);
1241
+ }, []);
1242
+ const onValidationError = useCallback2((title, detail) => {
1243
+ onValidationErrorRef.current?.(title, detail);
1244
+ }, []);
1245
+ const refetchUserProfile = useCallback2(async () => {
1246
+ try {
1247
+ const profile = await fetchUser();
1248
+ setUserProfile(profile);
1249
+ } catch (err) {
1250
+ console.error("[RecordProvider] Failed to refetch user profile:", err);
1251
+ }
1252
+ }, [fetchUser]);
1253
+ useEffect4(() => {
1254
+ let mounted = true;
1255
+ setUserProfileLoading(true);
1256
+ fetchUser().then((profile) => {
1257
+ if (mounted) setUserProfile(profile);
1258
+ }).finally(() => {
1259
+ if (mounted) setUserProfileLoading(false);
1260
+ });
1261
+ const interval = setInterval(() => {
1262
+ if (mounted) {
1263
+ fetchUser().then((p) => {
1264
+ if (mounted) setUserProfile(p);
1265
+ }).catch(() => {
1266
+ });
1267
+ }
1268
+ }, 3e4);
1269
+ return () => {
1270
+ mounted = false;
1271
+ clearInterval(interval);
1272
+ };
1273
+ }, [fetchUser]);
1274
+ const authValue = useMemo2(
1275
+ () => ({
1276
+ userProfile,
1277
+ userProfileLoading,
1278
+ refetchUserProfile,
1279
+ allowAnonymous,
1280
+ getAuthToken: getAuthTokenProp,
1281
+ onPermissionError,
1282
+ onValidationError
1283
+ }),
1284
+ [
1285
+ userProfile,
1286
+ userProfileLoading,
1287
+ refetchUserProfile,
1288
+ allowAnonymous,
1289
+ getAuthTokenProp,
1290
+ onPermissionError,
1291
+ onValidationError
1292
+ ]
1293
+ );
1294
+ return /* @__PURE__ */ jsx6(RecordAuthContext.Provider, { value: authValue, children: /* @__PURE__ */ jsx6(ScopeRegistryProvider, { children }) });
1295
+ }
1296
+ var seenWriteErrors = /* @__PURE__ */ new Set();
1297
+ function defaultOnWriteError(error) {
1298
+ const key = JSON.stringify([error.kind, error.title, error.detail]);
1299
+ if (seenWriteErrors.has(key)) return;
1300
+ if (seenWriteErrors.size >= 100) seenWriteErrors.clear();
1301
+ seenWriteErrors.add(key);
1302
+ console.error(
1303
+ `[deepspace] Write rejected \u2014 ${error.title}: ${error.detail}
1304
+ This error reached no UI. Pass onWriteError to <RecordProvider> (e.g. wire it to a toast) so users see rejected writes. (Further repeats of this error are suppressed.)`
1305
+ );
1306
+ }
1307
+ var warnedSignedOutNull = false;
1308
+ function SignedOutDiagnostic() {
1309
+ return /* @__PURE__ */ jsxs4(
1310
+ "div",
1311
+ {
1312
+ role: "alert",
1313
+ "data-deepspace-diagnostic": "signed-out",
1314
+ style: {
1315
+ margin: "24px auto",
1316
+ maxWidth: 560,
1317
+ padding: "16px 20px",
1318
+ border: "1px solid #d97706",
1319
+ borderRadius: 8,
1320
+ background: "#fffbeb",
1321
+ color: "#78350f",
1322
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
1323
+ fontSize: 13,
1324
+ lineHeight: 1.5
1325
+ },
1326
+ children: [
1327
+ /* @__PURE__ */ jsx6("strong", { children: "DeepSpace: nothing rendered because you're signed out." }),
1328
+ /* @__PURE__ */ jsxs4("p", { style: { margin: "8px 0 0" }, children: [
1329
+ "This page is inside a ",
1330
+ "<RecordProvider>",
1331
+ " without ",
1332
+ /* @__PURE__ */ jsx6("code", { children: "allowAnonymous" }),
1333
+ ", which renders nothing for signed-out visitors. Either add",
1334
+ " ",
1335
+ /* @__PURE__ */ jsx6("code", { children: "<RecordProvider allowAnonymous>" }),
1336
+ " to show the page publicly, or require sign-in before this point (e.g. a protected route / AuthGate)."
1337
+ ] }),
1338
+ /* @__PURE__ */ jsx6("p", { style: { margin: "8px 0 0", opacity: 0.8 }, children: "This notice only appears on localhost \u2014 a deployed app renders nothing here." })
1339
+ ]
1340
+ }
1341
+ );
1342
+ }
1343
+ function RecordProvider({
1344
+ children,
1345
+ allowAnonymous = false,
1346
+ getAuthToken: getAuthTokenProp,
1347
+ onWriteError = defaultOnWriteError
1348
+ }) {
1349
+ const { isLoaded, isSignedIn } = useAuth();
1350
+ const fetchUser = useCallback2(async () => {
1351
+ if (!isSignedIn) return null;
1352
+ const token = await getAuthToken();
1353
+ if (!token) return null;
1354
+ try {
1355
+ const parts = token.split(".");
1356
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
1357
+ return {
1358
+ id: payload.sub,
1359
+ name: payload.name ?? "",
1360
+ email: payload.email ?? "",
1361
+ imageUrl: payload.image ?? void 0
1362
+ };
1363
+ } catch {
1364
+ return null;
1365
+ }
1366
+ }, [isSignedIn]);
1367
+ const getAuthTokenFn = useCallback2(async () => {
1368
+ if (getAuthTokenProp) return getAuthTokenProp();
1369
+ if (!isSignedIn) return null;
1370
+ return getAuthToken();
1371
+ }, [getAuthTokenProp, isSignedIn]);
1372
+ if (!isLoaded) {
1373
+ return /* @__PURE__ */ jsx6(Fragment5, { children: null });
1374
+ }
1375
+ if (!isSignedIn && !allowAnonymous) {
1376
+ if (isLocalDevHost()) {
1377
+ if (!warnedSignedOutNull) {
1378
+ warnedSignedOutNull = true;
1379
+ console.warn(
1380
+ "[deepspace] RecordProvider rendered nothing: signed out and allowAnonymous is off. Add `allowAnonymous` to show this page to signed-out visitors, or require sign-in before this point."
1381
+ );
1382
+ }
1383
+ return /* @__PURE__ */ jsx6(SignedOutDiagnostic, {});
1384
+ }
1385
+ return /* @__PURE__ */ jsx6(Fragment5, { children: null });
1386
+ }
1387
+ const onPermissionError = (title, detail) => onWriteError({ kind: "permission", title, detail });
1388
+ const onValidationError = (title, detail) => onWriteError({ kind: "validation", title, detail });
1389
+ return /* @__PURE__ */ jsx6(
1390
+ RecordProviderState,
1391
+ {
1392
+ fetchUser,
1393
+ allowAnonymous,
1394
+ getAuthToken: getAuthTokenFn,
1395
+ onPermissionError,
1396
+ onValidationError,
1397
+ children
1398
+ }
1399
+ );
1400
+ }
1401
+
1402
+ // src/client/storage/RecordScope.tsx
1403
+ import { useEffect as useEffect5, useState as useState4, useCallback as useCallback3, useRef as useRef5, useMemo as useMemo3 } from "react";
1404
+
1159
1405
  // src/client/storage/store.ts
1160
1406
  var EMPTY_RECORDS = [];
1161
1407
  var EMPTY_SNAPSHOT = { status: "loading", records: EMPTY_RECORDS };
@@ -1457,9 +1703,6 @@ var RecordSocket = class {
1457
1703
  } catch {
1458
1704
  }
1459
1705
  if (this.connectToken !== myToken || this.destroyed) return;
1460
- for (const [key, value] of Object.entries(config.extraParams ?? {})) {
1461
- params.set(key, value);
1462
- }
1463
1706
  if (identityTag !== void 0) this.identityTag = identityTag;
1464
1707
  const protocol = typeof window !== "undefined" && window.location.protocol === "https:" ? "wss:" : "ws:";
1465
1708
  const baseUrl = config.wsUrl?.replace(/^http/, "ws") ?? `${protocol}//${typeof window !== "undefined" ? window.location.host : ""}`;
@@ -1590,588 +1833,111 @@ var RecordSocket = class {
1590
1833
  }
1591
1834
  };
1592
1835
  }
1593
- // ── message dispatch ──────────────────────────────────────────────────────
1594
- resubscribeAll(ws) {
1595
- for (const [subscriptionId, queryKey] of this.subscriptions) {
1596
- try {
1597
- const query = JSON.parse(queryKey);
1598
- ws.send(JSON.stringify({ type: MSG.SUBSCRIBE, payload: { subscriptionId, query } }));
1599
- } catch {
1600
- }
1601
- }
1602
- }
1603
- handleMessage(event) {
1604
- if (event.data instanceof ArrayBuffer) {
1605
- this.binaryHandlers.forEach((h) => h(event.data));
1606
- return;
1607
- }
1608
- let msg;
1609
- try {
1610
- msg = JSON.parse(event.data);
1611
- } catch {
1612
- console.error("[RecordSocket] Failed to parse message");
1613
- return;
1614
- }
1615
- const { type, payload } = msg;
1616
- const { store, listeners } = this.config;
1617
- switch (type) {
1618
- case MSG.USER_INFO: {
1619
- const { role } = payload;
1620
- listeners.onRole(role);
1621
- listeners.onReady(true);
1622
- this.reconnectAttempt = 0;
1623
- this.sendMessage({ type: MSG.USER_LIST, payload: {} });
1624
- break;
1625
- }
1626
- case MSG.USER_LIST:
1627
- listeners.onUsers(payload.users);
1628
- break;
1629
- case MSG.QUERY_RESULT: {
1630
- const { subscriptionId, records } = payload;
1631
- const queryKey = this.subscriptions.get(subscriptionId);
1632
- if (queryKey !== void 0) store.setQueryResult(queryKey, records);
1633
- break;
1634
- }
1635
- case MSG.RECORD_CHANGE: {
1636
- const { collection, record, changeType } = payload;
1637
- for (const queryKey of this.subscriptions.values()) {
1638
- try {
1639
- const query = JSON.parse(queryKey);
1640
- if (query.collection !== collection) continue;
1641
- const matches = recordMatchesWhere(record, query.where);
1642
- const exists = store.hasRecord(queryKey, record.recordId);
1643
- if (changeType === "delete") {
1644
- if (exists) store.applyChange(queryKey, record, "delete");
1645
- } else if (changeType === "create") {
1646
- if (matches) store.applyChange(queryKey, record, "create");
1647
- } else {
1648
- if (matches && exists) store.applyChange(queryKey, record, "update");
1649
- else if (matches && !exists) store.applyChange(queryKey, record, "create");
1650
- else if (!matches && exists) store.applyChange(queryKey, record, "delete");
1651
- }
1652
- } catch {
1653
- }
1654
- }
1655
- break;
1656
- }
1657
- case MSG.ERROR: {
1658
- const { subscriptionId, error } = payload;
1659
- if (subscriptionId) {
1660
- const queryKey = this.subscriptions.get(subscriptionId);
1661
- if (queryKey !== void 0) store.setError(queryKey, error);
1662
- } else {
1663
- const parsed = parseServerError(error);
1664
- if (parsed.isPermissionError) listeners.onPermissionError?.(parsed.title, parsed.detail);
1665
- else listeners.onValidationError?.(parsed.title, parsed.detail);
1666
- }
1667
- break;
1668
- }
1669
- case MSG.YJS_JOIN: {
1670
- const { collection, recordId, fieldName, canWrite } = payload;
1671
- const docKey = `${collection}:${recordId}:${fieldName}`;
1672
- this.yjsJoinHandlers.get(docKey)?.forEach((h) => h(canWrite));
1673
- break;
1674
- }
1675
- case MSG.ACK: {
1676
- const { requestId, success, error, ...rest } = payload;
1677
- const pending = this.pendingRequests.get(requestId);
1678
- if (pending) {
1679
- clearTimeout(pending.timer);
1680
- this.pendingRequests.delete(requestId);
1681
- if (success) pending.resolve(rest);
1682
- else pending.reject(new Error(error || "Mutation rejected"));
1683
- }
1684
- break;
1685
- }
1686
- case MSG.LIST_SCHEMAS: {
1687
- const { schemas } = payload;
1688
- listeners.onSchemas?.(schemas ?? []);
1689
- break;
1690
- }
1691
- case MSG.RESUBSCRIBE: {
1692
- if (this.ws?.readyState === WS_OPEN) this.resubscribeAll(this.ws);
1693
- break;
1694
- }
1695
- }
1696
- }
1697
- };
1698
-
1699
- // src/client/storage/ScopeRegistry.tsx
1700
- import { createContext, useContext, useCallback, useRef as useRef3, useMemo } from "react";
1701
- import { jsx as jsx5 } from "react/jsx-runtime";
1702
- var ScopeRegistryContext = createContext(null);
1703
- function useScopeRegistry() {
1704
- return useContext(ScopeRegistryContext);
1705
- }
1706
- function ScopeRegistryProvider({ children }) {
1707
- const mapRef = useRef3(/* @__PURE__ */ new Map());
1708
- const register = useCallback((scopeId, collections, entry) => {
1709
- for (const name of collections) {
1710
- mapRef.current.set(name, { scopeId, entry });
1711
- }
1712
- }, []);
1713
- const unregister = useCallback((scopeId, collections) => {
1714
- for (const name of collections) {
1715
- const existing = mapRef.current.get(name);
1716
- if (existing?.scopeId === scopeId) {
1717
- mapRef.current.delete(name);
1718
- }
1719
- }
1720
- }, []);
1721
- const resolve = useCallback((collection) => {
1722
- return mapRef.current.get(collection)?.entry ?? null;
1723
- }, []);
1724
- const value = useMemo(() => ({ register, unregister, resolve }), [register, unregister, resolve]);
1725
- return /* @__PURE__ */ jsx5(ScopeRegistryContext.Provider, { value, children });
1726
- }
1727
-
1728
- // src/client/storage/context.tsx
1729
- import { Fragment as Fragment5, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
1730
- var RecordContext = createContext2(null);
1731
- function useRecordContext() {
1732
- const ctx = useContext2(RecordContext);
1733
- if (!ctx) {
1734
- throw new Error("useRecordContext must be used within a RecordProvider");
1735
- }
1736
- return ctx;
1737
- }
1738
- var RecordAuthContext = createContext2(null);
1739
- function useRecordAuth() {
1740
- return useContext2(RecordAuthContext);
1741
- }
1742
- function RecordProviderCore({
1743
- roomId,
1744
- wsUrl,
1745
- children,
1746
- fetchUser,
1747
- allowAnonymous = false,
1748
- getAuthToken: getAuthTokenProp,
1749
- onPermissionError: onPermissionErrorProp,
1750
- onValidationError: onValidationErrorProp
1751
- }) {
1752
- const [userProfile, setUserProfile] = useState3(null);
1753
- const [userProfileLoading, setUserProfileLoading] = useState3(true);
1754
- const [roomRole, setRoomRole] = useState3(null);
1755
- const [allUsers, setAllUsers] = useState3([]);
1756
- const [usersLoaded, setUsersLoaded] = useState3(false);
1757
- const [status, setStatus] = useState3("connecting");
1758
- const [ready, setReady] = useState3(false);
1759
- const onPermissionErrorRef = useRef4(
1760
- void 0
1761
- );
1762
- const onValidationErrorRef = useRef4(
1763
- void 0
1764
- );
1765
- onPermissionErrorRef.current = onPermissionErrorProp;
1766
- onValidationErrorRef.current = onValidationErrorProp;
1767
- const storeRef = useRef4(new RecordStore());
1768
- const socketRef = useRef4(null);
1769
- const subscriptionsRef = useRef4(/* @__PURE__ */ new Map());
1770
- const binaryHandlersRef = useRef4(/* @__PURE__ */ new Set());
1771
- const yjsJoinHandlersRef = useRef4(/* @__PURE__ */ new Map());
1772
- const userProfileRef = useRef4(null);
1773
- userProfileRef.current = userProfile;
1774
- const getAuthTokenRef = useRef4(getAuthTokenProp);
1775
- getAuthTokenRef.current = getAuthTokenProp;
1776
- const refetchUserProfile = useCallback2(async () => {
1777
- try {
1778
- const profile = await fetchUser();
1779
- setUserProfile(profile);
1780
- } catch (err) {
1781
- console.error("[RecordProvider] Failed to refetch user profile:", err);
1782
- }
1783
- }, [fetchUser]);
1784
- useEffect4(() => {
1785
- let mounted = true;
1786
- setUserProfileLoading(true);
1787
- fetchUser().then((profile) => {
1788
- if (mounted) setUserProfile(profile);
1789
- }).finally(() => {
1790
- if (mounted) setUserProfileLoading(false);
1791
- });
1792
- const interval = setInterval(() => {
1793
- if (mounted)
1794
- fetchUser().then((p) => {
1795
- if (mounted) setUserProfile(p);
1796
- }).catch(() => {
1797
- });
1798
- }, 3e4);
1799
- return () => {
1800
- mounted = false;
1801
- clearInterval(interval);
1802
- };
1803
- }, [fetchUser]);
1804
- const userProfileId = userProfile?.id ?? null;
1805
- useEffect4(() => {
1806
- if (userProfileLoading) return;
1807
- const socket = new RecordSocket({
1808
- roomId,
1809
- store: storeRef.current,
1810
- subscriptions: subscriptionsRef.current,
1811
- binaryHandlers: binaryHandlersRef.current,
1812
- yjsJoinHandlers: yjsJoinHandlersRef.current,
1813
- getToken: () => (getAuthTokenRef.current ?? getAuthToken)(),
1814
- wsUrl,
1815
- log: (event, detail) => debugLog(`[WS] ${event}`, detail),
1816
- listeners: {
1817
- onStatus: setStatus,
1818
- onReady: setReady,
1819
- onRole: setRoomRole,
1820
- onUsers: (users) => {
1821
- setAllUsers(users);
1822
- setUsersLoaded(true);
1823
- },
1824
- onPermissionError: (title, detail) => onPermissionErrorRef.current?.(title, detail),
1825
- onValidationError: (title, detail) => onValidationErrorRef.current?.(title, detail)
1826
- }
1827
- });
1828
- socketRef.current = socket;
1829
- if (userProfileId || allowAnonymous) {
1830
- void socket.connect(userProfileId ?? "");
1831
- }
1832
- return () => {
1833
- socketRef.current = null;
1834
- socket.destroy();
1835
- setStatus("connecting");
1836
- setReady(false);
1837
- };
1838
- }, [roomId, wsUrl, userProfileId, userProfileLoading, allowAnonymous]);
1839
- useEffect4(() => {
1840
- const onVisible = () => {
1841
- if (document.visibilityState === "visible" && !socketRef.current?.isOpen) {
1842
- debugLog("[WS] Reconnecting after visibility change");
1843
- socketRef.current?.resetBackoff();
1844
- void socketRef.current?.connect();
1845
- }
1846
- };
1847
- document.addEventListener("visibilitychange", onVisible);
1848
- return () => document.removeEventListener("visibilitychange", onVisible);
1849
- }, []);
1850
- const sendMessage = useCallback2((message) => {
1851
- socketRef.current?.sendMessage(message);
1852
- }, []);
1853
- const setUserRole = useCallback2(
1854
- (userId, role) => {
1855
- sendMessage({ type: MSG.SET_ROLE, payload: { userId, role } });
1856
- },
1857
- [sendMessage]
1858
- );
1859
- const requestUserList = useCallback2(() => {
1860
- sendMessage({ type: MSG.USER_LIST, payload: {} });
1861
- }, [sendMessage]);
1862
- const registerSubscription = useCallback2((subscriptionId, queryKey) => {
1863
- subscriptionsRef.current.set(subscriptionId, queryKey);
1864
- }, []);
1865
- const unregisterSubscription = useCallback2((subscriptionId) => {
1866
- subscriptionsRef.current.delete(subscriptionId);
1867
- }, []);
1868
- const sendBinary = useCallback2((data) => {
1869
- socketRef.current?.sendBinary(data);
1870
- }, []);
1871
- const onBinaryMessage = useCallback2((handler) => {
1872
- binaryHandlersRef.current.add(handler);
1873
- return () => {
1874
- binaryHandlersRef.current.delete(handler);
1875
- };
1876
- }, []);
1877
- const registerYjsJoinHandler = useCallback2(
1878
- (docKey, handler) => {
1879
- const handlers = yjsJoinHandlersRef.current;
1880
- if (!handlers.has(docKey)) handlers.set(docKey, /* @__PURE__ */ new Set());
1881
- handlers.get(docKey).add(handler);
1882
- return () => {
1883
- const set = handlers.get(docKey);
1884
- if (set) {
1885
- set.delete(handler);
1886
- if (set.size === 0) handlers.delete(docKey);
1887
- }
1888
- };
1889
- },
1890
- []
1891
- );
1892
- const sendConfirmed = useCallback2(
1893
- (message, timeoutMs = 1e4) => {
1894
- const socket = socketRef.current;
1895
- if (!socket) return Promise.reject(new Error("WebSocket not connected"));
1896
- return socket.sendConfirmed(message, timeoutMs);
1897
- },
1898
- []
1899
- );
1900
- const value = useMemo2(
1901
- () => ({
1902
- store: storeRef.current,
1903
- roomId,
1904
- userProfile,
1905
- userProfileLoading,
1906
- refetchUserProfile,
1907
- roomRole,
1908
- allUsers,
1909
- usersLoaded,
1910
- status,
1911
- ready,
1912
- setUserRole,
1913
- requestUserList,
1914
- registerSubscription,
1915
- unregisterSubscription,
1916
- sendMessage,
1917
- sendBinary,
1918
- onBinaryMessage,
1919
- registerYjsJoinHandler,
1920
- sendConfirmed
1921
- }),
1922
- [
1923
- roomId,
1924
- userProfile,
1925
- userProfileLoading,
1926
- refetchUserProfile,
1927
- roomRole,
1928
- allUsers,
1929
- usersLoaded,
1930
- status,
1931
- ready,
1932
- setUserRole,
1933
- requestUserList,
1934
- registerSubscription,
1935
- unregisterSubscription,
1936
- sendMessage,
1937
- sendBinary,
1938
- onBinaryMessage,
1939
- registerYjsJoinHandler,
1940
- sendConfirmed
1941
- ]
1942
- );
1943
- const onPermissionError = useCallback2((title, detail) => {
1944
- onPermissionErrorRef.current?.(title, detail);
1945
- }, []);
1946
- const onValidationError = useCallback2((title, detail) => {
1947
- onValidationErrorRef.current?.(title, detail);
1948
- }, []);
1949
- const authValue = useMemo2(
1950
- () => ({
1951
- userProfile,
1952
- userProfileLoading,
1953
- refetchUserProfile,
1954
- allowAnonymous,
1955
- getAuthToken: getAuthTokenProp,
1956
- onPermissionError,
1957
- onValidationError
1958
- }),
1959
- [
1960
- userProfile,
1961
- userProfileLoading,
1962
- refetchUserProfile,
1963
- allowAnonymous,
1964
- getAuthTokenProp,
1965
- onPermissionError,
1966
- onValidationError
1967
- ]
1968
- );
1969
- return /* @__PURE__ */ jsx6(RecordAuthContext.Provider, { value: authValue, children: /* @__PURE__ */ jsx6(ScopeRegistryProvider, { children: /* @__PURE__ */ jsx6(RecordContext.Provider, { value, children }) }) });
1970
- }
1971
- function RecordProviderAuthOnly({
1972
- children,
1973
- fetchUser,
1974
- allowAnonymous = false,
1975
- getAuthToken: getAuthTokenProp,
1976
- onPermissionError: onPermissionErrorProp,
1977
- onValidationError: onValidationErrorProp
1978
- }) {
1979
- const [userProfile, setUserProfile] = useState3(null);
1980
- const [userProfileLoading, setUserProfileLoading] = useState3(true);
1981
- const onPermissionErrorRef = useRef4(onPermissionErrorProp);
1982
- onPermissionErrorRef.current = onPermissionErrorProp;
1983
- const onValidationErrorRef = useRef4(onValidationErrorProp);
1984
- onValidationErrorRef.current = onValidationErrorProp;
1985
- const onPermissionError = useCallback2((title, detail) => {
1986
- onPermissionErrorRef.current?.(title, detail);
1987
- }, []);
1988
- const onValidationError = useCallback2((title, detail) => {
1989
- onValidationErrorRef.current?.(title, detail);
1990
- }, []);
1991
- const refetchUserProfile = useCallback2(async () => {
1992
- try {
1993
- const profile = await fetchUser();
1994
- setUserProfile(profile);
1995
- } catch (err) {
1996
- console.error("[RecordProvider] Failed to refetch user profile:", err);
1997
- }
1998
- }, [fetchUser]);
1999
- useEffect4(() => {
2000
- let mounted = true;
2001
- setUserProfileLoading(true);
2002
- fetchUser().then((profile) => {
2003
- if (mounted) setUserProfile(profile);
2004
- }).finally(() => {
2005
- if (mounted) setUserProfileLoading(false);
2006
- });
2007
- const interval = setInterval(() => {
2008
- if (mounted) {
2009
- fetchUser().then((p) => {
2010
- if (mounted) setUserProfile(p);
2011
- }).catch(() => {
2012
- });
2013
- }
2014
- }, 3e4);
2015
- return () => {
2016
- mounted = false;
2017
- clearInterval(interval);
2018
- };
2019
- }, [fetchUser]);
2020
- const authValue = useMemo2(
2021
- () => ({
2022
- userProfile,
2023
- userProfileLoading,
2024
- refetchUserProfile,
2025
- allowAnonymous,
2026
- getAuthToken: getAuthTokenProp,
2027
- onPermissionError,
2028
- onValidationError
2029
- }),
2030
- [
2031
- userProfile,
2032
- userProfileLoading,
2033
- refetchUserProfile,
2034
- allowAnonymous,
2035
- getAuthTokenProp,
2036
- onPermissionError,
2037
- onValidationError
2038
- ]
2039
- );
2040
- return /* @__PURE__ */ jsx6(RecordAuthContext.Provider, { value: authValue, children: /* @__PURE__ */ jsx6(ScopeRegistryProvider, { children }) });
2041
- }
2042
- var seenWriteErrors = /* @__PURE__ */ new Set();
2043
- function defaultOnWriteError(error) {
2044
- const key = JSON.stringify([error.kind, error.title, error.detail]);
2045
- if (seenWriteErrors.has(key)) return;
2046
- if (seenWriteErrors.size >= 100) seenWriteErrors.clear();
2047
- seenWriteErrors.add(key);
2048
- console.error(
2049
- `[deepspace] Write rejected \u2014 ${error.title}: ${error.detail}
2050
- This error reached no UI. Pass onWriteError to <RecordProvider> (e.g. wire it to a toast) so users see rejected writes. (Further repeats of this error are suppressed.)`
2051
- );
2052
- }
2053
- var warnedIgnoredSchemas = false;
2054
- var warnedSignedOutNull = false;
2055
- function SignedOutDiagnostic() {
2056
- return /* @__PURE__ */ jsxs4(
2057
- "div",
2058
- {
2059
- role: "alert",
2060
- "data-deepspace-diagnostic": "signed-out",
2061
- style: {
2062
- margin: "24px auto",
2063
- maxWidth: 560,
2064
- padding: "16px 20px",
2065
- border: "1px solid #d97706",
2066
- borderRadius: 8,
2067
- background: "#fffbeb",
2068
- color: "#78350f",
2069
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
2070
- fontSize: 13,
2071
- lineHeight: 1.5
2072
- },
2073
- children: [
2074
- /* @__PURE__ */ jsx6("strong", { children: "DeepSpace: nothing rendered because you're signed out." }),
2075
- /* @__PURE__ */ jsxs4("p", { style: { margin: "8px 0 0" }, children: [
2076
- "This page is inside a ",
2077
- "<RecordProvider>",
2078
- " without ",
2079
- /* @__PURE__ */ jsx6("code", { children: "allowAnonymous" }),
2080
- ", which renders nothing for signed-out visitors. Either add",
2081
- " ",
2082
- /* @__PURE__ */ jsx6("code", { children: "<RecordProvider allowAnonymous>" }),
2083
- " to show the page publicly, or require sign-in before this point (e.g. a protected route / AuthGate)."
2084
- ] }),
2085
- /* @__PURE__ */ jsx6("p", { style: { margin: "8px 0 0", opacity: 0.8 }, children: "This notice only appears on localhost \u2014 a deployed app renders nothing here." })
2086
- ]
1836
+ // ── message dispatch ──────────────────────────────────────────────────────
1837
+ resubscribeAll(ws) {
1838
+ for (const [subscriptionId, queryKey] of this.subscriptions) {
1839
+ try {
1840
+ const query = JSON.parse(queryKey);
1841
+ ws.send(JSON.stringify({ type: MSG.SUBSCRIBE, payload: { subscriptionId, query } }));
1842
+ } catch {
1843
+ }
2087
1844
  }
2088
- );
2089
- }
2090
- function RecordProvider({
2091
- roomId,
2092
- schemas,
2093
- wsUrl,
2094
- children,
2095
- allowAnonymous = false,
2096
- getAuthToken: getAuthTokenProp,
2097
- onWriteError = defaultOnWriteError
2098
- }) {
2099
- const { isLoaded, isSignedIn } = useAuth();
2100
- if (schemas && roomId && isLocalDevHost() && !warnedIgnoredSchemas) {
2101
- warnedIgnoredSchemas = true;
2102
- console.warn(
2103
- "[deepspace] RecordProvider received a `schemas` prop alongside `roomId`, but the single-scope provider ignores it \u2014 schemas are resolved server-side in this mode. Remove the `schemas` prop, or adopt the multi-scope shape and pass schemas to <RecordScope>."
2104
- );
2105
1845
  }
2106
- const fetchUser = useCallback2(async () => {
2107
- if (!isSignedIn) return null;
2108
- const token = await getAuthToken();
2109
- if (!token) return null;
1846
+ handleMessage(event) {
1847
+ if (event.data instanceof ArrayBuffer) {
1848
+ this.binaryHandlers.forEach((h) => h(event.data));
1849
+ return;
1850
+ }
1851
+ let msg;
2110
1852
  try {
2111
- const parts = token.split(".");
2112
- const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
2113
- return {
2114
- id: payload.sub,
2115
- name: payload.name ?? "",
2116
- email: payload.email ?? "",
2117
- imageUrl: payload.image ?? void 0
2118
- };
1853
+ msg = JSON.parse(event.data);
2119
1854
  } catch {
2120
- return null;
1855
+ console.error("[RecordSocket] Failed to parse message");
1856
+ return;
2121
1857
  }
2122
- }, [isSignedIn]);
2123
- const getAuthTokenFn = useCallback2(async () => {
2124
- if (getAuthTokenProp) return getAuthTokenProp();
2125
- if (!isSignedIn) return null;
2126
- return getAuthToken();
2127
- }, [getAuthTokenProp, isSignedIn]);
2128
- if (!isLoaded) {
2129
- return /* @__PURE__ */ jsx6(Fragment5, { children: null });
2130
- }
2131
- if (!isSignedIn && !allowAnonymous) {
2132
- if (isLocalDevHost()) {
2133
- if (!warnedSignedOutNull) {
2134
- warnedSignedOutNull = true;
2135
- console.warn(
2136
- "[deepspace] RecordProvider rendered nothing: signed out and allowAnonymous is off. Add `allowAnonymous` to show this page to signed-out visitors, or require sign-in before this point."
2137
- );
1858
+ const { type, payload } = msg;
1859
+ const { store, listeners } = this.config;
1860
+ switch (type) {
1861
+ case MSG.USER_INFO: {
1862
+ const { role } = payload;
1863
+ listeners.onRole(role);
1864
+ listeners.onReady(true);
1865
+ this.reconnectAttempt = 0;
1866
+ this.sendMessage({ type: MSG.USER_LIST, payload: {} });
1867
+ break;
2138
1868
  }
2139
- return /* @__PURE__ */ jsx6(SignedOutDiagnostic, {});
2140
- }
2141
- return /* @__PURE__ */ jsx6(Fragment5, { children: null });
2142
- }
2143
- const onPermissionError = (title, detail) => onWriteError({ kind: "permission", title, detail });
2144
- const onValidationError = (title, detail) => onWriteError({ kind: "validation", title, detail });
2145
- if (roomId) {
2146
- return /* @__PURE__ */ jsx6(
2147
- RecordProviderCore,
2148
- {
2149
- roomId,
2150
- wsUrl,
2151
- fetchUser,
2152
- allowAnonymous,
2153
- getAuthToken: getAuthTokenFn,
2154
- onPermissionError,
2155
- onValidationError,
2156
- children
1869
+ case MSG.USER_LIST:
1870
+ listeners.onUsers(payload.users);
1871
+ break;
1872
+ case MSG.QUERY_RESULT: {
1873
+ const { subscriptionId, records } = payload;
1874
+ const queryKey = this.subscriptions.get(subscriptionId);
1875
+ if (queryKey !== void 0) store.setQueryResult(queryKey, records);
1876
+ break;
1877
+ }
1878
+ case MSG.RECORD_CHANGE: {
1879
+ const { collection, record, changeType } = payload;
1880
+ for (const queryKey of this.subscriptions.values()) {
1881
+ try {
1882
+ const query = JSON.parse(queryKey);
1883
+ if (query.collection !== collection) continue;
1884
+ const matches = recordMatchesWhere(record, query.where);
1885
+ const exists = store.hasRecord(queryKey, record.recordId);
1886
+ if (changeType === "delete") {
1887
+ if (exists) store.applyChange(queryKey, record, "delete");
1888
+ } else if (changeType === "create") {
1889
+ if (matches) store.applyChange(queryKey, record, "create");
1890
+ } else {
1891
+ if (matches && exists) store.applyChange(queryKey, record, "update");
1892
+ else if (matches && !exists) store.applyChange(queryKey, record, "create");
1893
+ else if (!matches && exists) store.applyChange(queryKey, record, "delete");
1894
+ }
1895
+ } catch {
1896
+ }
1897
+ }
1898
+ break;
1899
+ }
1900
+ case MSG.ERROR: {
1901
+ const { subscriptionId, error } = payload;
1902
+ if (subscriptionId) {
1903
+ const queryKey = this.subscriptions.get(subscriptionId);
1904
+ if (queryKey !== void 0) store.setError(queryKey, error);
1905
+ } else {
1906
+ const parsed = parseServerError(error);
1907
+ if (parsed.isPermissionError) listeners.onPermissionError?.(parsed.title, parsed.detail);
1908
+ else listeners.onValidationError?.(parsed.title, parsed.detail);
1909
+ }
1910
+ break;
1911
+ }
1912
+ case MSG.YJS_JOIN: {
1913
+ const { collection, recordId, fieldName, canWrite } = payload;
1914
+ const docKey = `${collection}:${recordId}:${fieldName}`;
1915
+ this.yjsJoinHandlers.get(docKey)?.forEach((h) => h(canWrite));
1916
+ break;
1917
+ }
1918
+ case MSG.ACK: {
1919
+ const { requestId, success, error, ...rest } = payload;
1920
+ const pending = this.pendingRequests.get(requestId);
1921
+ if (pending) {
1922
+ clearTimeout(pending.timer);
1923
+ this.pendingRequests.delete(requestId);
1924
+ if (success) pending.resolve(rest);
1925
+ else pending.reject(new Error(error || "Mutation rejected"));
1926
+ }
1927
+ break;
1928
+ }
1929
+ case MSG.LIST_SCHEMAS: {
1930
+ const { schemas } = payload;
1931
+ listeners.onSchemas?.(schemas ?? []);
1932
+ break;
1933
+ }
1934
+ case MSG.RESUBSCRIBE: {
1935
+ if (this.ws?.readyState === WS_OPEN) this.resubscribeAll(this.ws);
1936
+ break;
2157
1937
  }
2158
- );
2159
- }
2160
- return /* @__PURE__ */ jsx6(
2161
- RecordProviderAuthOnly,
2162
- {
2163
- fetchUser,
2164
- allowAnonymous,
2165
- getAuthToken: getAuthTokenFn,
2166
- onPermissionError,
2167
- onValidationError,
2168
- children
2169
1938
  }
2170
- );
2171
- }
2172
-
2173
- // src/client/storage/RecordScope.tsx
2174
- import { useEffect as useEffect5, useState as useState4, useCallback as useCallback3, useRef as useRef5, useMemo as useMemo3 } from "react";
1939
+ }
1940
+ };
2175
1941
 
2176
1942
  // src/client/storage/ws-log.ts
2177
1943
  var activeCount = 0;
@@ -2186,25 +1952,14 @@ import { Fragment as Fragment6, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-run
2186
1952
  function HeadlessScope({
2187
1953
  roomId,
2188
1954
  schemas,
2189
- appId,
2190
1955
  wsUrl,
2191
1956
  wsPathPrefix
2192
1957
  }) {
2193
- return /* @__PURE__ */ jsx7(
2194
- ScopeConnection,
2195
- {
2196
- roomId,
2197
- schemas,
2198
- appId,
2199
- wsUrl,
2200
- wsPathPrefix
2201
- }
2202
- );
1958
+ return /* @__PURE__ */ jsx7(ScopeConnection, { roomId, schemas, wsUrl, wsPathPrefix });
2203
1959
  }
2204
1960
  function ScopeConnection({
2205
1961
  roomId,
2206
1962
  schemas,
2207
- appId,
2208
1963
  children,
2209
1964
  wsUrl,
2210
1965
  wsPathPrefix = "/ws",
@@ -2248,7 +2003,6 @@ function ScopeConnection({
2248
2003
  getToken: () => (getAuthTokenRef.current ?? getAuthToken)(),
2249
2004
  wsUrl,
2250
2005
  wsPathPrefix,
2251
- extraParams: { appId },
2252
2006
  log: (event) => wsLog(event, roomId),
2253
2007
  listeners: {
2254
2008
  onStatus: setStatus,
@@ -2273,7 +2027,7 @@ function ScopeConnection({
2273
2027
  setStatus("connecting");
2274
2028
  setReady(false);
2275
2029
  };
2276
- }, [roomId, wsUrl, wsPathPrefix, appId, userProfileId, userProfileLoading, allowAnonymous]);
2030
+ }, [roomId, wsUrl, wsPathPrefix, userProfileId, userProfileLoading, allowAnonymous]);
2277
2031
  useEffect5(() => {
2278
2032
  const onVisible = () => {
2279
2033
  if (document.visibilityState === "visible" && !socketRef.current?.isOpen) {
@@ -2430,7 +2184,6 @@ function RecordScope({
2430
2184
  roomId,
2431
2185
  schemas,
2432
2186
  children,
2433
- appId,
2434
2187
  sharedScopes,
2435
2188
  wsUrl,
2436
2189
  wsPathPrefix = "/ws",
@@ -2442,7 +2195,6 @@ function RecordScope({
2442
2195
  {
2443
2196
  roomId: shared.roomId,
2444
2197
  schemas: shared.schemas,
2445
- appId,
2446
2198
  wsUrl,
2447
2199
  wsPathPrefix
2448
2200
  },
@@ -2453,7 +2205,6 @@ function RecordScope({
2453
2205
  {
2454
2206
  roomId,
2455
2207
  schemas,
2456
- appId,
2457
2208
  wsUrl,
2458
2209
  wsPathPrefix,
2459
2210
  isolated,
@@ -2491,7 +2242,7 @@ function useQuery(collection, options) {
2491
2242
  const unregisterSub = preferLocal ? recordCtx.unregisterSubscription : scopeEntry?.unregisterSubscription ?? recordCtx?.unregisterSubscription;
2492
2243
  if (!store || !sendMessage || !registerSub || !unregisterSub) {
2493
2244
  throw new Error(
2494
- `useQuery('${collection}'): No scope found. Wrap in a RecordProvider (with roomId) or a RecordScope that registers this collection.`
2245
+ `useQuery('${collection}'): No scope found. Wrap this hook in a RecordScope that registers the collection.`
2495
2246
  );
2496
2247
  }
2497
2248
  const optionsKey = JSON.stringify(options ?? null);
@@ -2552,7 +2303,7 @@ function useMutations(collection) {
2552
2303
  const sendConfirmed = preferLocal ? recordCtx.sendConfirmed : scopeEntry?.sendConfirmed ?? recordCtx?.sendConfirmed;
2553
2304
  if (!sendMessage || !sendConfirmed) {
2554
2305
  throw new Error(
2555
- `useMutations('${collection}'): No scope found. Wrap in a RecordProvider (with roomId) or a RecordScope that registers this collection.`
2306
+ `useMutations('${collection}'): No scope found. Wrap this hook in a RecordScope that registers the collection.`
2556
2307
  );
2557
2308
  }
2558
2309
  const create = useCallback5(
@@ -2619,7 +2370,7 @@ function useUsers() {
2619
2370
  }
2620
2371
 
2621
2372
  // src/client/storage/hooks/useYjs.ts
2622
- import { useState as useState5, useEffect as useEffect8, useCallback as useCallback6, useRef as useRef7 } from "react";
2373
+ import { useState as useState5, useEffect as useEffect8, useLayoutEffect, useCallback as useCallback6, useRef as useRef7, useMemo as useMemo8 } from "react";
2623
2374
  import * as Y2 from "yjs";
2624
2375
  function useYjsField(collection, recordId, fieldName) {
2625
2376
  const { sendMessage, sendBinary, onBinaryMessage, registerYjsJoinHandler, ready } = useRecordContext();
@@ -2627,17 +2378,23 @@ function useYjsField(collection, recordId, fieldName) {
2627
2378
  const [canWrite, setCanWrite] = useState5(false);
2628
2379
  const [updateCount, setUpdateCount] = useState5(0);
2629
2380
  const canWriteRef = useRef7(false);
2630
- const docRef = useRef7(null);
2631
- if (!docRef.current) {
2632
- docRef.current = new Y2.Doc();
2633
- }
2634
- const doc = docRef.current;
2635
- const awarenessRef = useRef7(null);
2636
- if (!awarenessRef.current) {
2637
- awarenessRef.current = new Awareness(doc);
2638
- }
2639
- const awareness = awarenessRef.current;
2640
2381
  const docKey = `${collection}:${recordId}:${fieldName}`;
2382
+ const { doc, awareness } = useMemo8(() => {
2383
+ const nextDoc = new Y2.Doc();
2384
+ return { doc: nextDoc, awareness: new Awareness(nextDoc) };
2385
+ }, [docKey]);
2386
+ useEffect8(() => {
2387
+ return () => {
2388
+ awareness.destroy();
2389
+ doc.destroy();
2390
+ };
2391
+ }, [awareness, doc]);
2392
+ useLayoutEffect(() => {
2393
+ setSynced(false);
2394
+ setCanWrite(false);
2395
+ setUpdateCount(0);
2396
+ canWriteRef.current = false;
2397
+ }, [docKey]);
2641
2398
  useEffect8(() => {
2642
2399
  canWriteRef.current = canWrite;
2643
2400
  }, [canWrite]);
@@ -2740,40 +2497,38 @@ function useYjsField(collection, recordId, fieldName) {
2740
2497
  }
2741
2498
  function useYjsText(collection, recordId, fieldName) {
2742
2499
  const { doc, synced, canWrite, updateCount } = useYjsField(collection, recordId, fieldName);
2743
- const [text, setTextState] = useState5("");
2744
- const yTextRef = useRef7(null);
2745
- const isLocalUpdateRef = useRef7(false);
2500
+ const yText = useMemo8(() => doc.getText(fieldName), [doc, fieldName]);
2501
+ const [textState, setTextState] = useState5(() => ({ doc, value: "" }));
2502
+ const text = textState.doc === doc ? textState.value : "";
2746
2503
  useEffect8(() => {
2747
- yTextRef.current = doc.getText(fieldName);
2748
- }, [doc, fieldName]);
2749
- useEffect8(() => {
2750
- if (yTextRef.current && synced && !isLocalUpdateRef.current) {
2751
- setTextState(yTextRef.current.toString());
2752
- }
2753
- isLocalUpdateRef.current = false;
2754
- }, [synced, updateCount]);
2504
+ if (!synced) return;
2505
+ const value = yText.toString();
2506
+ setTextState(
2507
+ (current) => current.doc === doc && current.value === value ? current : { doc, value }
2508
+ );
2509
+ }, [doc, yText, synced, updateCount]);
2755
2510
  const setText = useCallback6(
2756
2511
  (value) => {
2757
- setTextState(value);
2758
- if (yTextRef.current && canWrite) {
2759
- isLocalUpdateRef.current = true;
2512
+ setTextState(
2513
+ (current) => current.doc === doc && current.value === value ? current : { doc, value }
2514
+ );
2515
+ if (canWrite) {
2760
2516
  doc.transact(() => {
2761
- const yText = yTextRef.current;
2762
2517
  yText.delete(0, yText.length);
2763
2518
  yText.insert(0, value);
2764
2519
  });
2765
2520
  }
2766
2521
  },
2767
- [doc, canWrite]
2522
+ [doc, yText, canWrite]
2768
2523
  );
2769
2524
  return { text, setText, synced, canWrite };
2770
2525
  }
2771
2526
 
2772
2527
  // src/client/storage/hooks/useUserLookup.ts
2773
- import { useMemo as useMemo8, useCallback as useCallback7 } from "react";
2528
+ import { useMemo as useMemo9, useCallback as useCallback7 } from "react";
2774
2529
  function useUserLookup() {
2775
2530
  const { users, usersLoaded } = useUsers();
2776
- const userMap = useMemo8(() => {
2531
+ const userMap = useMemo9(() => {
2777
2532
  const map = /* @__PURE__ */ new Map();
2778
2533
  for (const user of users) {
2779
2534
  map.set(user.id, {
@@ -2855,23 +2610,50 @@ function usePresence(opts) {
2855
2610
  }
2856
2611
 
2857
2612
  // src/client/storage/hooks/useYjsRoom.ts
2858
- import { useState as useState7, useEffect as useEffect10, useCallback as useCallback9, useRef as useRef8, useMemo as useMemo9 } from "react";
2613
+ import { useState as useState7, useEffect as useEffect10, useLayoutEffect as useLayoutEffect2, useCallback as useCallback9, useRef as useRef8, useMemo as useMemo10 } from "react";
2859
2614
  import * as Y3 from "yjs";
2615
+ var UPDATE_BATCH_DELAY_MS = 16;
2860
2616
  function useYjsRoom(docId, fieldName) {
2861
- const [synced, setSynced] = useState7(false);
2617
+ const [syncedDocId, setSyncedDocId] = useState7(null);
2862
2618
  const [canWrite, setCanWrite] = useState7(false);
2619
+ const [writeAuthResolved, setWriteAuthResolved] = useState7(false);
2863
2620
  const [text, setTextState] = useState7("");
2864
2621
  const [, setUpdateCount] = useState7(0);
2865
- const docRef = useRef8(null);
2866
- if (!docRef.current) docRef.current = new Y3.Doc();
2867
- const doc = docRef.current;
2868
- const awarenessRef = useRef8(null);
2869
- if (!awarenessRef.current) awarenessRef.current = new Awareness(doc);
2870
- const awareness = awarenessRef.current;
2622
+ const synced = syncedDocId === docId;
2623
+ const { doc, awareness } = useMemo10(() => {
2624
+ const nextDoc = new Y3.Doc();
2625
+ return { doc: nextDoc, awareness: new Awareness(nextDoc) };
2626
+ }, [docId]);
2871
2627
  const wsRef = useRef8(null);
2872
2628
  const isLocalRef = useRef8(false);
2873
2629
  const applyingRemoteAwarenessRef = useRef8(false);
2874
- const yText = useMemo9(() => doc.getText(fieldName), [doc, fieldName]);
2630
+ const pendingUpdateRef = useRef8(null);
2631
+ const updateBatchTimerRef = useRef8(null);
2632
+ const yText = useMemo10(() => doc.getText(fieldName), [doc, fieldName]);
2633
+ const flushPendingUpdate = useCallback9((socket = wsRef.current) => {
2634
+ if (updateBatchTimerRef.current) clearTimeout(updateBatchTimerRef.current);
2635
+ updateBatchTimerRef.current = null;
2636
+ const update = pendingUpdateRef.current;
2637
+ pendingUpdateRef.current = null;
2638
+ if (!update || !socket || socket.readyState !== WebSocket.OPEN) return;
2639
+ const enc = createEncoder();
2640
+ writeVarUint(enc, MSG_SYNC);
2641
+ writeVarUint(enc, MSG_SYNC_UPDATE);
2642
+ writeVarUint8Array(enc, update);
2643
+ socket.send(toUint8Array(enc).buffer);
2644
+ }, []);
2645
+ useEffect10(() => {
2646
+ return () => {
2647
+ awareness.destroy();
2648
+ doc.destroy();
2649
+ };
2650
+ }, [awareness, doc]);
2651
+ useLayoutEffect2(() => {
2652
+ setSyncedDocId(null);
2653
+ setCanWrite(false);
2654
+ setWriteAuthResolved(false);
2655
+ setTextState("");
2656
+ }, [docId]);
2875
2657
  useEffect10(() => {
2876
2658
  const observer = () => {
2877
2659
  if (isLocalRef.current) return;
@@ -2884,8 +2666,15 @@ function useYjsRoom(docId, fieldName) {
2884
2666
  let ws = null;
2885
2667
  let reconnectTimer = null;
2886
2668
  let alive = true;
2669
+ const renewLocalAwareness = () => {
2670
+ const localState = awareness.getLocalState();
2671
+ if (localState) awareness.setLocalState(localState);
2672
+ };
2887
2673
  const connect = async () => {
2888
2674
  if (!alive) return;
2675
+ setCanWrite(false);
2676
+ setWriteAuthResolved(false);
2677
+ renewLocalAwareness();
2889
2678
  const token = await getAuthToken();
2890
2679
  if (!alive) return;
2891
2680
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
@@ -2898,13 +2687,17 @@ function useYjsRoom(docId, fieldName) {
2898
2687
  wsRef.current = ws;
2899
2688
  ws.onopen = () => {
2900
2689
  wsLog("connected", `yjs:${docId}`);
2901
- setSynced(false);
2690
+ setSyncedDocId(null);
2691
+ renewLocalAwareness();
2902
2692
  };
2903
2693
  ws.onmessage = (event) => {
2904
2694
  if (typeof event.data === "string") {
2905
2695
  try {
2906
2696
  const msg = JSON.parse(event.data);
2907
- if (msg.type === "auth" && typeof msg.canWrite === "boolean") setCanWrite(msg.canWrite);
2697
+ if (msg.type === "auth" && typeof msg.canWrite === "boolean") {
2698
+ setCanWrite(msg.canWrite);
2699
+ setWriteAuthResolved(true);
2700
+ }
2908
2701
  } catch {
2909
2702
  }
2910
2703
  return;
@@ -2923,13 +2716,13 @@ function useYjsRoom(docId, fieldName) {
2923
2716
  writeVarUint(enc, MSG_SYNC_STEP2);
2924
2717
  writeVarUint8Array(enc, diff);
2925
2718
  ws?.send(toUint8Array(enc).buffer);
2926
- setSynced(true);
2719
+ setSyncedDocId(docId);
2927
2720
  break;
2928
2721
  }
2929
2722
  case MSG_SYNC_STEP2: {
2930
2723
  Y3.applyUpdate(doc, payload, "server");
2931
2724
  setTextState(yText.toString());
2932
- setSynced(true);
2725
+ setSyncedDocId(docId);
2933
2726
  break;
2934
2727
  }
2935
2728
  case MSG_SYNC_UPDATE: {
@@ -2944,6 +2737,7 @@ function useYjsRoom(docId, fieldName) {
2944
2737
  applyingRemoteAwarenessRef.current = true;
2945
2738
  try {
2946
2739
  handleAwarenessMessage(awareness, data);
2740
+ } catch {
2947
2741
  } finally {
2948
2742
  applyingRemoteAwarenessRef.current = false;
2949
2743
  }
@@ -2952,7 +2746,9 @@ function useYjsRoom(docId, fieldName) {
2952
2746
  ws.onclose = () => {
2953
2747
  wsLog("disconnected", `yjs:${docId}`);
2954
2748
  wsRef.current = null;
2955
- setSynced(false);
2749
+ setSyncedDocId(null);
2750
+ setCanWrite(false);
2751
+ setWriteAuthResolved(false);
2956
2752
  if (alive) reconnectTimer = setTimeout(connect, 1e3);
2957
2753
  };
2958
2754
  ws.onerror = () => ws?.close();
@@ -2962,6 +2758,7 @@ function useYjsRoom(docId, fieldName) {
2962
2758
  wsLog("closing", `yjs:${docId}`);
2963
2759
  alive = false;
2964
2760
  if (reconnectTimer) clearTimeout(reconnectTimer);
2761
+ flushPendingUpdate(ws);
2965
2762
  if (ws) {
2966
2763
  ws.onclose = null;
2967
2764
  ws.onmessage = null;
@@ -2970,7 +2767,7 @@ function useYjsRoom(docId, fieldName) {
2970
2767
  }
2971
2768
  wsRef.current = null;
2972
2769
  };
2973
- }, [doc, awareness, docId, yText]);
2770
+ }, [doc, awareness, docId, flushPendingUpdate, yText]);
2974
2771
  useEffect10(() => {
2975
2772
  const handler = ({
2976
2773
  added,
@@ -2995,17 +2792,15 @@ function useYjsRoom(docId, fieldName) {
2995
2792
  if (origin === "server") return;
2996
2793
  const ws = wsRef.current;
2997
2794
  if (!ws || ws.readyState !== WebSocket.OPEN) return;
2998
- const enc = createEncoder();
2999
- writeVarUint(enc, MSG_SYNC);
3000
- writeVarUint(enc, MSG_SYNC_UPDATE);
3001
- writeVarUint8Array(enc, update);
3002
- ws.send(toUint8Array(enc).buffer);
2795
+ pendingUpdateRef.current = pendingUpdateRef.current ? Y3.mergeUpdates([pendingUpdateRef.current, update]) : update;
2796
+ updateBatchTimerRef.current ??= setTimeout(flushPendingUpdate, UPDATE_BATCH_DELAY_MS);
3003
2797
  };
3004
2798
  doc.on("update", handler);
3005
2799
  return () => {
3006
2800
  doc.off("update", handler);
2801
+ flushPendingUpdate();
3007
2802
  };
3008
- }, [doc]);
2803
+ }, [doc, flushPendingUpdate]);
3009
2804
  const setText = useCallback9(
3010
2805
  (value) => {
3011
2806
  setTextState(value);
@@ -3019,7 +2814,7 @@ function useYjsRoom(docId, fieldName) {
3019
2814
  },
3020
2815
  [doc, yText, canWrite]
3021
2816
  );
3022
- return { doc, awareness, text, setText, synced, canWrite };
2817
+ return { doc, awareness, text, setText, synced, canWrite, writeAuthResolved };
3023
2818
  }
3024
2819
 
3025
2820
  // src/client/storage/hooks/useGameRoom.ts
@@ -3568,17 +3363,20 @@ function toConnectionStatus(queryStatus) {
3568
3363
  }
3569
3364
 
3570
3365
  // src/client/storage/useR2Files.ts
3571
- import { useCallback as useCallback15, useState as useState13, useMemo as useMemo10 } from "react";
3366
+ import { useCallback as useCallback15, useState as useState13, useMemo as useMemo11 } from "react";
3572
3367
  function resolveFile(fileOrKey) {
3573
3368
  if (typeof fileOrKey === "string") {
3574
3369
  return { key: fileOrKey, name: void 0 };
3575
3370
  }
3576
3371
  return { key: fileOrKey.key, name: fileOrKey.originalName };
3577
3372
  }
3373
+ function encodeKeyPath(key) {
3374
+ return key.split("/").map(encodeURIComponent).join("/");
3375
+ }
3578
3376
  function useR2Files(options) {
3579
3377
  const [isUploading, setIsUploading] = useState13(false);
3580
3378
  const scope = options?.scope ?? "self";
3581
- const scopeParams = useMemo10(() => {
3379
+ const scopeParams = useMemo11(() => {
3582
3380
  const params = new URLSearchParams();
3583
3381
  params.set("scope", scope);
3584
3382
  return params.toString();
@@ -3590,104 +3388,128 @@ function useR2Files(options) {
3590
3388
  }
3591
3389
  return {};
3592
3390
  }, []);
3593
- const upload = useCallback15(async (file, name) => {
3594
- setIsUploading(true);
3595
- try {
3596
- const headers = await authHeaders();
3597
- const formData = new FormData();
3598
- formData.append("file", file);
3599
- if (name) formData.append("name", name);
3600
- const response = await fetch(`/api/files/upload?${scopeParams}`, {
3601
- method: "POST",
3602
- headers,
3603
- body: formData
3604
- });
3605
- return await response.json();
3606
- } catch (err) {
3607
- return { success: false, error: err instanceof Error ? err.message : "Upload failed" };
3608
- } finally {
3609
- setIsUploading(false);
3610
- }
3611
- }, [scopeParams, authHeaders]);
3612
- const uploadBase64 = useCallback15(async (base64Data, name, mimeType) => {
3613
- setIsUploading(true);
3614
- try {
3615
- const headers = await authHeaders();
3616
- const response = await fetch(`/api/files/upload?${scopeParams}`, {
3617
- method: "POST",
3618
- headers: { ...headers, "Content-Type": "application/json" },
3619
- body: JSON.stringify({ data: base64Data, name, mimeType })
3620
- });
3621
- return await response.json();
3622
- } catch (err) {
3623
- return { success: false, error: err instanceof Error ? err.message : "Upload failed" };
3624
- } finally {
3625
- setIsUploading(false);
3626
- }
3627
- }, [scopeParams, authHeaders]);
3628
- const deleteFile = useCallback15(async (fileOrKey) => {
3629
- const { key } = resolveFile(fileOrKey);
3630
- try {
3631
- const headers = await authHeaders();
3632
- const response = await fetch(`/api/files/${key}?${scopeParams}`, { method: "DELETE", headers });
3633
- return await response.json();
3634
- } catch (err) {
3635
- return { success: false, error: err instanceof Error ? err.message : "Delete failed" };
3636
- }
3637
- }, [scopeParams, authHeaders]);
3638
- const list = useCallback15(async (prefix) => {
3639
- try {
3640
- const headers = await authHeaders();
3641
- const params = new URLSearchParams(scopeParams);
3642
- if (prefix) params.set("prefix", prefix);
3643
- const response = await fetch(`/api/files?${params.toString()}`, { headers });
3644
- const result = await response.json();
3645
- return result.files || [];
3646
- } catch {
3647
- return [];
3648
- }
3649
- }, [scopeParams, authHeaders]);
3650
- const downloadFile = useCallback15(async (fileOrKey, fileName) => {
3651
- const { key, name: infoName } = resolveFile(fileOrKey);
3652
- try {
3391
+ const upload = useCallback15(
3392
+ async (file, name) => {
3393
+ setIsUploading(true);
3394
+ try {
3395
+ const headers = await authHeaders();
3396
+ const formData = new FormData();
3397
+ formData.append("file", file);
3398
+ if (name) formData.append("name", name);
3399
+ const response = await fetch(`/api/files/upload?${scopeParams}`, {
3400
+ method: "POST",
3401
+ headers,
3402
+ body: formData
3403
+ });
3404
+ return await response.json();
3405
+ } catch (err) {
3406
+ return { success: false, error: err instanceof Error ? err.message : "Upload failed" };
3407
+ } finally {
3408
+ setIsUploading(false);
3409
+ }
3410
+ },
3411
+ [scopeParams, authHeaders]
3412
+ );
3413
+ const uploadBase64 = useCallback15(
3414
+ async (base64Data, name, mimeType) => {
3415
+ setIsUploading(true);
3416
+ try {
3417
+ const headers = await authHeaders();
3418
+ const response = await fetch(`/api/files/upload?${scopeParams}`, {
3419
+ method: "POST",
3420
+ headers: { ...headers, "Content-Type": "application/json" },
3421
+ body: JSON.stringify({ data: base64Data, name, mimeType })
3422
+ });
3423
+ return await response.json();
3424
+ } catch (err) {
3425
+ return { success: false, error: err instanceof Error ? err.message : "Upload failed" };
3426
+ } finally {
3427
+ setIsUploading(false);
3428
+ }
3429
+ },
3430
+ [scopeParams, authHeaders]
3431
+ );
3432
+ const deleteFile = useCallback15(
3433
+ async (fileOrKey) => {
3434
+ const { key } = resolveFile(fileOrKey);
3435
+ try {
3436
+ const headers = await authHeaders();
3437
+ const response = await fetch(`/api/files/${encodeKeyPath(key)}?${scopeParams}`, {
3438
+ method: "DELETE",
3439
+ headers
3440
+ });
3441
+ return await response.json();
3442
+ } catch (err) {
3443
+ return { success: false, error: err instanceof Error ? err.message : "Delete failed" };
3444
+ }
3445
+ },
3446
+ [scopeParams, authHeaders]
3447
+ );
3448
+ const list = useCallback15(
3449
+ async (prefix) => {
3450
+ try {
3451
+ const headers = await authHeaders();
3452
+ const params = new URLSearchParams(scopeParams);
3453
+ if (prefix) params.set("prefix", prefix);
3454
+ const response = await fetch(`/api/files?${params.toString()}`, { headers });
3455
+ const result = await response.json();
3456
+ return result.files || [];
3457
+ } catch {
3458
+ return [];
3459
+ }
3460
+ },
3461
+ [scopeParams, authHeaders]
3462
+ );
3463
+ const downloadFile = useCallback15(
3464
+ async (fileOrKey, fileName) => {
3465
+ const { key, name: infoName } = resolveFile(fileOrKey);
3466
+ try {
3467
+ const headers = await authHeaders();
3468
+ const response = await fetch(`/api/files/${encodeKeyPath(key)}?${scopeParams}`, { headers });
3469
+ if (!response.ok) {
3470
+ const body = await response.json().catch(() => null);
3471
+ return { success: false, error: body?.error ?? `Download failed (${response.status})` };
3472
+ }
3473
+ const disposition = response.headers.get("Content-Disposition");
3474
+ const dispositionName = disposition?.match(/filename="(.+?)"/)?.[1];
3475
+ const fallbackName = key.split("/").pop() ?? "download";
3476
+ const resolvedName = fileName ?? infoName ?? dispositionName ?? fallbackName;
3477
+ const blob = await response.blob();
3478
+ const blobUrl = URL.createObjectURL(blob);
3479
+ const anchor = document.createElement("a");
3480
+ anchor.href = blobUrl;
3481
+ anchor.download = resolvedName;
3482
+ document.body.appendChild(anchor);
3483
+ anchor.click();
3484
+ document.body.removeChild(anchor);
3485
+ URL.revokeObjectURL(blobUrl);
3486
+ return { success: true };
3487
+ } catch (err) {
3488
+ return { success: false, error: err instanceof Error ? err.message : "Download failed" };
3489
+ }
3490
+ },
3491
+ [scopeParams, authHeaders]
3492
+ );
3493
+ const readFile = useCallback15(
3494
+ async (fileOrKey) => {
3495
+ const { key } = resolveFile(fileOrKey);
3653
3496
  const headers = await authHeaders();
3654
- const response = await fetch(`/api/files/${key}?${scopeParams}`, { headers });
3497
+ const response = await fetch(`/api/files/${encodeKeyPath(key)}?${scopeParams}`, { headers });
3655
3498
  if (!response.ok) {
3656
- const body = await response.json().catch(() => null);
3657
- return { success: false, error: body?.error ?? `Download failed (${response.status})` };
3499
+ const body = await response.text().catch(() => "");
3500
+ throw new Error(`Failed to read file (${response.status}): ${body}`);
3658
3501
  }
3659
- const disposition = response.headers.get("Content-Disposition");
3660
- const dispositionName = disposition?.match(/filename="(.+?)"/)?.[1];
3661
- const fallbackName = key.split("/").pop() ?? "download";
3662
- const resolvedName = fileName ?? infoName ?? dispositionName ?? fallbackName;
3663
- const blob = await response.blob();
3664
- const blobUrl = URL.createObjectURL(blob);
3665
- const anchor = document.createElement("a");
3666
- anchor.href = blobUrl;
3667
- anchor.download = resolvedName;
3668
- document.body.appendChild(anchor);
3669
- anchor.click();
3670
- document.body.removeChild(anchor);
3671
- URL.revokeObjectURL(blobUrl);
3672
- return { success: true };
3673
- } catch (err) {
3674
- return { success: false, error: err instanceof Error ? err.message : "Download failed" };
3675
- }
3676
- }, [scopeParams, authHeaders]);
3677
- const readFile = useCallback15(async (fileOrKey) => {
3678
- const { key } = resolveFile(fileOrKey);
3679
- const headers = await authHeaders();
3680
- const response = await fetch(`/api/files/${key}?${scopeParams}`, { headers });
3681
- if (!response.ok) {
3682
- const body = await response.text().catch(() => "");
3683
- throw new Error(`Failed to read file (${response.status}): ${body}`);
3684
- }
3685
- return response;
3686
- }, [scopeParams, authHeaders]);
3687
- const getUrl = useCallback15((fileOrKey) => {
3688
- const { key } = resolveFile(fileOrKey);
3689
- return `/api/files/${key}?${scopeParams}`;
3690
- }, [scopeParams]);
3502
+ return response;
3503
+ },
3504
+ [scopeParams, authHeaders]
3505
+ );
3506
+ const getUrl = useCallback15(
3507
+ (fileOrKey) => {
3508
+ const { key } = resolveFile(fileOrKey);
3509
+ return `/api/files/${encodeKeyPath(key)}?${scopeParams}`;
3510
+ },
3511
+ [scopeParams]
3512
+ );
3691
3513
  return { upload, uploadBase64, deleteFile, downloadFile, readFile, list, getUrl, isUploading };
3692
3514
  }
3693
3515
 
@@ -3727,9 +3549,9 @@ function getUserColor(userId, palette = DEFAULT_USER_COLORS) {
3727
3549
  }
3728
3550
 
3729
3551
  // src/client/messaging/useMessages.ts
3730
- import { useCallback as useCallback16, useMemo as useMemo11 } from "react";
3552
+ import { useCallback as useCallback16, useMemo as useMemo12 } from "react";
3731
3553
  function useMessages(channelId, options) {
3732
- const where = useMemo11(() => {
3554
+ const where = useMemo12(() => {
3733
3555
  if (!channelId) return { channelId: "__none__" };
3734
3556
  const w = { channelId };
3735
3557
  if (options?.parentMessageId !== void 0) {
@@ -3925,7 +3747,7 @@ function useReadReceipts() {
3925
3747
  }
3926
3748
 
3927
3749
  // src/client/messaging/useConversation.ts
3928
- import { useCallback as useCallback21, useMemo as useMemo12 } from "react";
3750
+ import { useCallback as useCallback21, useMemo as useMemo13 } from "react";
3929
3751
  function useConversation(opts) {
3930
3752
  const { user } = useUser();
3931
3753
  const { ready } = useRecordContext();
@@ -3983,7 +3805,7 @@ function useConversation(opts) {
3983
3805
  },
3984
3806
  [reactionMut, allReactions, user?.id]
3985
3807
  );
3986
- return useMemo12(
3808
+ return useMemo13(
3987
3809
  () => ({
3988
3810
  messages: allMessages,
3989
3811
  reactions: allReactions,
@@ -4103,7 +3925,7 @@ function parseMessageMetadata(raw) {
4103
3925
  }
4104
3926
 
4105
3927
  // src/client/directory/useConversations.ts
4106
- import { useCallback as useCallback22, useMemo as useMemo13, useRef as useRef14 } from "react";
3928
+ import { useCallback as useCallback22, useMemo as useMemo14, useRef as useRef14 } from "react";
4107
3929
  var defaultConvFields = {
4108
3930
  Visibility: "public",
4109
3931
  Status: "active",
@@ -4124,19 +3946,19 @@ function useConversations() {
4124
3946
  recentCreationsRef.current.delete(id);
4125
3947
  }
4126
3948
  }
4127
- const readStateMap = useMemo13(
3949
+ const readStateMap = useMemo14(
4128
3950
  () => new Map(stateRecords.map((r) => [r.data.ConversationId, r.data.LastReadAt])),
4129
3951
  [stateRecords]
4130
3952
  );
4131
- const readMessageCountMap = useMemo13(
3953
+ const readMessageCountMap = useMemo14(
4132
3954
  () => new Map(stateRecords.map((r) => [r.data.ConversationId, r.data.LastReadMessageCount ?? 0])),
4133
3955
  [stateRecords]
4134
3956
  );
4135
- const starredSet = useMemo13(
3957
+ const starredSet = useMemo14(
4136
3958
  () => new Set(stateRecords.filter((r) => r.data.Starred).map((r) => r.data.ConversationId)),
4137
3959
  [stateRecords]
4138
3960
  );
4139
- const archivedSet = useMemo13(
3961
+ const archivedSet = useMemo14(
4140
3962
  () => new Set(stateRecords.filter((r) => r.data.Archived).map((r) => r.data.ConversationId)),
4141
3963
  [stateRecords]
4142
3964
  );
@@ -4338,7 +4160,7 @@ function useConversations() {
4338
4160
  }
4339
4161
 
4340
4162
  // src/client/directory/useCommunities.ts
4341
- import { useCallback as useCallback23, useMemo as useMemo14 } from "react";
4163
+ import { useCallback as useCallback23, useMemo as useMemo15 } from "react";
4342
4164
  function useCommunities() {
4343
4165
  const { records: communities, status } = useQuery("communities");
4344
4166
  const communityMut = useMutations("communities");
@@ -4346,7 +4168,7 @@ function useCommunities() {
4346
4168
  const membershipMut = useMutations("memberships");
4347
4169
  const { user } = useUser();
4348
4170
  const ready = status === "ready";
4349
- const membershipMap = useMemo14(() => {
4171
+ const membershipMap = useMemo15(() => {
4350
4172
  const map = /* @__PURE__ */ new Map();
4351
4173
  for (const m of memberships) {
4352
4174
  const list = map.get(m.data.CommunityId) ?? [];
@@ -4355,7 +4177,7 @@ function useCommunities() {
4355
4177
  }
4356
4178
  return map;
4357
4179
  }, [memberships]);
4358
- const myMemberships = useMemo14(
4180
+ const myMemberships = useMemo15(
4359
4181
  () => new Set(
4360
4182
  memberships.filter((m) => m.data.UserId === user?.id).map((m) => m.data.CommunityId)
4361
4183
  ),
@@ -5024,22 +4846,29 @@ function usePlatformWS(options) {
5024
4846
  return { state, send };
5025
4847
  }
5026
4848
 
5027
- // src/client/integration/index.ts
4849
+ // src/client/integration.ts
5028
4850
  var ENDPOINT_PREFIX = "/api/integrations";
5029
4851
  var DEFAULT_TIMEOUT_MS2 = 12e4;
4852
+ var ENDPOINT_RE = /^[a-z0-9][a-z0-9._~-]*(?:\/[a-z0-9][a-z0-9._~-]*)*$/i;
5030
4853
  function resolveUrl(endpoint) {
5031
- if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) {
5032
- return endpoint;
4854
+ if (endpoint !== "" && !ENDPOINT_RE.test(endpoint)) {
4855
+ throw new TypeError(
4856
+ 'Integration endpoints must be relative route segments, for example "openai/chat-completion".'
4857
+ );
5033
4858
  }
5034
- const path = endpoint.startsWith("/") ? endpoint : endpoint ? `${ENDPOINT_PREFIX}/${endpoint}` : ENDPOINT_PREFIX;
4859
+ const path = endpoint ? `${ENDPOINT_PREFIX}/${endpoint}` : ENDPOINT_PREFIX;
5035
4860
  if (typeof window !== "undefined") {
5036
4861
  return `${window.location.origin}${path}`;
5037
4862
  }
5038
4863
  return path;
5039
4864
  }
5040
- async function request(method, endpoint, body, options) {
5041
- const url = resolveUrl(endpoint);
4865
+ async function request(method, endpoint, body, options, query) {
4866
+ const queryString = query?.toString();
4867
+ const url = `${resolveUrl(endpoint)}${queryString ? `?${queryString}` : ""}`;
5042
4868
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
4869
+ if (options?.signal?.aborted) {
4870
+ return { success: false, error: "Request cancelled" };
4871
+ }
5043
4872
  const headers = {
5044
4873
  "Content-Type": "application/json",
5045
4874
  ...options?.headers
@@ -5051,9 +4880,12 @@ async function request(method, endpoint, body, options) {
5051
4880
  }
5052
4881
  const controller = new AbortController();
5053
4882
  const abortFromCaller = () => controller.abort();
5054
- if (options?.signal?.aborted) controller.abort();
5055
- else options?.signal?.addEventListener("abort", abortFromCaller, { once: true });
5056
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
4883
+ options?.signal?.addEventListener("abort", abortFromCaller, { once: true });
4884
+ let timedOut = false;
4885
+ const timeout = setTimeout(() => {
4886
+ timedOut = true;
4887
+ controller.abort();
4888
+ }, timeoutMs);
5057
4889
  try {
5058
4890
  const res = await fetch(url, {
5059
4891
  method,
@@ -5082,7 +4914,10 @@ async function request(method, endpoint, body, options) {
5082
4914
  return { success: true, data: rest };
5083
4915
  } catch (err) {
5084
4916
  if (err instanceof DOMException && err.name === "AbortError") {
5085
- return { success: false, error: `Request timed out after ${timeoutMs}ms` };
4917
+ return {
4918
+ success: false,
4919
+ error: timedOut ? `Request timed out after ${timeoutMs}ms` : "Request cancelled"
4920
+ };
5086
4921
  }
5087
4922
  return {
5088
4923
  success: false,
@@ -5098,16 +4933,14 @@ var integration = {
5098
4933
  return request("POST", endpoint, data, options);
5099
4934
  },
5100
4935
  get(endpoint, params, options) {
5101
- let url = endpoint;
4936
+ let searchParams;
5102
4937
  if (params) {
5103
- const searchParams = new URLSearchParams();
4938
+ searchParams = new URLSearchParams();
5104
4939
  for (const [k, v] of Object.entries(params)) {
5105
4940
  if (v != null) searchParams.set(k, String(v));
5106
4941
  }
5107
- const qs = searchParams.toString();
5108
- if (qs) url = `${endpoint}?${qs}`;
5109
4942
  }
5110
- return request("GET", url, void 0, options);
4943
+ return request("GET", endpoint, void 0, options, searchParams);
5111
4944
  },
5112
4945
  put(endpoint, data, options) {
5113
4946
  return request("PUT", endpoint, data, options);
@@ -5415,7 +5248,7 @@ function decodeAiStreamChunk(chunk) {
5415
5248
  }
5416
5249
 
5417
5250
  // src/client/status.ts
5418
- import { useCallback as useCallback27, useEffect as useEffect21, useMemo as useMemo15, useRef as useRef18, useState as useState18 } from "react";
5251
+ import { useCallback as useCallback27, useEffect as useEffect21, useMemo as useMemo16, useRef as useRef18, useState as useState18 } from "react";
5419
5252
 
5420
5253
  // src/client/status-core.ts
5421
5254
  var ASYNC_RESOURCE_DEFAULTS = {
@@ -5493,7 +5326,6 @@ function useAuthProfileReady(options = {}) {
5493
5326
  isReady: status === "ready"
5494
5327
  };
5495
5328
  }
5496
- var useAuthReady = useAuthProfileReady;
5497
5329
  function useAsyncResource(fetcher, deps, options = {}) {
5498
5330
  const enabled = options.enabled ?? true;
5499
5331
  const keepPreviousData = options.keepPreviousData ?? true;
@@ -5590,7 +5422,7 @@ function useAsyncResource(fetcher, deps, options = {}) {
5590
5422
  controller.abort();
5591
5423
  };
5592
5424
  }, [enabled, keepPreviousData, retry, retryDelayMs, slowAfterMs, reloadTick, ...deps]);
5593
- return useMemo15(() => ({ ...state, reload }), [state, reload]);
5425
+ return useMemo16(() => ({ ...state, reload }), [state, reload]);
5594
5426
  }
5595
5427
  function usePagedResource(fetchPage, deps, options = {}) {
5596
5428
  const enabled = options.enabled ?? true;
@@ -5762,7 +5594,7 @@ function usePagedResource(fetchPage, deps, options = {}) {
5762
5594
  loadingRef.current = false;
5763
5595
  };
5764
5596
  }, [enabled, pageSize, maxItemsPerPage, keepPreviousData, autoRetryOnError, retryDelayMs, maxRetryDelayMs, ...deps]);
5765
- return useMemo15(() => ({
5597
+ return useMemo16(() => ({
5766
5598
  ...state,
5767
5599
  loadMore,
5768
5600
  retry,
@@ -5940,7 +5772,7 @@ function PricingTable(props) {
5940
5772
  }
5941
5773
 
5942
5774
  // src/client/charges/useCheckout.ts
5943
- import { useCallback as useCallback29, useEffect as useEffect23, useMemo as useMemo16, useState as useState20 } from "react";
5775
+ import { useCallback as useCallback29, useEffect as useEffect23, useMemo as useMemo17, useState as useState20 } from "react";
5944
5776
  async function authedJson2(path, init) {
5945
5777
  const token = await getAuthToken();
5946
5778
  const res = await fetch(path, {
@@ -6017,7 +5849,7 @@ function useCheckout(opts) {
6017
5849
  (productId) => purchases.some((p) => p.productId === productId),
6018
5850
  [purchases]
6019
5851
  );
6020
- const owned = useMemo16(
5852
+ const owned = useMemo17(
6021
5853
  () => !!opts?.productId && purchases.some((p) => p.productId === opts.productId),
6022
5854
  [opts?.productId, purchases]
6023
5855
  );
@@ -6270,7 +6102,6 @@ export {
6270
6102
  useAsyncResource,
6271
6103
  useAuth,
6272
6104
  useAuthProfileReady,
6273
- useAuthReady,
6274
6105
  useAuthStatus,
6275
6106
  useAuthUser,
6276
6107
  useCanvas,