deepspace 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1361,7 +1361,7 @@ declare function encode<M extends BaseMessage<string, unknown>>(message: M): str
1361
1361
  * Provides:
1362
1362
  * - WebSocket upgrade with Cloudflare hibernation API
1363
1363
  * - Connection tracking (WebSocket -> UserAttachment)
1364
- * - Auth: parse JWT-verified user info from URL search params
1364
+ * - Auth: parse JWT-verified user info from internal request headers
1365
1365
  * - Presence: connected users list, awareness on connect/disconnect
1366
1366
  * - Message routing: JSON parse -> dispatch by `type` field, binary hook
1367
1367
  * - Raw SQLite access via this.sql
@@ -1396,8 +1396,8 @@ interface ConnectionAttachment extends UserAttachment {
1396
1396
  subscriptions: Subscription[];
1397
1397
  /** Yjs docs this connection is editing */
1398
1398
  yjsSubscriptions: YjsSubscription[];
1399
- /** Client-side Yjs awareness clientId (extracted from first awareness message) */
1400
- awarenessClientId?: number;
1399
+ /** Client-side Yjs awareness clientId for each subscribed document. */
1400
+ awarenessClientIds?: Partial<Record<YjsDocKey, number>>;
1401
1401
  }
1402
1402
  /**
1403
1403
  * Context passed to handlers for accessing shared resources.
@@ -1418,6 +1418,8 @@ interface HandlerContext {
1418
1418
  /** Asset-layer misses became real 404s, and the worker owns the fallback. The
1419
1419
  * config and the worker's fallback path have to move together. */
1420
1420
  declare const WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found";
1421
+ /** Room identity moved to internal headers and privileged room routes gained app-role checks. */
1422
+ declare const SECURE_ROOM_BOUNDARIES_MIGRATION_ID = "2026-08-secure-room-boundaries";
1421
1423
  type AppMigrationIdsValidation = {
1422
1424
  valid: true;
1423
1425
  ids: string[];
@@ -3065,6 +3067,8 @@ declare function useQuery<T = unknown>(collection: string, options?: {
3065
3067
  * ```
3066
3068
  */
3067
3069
  declare function useMutations<T = unknown>(collection: string): {
3070
+ /** True once the collection's RecordRoom can accept writes. */
3071
+ ready: boolean;
3068
3072
  create: (data: T) => Promise<string>;
3069
3073
  put: (recordId: string, data: Partial<T>) => Promise<void>;
3070
3074
  remove: (recordId: string) => Promise<void>;
@@ -3073,6 +3077,12 @@ declare function useMutations<T = unknown>(collection: string): {
3073
3077
  removeConfirmed: (recordId: string) => Promise<void>;
3074
3078
  };
3075
3079
 
3080
+ /** A write was attempted before its RecordRoom connection became ready. */
3081
+ declare class RecordRoomNotReadyError extends Error {
3082
+ readonly code = "not_ready";
3083
+ constructor(collection?: string);
3084
+ }
3085
+
3076
3086
  /**
3077
3087
  * useUsers Hook
3078
3088
  *
@@ -3220,7 +3230,7 @@ declare function usePresence(opts?: UsePresenceOptions): {
3220
3230
  * Uses the shared yjs-protocol.ts encoding — no duplication.
3221
3231
  *
3222
3232
  * @example
3223
- * const { doc, text, setText, synced, canWrite } = useYjsRoom(docId, 'content')
3233
+ * const { doc, text, setText, connected, synced, canWrite } = useYjsRoom(docId, 'content')
3224
3234
  */
3225
3235
 
3226
3236
  interface UseYjsRoomResult {
@@ -3232,6 +3242,8 @@ interface UseYjsRoomResult {
3232
3242
  text: string;
3233
3243
  /** Set text (replaces full content) */
3234
3244
  setText: (value: string) => void;
3245
+ /** Whether the current WebSocket transport is open */
3246
+ connected: boolean;
3235
3247
  /** Whether initial sync is complete */
3236
3248
  synced: boolean;
3237
3249
  /** Whether user has write access */
@@ -3432,7 +3444,7 @@ declare function useJobs<P = unknown, R = unknown>(roomId: string): UseJobsResul
3432
3444
  * Opens a WebSocket to /ws/presence/:scopeId for real-time presence tracking.
3433
3445
  * Use with any scope: canvas, doc, thread, page, etc.
3434
3446
  *
3435
- * Peers can share arbitrary state (cursor position, typing indicator,
3447
+ * Peers can share small ephemeral state (cursor position, typing indicator,
3436
3448
  * viewport, selection) via updateState().
3437
3449
  *
3438
3450
  * @example
@@ -3451,8 +3463,6 @@ declare function useJobs<P = unknown, R = unknown>(roomId: string): UseJobsResul
3451
3463
  interface PresencePeerClient {
3452
3464
  userId: string;
3453
3465
  userName: string;
3454
- userEmail: string;
3455
- userImageUrl?: string;
3456
3466
  joinedAt: string;
3457
3467
  state: Record<string, unknown>;
3458
3468
  }
@@ -4839,4 +4849,4 @@ declare const ACCOUNT_STORAGE_LIMIT_BYTES: {
4839
4849
  declare function storageLimitForTier(tier: string | null | undefined): number;
4840
4850
  declare function formatBytes(bytes: number): string;
4841
4851
 
4842
- export { ACCOUNT_STORAGE_LIMIT_BYTES, APP_LOG_EVENT_KEYS, ASYNC_RESOURCE_DEFAULTS, type AdHocChargeOpts, type AiStreamAction, type AiStreamChunk, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, type AppMigrationIdsValidation, type AsyncResourceDefaults, type AsyncResourceState, type AsyncResourceStatus, AuthGate, type AuthGateProps, AuthOverlay, Awareness, type AwarenessState, type AwarenessStates, type BaseMessage, type CanvasShapeClient, type Channel, type ChannelInvitation, type ChannelMember, type ChargeOnceOpts, type ChargeOnceResult, type ClientErrorKind, type ClientErrorReport, type ClientErrorReporterOptions, type ClientMessage, type CollectionSchema, type ConnectionAttachment, type ConnectionStatus, type ContentSegment, type ConversationObject, type CronHistoryEntry, type CronTaskState, DEEPSPACE_AGENT_PROFILES, DEEPSPACE_AI_DEFAULTS, DEEPSPACE_AI_MODELS, DEEPSPACE_MODEL_CATALOG_PROVENANCE, DEEPSPACE_THEME_PROPERTIES, DEFAULT_USER_COLORS, type DeepSpaceAIModel, type DeepSpaceAIModelId, type DeepSpaceAIProvider, type DeepSpaceAgentProfile, type DeepSpaceAgentProfileId, type DeepSpaceAgentSupport, DeepSpaceAuthProvider, type DeepSpaceThemeConfig, DeepSpaceThemeProvider, type DeepSpaceThemeProviderProps, type DeriveLoadStateInput, ENV, type Environment, type FileAttachment, type GroupedReaction, GuestBanner, type HandlerContext, type InboxEntry, type IntegrationResponse, type JobStatusView, type JobView, LOG_LEVELS, type LinkPreviewData, type LoadState, type LoadStateAuthInput, type LoadStateStatusInput, type LogLevel, MSG, MSG_AWARENESS, MSG_SYNC, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE, MSG_YJS_AWARENESS, MSG_YJS_SYNC, type MemberRecord, type Message, type MessageHandlers, type MessageRecord, type MsgType, type PagedResourceFetchArgs, type PagedResourcePage, type PagedResourceState, type PagedResourceStatus, type PlanInfo, PlatformContext, type PlatformContextValue, PlatformProvider, type PlatformWSOptions, type PlatformWSResult, type PresencePeerClient, PricingTable, type PricingTablePlan, type PricingTablePrice, type ProductChargeOpts, type ProtocolMessage, type Purchase, type Query, type R2FileInfo, type R2Scope, type R2UploadResult, ROLES, ROLE_ADMIN, ROLE_ANONYMOUS, ROLE_CONFIG, ROLE_DEFAULT, type Reaction, type ReactionRecord, type ReadCursorRecord, type ReadReceipt, type RecordData, RecordProvider, type RecordProviderProps, RecordScope, type RequestOptions, type ResolvedDeepSpaceAgentModel, type Role, type RoomUser, type ScopeEntry, ScopeRegistryProvider, type ServerMessage, SignedIn, SignedOut, type SubscribeOpts, type SubscribeResult, type SubscriptionState, type SubscriptionStatus, type SyncResult, type UseAsyncResourceOptions, type UseAuthProfileReadyOptions, type UseAuthStatusOptions, type UseCanvasResult, type UseCheckoutOpts, type UseCronMonitorResult, type UseJobsResult, type UsePagedResourceOptions, type UsePresenceRoomResult, type UseR2FilesReturn, type UseVoiceAgentOptions, type UseVoiceAgentResult, type UseYjsFieldResult, type UseYjsRoomResult, type UseYjsTextResult, type User, type UserCredits, type UserInfo, type UserKarma, type UserProfile, type ViewportClient, type VoiceStatus, type VoiceTranscriptEntry, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, type WriteError, applyDeepSpaceTheme, applyUIThemeTokens, authClient, canAutoLoadPagedResource, clampPagedResourceItems, clearAuthToken, clearDeepSpaceTheme, clientBuild, createDecoder, createEncoder, decodeAiStreamChunk, deriveLoadState, detectEnvironment, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, formatBytes, formatFileSize, formatFullTimestamp, formatMessageTime, getApiUrl, getAuthToken, getAuthUrl, getConversationDisplayName, getConversationParticipantIds, getDeepSpaceAIModel, getEnvironmentConfig, getMessageType, getPagedResourceCooldownMs, getPlatformWorkerUrl, getThreadCounts, getUserColor, groupReactionsForMessage, handleAwarenessMessage, handleSyncMessage, hasContent, installClientErrorReporter, integration, isAsyncResourceSlow, isDMConversation, isDarkColor, isImageFile, isLocalDev, isProduction, listDeepSpaceAgentModels, parseMessageMetadata, parseSseLine, readThemeFromDOM, readVarUint, readVarUint8Array, reportClientError, resetEnvironmentCache, resolveDeepSpaceAgentModel, serverBuild, shouldGroupMessages, signIn, signOut, storageLimitForTier, toConnectionStatus, toUint8Array, useAsyncResource, useAuth, useAuthProfileReady, useAuthStatus, useAuthUser, useCanvas, useChannelMembers, useChannels, useCheckout, useCommunities, useConversation, useConversations, useCronMonitor, useDisplayName, useInbox, useIsDarkTheme, useJobs, useMessages, useMutations, usePagedResource, usePlatform, usePlatformWS, usePosts, usePresence, usePresenceRoom, useQuery, useR2Files, useReactions, useReadReceipts, useRecordContext, useSession, useSubscription, useUser, useUserLookup, useUsers, useVoiceAgent, useYjsField, useYjsRoom, useYjsText, validateAppMigrationIds, writeVarUint, writeVarUint8Array };
4852
+ export { ACCOUNT_STORAGE_LIMIT_BYTES, APP_LOG_EVENT_KEYS, ASYNC_RESOURCE_DEFAULTS, type AdHocChargeOpts, type AiStreamAction, type AiStreamChunk, type AppLogEvent, type AppLogEventKey, type AppLogsResponse, type AppMigrationIdsValidation, type AsyncResourceDefaults, type AsyncResourceState, type AsyncResourceStatus, AuthGate, type AuthGateProps, AuthOverlay, Awareness, type AwarenessState, type AwarenessStates, type BaseMessage, type CanvasShapeClient, type Channel, type ChannelInvitation, type ChannelMember, type ChargeOnceOpts, type ChargeOnceResult, type ClientErrorKind, type ClientErrorReport, type ClientErrorReporterOptions, type ClientMessage, type CollectionSchema, type ConnectionAttachment, type ConnectionStatus, type ContentSegment, type ConversationObject, type CronHistoryEntry, type CronTaskState, DEEPSPACE_AGENT_PROFILES, DEEPSPACE_AI_DEFAULTS, DEEPSPACE_AI_MODELS, DEEPSPACE_MODEL_CATALOG_PROVENANCE, DEEPSPACE_THEME_PROPERTIES, DEFAULT_USER_COLORS, type DeepSpaceAIModel, type DeepSpaceAIModelId, type DeepSpaceAIProvider, type DeepSpaceAgentProfile, type DeepSpaceAgentProfileId, type DeepSpaceAgentSupport, DeepSpaceAuthProvider, type DeepSpaceThemeConfig, DeepSpaceThemeProvider, type DeepSpaceThemeProviderProps, type DeriveLoadStateInput, ENV, type Environment, type FileAttachment, type GroupedReaction, GuestBanner, type HandlerContext, type InboxEntry, type IntegrationResponse, type JobStatusView, type JobView, LOG_LEVELS, type LinkPreviewData, type LoadState, type LoadStateAuthInput, type LoadStateStatusInput, type LogLevel, MSG, MSG_AWARENESS, MSG_SYNC, MSG_SYNC_STEP1, MSG_SYNC_STEP2, MSG_SYNC_UPDATE, MSG_YJS_AWARENESS, MSG_YJS_SYNC, type MemberRecord, type Message, type MessageHandlers, type MessageRecord, type MsgType, type PagedResourceFetchArgs, type PagedResourcePage, type PagedResourceState, type PagedResourceStatus, type PlanInfo, PlatformContext, type PlatformContextValue, PlatformProvider, type PlatformWSOptions, type PlatformWSResult, type PresencePeerClient, PricingTable, type PricingTablePlan, type PricingTablePrice, type ProductChargeOpts, type ProtocolMessage, type Purchase, type Query, type R2FileInfo, type R2Scope, type R2UploadResult, ROLES, ROLE_ADMIN, ROLE_ANONYMOUS, ROLE_CONFIG, ROLE_DEFAULT, type Reaction, type ReactionRecord, type ReadCursorRecord, type ReadReceipt, type RecordData, RecordProvider, type RecordProviderProps, RecordRoomNotReadyError, RecordScope, type RequestOptions, type ResolvedDeepSpaceAgentModel, type Role, type RoomUser, SECURE_ROOM_BOUNDARIES_MIGRATION_ID, type ScopeEntry, ScopeRegistryProvider, type ServerMessage, SignedIn, SignedOut, type SubscribeOpts, type SubscribeResult, type SubscriptionState, type SubscriptionStatus, type SyncResult, type UseAsyncResourceOptions, type UseAuthProfileReadyOptions, type UseAuthStatusOptions, type UseCanvasResult, type UseCheckoutOpts, type UseCronMonitorResult, type UseJobsResult, type UsePagedResourceOptions, type UsePresenceRoomResult, type UseR2FilesReturn, type UseVoiceAgentOptions, type UseVoiceAgentResult, type UseYjsFieldResult, type UseYjsRoomResult, type UseYjsTextResult, type User, type UserCredits, type UserInfo, type UserKarma, type UserProfile, type ViewportClient, type VoiceStatus, type VoiceTranscriptEntry, WORKER_OWNED_NOT_FOUND_MIGRATION_ID, type WriteError, applyDeepSpaceTheme, applyUIThemeTokens, authClient, canAutoLoadPagedResource, clampPagedResourceItems, clearAuthToken, clearDeepSpaceTheme, clientBuild, createDecoder, createEncoder, decodeAiStreamChunk, deriveLoadState, detectEnvironment, dispatch, encode, encodeAwarenessMessage, encodeSyncStep1, encodeSyncStep2, encodeUpdate, formatBytes, formatFileSize, formatFullTimestamp, formatMessageTime, getApiUrl, getAuthToken, getAuthUrl, getConversationDisplayName, getConversationParticipantIds, getDeepSpaceAIModel, getEnvironmentConfig, getMessageType, getPagedResourceCooldownMs, getPlatformWorkerUrl, getThreadCounts, getUserColor, groupReactionsForMessage, handleAwarenessMessage, handleSyncMessage, hasContent, installClientErrorReporter, integration, isAsyncResourceSlow, isDMConversation, isDarkColor, isImageFile, isLocalDev, isProduction, listDeepSpaceAgentModels, parseMessageMetadata, parseSseLine, readThemeFromDOM, readVarUint, readVarUint8Array, reportClientError, resetEnvironmentCache, resolveDeepSpaceAgentModel, serverBuild, shouldGroupMessages, signIn, signOut, storageLimitForTier, toConnectionStatus, toUint8Array, useAsyncResource, useAuth, useAuthProfileReady, useAuthStatus, useAuthUser, useCanvas, useChannelMembers, useChannels, useCheckout, useCommunities, useConversation, useConversations, useCronMonitor, useDisplayName, useInbox, useIsDarkTheme, useJobs, useMessages, useMutations, usePagedResource, usePlatform, usePlatformWS, usePosts, usePresence, usePresenceRoom, useQuery, useR2Files, useReactions, useReadReceipts, useRecordContext, useSession, useSubscription, useUser, useUserLookup, useUsers, useVoiceAgent, useYjsField, useYjsRoom, useYjsText, validateAppMigrationIds, writeVarUint, writeVarUint8Array };
package/dist/index.js CHANGED
@@ -749,6 +749,7 @@ function encode(message) {
749
749
  var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
750
750
  var MAX_APP_MIGRATION_IDS = 256;
751
751
  var WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found";
752
+ var SECURE_ROOM_BOUNDARIES_MIGRATION_ID = "2026-08-secure-room-boundaries";
752
753
  function validateAppMigrationIds(value) {
753
754
  if (!Array.isArray(value)) {
754
755
  return { valid: false, reason: "app migrations must be a JSON string array" };
@@ -1688,6 +1689,17 @@ function reconnectDelayMs(attempt) {
1688
1689
  return Math.min(1e3 * Math.pow(2, attempt), 3e4);
1689
1690
  }
1690
1691
 
1692
+ // src/client/storage/errors.ts
1693
+ var RecordRoomNotReadyError = class extends Error {
1694
+ code = "not_ready";
1695
+ constructor(collection) {
1696
+ super(
1697
+ collection ? `RecordRoom is not ready for "${collection}" writes.` : "RecordRoom is not ready for writes."
1698
+ );
1699
+ this.name = "RecordRoomNotReadyError";
1700
+ }
1701
+ };
1702
+
1691
1703
  // src/client/storage/serverErrors.ts
1692
1704
  function humanizeCollection(slug) {
1693
1705
  return slug.replace(/^rbac-/, "").split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -1982,7 +1994,7 @@ var RecordSocket = class {
1982
1994
  sendConfirmed(message, timeoutMs = 1e4) {
1983
1995
  const ws = this.ws;
1984
1996
  if (!ws || ws.readyState !== WS_OPEN) {
1985
- return Promise.reject(new Error("WebSocket not connected"));
1997
+ return Promise.reject(new RecordRoomNotReadyError());
1986
1998
  }
1987
1999
  const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1988
2000
  return new Promise((resolve, reject) => {
@@ -2065,10 +2077,17 @@ var RecordSocket = class {
2065
2077
  }
2066
2078
  case MSG.RECORD_CHANGE: {
2067
2079
  const { collection, record, changeType } = payload;
2068
- for (const queryKey of this.subscriptions.values()) {
2080
+ for (const [subscriptionId, queryKey] of this.subscriptions) {
2069
2081
  try {
2070
2082
  const query = JSON.parse(queryKey);
2071
2083
  if (query.collection !== collection) continue;
2084
+ if (query.orderBy !== void 0 || query.limit !== void 0) {
2085
+ this.sendMessage({
2086
+ type: MSG.SUBSCRIBE,
2087
+ payload: { subscriptionId, query }
2088
+ });
2089
+ continue;
2090
+ }
2072
2091
  const matches = recordMatchesWhere(record, query.where);
2073
2092
  const exists = store.hasRecord(queryKey, record.recordId);
2074
2093
  if (changeType === "delete") {
@@ -2256,7 +2275,7 @@ function ScopeConnection({
2256
2275
  const sendConfirmed = useCallback3(
2257
2276
  (message, timeoutMs = 1e4) => {
2258
2277
  const socket = socketRef.current;
2259
- if (!socket) return Promise.reject(new Error("WebSocket not connected"));
2278
+ if (!socket) return Promise.reject(new RecordRoomNotReadyError());
2260
2279
  return socket.sendConfirmed(message, timeoutMs);
2261
2280
  },
2262
2281
  []
@@ -2489,33 +2508,41 @@ function useMutations(collection) {
2489
2508
  const preferLocal = recordCtx?.registeredCollections?.has(collection) ?? false;
2490
2509
  const sendMessage = preferLocal ? recordCtx.sendMessage : scopeEntry?.sendMessage ?? recordCtx?.sendMessage;
2491
2510
  const sendConfirmed = preferLocal ? recordCtx.sendConfirmed : scopeEntry?.sendConfirmed ?? recordCtx?.sendConfirmed;
2511
+ const ready = preferLocal ? recordCtx.ready : scopeEntry?.ready ?? recordCtx?.ready ?? false;
2492
2512
  if (!sendMessage || !sendConfirmed) {
2493
2513
  throw new Error(
2494
2514
  `useMutations('${collection}'): No scope found. Wrap this hook in a RecordScope that registers the collection.`
2495
2515
  );
2496
2516
  }
2517
+ const assertReady = useCallback5(() => {
2518
+ if (!ready) throw new RecordRoomNotReadyError(collection);
2519
+ }, [ready, collection]);
2497
2520
  const create = useCallback5(
2498
2521
  async (data) => {
2522
+ assertReady();
2499
2523
  const recordId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
2500
2524
  sendMessage({ type: MSG.PUT, payload: { collection, recordId, data } });
2501
2525
  return recordId;
2502
2526
  },
2503
- [sendMessage, collection]
2527
+ [assertReady, sendMessage, collection]
2504
2528
  );
2505
2529
  const put = useCallback5(
2506
2530
  async (recordId, data) => {
2531
+ assertReady();
2507
2532
  sendMessage({ type: MSG.PUT, payload: { collection, recordId, data } });
2508
2533
  },
2509
- [sendMessage, collection]
2534
+ [assertReady, sendMessage, collection]
2510
2535
  );
2511
2536
  const remove = useCallback5(
2512
2537
  async (recordId) => {
2538
+ assertReady();
2513
2539
  sendMessage({ type: MSG.DELETE, payload: { collection, recordId } });
2514
2540
  },
2515
- [sendMessage, collection]
2541
+ [assertReady, sendMessage, collection]
2516
2542
  );
2517
2543
  const createConfirmed = useCallback5(
2518
2544
  async (data) => {
2545
+ assertReady();
2519
2546
  const recordId = `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
2520
2547
  await sendConfirmed({
2521
2548
  type: MSG.PUT,
@@ -2523,26 +2550,28 @@ function useMutations(collection) {
2523
2550
  });
2524
2551
  return recordId;
2525
2552
  },
2526
- [sendConfirmed, collection]
2553
+ [assertReady, sendConfirmed, collection]
2527
2554
  );
2528
2555
  const putConfirmed = useCallback5(
2529
2556
  async (recordId, data) => {
2557
+ assertReady();
2530
2558
  await sendConfirmed({
2531
2559
  type: MSG.PUT,
2532
2560
  payload: { collection, recordId, data }
2533
2561
  });
2534
2562
  },
2535
- [sendConfirmed, collection]
2563
+ [assertReady, sendConfirmed, collection]
2536
2564
  );
2537
2565
  const removeConfirmed = useCallback5(
2538
2566
  async (recordId) => {
2567
+ assertReady();
2539
2568
  await sendConfirmed({ type: MSG.DELETE, payload: { collection, recordId } });
2540
2569
  },
2541
- [sendConfirmed, collection]
2570
+ [assertReady, sendConfirmed, collection]
2542
2571
  );
2543
2572
  return useMemo6(
2544
- () => ({ create, put, remove, createConfirmed, putConfirmed, removeConfirmed }),
2545
- [create, put, remove, createConfirmed, putConfirmed, removeConfirmed]
2573
+ () => ({ ready, create, put, remove, createConfirmed, putConfirmed, removeConfirmed }),
2574
+ [ready, create, put, remove, createConfirmed, putConfirmed, removeConfirmed]
2546
2575
  );
2547
2576
  }
2548
2577
 
@@ -2802,6 +2831,7 @@ import { useState as useState7, useEffect as useEffect10, useLayoutEffect as use
2802
2831
  import * as Y3 from "yjs";
2803
2832
  var UPDATE_BATCH_DELAY_MS = 16;
2804
2833
  function useYjsRoom(docId, fieldName) {
2834
+ const [connected, setConnected] = useState7(false);
2805
2835
  const [syncedDocId, setSyncedDocId] = useState7(null);
2806
2836
  const [canWrite, setCanWrite] = useState7(false);
2807
2837
  const [writeAuthResolved, setWriteAuthResolved] = useState7(false);
@@ -2837,6 +2867,7 @@ function useYjsRoom(docId, fieldName) {
2837
2867
  };
2838
2868
  }, [awareness, doc]);
2839
2869
  useLayoutEffect2(() => {
2870
+ setConnected(false);
2840
2871
  setSyncedDocId(null);
2841
2872
  setCanWrite(false);
2842
2873
  setWriteAuthResolved(false);
@@ -2854,31 +2885,44 @@ function useYjsRoom(docId, fieldName) {
2854
2885
  let ws = null;
2855
2886
  let reconnectTimer = null;
2856
2887
  let alive = true;
2888
+ let browserOnline = typeof navigator === "undefined" || navigator.onLine;
2889
+ let connecting = false;
2857
2890
  const renewLocalAwareness = () => {
2858
2891
  const localState = awareness.getLocalState();
2859
2892
  if (localState) awareness.setLocalState(localState);
2860
2893
  };
2861
2894
  const connect = async () => {
2862
- if (!alive) return;
2895
+ if (!alive || !browserOnline) return;
2896
+ if (connecting) return;
2897
+ if (ws?.readyState === WebSocket.CONNECTING || ws?.readyState === WebSocket.OPEN) return;
2898
+ connecting = true;
2863
2899
  setCanWrite(false);
2864
2900
  setWriteAuthResolved(false);
2865
2901
  renewLocalAwareness();
2866
- const token = await getAuthToken();
2867
- if (!alive) return;
2902
+ let token;
2903
+ try {
2904
+ token = await getAuthToken();
2905
+ } finally {
2906
+ connecting = false;
2907
+ }
2908
+ if (!alive || !browserOnline) return;
2868
2909
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
2869
2910
  const baseUrl = `${protocol}//${window.location.host}`;
2870
2911
  const url = new URL(`/ws/yjs/${encodeURIComponent(docId)}`, baseUrl);
2871
2912
  if (token) url.searchParams.set("token", token);
2872
2913
  wsLog("connecting", `yjs:${docId}`);
2873
- ws = new WebSocket(url.toString());
2874
- ws.binaryType = "arraybuffer";
2875
- wsRef.current = ws;
2876
- ws.onopen = () => {
2914
+ const socket = new WebSocket(url.toString());
2915
+ ws = socket;
2916
+ socket.binaryType = "arraybuffer";
2917
+ wsRef.current = socket;
2918
+ socket.onopen = () => {
2919
+ if (ws !== socket) return socket.close();
2877
2920
  wsLog("connected", `yjs:${docId}`);
2921
+ setConnected(true);
2878
2922
  setSyncedDocId(null);
2879
2923
  renewLocalAwareness();
2880
2924
  };
2881
- ws.onmessage = (event) => {
2925
+ socket.onmessage = (event) => {
2882
2926
  if (typeof event.data === "string") {
2883
2927
  try {
2884
2928
  const msg = JSON.parse(event.data);
@@ -2903,7 +2947,7 @@ function useYjsRoom(docId, fieldName) {
2903
2947
  writeVarUint(enc, MSG_SYNC);
2904
2948
  writeVarUint(enc, MSG_SYNC_STEP2);
2905
2949
  writeVarUint8Array(enc, diff);
2906
- ws?.send(toUint8Array(enc).buffer);
2950
+ socket.send(toUint8Array(enc).buffer);
2907
2951
  setSyncedDocId(docId);
2908
2952
  break;
2909
2953
  }
@@ -2931,21 +2975,40 @@ function useYjsRoom(docId, fieldName) {
2931
2975
  }
2932
2976
  }
2933
2977
  };
2934
- ws.onclose = () => {
2978
+ socket.onclose = () => {
2979
+ if (ws !== socket) return;
2935
2980
  wsLog("disconnected", `yjs:${docId}`);
2981
+ ws = null;
2936
2982
  wsRef.current = null;
2983
+ setConnected(false);
2937
2984
  setSyncedDocId(null);
2938
2985
  setCanWrite(false);
2939
2986
  setWriteAuthResolved(false);
2940
- if (alive) reconnectTimer = setTimeout(connect, 1e3);
2987
+ if (alive && browserOnline) reconnectTimer = setTimeout(connect, 1e3);
2941
2988
  };
2942
- ws.onerror = () => ws?.close();
2989
+ socket.onerror = () => socket.close();
2943
2990
  };
2944
- connect();
2991
+ const handleOffline = () => {
2992
+ browserOnline = false;
2993
+ setConnected(false);
2994
+ setSyncedDocId(null);
2995
+ setCanWrite(false);
2996
+ setWriteAuthResolved(false);
2997
+ ws?.close();
2998
+ };
2999
+ const handleOnline = () => {
3000
+ browserOnline = true;
3001
+ void connect();
3002
+ };
3003
+ window.addEventListener("offline", handleOffline);
3004
+ window.addEventListener("online", handleOnline);
3005
+ void connect();
2945
3006
  return () => {
2946
3007
  wsLog("closing", `yjs:${docId}`);
2947
3008
  alive = false;
2948
3009
  if (reconnectTimer) clearTimeout(reconnectTimer);
3010
+ window.removeEventListener("offline", handleOffline);
3011
+ window.removeEventListener("online", handleOnline);
2949
3012
  flushPendingUpdate(ws);
2950
3013
  if (ws) {
2951
3014
  ws.onclose = null;
@@ -3002,7 +3065,7 @@ function useYjsRoom(docId, fieldName) {
3002
3065
  },
3003
3066
  [doc, yText, canWrite]
3004
3067
  );
3005
- return { doc, awareness, text, setText, synced, canWrite, writeAuthResolved };
3068
+ return { doc, awareness, text, setText, connected, synced, canWrite, writeAuthResolved };
3006
3069
  }
3007
3070
 
3008
3071
  // src/client/storage/hooks/useCanvas.ts
@@ -3270,6 +3333,9 @@ function useJobs(roomId) {
3270
3333
  ws.onopen = () => setConnected(true);
3271
3334
  ws.onmessage = (event) => {
3272
3335
  dispatch(event.data, {
3336
+ [MSG.AUTH]: (p) => {
3337
+ if (!p.canWrite) rejectAllPending(new Error("JobRoom write access denied"));
3338
+ },
3273
3339
  [MSG.JOB_UPDATE]: (p) => {
3274
3340
  if (p.kind === "snapshot" && Array.isArray(p.jobs)) {
3275
3341
  setJobs(p.jobs);
@@ -6263,7 +6329,9 @@ export {
6263
6329
  ROLE_CONFIG,
6264
6330
  ROLE_DEFAULT,
6265
6331
  RecordProvider,
6332
+ RecordRoomNotReadyError,
6266
6333
  RecordScope,
6334
+ SECURE_ROOM_BOUNDARIES_MIGRATION_ID,
6267
6335
  ScopeRegistryProvider,
6268
6336
  SignedIn,
6269
6337
  SignedOut,