@tldraw/sync 4.2.0-canary.fda2467026d5 → 4.2.0-next.47462e908ff5

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.
@@ -1,7 +1,6 @@
1
1
  import { Editor } from 'tldraw';
2
2
  import { Signal } from 'tldraw';
3
3
  import { TLAssetStore } from 'tldraw';
4
- import { TLPersistentClientSocket } from '@tldraw/sync-core';
5
4
  import { TLPresenceStateInfo } from 'tldraw';
6
5
  import { TLPresenceUserInfo } from 'tldraw';
7
6
  import { TLStore } from 'tldraw';
@@ -130,12 +129,6 @@ export declare type RemoteTLStoreWithStatus = Exclude<TLStoreWithStatus, {
130
129
  */
131
130
  export declare function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus;
132
131
 
133
- /** @public */
134
- export declare type UseSyncConnectFn = (query: {
135
- sessionId: string;
136
- storeId: string;
137
- }) => TLPersistentClientSocket;
138
-
139
132
  /**
140
133
  * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.
141
134
  *
@@ -182,12 +175,6 @@ export declare interface UseSyncDemoOptions {
182
175
  getUserPresence?(store: TLStore, user: TLPresenceUserInfo): null | TLPresenceStateInfo;
183
176
  }
184
177
 
185
- /**
186
- * Options for the {@link useSync} hook.
187
- * @public
188
- */
189
- export declare type UseSyncOptions = UseSyncOptionsWithConnectFn | UseSyncOptionsWithUri;
190
-
191
178
  /**
192
179
  * Configuration options for the {@link useSync} hook to establish multiplayer collaboration.
193
180
  *
@@ -214,7 +201,33 @@ export declare type UseSyncOptions = UseSyncOptionsWithConnectFn | UseSyncOption
214
201
  *
215
202
  * @public
216
203
  */
217
- export declare interface UseSyncOptionsBase {
204
+ export declare interface UseSyncOptions {
205
+ /**
206
+ * The WebSocket URI of the multiplayer server for real-time synchronization.
207
+ *
208
+ * Must include the protocol (wss:// for secure, ws:// for local development).
209
+ * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.
210
+ *
211
+ * Can be a static string or a function that returns a URI (useful for dynamic
212
+ * authentication tokens or room routing). The function is called on each
213
+ * connection attempt, allowing for token refresh and dynamic routing.
214
+ *
215
+ * Reserved query parameters `sessionId` and `storeId` are automatically added
216
+ * by the sync system and should not be included in your URI.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * // Static URI
221
+ * uri: 'wss://myserver.com/sync/room-123'
222
+ *
223
+ * // Dynamic URI with authentication
224
+ * uri: async () => {
225
+ * const token = await getAuthToken()
226
+ * return `wss://myserver.com/sync/room-123?token=${token}`
227
+ * }
228
+ * ```
229
+ */
230
+ uri: (() => Promise<string> | string) | string;
218
231
  /**
219
232
  * User information for multiplayer presence and identification.
220
233
  *
@@ -319,48 +332,6 @@ export declare interface UseSyncOptionsBase {
319
332
  getUserPresence?(store: TLStore, user: TLPresenceUserInfo): null | TLPresenceStateInfo;
320
333
  }
321
334
 
322
- /** @public */
323
- export declare interface UseSyncOptionsWithConnectFn extends UseSyncOptionsBase {
324
- /**
325
- * Create a connection to the server. Mostly you should use {@link UseSyncOptionsWithUri.uri}
326
- * instead, but this is useful if you want to use a custom transport to connect to the server,
327
- * instead of our default websocket-based transport.
328
- */
329
- connect: UseSyncConnectFn;
330
- uri?: never;
331
- }
332
-
333
- /** @public */
334
- export declare interface UseSyncOptionsWithUri extends UseSyncOptionsBase {
335
- /**
336
- * The WebSocket URI of the multiplayer server for real-time synchronization.
337
- *
338
- * Must include the protocol (wss:// for secure, ws:// for local development).
339
- * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.
340
- *
341
- * Can be a static string or a function that returns a URI (useful for dynamic
342
- * authentication tokens or room routing). The function is called on each
343
- * connection attempt, allowing for token refresh and dynamic routing.
344
- *
345
- * Reserved query parameters `sessionId` and `storeId` are automatically added
346
- * by the sync system and should not be included in your URI.
347
- *
348
- * @example
349
- * ```ts
350
- * // Static URI
351
- * uri: 'wss://myserver.com/sync/room-123'
352
- *
353
- * // Dynamic URI with authentication
354
- * uri: async () => {
355
- * const token = await getAuthToken()
356
- * return `wss://myserver.com/sync/room-123?token=${token}`
357
- * }
358
- * ```
359
- */
360
- uri: (() => Promise<string> | string) | string;
361
- connect?: never;
362
- }
363
-
364
335
 
365
336
  export * from "@tldraw/sync-core";
366
337
 
package/dist-cjs/index.js CHANGED
@@ -29,7 +29,7 @@ var import_useSync = require("./useSync");
29
29
  var import_useSyncDemo = require("./useSyncDemo");
30
30
  (0, import_utils.registerTldrawLibraryVersion)(
31
31
  "@tldraw/sync",
32
- "4.2.0-canary.fda2467026d5",
32
+ "4.2.0-next.47462e908ff5",
33
33
  "cjs"
34
34
  );
35
35
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\n// eslint-disable-next-line local/no-export-star\nexport * from '@tldraw/sync-core'\n\nexport {\n\tuseSync,\n\ttype RemoteTLStoreWithStatus,\n\ttype UseSyncConnectFn,\n\ttype UseSyncOptions,\n\ttype UseSyncOptionsBase,\n\ttype UseSyncOptionsWithConnectFn,\n\ttype UseSyncOptionsWithUri,\n} from './useSync'\nexport { useSyncDemo, type UseSyncDemoOptions } from './useSyncDemo'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAE7C,0BAAc,8BAFd;AAIA,qBAQO;AACP,yBAAqD;AAAA,IAErD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\n// eslint-disable-next-line local/no-export-star\nexport * from '@tldraw/sync-core'\n\nexport { useSync, type RemoteTLStoreWithStatus, type UseSyncOptions } from './useSync'\nexport { useSyncDemo, type UseSyncDemoOptions } from './useSyncDemo'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAE7C,0BAAc,8BAFd;AAIA,qBAA2E;AAC3E,yBAAqD;AAAA,IAErD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -36,7 +36,6 @@ function useSync(opts) {
36
36
  roomId = "default",
37
37
  assets,
38
38
  onMount,
39
- connect,
40
39
  trackAnalyticsEvent: track,
41
40
  userInfo,
42
41
  getUserPresence: _getUserPresence,
@@ -69,47 +68,28 @@ function useSync(opts) {
69
68
  };
70
69
  }
71
70
  );
72
- let socket;
73
- if (connect) {
74
- if (uri) {
75
- throw new Error("uri and connect cannot be used together");
71
+ const socket = new import_sync_core.ClientWebSocketAdapter(async () => {
72
+ const uriString = typeof uri === "string" ? uri : await uri();
73
+ const withParams = new URL(uriString);
74
+ if (withParams.searchParams.has("sessionId")) {
75
+ throw new Error(
76
+ 'useSync. "sessionId" is a reserved query param name. Please use a different name'
77
+ );
76
78
  }
77
- socket = connect({
78
- sessionId: import_tldraw.TAB_ID,
79
- storeId
80
- });
81
- } else if (uri) {
82
- if (connect) {
83
- throw new Error("uri and connect cannot be used together");
79
+ if (withParams.searchParams.has("storeId")) {
80
+ throw new Error(
81
+ 'useSync. "storeId" is a reserved query param name. Please use a different name'
82
+ );
84
83
  }
85
- socket = new import_sync_core.ClientWebSocketAdapter(async () => {
86
- const uriString = typeof uri === "string" ? uri : await uri();
87
- const withParams = new URL(uriString);
88
- if (withParams.searchParams.has("sessionId")) {
89
- throw new Error(
90
- 'useSync. "sessionId" is a reserved query param name. Please use a different name'
91
- );
92
- }
93
- if (withParams.searchParams.has("storeId")) {
94
- throw new Error(
95
- 'useSync. "storeId" is a reserved query param name. Please use a different name'
96
- );
97
- }
98
- withParams.searchParams.set("sessionId", import_tldraw.TAB_ID);
99
- withParams.searchParams.set("storeId", storeId);
100
- return withParams.toString();
101
- });
102
- } else {
103
- throw new Error("uri or connect must be provided");
104
- }
105
- let didCancel = false;
106
- function getConnectionStatus() {
107
- return socket.connectionStatus === "error" ? "offline" : socket.connectionStatus;
108
- }
109
- const collaborationStatusSignal = (0, import_state.atom)("collaboration status", getConnectionStatus());
110
- const unsubscribeFromConnectionStatus = socket.onStatusChange(() => {
111
- collaborationStatusSignal.set(getConnectionStatus());
84
+ withParams.searchParams.set("sessionId", import_tldraw.TAB_ID);
85
+ withParams.searchParams.set("storeId", storeId);
86
+ return withParams.toString();
112
87
  });
88
+ let didCancel = false;
89
+ const collaborationStatusSignal = (0, import_tldraw.computed)(
90
+ "collaboration status",
91
+ () => socket.connectionStatus === "error" ? "offline" : socket.connectionStatus
92
+ );
113
93
  const syncMode = (0, import_state.atom)("sync mode", "readwrite");
114
94
  const store = (0, import_tldraw.createTLStore)({
115
95
  id: storeId,
@@ -178,14 +158,13 @@ function useSync(opts) {
178
158
  });
179
159
  return () => {
180
160
  didCancel = true;
181
- unsubscribeFromConnectionStatus();
182
161
  client.close();
183
162
  socket.close();
163
+ setState(null);
184
164
  };
185
165
  }, [
186
166
  assets,
187
167
  onMount,
188
- connect,
189
168
  userAtom,
190
169
  roomId,
191
170
  schema,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/useSync.ts"],
4
- "sourcesContent": ["import { atom, isSignal, transact } from '@tldraw/state'\nimport { useAtom } from '@tldraw/state-react'\nimport {\n\tClientWebSocketAdapter,\n\tTLCustomMessageHandler,\n\tTLPersistentClientSocket,\n\tTLPresenceMode,\n\tTLRemoteSyncError,\n\tTLSocketClientSentEvent,\n\tTLSocketServerSentEvent,\n\tTLSyncClient,\n\tTLSyncErrorCloseEventReason,\n} from '@tldraw/sync-core'\nimport { useEffect } from 'react'\nimport {\n\tEditor,\n\tInstancePresenceRecordType,\n\tSignal,\n\tTAB_ID,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLRecord,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLStoreWithStatus,\n\tcomputed,\n\tcreateTLStore,\n\tdefaultUserPreferences,\n\tgetDefaultUserPresence,\n\tgetUserPreferences,\n\tuniqueId,\n\tuseEvent,\n\tuseReactiveEvent,\n\tuseRefState,\n\tuseShallowObjectIdentity,\n\tuseTLSchemaFromUtils,\n\tuseValue,\n} from 'tldraw'\n\nconst MULTIPLAYER_EVENT_NAME = 'multiplayer.client'\n\nconst defaultCustomMessageHandler: TLCustomMessageHandler = () => {}\n\n/**\n * A store wrapper specifically for remote collaboration that excludes local-only states.\n * This type represents a tldraw store that is synchronized with a remote multiplayer server.\n *\n * Unlike the base TLStoreWithStatus, this excludes 'synced-local' and 'not-synced' states\n * since remote stores are always either loading, connected to a server, or in an error state.\n *\n * @example\n * ```tsx\n * function MyCollaborativeApp() {\n * const store: RemoteTLStoreWithStatus = useSync({\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore\n * })\n *\n * if (store.status === 'loading') {\n * return <div>Connecting to multiplayer session...</div>\n * }\n *\n * if (store.status === 'error') {\n * return <div>Connection failed: {store.error.message}</div>\n * }\n *\n * // store.status === 'synced-remote'\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @public\n */\nexport type RemoteTLStoreWithStatus = Exclude<\n\tTLStoreWithStatus,\n\t{ status: 'synced-local' } | { status: 'not-synced' }\n>\n\n/**\n * Creates a reactive store synchronized with a multiplayer server for real-time collaboration.\n *\n * This hook manages the complete lifecycle of a collaborative tldraw session, including\n * WebSocket connection establishment, state synchronization, user presence, and error handling.\n * The returned store can be passed directly to the Tldraw component to enable multiplayer features.\n *\n * The store progresses through multiple states:\n * - `loading`: Establishing connection and synchronizing initial state\n * - `synced-remote`: Successfully connected and actively synchronizing changes\n * - `error`: Connection failed or synchronization error occurred\n *\n * For optimal performance with media assets, provide an `assets` store that implements\n * external blob storage. Without this, large images and videos will be stored inline\n * as base64, causing performance issues during serialization.\n *\n * @param opts - Configuration options for multiplayer synchronization\n * - `uri` - WebSocket server URI (string or async function returning URI)\n * - `assets` - Asset store for blob storage (required for production use)\n * - `userInfo` - User information for presence system (can be reactive signal)\n * - `getUserPresence` - Optional function to customize presence data\n * - `onCustomMessageReceived` - Handler for custom socket messages\n * - `roomId` - Room identifier for analytics (internal use)\n * - `onMount` - Callback when editor mounts (internal use)\n * - `trackAnalyticsEvent` - Analytics event tracker (internal use)\n *\n * @returns A reactive store wrapper with connection status and synchronized store\n *\n * @example\n * ```tsx\n * // Basic multiplayer setup\n * function CollaborativeApp() {\n * const store = useSync({\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore,\n * userInfo: {\n * id: 'user-1',\n * name: 'Alice',\n * color: '#ff0000'\n * }\n * })\n *\n * if (store.status === 'loading') {\n * return <div>Connecting to collaboration session...</div>\n * }\n *\n * if (store.status === 'error') {\n * return <div>Failed to connect: {store.error.message}</div>\n * }\n *\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Dynamic authentication with reactive user info\n * import { atom } from '@tldraw/state'\n *\n * function AuthenticatedApp() {\n * const currentUser = atom('user', { id: 'user-1', name: 'Alice', color: '#ff0000' })\n *\n * const store = useSync({\n * uri: async () => {\n * const token = await getAuthToken()\n * return `wss://myserver.com/sync/room-123?token=${token}`\n * },\n * assets: authenticatedAssetStore,\n * userInfo: currentUser, // Reactive signal\n * getUserPresence: (store, user) => {\n * return {\n * userId: user.id,\n * userName: user.name,\n * cursor: getCurrentCursor(store)\n * }\n * }\n * })\n *\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @public\n */\nexport function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus {\n\tconst [state, setState] = useRefState<{\n\t\treadyClient?: TLSyncClient<TLRecord, TLStore>\n\t\terror?: Error\n\t} | null>(null)\n\tconst {\n\t\turi,\n\t\troomId = 'default',\n\t\tassets,\n\t\tonMount,\n\t\tconnect,\n\t\ttrackAnalyticsEvent: track,\n\t\tuserInfo,\n\t\tgetUserPresence: _getUserPresence,\n\t\tonCustomMessageReceived: _onCustomMessageReceived,\n\t\t...schemaOpts\n\t} = opts\n\n\t// This line will throw a type error if we add any new options to the useSync hook but we don't destructure them\n\t// This is required because otherwise the useTLSchemaFromUtils might return a new schema on every render if the newly-added option\n\t// is allowed to be unstable (e.g. userInfo)\n\tconst __never__: never = 0 as any as keyof Omit<typeof schemaOpts, keyof TLStoreSchemaOptions>\n\n\tconst schema = useTLSchemaFromUtils(schemaOpts)\n\n\tconst prefs = useShallowObjectIdentity(userInfo)\n\tconst getUserPresence = useReactiveEvent(_getUserPresence ?? getDefaultUserPresence)\n\tconst onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)\n\n\tconst userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(\n\t\t'userAtom',\n\t\tprefs\n\t)\n\n\tuseEffect(() => {\n\t\tuserAtom.set(prefs)\n\t}, [prefs, userAtom])\n\n\tuseEffect(() => {\n\t\tconst storeId = uniqueId()\n\n\t\tconst userPreferences = computed<{ id: string; color: string; name: string }>(\n\t\t\t'userPreferences',\n\t\t\t() => {\n\t\t\t\tconst userStuff = userAtom.get()\n\t\t\t\tconst user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()\n\t\t\t\treturn {\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tcolor: user.color ?? defaultUserPreferences.color,\n\t\t\t\t\tname: user.name ?? defaultUserPreferences.name,\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\tlet socket: TLPersistentClientSocket<\n\t\t\tTLSocketClientSentEvent<TLRecord>,\n\t\t\tTLSocketServerSentEvent<TLRecord>\n\t\t>\n\t\tif (connect) {\n\t\t\tif (uri) {\n\t\t\t\tthrow new Error('uri and connect cannot be used together')\n\t\t\t}\n\n\t\t\tsocket = connect({\n\t\t\t\tsessionId: TAB_ID,\n\t\t\t\tstoreId,\n\t\t\t}) as TLPersistentClientSocket<\n\t\t\t\tTLSocketClientSentEvent<TLRecord>,\n\t\t\t\tTLSocketServerSentEvent<TLRecord>\n\t\t\t>\n\t\t} else if (uri) {\n\t\t\tif (connect) {\n\t\t\t\tthrow new Error('uri and connect cannot be used together')\n\t\t\t}\n\n\t\t\tsocket = new ClientWebSocketAdapter(async () => {\n\t\t\t\tconst uriString = typeof uri === 'string' ? uri : await uri()\n\n\t\t\t\t// set sessionId as a query param on the uri\n\t\t\t\tconst withParams = new URL(uriString)\n\t\t\t\tif (withParams.searchParams.has('sessionId')) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'useSync. \"sessionId\" is a reserved query param name. Please use a different name'\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tif (withParams.searchParams.has('storeId')) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'useSync. \"storeId\" is a reserved query param name. Please use a different name'\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\twithParams.searchParams.set('sessionId', TAB_ID)\n\t\t\t\twithParams.searchParams.set('storeId', storeId)\n\t\t\t\treturn withParams.toString()\n\t\t\t})\n\t\t} else {\n\t\t\tthrow new Error('uri or connect must be provided')\n\t\t}\n\n\t\tlet didCancel = false\n\n\t\tfunction getConnectionStatus() {\n\t\t\treturn socket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus\n\t\t}\n\t\tconst collaborationStatusSignal = atom('collaboration status', getConnectionStatus())\n\t\tconst unsubscribeFromConnectionStatus = socket.onStatusChange(() => {\n\t\t\tcollaborationStatusSignal.set(getConnectionStatus())\n\t\t})\n\n\t\tconst syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')\n\n\t\tconst store = createTLStore({\n\t\t\tid: storeId,\n\t\t\tschema,\n\t\t\tassets,\n\t\t\tonMount,\n\t\t\tcollaboration: {\n\t\t\t\tstatus: collaborationStatusSignal,\n\t\t\t\tmode: syncMode,\n\t\t\t},\n\t\t})\n\n\t\tconst presence = computed('instancePresence', () => {\n\t\t\tconst presenceState = getUserPresence(store, userPreferences.get())\n\t\t\tif (!presenceState) return null\n\n\t\t\treturn InstancePresenceRecordType.create({\n\t\t\t\t...presenceState,\n\t\t\t\tid: InstancePresenceRecordType.createId(store.id),\n\t\t\t})\n\t\t})\n\n\t\tconst otherUserPresences = store.query.ids('instance_presence', () => ({\n\t\t\tuserId: { neq: userPreferences.get().id },\n\t\t}))\n\n\t\tconst presenceMode = computed<TLPresenceMode>('presenceMode', () => {\n\t\t\tif (otherUserPresences.get().size === 0) return 'solo'\n\t\t\treturn 'full'\n\t\t})\n\n\t\tconst client = new TLSyncClient<TLRecord, TLStore>({\n\t\t\tstore,\n\t\t\tsocket,\n\t\t\tdidCancel: () => didCancel,\n\t\t\tonLoad(client) {\n\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })\n\t\t\t\tsetState({ readyClient: client })\n\t\t\t},\n\t\t\tonSyncError(reason) {\n\t\t\t\tconsole.error('sync error', reason)\n\n\t\t\t\tswitch (reason) {\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_FOUND:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.FORBIDDEN:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'forbidden', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'not-authenticated', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.RATE_LIMITED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'rate-limited', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error:' + reason, roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsetState({ error: new TLRemoteSyncError(reason) })\n\t\t\t\tsocket.close()\n\t\t\t},\n\t\t\tonAfterConnect(_, { isReadonly }) {\n\t\t\t\ttransact(() => {\n\t\t\t\t\tsyncMode.set(isReadonly ? 'readonly' : 'readwrite')\n\t\t\t\t\t// if the server crashes and loses all data it can return an empty document\n\t\t\t\t\t// when it comes back up. This is a safety check to make sure that if something like\n\t\t\t\t\t// that happens, it won't render the app broken and require a restart. The user will\n\t\t\t\t\t// most likely lose all their changes though since they'll have been working with pages\n\t\t\t\t\t// that won't exist. There's certainly something we can do to make this better.\n\t\t\t\t\t// but the likelihood of this happening is very low and maybe not worth caring about beyond this.\n\t\t\t\t\tstore.ensureStoreIsUsable()\n\t\t\t\t})\n\t\t\t},\n\t\t\tonCustomMessageReceived,\n\t\t\tpresence,\n\t\t\tpresenceMode,\n\t\t})\n\n\t\treturn () => {\n\t\t\tdidCancel = true\n\t\t\tunsubscribeFromConnectionStatus()\n\t\t\tclient.close()\n\t\t\tsocket.close()\n\t\t}\n\t}, [\n\t\tassets,\n\t\tonMount,\n\t\tconnect,\n\t\tuserAtom,\n\t\troomId,\n\t\tschema,\n\t\tsetState,\n\t\ttrack,\n\t\turi,\n\t\tgetUserPresence,\n\t\tonCustomMessageReceived,\n\t])\n\n\treturn useValue<RemoteTLStoreWithStatus>(\n\t\t'remote synced store',\n\t\t() => {\n\t\t\tif (!state) return { status: 'loading' }\n\t\t\tif (state.error) return { status: 'error', error: state.error }\n\t\t\tif (!state.readyClient) return { status: 'loading' }\n\t\t\tconst connectionStatus = state.readyClient.socket.connectionStatus\n\t\t\treturn {\n\t\t\t\tstatus: 'synced-remote',\n\t\t\t\tconnectionStatus: connectionStatus === 'error' ? 'offline' : connectionStatus,\n\t\t\t\tstore: state.readyClient.store,\n\t\t\t}\n\t\t},\n\t\t[state]\n\t)\n}\n\n/**\n * Configuration options for the {@link useSync} hook to establish multiplayer collaboration.\n *\n * This interface defines the required and optional settings for connecting to a multiplayer\n * server, managing user presence, handling assets, and customizing the collaboration experience.\n *\n * @example\n * ```tsx\n * const syncOptions: UseSyncOptions = {\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore,\n * userInfo: {\n * id: 'user-1',\n * name: 'Alice',\n * color: '#ff0000'\n * },\n * getUserPresence: (store, user) => ({\n * userId: user.id,\n * userName: user.name,\n * cursor: getCursorPosition()\n * })\n * }\n * ```\n *\n * @public\n */\nexport interface UseSyncOptionsBase {\n\t/**\n\t * User information for multiplayer presence and identification.\n\t *\n\t * Can be a static object or a reactive signal that updates when user\n\t * information changes. The presence system automatically updates when\n\t * reactive signals change, allowing real-time user profile updates.\n\t *\n\t * Should be synchronized with the `userPreferences` prop of the main\n\t * Tldraw component for consistent user experience. If not provided,\n\t * defaults to localStorage-based user preferences.\n\t *\n\t * @example\n\t * ```ts\n\t * // Static user info\n\t * userInfo: { id: 'user-123', name: 'Alice', color: '#ff0000' }\n\t *\n\t * // Reactive user info\n\t * const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000' })\n\t * userInfo: userSignal\n\t * ```\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\n\n\t/**\n\t * Asset store implementation for handling file uploads and storage.\n\t *\n\t * Required for production applications to handle images, videos, and other\n\t * media efficiently. Without an asset store, files are stored inline as\n\t * base64, which causes performance issues with large files.\n\t *\n\t * The asset store must implement upload (for new files) and resolve\n\t * (for displaying existing files) methods. For prototyping, you can use\n\t * {@link tldraw#inlineBase64AssetStore} but this is not recommended for production.\n\t *\n\t * @example\n\t * ```ts\n\t * const myAssetStore: TLAssetStore = {\n\t * upload: async (asset, file) => {\n\t * const url = await uploadToCloudStorage(file)\n\t * return { src: url }\n\t * },\n\t * resolve: (asset, context) => {\n\t * return getOptimizedUrl(asset.src, context)\n\t * }\n\t * }\n\t * ```\n\t */\n\tassets: TLAssetStore\n\n\t/**\n\t * Handler for receiving custom messages sent through the multiplayer connection.\n\t *\n\t * Use this to implement custom communication channels between clients beyond\n\t * the standard shape and presence synchronization. Messages are sent using\n\t * the TLSyncClient's sendMessage method.\n\t *\n\t * @param data - The custom message data received from another client\n\t *\n\t * @example\n\t * ```ts\n\t * onCustomMessageReceived: (data) => {\n\t * if (data.type === 'chat') {\n\t * displayChatMessage(data.message, data.userId)\n\t * }\n\t * }\n\t * ```\n\t */\n\tonCustomMessageReceived?(data: any): void\n\n\t/** @internal */\n\tonMount?(editor: Editor): void\n\t/** @internal used for analytics only, we should refactor this away */\n\troomId?: string\n\t/** @internal */\n\ttrackAnalyticsEvent?(name: string, data: { [key: string]: any }): void\n\n\t/**\n\t * A reactive function that returns a {@link @tldraw/tlschema#TLInstancePresence} object.\n\t *\n\t * This function is called reactively whenever the store state changes and\n\t * determines what presence information to broadcast to other clients. The\n\t * result is synchronized across all connected clients for displaying cursors,\n\t * selections, and other collaborative indicators.\n\t *\n\t * If not provided, uses the default implementation which includes standard\n\t * cursor position and selection state. Custom implementations allow you to\n\t * add additional presence data like current tool, view state, or custom status.\n\t *\n\t * See {@link @tldraw/tlschema#getDefaultUserPresence} for\n\t * the default implementation of this function.\n\t *\n\t * @param store - The current TLStore\n\t * @param user - The current user information\n\t * @returns Presence state to broadcast to other clients, or null to hide presence\n\t *\n\t * @example\n\t * ```ts\n\t * getUserPresence: (store, user) => {\n\t * return {\n\t * userId: user.id,\n\t * userName: user.name,\n\t * cursor: { x: 100, y: 200 },\n\t * currentTool: 'select',\n\t * isActive: true\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n\n/** @public */\nexport interface UseSyncOptionsWithUri extends UseSyncOptionsBase {\n\t/**\n\t * The WebSocket URI of the multiplayer server for real-time synchronization.\n\t *\n\t * Must include the protocol (wss:// for secure, ws:// for local development).\n\t * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.\n\t *\n\t * Can be a static string or a function that returns a URI (useful for dynamic\n\t * authentication tokens or room routing). The function is called on each\n\t * connection attempt, allowing for token refresh and dynamic routing.\n\t *\n\t * Reserved query parameters `sessionId` and `storeId` are automatically added\n\t * by the sync system and should not be included in your URI.\n\t *\n\t * @example\n\t * ```ts\n\t * // Static URI\n\t * uri: 'wss://myserver.com/sync/room-123'\n\t *\n\t * // Dynamic URI with authentication\n\t * uri: async () => {\n\t * const token = await getAuthToken()\n\t * return `wss://myserver.com/sync/room-123?token=${token}`\n\t * }\n\t * ```\n\t */\n\turi: string | (() => string | Promise<string>)\n\tconnect?: never\n}\n\n/** @public */\nexport interface UseSyncOptionsWithConnectFn extends UseSyncOptionsBase {\n\t/**\n\t * Create a connection to the server. Mostly you should use {@link UseSyncOptionsWithUri.uri}\n\t * instead, but this is useful if you want to use a custom transport to connect to the server,\n\t * instead of our default websocket-based transport.\n\t */\n\tconnect: UseSyncConnectFn\n\turi?: never\n}\n\n/** @public */\nexport type UseSyncConnectFn = (query: {\n\tsessionId: string\n\tstoreId: string\n}) => TLPersistentClientSocket\n\n/**\n * Options for the {@link useSync} hook.\n * @public\n */\nexport type UseSyncOptions = UseSyncOptionsWithUri | UseSyncOptionsWithConnectFn\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { atom, isSignal, transact } from '@tldraw/state'\nimport { useAtom } from '@tldraw/state-react'\nimport {\n\tClientWebSocketAdapter,\n\tTLCustomMessageHandler,\n\tTLPresenceMode,\n\tTLRemoteSyncError,\n\tTLSyncClient,\n\tTLSyncErrorCloseEventReason,\n} from '@tldraw/sync-core'\nimport { useEffect } from 'react'\nimport {\n\tEditor,\n\tInstancePresenceRecordType,\n\tSignal,\n\tTAB_ID,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLRecord,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLStoreWithStatus,\n\tcomputed,\n\tcreateTLStore,\n\tdefaultUserPreferences,\n\tgetDefaultUserPresence,\n\tgetUserPreferences,\n\tuniqueId,\n\tuseEvent,\n\tuseReactiveEvent,\n\tuseRefState,\n\tuseShallowObjectIdentity,\n\tuseTLSchemaFromUtils,\n\tuseValue,\n} from 'tldraw'\n\nconst MULTIPLAYER_EVENT_NAME = 'multiplayer.client'\n\nconst defaultCustomMessageHandler: TLCustomMessageHandler = () => {}\n\n/**\n * A store wrapper specifically for remote collaboration that excludes local-only states.\n * This type represents a tldraw store that is synchronized with a remote multiplayer server.\n *\n * Unlike the base TLStoreWithStatus, this excludes 'synced-local' and 'not-synced' states\n * since remote stores are always either loading, connected to a server, or in an error state.\n *\n * @example\n * ```tsx\n * function MyCollaborativeApp() {\n * const store: RemoteTLStoreWithStatus = useSync({\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore\n * })\n *\n * if (store.status === 'loading') {\n * return <div>Connecting to multiplayer session...</div>\n * }\n *\n * if (store.status === 'error') {\n * return <div>Connection failed: {store.error.message}</div>\n * }\n *\n * // store.status === 'synced-remote'\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @public\n */\nexport type RemoteTLStoreWithStatus = Exclude<\n\tTLStoreWithStatus,\n\t{ status: 'synced-local' } | { status: 'not-synced' }\n>\n\n/**\n * Creates a reactive store synchronized with a multiplayer server for real-time collaboration.\n *\n * This hook manages the complete lifecycle of a collaborative tldraw session, including\n * WebSocket connection establishment, state synchronization, user presence, and error handling.\n * The returned store can be passed directly to the Tldraw component to enable multiplayer features.\n *\n * The store progresses through multiple states:\n * - `loading`: Establishing connection and synchronizing initial state\n * - `synced-remote`: Successfully connected and actively synchronizing changes\n * - `error`: Connection failed or synchronization error occurred\n *\n * For optimal performance with media assets, provide an `assets` store that implements\n * external blob storage. Without this, large images and videos will be stored inline\n * as base64, causing performance issues during serialization.\n *\n * @param opts - Configuration options for multiplayer synchronization\n * - `uri` - WebSocket server URI (string or async function returning URI)\n * - `assets` - Asset store for blob storage (required for production use)\n * - `userInfo` - User information for presence system (can be reactive signal)\n * - `getUserPresence` - Optional function to customize presence data\n * - `onCustomMessageReceived` - Handler for custom socket messages\n * - `roomId` - Room identifier for analytics (internal use)\n * - `onMount` - Callback when editor mounts (internal use)\n * - `trackAnalyticsEvent` - Analytics event tracker (internal use)\n *\n * @returns A reactive store wrapper with connection status and synchronized store\n *\n * @example\n * ```tsx\n * // Basic multiplayer setup\n * function CollaborativeApp() {\n * const store = useSync({\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore,\n * userInfo: {\n * id: 'user-1',\n * name: 'Alice',\n * color: '#ff0000'\n * }\n * })\n *\n * if (store.status === 'loading') {\n * return <div>Connecting to collaboration session...</div>\n * }\n *\n * if (store.status === 'error') {\n * return <div>Failed to connect: {store.error.message}</div>\n * }\n *\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Dynamic authentication with reactive user info\n * import { atom } from '@tldraw/state'\n *\n * function AuthenticatedApp() {\n * const currentUser = atom('user', { id: 'user-1', name: 'Alice', color: '#ff0000' })\n *\n * const store = useSync({\n * uri: async () => {\n * const token = await getAuthToken()\n * return `wss://myserver.com/sync/room-123?token=${token}`\n * },\n * assets: authenticatedAssetStore,\n * userInfo: currentUser, // Reactive signal\n * getUserPresence: (store, user) => {\n * return {\n * userId: user.id,\n * userName: user.name,\n * cursor: getCurrentCursor(store)\n * }\n * }\n * })\n *\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @public\n */\nexport function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus {\n\tconst [state, setState] = useRefState<{\n\t\treadyClient?: TLSyncClient<TLRecord, TLStore>\n\t\terror?: Error\n\t} | null>(null)\n\tconst {\n\t\turi,\n\t\troomId = 'default',\n\t\tassets,\n\t\tonMount,\n\t\ttrackAnalyticsEvent: track,\n\t\tuserInfo,\n\t\tgetUserPresence: _getUserPresence,\n\t\tonCustomMessageReceived: _onCustomMessageReceived,\n\t\t...schemaOpts\n\t} = opts\n\n\t// This line will throw a type error if we add any new options to the useSync hook but we don't destructure them\n\t// This is required because otherwise the useTLSchemaFromUtils might return a new schema on every render if the newly-added option\n\t// is allowed to be unstable (e.g. userInfo)\n\tconst __never__: never = 0 as any as keyof Omit<typeof schemaOpts, keyof TLStoreSchemaOptions>\n\n\tconst schema = useTLSchemaFromUtils(schemaOpts)\n\n\tconst prefs = useShallowObjectIdentity(userInfo)\n\tconst getUserPresence = useReactiveEvent(_getUserPresence ?? getDefaultUserPresence)\n\tconst onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)\n\n\tconst userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(\n\t\t'userAtom',\n\t\tprefs\n\t)\n\n\tuseEffect(() => {\n\t\tuserAtom.set(prefs)\n\t}, [prefs, userAtom])\n\n\tuseEffect(() => {\n\t\tconst storeId = uniqueId()\n\n\t\tconst userPreferences = computed<{ id: string; color: string; name: string }>(\n\t\t\t'userPreferences',\n\t\t\t() => {\n\t\t\t\tconst userStuff = userAtom.get()\n\t\t\t\tconst user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()\n\t\t\t\treturn {\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tcolor: user.color ?? defaultUserPreferences.color,\n\t\t\t\t\tname: user.name ?? defaultUserPreferences.name,\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\tconst socket = new ClientWebSocketAdapter(async () => {\n\t\t\tconst uriString = typeof uri === 'string' ? uri : await uri()\n\n\t\t\t// set sessionId as a query param on the uri\n\t\t\tconst withParams = new URL(uriString)\n\t\t\tif (withParams.searchParams.has('sessionId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"sessionId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (withParams.searchParams.has('storeId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"storeId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\n\t\t\twithParams.searchParams.set('sessionId', TAB_ID)\n\t\t\twithParams.searchParams.set('storeId', storeId)\n\t\t\treturn withParams.toString()\n\t\t})\n\n\t\tlet didCancel = false\n\n\t\tconst collaborationStatusSignal = computed('collaboration status', () =>\n\t\t\tsocket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus\n\t\t)\n\n\t\tconst syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')\n\n\t\tconst store = createTLStore({\n\t\t\tid: storeId,\n\t\t\tschema,\n\t\t\tassets,\n\t\t\tonMount,\n\t\t\tcollaboration: {\n\t\t\t\tstatus: collaborationStatusSignal,\n\t\t\t\tmode: syncMode,\n\t\t\t},\n\t\t})\n\n\t\tconst presence = computed('instancePresence', () => {\n\t\t\tconst presenceState = getUserPresence(store, userPreferences.get())\n\t\t\tif (!presenceState) return null\n\n\t\t\treturn InstancePresenceRecordType.create({\n\t\t\t\t...presenceState,\n\t\t\t\tid: InstancePresenceRecordType.createId(store.id),\n\t\t\t})\n\t\t})\n\n\t\tconst otherUserPresences = store.query.ids('instance_presence', () => ({\n\t\t\tuserId: { neq: userPreferences.get().id },\n\t\t}))\n\n\t\tconst presenceMode = computed<TLPresenceMode>('presenceMode', () => {\n\t\t\tif (otherUserPresences.get().size === 0) return 'solo'\n\t\t\treturn 'full'\n\t\t})\n\n\t\tconst client = new TLSyncClient({\n\t\t\tstore,\n\t\t\tsocket,\n\t\t\tdidCancel: () => didCancel,\n\t\t\tonLoad(client) {\n\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })\n\t\t\t\tsetState({ readyClient: client })\n\t\t\t},\n\t\t\tonSyncError(reason) {\n\t\t\t\tconsole.error('sync error', reason)\n\n\t\t\t\tswitch (reason) {\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_FOUND:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.FORBIDDEN:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'forbidden', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'not-authenticated', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.RATE_LIMITED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'rate-limited', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error:' + reason, roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsetState({ error: new TLRemoteSyncError(reason) })\n\t\t\t\tsocket.close()\n\t\t\t},\n\t\t\tonAfterConnect(_, { isReadonly }) {\n\t\t\t\ttransact(() => {\n\t\t\t\t\tsyncMode.set(isReadonly ? 'readonly' : 'readwrite')\n\t\t\t\t\t// if the server crashes and loses all data it can return an empty document\n\t\t\t\t\t// when it comes back up. This is a safety check to make sure that if something like\n\t\t\t\t\t// that happens, it won't render the app broken and require a restart. The user will\n\t\t\t\t\t// most likely lose all their changes though since they'll have been working with pages\n\t\t\t\t\t// that won't exist. There's certainly something we can do to make this better.\n\t\t\t\t\t// but the likelihood of this happening is very low and maybe not worth caring about beyond this.\n\t\t\t\t\tstore.ensureStoreIsUsable()\n\t\t\t\t})\n\t\t\t},\n\t\t\tonCustomMessageReceived,\n\t\t\tpresence,\n\t\t\tpresenceMode,\n\t\t})\n\n\t\treturn () => {\n\t\t\tdidCancel = true\n\t\t\tclient.close()\n\t\t\tsocket.close()\n\t\t\tsetState(null)\n\t\t}\n\t}, [\n\t\tassets,\n\t\tonMount,\n\t\tuserAtom,\n\t\troomId,\n\t\tschema,\n\t\tsetState,\n\t\ttrack,\n\t\turi,\n\t\tgetUserPresence,\n\t\tonCustomMessageReceived,\n\t])\n\n\treturn useValue<RemoteTLStoreWithStatus>(\n\t\t'remote synced store',\n\t\t() => {\n\t\t\tif (!state) return { status: 'loading' }\n\t\t\tif (state.error) return { status: 'error', error: state.error }\n\t\t\tif (!state.readyClient) return { status: 'loading' }\n\t\t\tconst connectionStatus = state.readyClient.socket.connectionStatus\n\t\t\treturn {\n\t\t\t\tstatus: 'synced-remote',\n\t\t\t\tconnectionStatus: connectionStatus === 'error' ? 'offline' : connectionStatus,\n\t\t\t\tstore: state.readyClient.store,\n\t\t\t}\n\t\t},\n\t\t[state]\n\t)\n}\n\n/**\n * Configuration options for the {@link useSync} hook to establish multiplayer collaboration.\n *\n * This interface defines the required and optional settings for connecting to a multiplayer\n * server, managing user presence, handling assets, and customizing the collaboration experience.\n *\n * @example\n * ```tsx\n * const syncOptions: UseSyncOptions = {\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore,\n * userInfo: {\n * id: 'user-1',\n * name: 'Alice',\n * color: '#ff0000'\n * },\n * getUserPresence: (store, user) => ({\n * userId: user.id,\n * userName: user.name,\n * cursor: getCursorPosition()\n * })\n * }\n * ```\n *\n * @public\n */\nexport interface UseSyncOptions {\n\t/**\n\t * The WebSocket URI of the multiplayer server for real-time synchronization.\n\t *\n\t * Must include the protocol (wss:// for secure, ws:// for local development).\n\t * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.\n\t *\n\t * Can be a static string or a function that returns a URI (useful for dynamic\n\t * authentication tokens or room routing). The function is called on each\n\t * connection attempt, allowing for token refresh and dynamic routing.\n\t *\n\t * Reserved query parameters `sessionId` and `storeId` are automatically added\n\t * by the sync system and should not be included in your URI.\n\t *\n\t * @example\n\t * ```ts\n\t * // Static URI\n\t * uri: 'wss://myserver.com/sync/room-123'\n\t *\n\t * // Dynamic URI with authentication\n\t * uri: async () => {\n\t * const token = await getAuthToken()\n\t * return `wss://myserver.com/sync/room-123?token=${token}`\n\t * }\n\t * ```\n\t */\n\turi: string | (() => string | Promise<string>)\n\n\t/**\n\t * User information for multiplayer presence and identification.\n\t *\n\t * Can be a static object or a reactive signal that updates when user\n\t * information changes. The presence system automatically updates when\n\t * reactive signals change, allowing real-time user profile updates.\n\t *\n\t * Should be synchronized with the `userPreferences` prop of the main\n\t * Tldraw component for consistent user experience. If not provided,\n\t * defaults to localStorage-based user preferences.\n\t *\n\t * @example\n\t * ```ts\n\t * // Static user info\n\t * userInfo: { id: 'user-123', name: 'Alice', color: '#ff0000' }\n\t *\n\t * // Reactive user info\n\t * const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000' })\n\t * userInfo: userSignal\n\t * ```\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\n\n\t/**\n\t * Asset store implementation for handling file uploads and storage.\n\t *\n\t * Required for production applications to handle images, videos, and other\n\t * media efficiently. Without an asset store, files are stored inline as\n\t * base64, which causes performance issues with large files.\n\t *\n\t * The asset store must implement upload (for new files) and resolve\n\t * (for displaying existing files) methods. For prototyping, you can use\n\t * {@link tldraw#inlineBase64AssetStore} but this is not recommended for production.\n\t *\n\t * @example\n\t * ```ts\n\t * const myAssetStore: TLAssetStore = {\n\t * upload: async (asset, file) => {\n\t * const url = await uploadToCloudStorage(file)\n\t * return { src: url }\n\t * },\n\t * resolve: (asset, context) => {\n\t * return getOptimizedUrl(asset.src, context)\n\t * }\n\t * }\n\t * ```\n\t */\n\tassets: TLAssetStore\n\n\t/**\n\t * Handler for receiving custom messages sent through the multiplayer connection.\n\t *\n\t * Use this to implement custom communication channels between clients beyond\n\t * the standard shape and presence synchronization. Messages are sent using\n\t * the TLSyncClient's sendMessage method.\n\t *\n\t * @param data - The custom message data received from another client\n\t *\n\t * @example\n\t * ```ts\n\t * onCustomMessageReceived: (data) => {\n\t * if (data.type === 'chat') {\n\t * displayChatMessage(data.message, data.userId)\n\t * }\n\t * }\n\t * ```\n\t */\n\tonCustomMessageReceived?(data: any): void\n\n\t/** @internal */\n\tonMount?(editor: Editor): void\n\t/** @internal used for analytics only, we should refactor this away */\n\troomId?: string\n\t/** @internal */\n\ttrackAnalyticsEvent?(name: string, data: { [key: string]: any }): void\n\n\t/**\n\t * A reactive function that returns a {@link @tldraw/tlschema#TLInstancePresence} object.\n\t *\n\t * This function is called reactively whenever the store state changes and\n\t * determines what presence information to broadcast to other clients. The\n\t * result is synchronized across all connected clients for displaying cursors,\n\t * selections, and other collaborative indicators.\n\t *\n\t * If not provided, uses the default implementation which includes standard\n\t * cursor position and selection state. Custom implementations allow you to\n\t * add additional presence data like current tool, view state, or custom status.\n\t *\n\t * See {@link @tldraw/tlschema#getDefaultUserPresence} for\n\t * the default implementation of this function.\n\t *\n\t * @param store - The current TLStore\n\t * @param user - The current user information\n\t * @returns Presence state to broadcast to other clients, or null to hide presence\n\t *\n\t * @example\n\t * ```ts\n\t * getUserPresence: (store, user) => {\n\t * return {\n\t * userId: user.id,\n\t * userName: user.name,\n\t * cursor: { x: 100, y: 200 },\n\t * currentTool: 'select',\n\t * isActive: true\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["client"]
7
7
  }
@@ -1,7 +1,6 @@
1
1
  import { Editor } from 'tldraw';
2
2
  import { Signal } from 'tldraw';
3
3
  import { TLAssetStore } from 'tldraw';
4
- import { TLPersistentClientSocket } from '@tldraw/sync-core';
5
4
  import { TLPresenceStateInfo } from 'tldraw';
6
5
  import { TLPresenceUserInfo } from 'tldraw';
7
6
  import { TLStore } from 'tldraw';
@@ -130,12 +129,6 @@ export declare type RemoteTLStoreWithStatus = Exclude<TLStoreWithStatus, {
130
129
  */
131
130
  export declare function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus;
132
131
 
133
- /** @public */
134
- export declare type UseSyncConnectFn = (query: {
135
- sessionId: string;
136
- storeId: string;
137
- }) => TLPersistentClientSocket;
138
-
139
132
  /**
140
133
  * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.
141
134
  *
@@ -182,12 +175,6 @@ export declare interface UseSyncDemoOptions {
182
175
  getUserPresence?(store: TLStore, user: TLPresenceUserInfo): null | TLPresenceStateInfo;
183
176
  }
184
177
 
185
- /**
186
- * Options for the {@link useSync} hook.
187
- * @public
188
- */
189
- export declare type UseSyncOptions = UseSyncOptionsWithConnectFn | UseSyncOptionsWithUri;
190
-
191
178
  /**
192
179
  * Configuration options for the {@link useSync} hook to establish multiplayer collaboration.
193
180
  *
@@ -214,7 +201,33 @@ export declare type UseSyncOptions = UseSyncOptionsWithConnectFn | UseSyncOption
214
201
  *
215
202
  * @public
216
203
  */
217
- export declare interface UseSyncOptionsBase {
204
+ export declare interface UseSyncOptions {
205
+ /**
206
+ * The WebSocket URI of the multiplayer server for real-time synchronization.
207
+ *
208
+ * Must include the protocol (wss:// for secure, ws:// for local development).
209
+ * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.
210
+ *
211
+ * Can be a static string or a function that returns a URI (useful for dynamic
212
+ * authentication tokens or room routing). The function is called on each
213
+ * connection attempt, allowing for token refresh and dynamic routing.
214
+ *
215
+ * Reserved query parameters `sessionId` and `storeId` are automatically added
216
+ * by the sync system and should not be included in your URI.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * // Static URI
221
+ * uri: 'wss://myserver.com/sync/room-123'
222
+ *
223
+ * // Dynamic URI with authentication
224
+ * uri: async () => {
225
+ * const token = await getAuthToken()
226
+ * return `wss://myserver.com/sync/room-123?token=${token}`
227
+ * }
228
+ * ```
229
+ */
230
+ uri: (() => Promise<string> | string) | string;
218
231
  /**
219
232
  * User information for multiplayer presence and identification.
220
233
  *
@@ -319,48 +332,6 @@ export declare interface UseSyncOptionsBase {
319
332
  getUserPresence?(store: TLStore, user: TLPresenceUserInfo): null | TLPresenceStateInfo;
320
333
  }
321
334
 
322
- /** @public */
323
- export declare interface UseSyncOptionsWithConnectFn extends UseSyncOptionsBase {
324
- /**
325
- * Create a connection to the server. Mostly you should use {@link UseSyncOptionsWithUri.uri}
326
- * instead, but this is useful if you want to use a custom transport to connect to the server,
327
- * instead of our default websocket-based transport.
328
- */
329
- connect: UseSyncConnectFn;
330
- uri?: never;
331
- }
332
-
333
- /** @public */
334
- export declare interface UseSyncOptionsWithUri extends UseSyncOptionsBase {
335
- /**
336
- * The WebSocket URI of the multiplayer server for real-time synchronization.
337
- *
338
- * Must include the protocol (wss:// for secure, ws:// for local development).
339
- * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.
340
- *
341
- * Can be a static string or a function that returns a URI (useful for dynamic
342
- * authentication tokens or room routing). The function is called on each
343
- * connection attempt, allowing for token refresh and dynamic routing.
344
- *
345
- * Reserved query parameters `sessionId` and `storeId` are automatically added
346
- * by the sync system and should not be included in your URI.
347
- *
348
- * @example
349
- * ```ts
350
- * // Static URI
351
- * uri: 'wss://myserver.com/sync/room-123'
352
- *
353
- * // Dynamic URI with authentication
354
- * uri: async () => {
355
- * const token = await getAuthToken()
356
- * return `wss://myserver.com/sync/room-123?token=${token}`
357
- * }
358
- * ```
359
- */
360
- uri: (() => Promise<string> | string) | string;
361
- connect?: never;
362
- }
363
-
364
335
 
365
336
  export * from "@tldraw/sync-core";
366
337
 
@@ -1,12 +1,10 @@
1
1
  import { registerTldrawLibraryVersion } from "@tldraw/utils";
2
2
  export * from "@tldraw/sync-core";
3
- import {
4
- useSync
5
- } from "./useSync.mjs";
3
+ import { useSync } from "./useSync.mjs";
6
4
  import { useSyncDemo } from "./useSyncDemo.mjs";
7
5
  registerTldrawLibraryVersion(
8
6
  "@tldraw/sync",
9
- "4.2.0-canary.fda2467026d5",
7
+ "4.2.0-next.47462e908ff5",
10
8
  "esm"
11
9
  );
12
10
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\n// eslint-disable-next-line local/no-export-star\nexport * from '@tldraw/sync-core'\n\nexport {\n\tuseSync,\n\ttype RemoteTLStoreWithStatus,\n\ttype UseSyncConnectFn,\n\ttype UseSyncOptions,\n\ttype UseSyncOptionsBase,\n\ttype UseSyncOptionsWithConnectFn,\n\ttype UseSyncOptionsWithUri,\n} from './useSync'\nexport { useSyncDemo, type UseSyncDemoOptions } from './useSyncDemo'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": "AAAA,SAAS,oCAAoC;AAE7C,cAAc;AAEd;AAAA,EACC;AAAA,OAOM;AACP,SAAS,mBAA4C;AAErD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\n// eslint-disable-next-line local/no-export-star\nexport * from '@tldraw/sync-core'\n\nexport { useSync, type RemoteTLStoreWithStatus, type UseSyncOptions } from './useSync'\nexport { useSyncDemo, type UseSyncDemoOptions } from './useSyncDemo'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": "AAAA,SAAS,oCAAoC;AAE7C,cAAc;AAEd,SAAS,eAAkE;AAC3E,SAAS,mBAA4C;AAErD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -33,7 +33,6 @@ function useSync(opts) {
33
33
  roomId = "default",
34
34
  assets,
35
35
  onMount,
36
- connect,
37
36
  trackAnalyticsEvent: track,
38
37
  userInfo,
39
38
  getUserPresence: _getUserPresence,
@@ -66,47 +65,28 @@ function useSync(opts) {
66
65
  };
67
66
  }
68
67
  );
69
- let socket;
70
- if (connect) {
71
- if (uri) {
72
- throw new Error("uri and connect cannot be used together");
68
+ const socket = new ClientWebSocketAdapter(async () => {
69
+ const uriString = typeof uri === "string" ? uri : await uri();
70
+ const withParams = new URL(uriString);
71
+ if (withParams.searchParams.has("sessionId")) {
72
+ throw new Error(
73
+ 'useSync. "sessionId" is a reserved query param name. Please use a different name'
74
+ );
73
75
  }
74
- socket = connect({
75
- sessionId: TAB_ID,
76
- storeId
77
- });
78
- } else if (uri) {
79
- if (connect) {
80
- throw new Error("uri and connect cannot be used together");
76
+ if (withParams.searchParams.has("storeId")) {
77
+ throw new Error(
78
+ 'useSync. "storeId" is a reserved query param name. Please use a different name'
79
+ );
81
80
  }
82
- socket = new ClientWebSocketAdapter(async () => {
83
- const uriString = typeof uri === "string" ? uri : await uri();
84
- const withParams = new URL(uriString);
85
- if (withParams.searchParams.has("sessionId")) {
86
- throw new Error(
87
- 'useSync. "sessionId" is a reserved query param name. Please use a different name'
88
- );
89
- }
90
- if (withParams.searchParams.has("storeId")) {
91
- throw new Error(
92
- 'useSync. "storeId" is a reserved query param name. Please use a different name'
93
- );
94
- }
95
- withParams.searchParams.set("sessionId", TAB_ID);
96
- withParams.searchParams.set("storeId", storeId);
97
- return withParams.toString();
98
- });
99
- } else {
100
- throw new Error("uri or connect must be provided");
101
- }
102
- let didCancel = false;
103
- function getConnectionStatus() {
104
- return socket.connectionStatus === "error" ? "offline" : socket.connectionStatus;
105
- }
106
- const collaborationStatusSignal = atom("collaboration status", getConnectionStatus());
107
- const unsubscribeFromConnectionStatus = socket.onStatusChange(() => {
108
- collaborationStatusSignal.set(getConnectionStatus());
81
+ withParams.searchParams.set("sessionId", TAB_ID);
82
+ withParams.searchParams.set("storeId", storeId);
83
+ return withParams.toString();
109
84
  });
85
+ let didCancel = false;
86
+ const collaborationStatusSignal = computed(
87
+ "collaboration status",
88
+ () => socket.connectionStatus === "error" ? "offline" : socket.connectionStatus
89
+ );
110
90
  const syncMode = atom("sync mode", "readwrite");
111
91
  const store = createTLStore({
112
92
  id: storeId,
@@ -175,14 +155,13 @@ function useSync(opts) {
175
155
  });
176
156
  return () => {
177
157
  didCancel = true;
178
- unsubscribeFromConnectionStatus();
179
158
  client.close();
180
159
  socket.close();
160
+ setState(null);
181
161
  };
182
162
  }, [
183
163
  assets,
184
164
  onMount,
185
- connect,
186
165
  userAtom,
187
166
  roomId,
188
167
  schema,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/useSync.ts"],
4
- "sourcesContent": ["import { atom, isSignal, transact } from '@tldraw/state'\nimport { useAtom } from '@tldraw/state-react'\nimport {\n\tClientWebSocketAdapter,\n\tTLCustomMessageHandler,\n\tTLPersistentClientSocket,\n\tTLPresenceMode,\n\tTLRemoteSyncError,\n\tTLSocketClientSentEvent,\n\tTLSocketServerSentEvent,\n\tTLSyncClient,\n\tTLSyncErrorCloseEventReason,\n} from '@tldraw/sync-core'\nimport { useEffect } from 'react'\nimport {\n\tEditor,\n\tInstancePresenceRecordType,\n\tSignal,\n\tTAB_ID,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLRecord,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLStoreWithStatus,\n\tcomputed,\n\tcreateTLStore,\n\tdefaultUserPreferences,\n\tgetDefaultUserPresence,\n\tgetUserPreferences,\n\tuniqueId,\n\tuseEvent,\n\tuseReactiveEvent,\n\tuseRefState,\n\tuseShallowObjectIdentity,\n\tuseTLSchemaFromUtils,\n\tuseValue,\n} from 'tldraw'\n\nconst MULTIPLAYER_EVENT_NAME = 'multiplayer.client'\n\nconst defaultCustomMessageHandler: TLCustomMessageHandler = () => {}\n\n/**\n * A store wrapper specifically for remote collaboration that excludes local-only states.\n * This type represents a tldraw store that is synchronized with a remote multiplayer server.\n *\n * Unlike the base TLStoreWithStatus, this excludes 'synced-local' and 'not-synced' states\n * since remote stores are always either loading, connected to a server, or in an error state.\n *\n * @example\n * ```tsx\n * function MyCollaborativeApp() {\n * const store: RemoteTLStoreWithStatus = useSync({\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore\n * })\n *\n * if (store.status === 'loading') {\n * return <div>Connecting to multiplayer session...</div>\n * }\n *\n * if (store.status === 'error') {\n * return <div>Connection failed: {store.error.message}</div>\n * }\n *\n * // store.status === 'synced-remote'\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @public\n */\nexport type RemoteTLStoreWithStatus = Exclude<\n\tTLStoreWithStatus,\n\t{ status: 'synced-local' } | { status: 'not-synced' }\n>\n\n/**\n * Creates a reactive store synchronized with a multiplayer server for real-time collaboration.\n *\n * This hook manages the complete lifecycle of a collaborative tldraw session, including\n * WebSocket connection establishment, state synchronization, user presence, and error handling.\n * The returned store can be passed directly to the Tldraw component to enable multiplayer features.\n *\n * The store progresses through multiple states:\n * - `loading`: Establishing connection and synchronizing initial state\n * - `synced-remote`: Successfully connected and actively synchronizing changes\n * - `error`: Connection failed or synchronization error occurred\n *\n * For optimal performance with media assets, provide an `assets` store that implements\n * external blob storage. Without this, large images and videos will be stored inline\n * as base64, causing performance issues during serialization.\n *\n * @param opts - Configuration options for multiplayer synchronization\n * - `uri` - WebSocket server URI (string or async function returning URI)\n * - `assets` - Asset store for blob storage (required for production use)\n * - `userInfo` - User information for presence system (can be reactive signal)\n * - `getUserPresence` - Optional function to customize presence data\n * - `onCustomMessageReceived` - Handler for custom socket messages\n * - `roomId` - Room identifier for analytics (internal use)\n * - `onMount` - Callback when editor mounts (internal use)\n * - `trackAnalyticsEvent` - Analytics event tracker (internal use)\n *\n * @returns A reactive store wrapper with connection status and synchronized store\n *\n * @example\n * ```tsx\n * // Basic multiplayer setup\n * function CollaborativeApp() {\n * const store = useSync({\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore,\n * userInfo: {\n * id: 'user-1',\n * name: 'Alice',\n * color: '#ff0000'\n * }\n * })\n *\n * if (store.status === 'loading') {\n * return <div>Connecting to collaboration session...</div>\n * }\n *\n * if (store.status === 'error') {\n * return <div>Failed to connect: {store.error.message}</div>\n * }\n *\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Dynamic authentication with reactive user info\n * import { atom } from '@tldraw/state'\n *\n * function AuthenticatedApp() {\n * const currentUser = atom('user', { id: 'user-1', name: 'Alice', color: '#ff0000' })\n *\n * const store = useSync({\n * uri: async () => {\n * const token = await getAuthToken()\n * return `wss://myserver.com/sync/room-123?token=${token}`\n * },\n * assets: authenticatedAssetStore,\n * userInfo: currentUser, // Reactive signal\n * getUserPresence: (store, user) => {\n * return {\n * userId: user.id,\n * userName: user.name,\n * cursor: getCurrentCursor(store)\n * }\n * }\n * })\n *\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @public\n */\nexport function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus {\n\tconst [state, setState] = useRefState<{\n\t\treadyClient?: TLSyncClient<TLRecord, TLStore>\n\t\terror?: Error\n\t} | null>(null)\n\tconst {\n\t\turi,\n\t\troomId = 'default',\n\t\tassets,\n\t\tonMount,\n\t\tconnect,\n\t\ttrackAnalyticsEvent: track,\n\t\tuserInfo,\n\t\tgetUserPresence: _getUserPresence,\n\t\tonCustomMessageReceived: _onCustomMessageReceived,\n\t\t...schemaOpts\n\t} = opts\n\n\t// This line will throw a type error if we add any new options to the useSync hook but we don't destructure them\n\t// This is required because otherwise the useTLSchemaFromUtils might return a new schema on every render if the newly-added option\n\t// is allowed to be unstable (e.g. userInfo)\n\tconst __never__: never = 0 as any as keyof Omit<typeof schemaOpts, keyof TLStoreSchemaOptions>\n\n\tconst schema = useTLSchemaFromUtils(schemaOpts)\n\n\tconst prefs = useShallowObjectIdentity(userInfo)\n\tconst getUserPresence = useReactiveEvent(_getUserPresence ?? getDefaultUserPresence)\n\tconst onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)\n\n\tconst userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(\n\t\t'userAtom',\n\t\tprefs\n\t)\n\n\tuseEffect(() => {\n\t\tuserAtom.set(prefs)\n\t}, [prefs, userAtom])\n\n\tuseEffect(() => {\n\t\tconst storeId = uniqueId()\n\n\t\tconst userPreferences = computed<{ id: string; color: string; name: string }>(\n\t\t\t'userPreferences',\n\t\t\t() => {\n\t\t\t\tconst userStuff = userAtom.get()\n\t\t\t\tconst user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()\n\t\t\t\treturn {\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tcolor: user.color ?? defaultUserPreferences.color,\n\t\t\t\t\tname: user.name ?? defaultUserPreferences.name,\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\tlet socket: TLPersistentClientSocket<\n\t\t\tTLSocketClientSentEvent<TLRecord>,\n\t\t\tTLSocketServerSentEvent<TLRecord>\n\t\t>\n\t\tif (connect) {\n\t\t\tif (uri) {\n\t\t\t\tthrow new Error('uri and connect cannot be used together')\n\t\t\t}\n\n\t\t\tsocket = connect({\n\t\t\t\tsessionId: TAB_ID,\n\t\t\t\tstoreId,\n\t\t\t}) as TLPersistentClientSocket<\n\t\t\t\tTLSocketClientSentEvent<TLRecord>,\n\t\t\t\tTLSocketServerSentEvent<TLRecord>\n\t\t\t>\n\t\t} else if (uri) {\n\t\t\tif (connect) {\n\t\t\t\tthrow new Error('uri and connect cannot be used together')\n\t\t\t}\n\n\t\t\tsocket = new ClientWebSocketAdapter(async () => {\n\t\t\t\tconst uriString = typeof uri === 'string' ? uri : await uri()\n\n\t\t\t\t// set sessionId as a query param on the uri\n\t\t\t\tconst withParams = new URL(uriString)\n\t\t\t\tif (withParams.searchParams.has('sessionId')) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'useSync. \"sessionId\" is a reserved query param name. Please use a different name'\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tif (withParams.searchParams.has('storeId')) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'useSync. \"storeId\" is a reserved query param name. Please use a different name'\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\twithParams.searchParams.set('sessionId', TAB_ID)\n\t\t\t\twithParams.searchParams.set('storeId', storeId)\n\t\t\t\treturn withParams.toString()\n\t\t\t})\n\t\t} else {\n\t\t\tthrow new Error('uri or connect must be provided')\n\t\t}\n\n\t\tlet didCancel = false\n\n\t\tfunction getConnectionStatus() {\n\t\t\treturn socket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus\n\t\t}\n\t\tconst collaborationStatusSignal = atom('collaboration status', getConnectionStatus())\n\t\tconst unsubscribeFromConnectionStatus = socket.onStatusChange(() => {\n\t\t\tcollaborationStatusSignal.set(getConnectionStatus())\n\t\t})\n\n\t\tconst syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')\n\n\t\tconst store = createTLStore({\n\t\t\tid: storeId,\n\t\t\tschema,\n\t\t\tassets,\n\t\t\tonMount,\n\t\t\tcollaboration: {\n\t\t\t\tstatus: collaborationStatusSignal,\n\t\t\t\tmode: syncMode,\n\t\t\t},\n\t\t})\n\n\t\tconst presence = computed('instancePresence', () => {\n\t\t\tconst presenceState = getUserPresence(store, userPreferences.get())\n\t\t\tif (!presenceState) return null\n\n\t\t\treturn InstancePresenceRecordType.create({\n\t\t\t\t...presenceState,\n\t\t\t\tid: InstancePresenceRecordType.createId(store.id),\n\t\t\t})\n\t\t})\n\n\t\tconst otherUserPresences = store.query.ids('instance_presence', () => ({\n\t\t\tuserId: { neq: userPreferences.get().id },\n\t\t}))\n\n\t\tconst presenceMode = computed<TLPresenceMode>('presenceMode', () => {\n\t\t\tif (otherUserPresences.get().size === 0) return 'solo'\n\t\t\treturn 'full'\n\t\t})\n\n\t\tconst client = new TLSyncClient<TLRecord, TLStore>({\n\t\t\tstore,\n\t\t\tsocket,\n\t\t\tdidCancel: () => didCancel,\n\t\t\tonLoad(client) {\n\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })\n\t\t\t\tsetState({ readyClient: client })\n\t\t\t},\n\t\t\tonSyncError(reason) {\n\t\t\t\tconsole.error('sync error', reason)\n\n\t\t\t\tswitch (reason) {\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_FOUND:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.FORBIDDEN:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'forbidden', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'not-authenticated', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.RATE_LIMITED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'rate-limited', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error:' + reason, roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsetState({ error: new TLRemoteSyncError(reason) })\n\t\t\t\tsocket.close()\n\t\t\t},\n\t\t\tonAfterConnect(_, { isReadonly }) {\n\t\t\t\ttransact(() => {\n\t\t\t\t\tsyncMode.set(isReadonly ? 'readonly' : 'readwrite')\n\t\t\t\t\t// if the server crashes and loses all data it can return an empty document\n\t\t\t\t\t// when it comes back up. This is a safety check to make sure that if something like\n\t\t\t\t\t// that happens, it won't render the app broken and require a restart. The user will\n\t\t\t\t\t// most likely lose all their changes though since they'll have been working with pages\n\t\t\t\t\t// that won't exist. There's certainly something we can do to make this better.\n\t\t\t\t\t// but the likelihood of this happening is very low and maybe not worth caring about beyond this.\n\t\t\t\t\tstore.ensureStoreIsUsable()\n\t\t\t\t})\n\t\t\t},\n\t\t\tonCustomMessageReceived,\n\t\t\tpresence,\n\t\t\tpresenceMode,\n\t\t})\n\n\t\treturn () => {\n\t\t\tdidCancel = true\n\t\t\tunsubscribeFromConnectionStatus()\n\t\t\tclient.close()\n\t\t\tsocket.close()\n\t\t}\n\t}, [\n\t\tassets,\n\t\tonMount,\n\t\tconnect,\n\t\tuserAtom,\n\t\troomId,\n\t\tschema,\n\t\tsetState,\n\t\ttrack,\n\t\turi,\n\t\tgetUserPresence,\n\t\tonCustomMessageReceived,\n\t])\n\n\treturn useValue<RemoteTLStoreWithStatus>(\n\t\t'remote synced store',\n\t\t() => {\n\t\t\tif (!state) return { status: 'loading' }\n\t\t\tif (state.error) return { status: 'error', error: state.error }\n\t\t\tif (!state.readyClient) return { status: 'loading' }\n\t\t\tconst connectionStatus = state.readyClient.socket.connectionStatus\n\t\t\treturn {\n\t\t\t\tstatus: 'synced-remote',\n\t\t\t\tconnectionStatus: connectionStatus === 'error' ? 'offline' : connectionStatus,\n\t\t\t\tstore: state.readyClient.store,\n\t\t\t}\n\t\t},\n\t\t[state]\n\t)\n}\n\n/**\n * Configuration options for the {@link useSync} hook to establish multiplayer collaboration.\n *\n * This interface defines the required and optional settings for connecting to a multiplayer\n * server, managing user presence, handling assets, and customizing the collaboration experience.\n *\n * @example\n * ```tsx\n * const syncOptions: UseSyncOptions = {\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore,\n * userInfo: {\n * id: 'user-1',\n * name: 'Alice',\n * color: '#ff0000'\n * },\n * getUserPresence: (store, user) => ({\n * userId: user.id,\n * userName: user.name,\n * cursor: getCursorPosition()\n * })\n * }\n * ```\n *\n * @public\n */\nexport interface UseSyncOptionsBase {\n\t/**\n\t * User information for multiplayer presence and identification.\n\t *\n\t * Can be a static object or a reactive signal that updates when user\n\t * information changes. The presence system automatically updates when\n\t * reactive signals change, allowing real-time user profile updates.\n\t *\n\t * Should be synchronized with the `userPreferences` prop of the main\n\t * Tldraw component for consistent user experience. If not provided,\n\t * defaults to localStorage-based user preferences.\n\t *\n\t * @example\n\t * ```ts\n\t * // Static user info\n\t * userInfo: { id: 'user-123', name: 'Alice', color: '#ff0000' }\n\t *\n\t * // Reactive user info\n\t * const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000' })\n\t * userInfo: userSignal\n\t * ```\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\n\n\t/**\n\t * Asset store implementation for handling file uploads and storage.\n\t *\n\t * Required for production applications to handle images, videos, and other\n\t * media efficiently. Without an asset store, files are stored inline as\n\t * base64, which causes performance issues with large files.\n\t *\n\t * The asset store must implement upload (for new files) and resolve\n\t * (for displaying existing files) methods. For prototyping, you can use\n\t * {@link tldraw#inlineBase64AssetStore} but this is not recommended for production.\n\t *\n\t * @example\n\t * ```ts\n\t * const myAssetStore: TLAssetStore = {\n\t * upload: async (asset, file) => {\n\t * const url = await uploadToCloudStorage(file)\n\t * return { src: url }\n\t * },\n\t * resolve: (asset, context) => {\n\t * return getOptimizedUrl(asset.src, context)\n\t * }\n\t * }\n\t * ```\n\t */\n\tassets: TLAssetStore\n\n\t/**\n\t * Handler for receiving custom messages sent through the multiplayer connection.\n\t *\n\t * Use this to implement custom communication channels between clients beyond\n\t * the standard shape and presence synchronization. Messages are sent using\n\t * the TLSyncClient's sendMessage method.\n\t *\n\t * @param data - The custom message data received from another client\n\t *\n\t * @example\n\t * ```ts\n\t * onCustomMessageReceived: (data) => {\n\t * if (data.type === 'chat') {\n\t * displayChatMessage(data.message, data.userId)\n\t * }\n\t * }\n\t * ```\n\t */\n\tonCustomMessageReceived?(data: any): void\n\n\t/** @internal */\n\tonMount?(editor: Editor): void\n\t/** @internal used for analytics only, we should refactor this away */\n\troomId?: string\n\t/** @internal */\n\ttrackAnalyticsEvent?(name: string, data: { [key: string]: any }): void\n\n\t/**\n\t * A reactive function that returns a {@link @tldraw/tlschema#TLInstancePresence} object.\n\t *\n\t * This function is called reactively whenever the store state changes and\n\t * determines what presence information to broadcast to other clients. The\n\t * result is synchronized across all connected clients for displaying cursors,\n\t * selections, and other collaborative indicators.\n\t *\n\t * If not provided, uses the default implementation which includes standard\n\t * cursor position and selection state. Custom implementations allow you to\n\t * add additional presence data like current tool, view state, or custom status.\n\t *\n\t * See {@link @tldraw/tlschema#getDefaultUserPresence} for\n\t * the default implementation of this function.\n\t *\n\t * @param store - The current TLStore\n\t * @param user - The current user information\n\t * @returns Presence state to broadcast to other clients, or null to hide presence\n\t *\n\t * @example\n\t * ```ts\n\t * getUserPresence: (store, user) => {\n\t * return {\n\t * userId: user.id,\n\t * userName: user.name,\n\t * cursor: { x: 100, y: 200 },\n\t * currentTool: 'select',\n\t * isActive: true\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n\n/** @public */\nexport interface UseSyncOptionsWithUri extends UseSyncOptionsBase {\n\t/**\n\t * The WebSocket URI of the multiplayer server for real-time synchronization.\n\t *\n\t * Must include the protocol (wss:// for secure, ws:// for local development).\n\t * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.\n\t *\n\t * Can be a static string or a function that returns a URI (useful for dynamic\n\t * authentication tokens or room routing). The function is called on each\n\t * connection attempt, allowing for token refresh and dynamic routing.\n\t *\n\t * Reserved query parameters `sessionId` and `storeId` are automatically added\n\t * by the sync system and should not be included in your URI.\n\t *\n\t * @example\n\t * ```ts\n\t * // Static URI\n\t * uri: 'wss://myserver.com/sync/room-123'\n\t *\n\t * // Dynamic URI with authentication\n\t * uri: async () => {\n\t * const token = await getAuthToken()\n\t * return `wss://myserver.com/sync/room-123?token=${token}`\n\t * }\n\t * ```\n\t */\n\turi: string | (() => string | Promise<string>)\n\tconnect?: never\n}\n\n/** @public */\nexport interface UseSyncOptionsWithConnectFn extends UseSyncOptionsBase {\n\t/**\n\t * Create a connection to the server. Mostly you should use {@link UseSyncOptionsWithUri.uri}\n\t * instead, but this is useful if you want to use a custom transport to connect to the server,\n\t * instead of our default websocket-based transport.\n\t */\n\tconnect: UseSyncConnectFn\n\turi?: never\n}\n\n/** @public */\nexport type UseSyncConnectFn = (query: {\n\tsessionId: string\n\tstoreId: string\n}) => TLPersistentClientSocket\n\n/**\n * Options for the {@link useSync} hook.\n * @public\n */\nexport type UseSyncOptions = UseSyncOptionsWithUri | UseSyncOptionsWithConnectFn\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { atom, isSignal, transact } from '@tldraw/state'\nimport { useAtom } from '@tldraw/state-react'\nimport {\n\tClientWebSocketAdapter,\n\tTLCustomMessageHandler,\n\tTLPresenceMode,\n\tTLRemoteSyncError,\n\tTLSyncClient,\n\tTLSyncErrorCloseEventReason,\n} from '@tldraw/sync-core'\nimport { useEffect } from 'react'\nimport {\n\tEditor,\n\tInstancePresenceRecordType,\n\tSignal,\n\tTAB_ID,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLRecord,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLStoreWithStatus,\n\tcomputed,\n\tcreateTLStore,\n\tdefaultUserPreferences,\n\tgetDefaultUserPresence,\n\tgetUserPreferences,\n\tuniqueId,\n\tuseEvent,\n\tuseReactiveEvent,\n\tuseRefState,\n\tuseShallowObjectIdentity,\n\tuseTLSchemaFromUtils,\n\tuseValue,\n} from 'tldraw'\n\nconst MULTIPLAYER_EVENT_NAME = 'multiplayer.client'\n\nconst defaultCustomMessageHandler: TLCustomMessageHandler = () => {}\n\n/**\n * A store wrapper specifically for remote collaboration that excludes local-only states.\n * This type represents a tldraw store that is synchronized with a remote multiplayer server.\n *\n * Unlike the base TLStoreWithStatus, this excludes 'synced-local' and 'not-synced' states\n * since remote stores are always either loading, connected to a server, or in an error state.\n *\n * @example\n * ```tsx\n * function MyCollaborativeApp() {\n * const store: RemoteTLStoreWithStatus = useSync({\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore\n * })\n *\n * if (store.status === 'loading') {\n * return <div>Connecting to multiplayer session...</div>\n * }\n *\n * if (store.status === 'error') {\n * return <div>Connection failed: {store.error.message}</div>\n * }\n *\n * // store.status === 'synced-remote'\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @public\n */\nexport type RemoteTLStoreWithStatus = Exclude<\n\tTLStoreWithStatus,\n\t{ status: 'synced-local' } | { status: 'not-synced' }\n>\n\n/**\n * Creates a reactive store synchronized with a multiplayer server for real-time collaboration.\n *\n * This hook manages the complete lifecycle of a collaborative tldraw session, including\n * WebSocket connection establishment, state synchronization, user presence, and error handling.\n * The returned store can be passed directly to the Tldraw component to enable multiplayer features.\n *\n * The store progresses through multiple states:\n * - `loading`: Establishing connection and synchronizing initial state\n * - `synced-remote`: Successfully connected and actively synchronizing changes\n * - `error`: Connection failed or synchronization error occurred\n *\n * For optimal performance with media assets, provide an `assets` store that implements\n * external blob storage. Without this, large images and videos will be stored inline\n * as base64, causing performance issues during serialization.\n *\n * @param opts - Configuration options for multiplayer synchronization\n * - `uri` - WebSocket server URI (string or async function returning URI)\n * - `assets` - Asset store for blob storage (required for production use)\n * - `userInfo` - User information for presence system (can be reactive signal)\n * - `getUserPresence` - Optional function to customize presence data\n * - `onCustomMessageReceived` - Handler for custom socket messages\n * - `roomId` - Room identifier for analytics (internal use)\n * - `onMount` - Callback when editor mounts (internal use)\n * - `trackAnalyticsEvent` - Analytics event tracker (internal use)\n *\n * @returns A reactive store wrapper with connection status and synchronized store\n *\n * @example\n * ```tsx\n * // Basic multiplayer setup\n * function CollaborativeApp() {\n * const store = useSync({\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore,\n * userInfo: {\n * id: 'user-1',\n * name: 'Alice',\n * color: '#ff0000'\n * }\n * })\n *\n * if (store.status === 'loading') {\n * return <div>Connecting to collaboration session...</div>\n * }\n *\n * if (store.status === 'error') {\n * return <div>Failed to connect: {store.error.message}</div>\n * }\n *\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Dynamic authentication with reactive user info\n * import { atom } from '@tldraw/state'\n *\n * function AuthenticatedApp() {\n * const currentUser = atom('user', { id: 'user-1', name: 'Alice', color: '#ff0000' })\n *\n * const store = useSync({\n * uri: async () => {\n * const token = await getAuthToken()\n * return `wss://myserver.com/sync/room-123?token=${token}`\n * },\n * assets: authenticatedAssetStore,\n * userInfo: currentUser, // Reactive signal\n * getUserPresence: (store, user) => {\n * return {\n * userId: user.id,\n * userName: user.name,\n * cursor: getCurrentCursor(store)\n * }\n * }\n * })\n *\n * return <Tldraw store={store.store} />\n * }\n * ```\n *\n * @public\n */\nexport function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus {\n\tconst [state, setState] = useRefState<{\n\t\treadyClient?: TLSyncClient<TLRecord, TLStore>\n\t\terror?: Error\n\t} | null>(null)\n\tconst {\n\t\turi,\n\t\troomId = 'default',\n\t\tassets,\n\t\tonMount,\n\t\ttrackAnalyticsEvent: track,\n\t\tuserInfo,\n\t\tgetUserPresence: _getUserPresence,\n\t\tonCustomMessageReceived: _onCustomMessageReceived,\n\t\t...schemaOpts\n\t} = opts\n\n\t// This line will throw a type error if we add any new options to the useSync hook but we don't destructure them\n\t// This is required because otherwise the useTLSchemaFromUtils might return a new schema on every render if the newly-added option\n\t// is allowed to be unstable (e.g. userInfo)\n\tconst __never__: never = 0 as any as keyof Omit<typeof schemaOpts, keyof TLStoreSchemaOptions>\n\n\tconst schema = useTLSchemaFromUtils(schemaOpts)\n\n\tconst prefs = useShallowObjectIdentity(userInfo)\n\tconst getUserPresence = useReactiveEvent(_getUserPresence ?? getDefaultUserPresence)\n\tconst onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)\n\n\tconst userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(\n\t\t'userAtom',\n\t\tprefs\n\t)\n\n\tuseEffect(() => {\n\t\tuserAtom.set(prefs)\n\t}, [prefs, userAtom])\n\n\tuseEffect(() => {\n\t\tconst storeId = uniqueId()\n\n\t\tconst userPreferences = computed<{ id: string; color: string; name: string }>(\n\t\t\t'userPreferences',\n\t\t\t() => {\n\t\t\t\tconst userStuff = userAtom.get()\n\t\t\t\tconst user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()\n\t\t\t\treturn {\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tcolor: user.color ?? defaultUserPreferences.color,\n\t\t\t\t\tname: user.name ?? defaultUserPreferences.name,\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\tconst socket = new ClientWebSocketAdapter(async () => {\n\t\t\tconst uriString = typeof uri === 'string' ? uri : await uri()\n\n\t\t\t// set sessionId as a query param on the uri\n\t\t\tconst withParams = new URL(uriString)\n\t\t\tif (withParams.searchParams.has('sessionId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"sessionId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (withParams.searchParams.has('storeId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"storeId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\n\t\t\twithParams.searchParams.set('sessionId', TAB_ID)\n\t\t\twithParams.searchParams.set('storeId', storeId)\n\t\t\treturn withParams.toString()\n\t\t})\n\n\t\tlet didCancel = false\n\n\t\tconst collaborationStatusSignal = computed('collaboration status', () =>\n\t\t\tsocket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus\n\t\t)\n\n\t\tconst syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')\n\n\t\tconst store = createTLStore({\n\t\t\tid: storeId,\n\t\t\tschema,\n\t\t\tassets,\n\t\t\tonMount,\n\t\t\tcollaboration: {\n\t\t\t\tstatus: collaborationStatusSignal,\n\t\t\t\tmode: syncMode,\n\t\t\t},\n\t\t})\n\n\t\tconst presence = computed('instancePresence', () => {\n\t\t\tconst presenceState = getUserPresence(store, userPreferences.get())\n\t\t\tif (!presenceState) return null\n\n\t\t\treturn InstancePresenceRecordType.create({\n\t\t\t\t...presenceState,\n\t\t\t\tid: InstancePresenceRecordType.createId(store.id),\n\t\t\t})\n\t\t})\n\n\t\tconst otherUserPresences = store.query.ids('instance_presence', () => ({\n\t\t\tuserId: { neq: userPreferences.get().id },\n\t\t}))\n\n\t\tconst presenceMode = computed<TLPresenceMode>('presenceMode', () => {\n\t\t\tif (otherUserPresences.get().size === 0) return 'solo'\n\t\t\treturn 'full'\n\t\t})\n\n\t\tconst client = new TLSyncClient({\n\t\t\tstore,\n\t\t\tsocket,\n\t\t\tdidCancel: () => didCancel,\n\t\t\tonLoad(client) {\n\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })\n\t\t\t\tsetState({ readyClient: client })\n\t\t\t},\n\t\t\tonSyncError(reason) {\n\t\t\t\tconsole.error('sync error', reason)\n\n\t\t\t\tswitch (reason) {\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_FOUND:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.FORBIDDEN:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'forbidden', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'not-authenticated', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.RATE_LIMITED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'rate-limited', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error:' + reason, roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsetState({ error: new TLRemoteSyncError(reason) })\n\t\t\t\tsocket.close()\n\t\t\t},\n\t\t\tonAfterConnect(_, { isReadonly }) {\n\t\t\t\ttransact(() => {\n\t\t\t\t\tsyncMode.set(isReadonly ? 'readonly' : 'readwrite')\n\t\t\t\t\t// if the server crashes and loses all data it can return an empty document\n\t\t\t\t\t// when it comes back up. This is a safety check to make sure that if something like\n\t\t\t\t\t// that happens, it won't render the app broken and require a restart. The user will\n\t\t\t\t\t// most likely lose all their changes though since they'll have been working with pages\n\t\t\t\t\t// that won't exist. There's certainly something we can do to make this better.\n\t\t\t\t\t// but the likelihood of this happening is very low and maybe not worth caring about beyond this.\n\t\t\t\t\tstore.ensureStoreIsUsable()\n\t\t\t\t})\n\t\t\t},\n\t\t\tonCustomMessageReceived,\n\t\t\tpresence,\n\t\t\tpresenceMode,\n\t\t})\n\n\t\treturn () => {\n\t\t\tdidCancel = true\n\t\t\tclient.close()\n\t\t\tsocket.close()\n\t\t\tsetState(null)\n\t\t}\n\t}, [\n\t\tassets,\n\t\tonMount,\n\t\tuserAtom,\n\t\troomId,\n\t\tschema,\n\t\tsetState,\n\t\ttrack,\n\t\turi,\n\t\tgetUserPresence,\n\t\tonCustomMessageReceived,\n\t])\n\n\treturn useValue<RemoteTLStoreWithStatus>(\n\t\t'remote synced store',\n\t\t() => {\n\t\t\tif (!state) return { status: 'loading' }\n\t\t\tif (state.error) return { status: 'error', error: state.error }\n\t\t\tif (!state.readyClient) return { status: 'loading' }\n\t\t\tconst connectionStatus = state.readyClient.socket.connectionStatus\n\t\t\treturn {\n\t\t\t\tstatus: 'synced-remote',\n\t\t\t\tconnectionStatus: connectionStatus === 'error' ? 'offline' : connectionStatus,\n\t\t\t\tstore: state.readyClient.store,\n\t\t\t}\n\t\t},\n\t\t[state]\n\t)\n}\n\n/**\n * Configuration options for the {@link useSync} hook to establish multiplayer collaboration.\n *\n * This interface defines the required and optional settings for connecting to a multiplayer\n * server, managing user presence, handling assets, and customizing the collaboration experience.\n *\n * @example\n * ```tsx\n * const syncOptions: UseSyncOptions = {\n * uri: 'wss://myserver.com/sync/room-123',\n * assets: myAssetStore,\n * userInfo: {\n * id: 'user-1',\n * name: 'Alice',\n * color: '#ff0000'\n * },\n * getUserPresence: (store, user) => ({\n * userId: user.id,\n * userName: user.name,\n * cursor: getCursorPosition()\n * })\n * }\n * ```\n *\n * @public\n */\nexport interface UseSyncOptions {\n\t/**\n\t * The WebSocket URI of the multiplayer server for real-time synchronization.\n\t *\n\t * Must include the protocol (wss:// for secure, ws:// for local development).\n\t * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.\n\t *\n\t * Can be a static string or a function that returns a URI (useful for dynamic\n\t * authentication tokens or room routing). The function is called on each\n\t * connection attempt, allowing for token refresh and dynamic routing.\n\t *\n\t * Reserved query parameters `sessionId` and `storeId` are automatically added\n\t * by the sync system and should not be included in your URI.\n\t *\n\t * @example\n\t * ```ts\n\t * // Static URI\n\t * uri: 'wss://myserver.com/sync/room-123'\n\t *\n\t * // Dynamic URI with authentication\n\t * uri: async () => {\n\t * const token = await getAuthToken()\n\t * return `wss://myserver.com/sync/room-123?token=${token}`\n\t * }\n\t * ```\n\t */\n\turi: string | (() => string | Promise<string>)\n\n\t/**\n\t * User information for multiplayer presence and identification.\n\t *\n\t * Can be a static object or a reactive signal that updates when user\n\t * information changes. The presence system automatically updates when\n\t * reactive signals change, allowing real-time user profile updates.\n\t *\n\t * Should be synchronized with the `userPreferences` prop of the main\n\t * Tldraw component for consistent user experience. If not provided,\n\t * defaults to localStorage-based user preferences.\n\t *\n\t * @example\n\t * ```ts\n\t * // Static user info\n\t * userInfo: { id: 'user-123', name: 'Alice', color: '#ff0000' }\n\t *\n\t * // Reactive user info\n\t * const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000' })\n\t * userInfo: userSignal\n\t * ```\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\n\n\t/**\n\t * Asset store implementation for handling file uploads and storage.\n\t *\n\t * Required for production applications to handle images, videos, and other\n\t * media efficiently. Without an asset store, files are stored inline as\n\t * base64, which causes performance issues with large files.\n\t *\n\t * The asset store must implement upload (for new files) and resolve\n\t * (for displaying existing files) methods. For prototyping, you can use\n\t * {@link tldraw#inlineBase64AssetStore} but this is not recommended for production.\n\t *\n\t * @example\n\t * ```ts\n\t * const myAssetStore: TLAssetStore = {\n\t * upload: async (asset, file) => {\n\t * const url = await uploadToCloudStorage(file)\n\t * return { src: url }\n\t * },\n\t * resolve: (asset, context) => {\n\t * return getOptimizedUrl(asset.src, context)\n\t * }\n\t * }\n\t * ```\n\t */\n\tassets: TLAssetStore\n\n\t/**\n\t * Handler for receiving custom messages sent through the multiplayer connection.\n\t *\n\t * Use this to implement custom communication channels between clients beyond\n\t * the standard shape and presence synchronization. Messages are sent using\n\t * the TLSyncClient's sendMessage method.\n\t *\n\t * @param data - The custom message data received from another client\n\t *\n\t * @example\n\t * ```ts\n\t * onCustomMessageReceived: (data) => {\n\t * if (data.type === 'chat') {\n\t * displayChatMessage(data.message, data.userId)\n\t * }\n\t * }\n\t * ```\n\t */\n\tonCustomMessageReceived?(data: any): void\n\n\t/** @internal */\n\tonMount?(editor: Editor): void\n\t/** @internal used for analytics only, we should refactor this away */\n\troomId?: string\n\t/** @internal */\n\ttrackAnalyticsEvent?(name: string, data: { [key: string]: any }): void\n\n\t/**\n\t * A reactive function that returns a {@link @tldraw/tlschema#TLInstancePresence} object.\n\t *\n\t * This function is called reactively whenever the store state changes and\n\t * determines what presence information to broadcast to other clients. The\n\t * result is synchronized across all connected clients for displaying cursors,\n\t * selections, and other collaborative indicators.\n\t *\n\t * If not provided, uses the default implementation which includes standard\n\t * cursor position and selection state. Custom implementations allow you to\n\t * add additional presence data like current tool, view state, or custom status.\n\t *\n\t * See {@link @tldraw/tlschema#getDefaultUserPresence} for\n\t * the default implementation of this function.\n\t *\n\t * @param store - The current TLStore\n\t * @param user - The current user information\n\t * @returns Presence state to broadcast to other clients, or null to hide presence\n\t *\n\t * @example\n\t * ```ts\n\t * getUserPresence: (store, user) => {\n\t * return {\n\t * userId: user.id,\n\t * userName: user.name,\n\t * cursor: { x: 100, y: 200 },\n\t * currentTool: 'select',\n\t * isActive: true\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["client"]
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync react bindings).",
4
- "version": "4.2.0-canary.fda2467026d5",
4
+ "version": "4.2.0-next.47462e908ff5",
5
5
  "author": {
6
6
  "name": "tldraw GB Ltd.",
7
7
  "email": "hello@tldraw.com"
@@ -53,12 +53,12 @@
53
53
  "vitest": "^3.2.4"
54
54
  },
55
55
  "dependencies": {
56
- "@tldraw/state": "4.2.0-canary.fda2467026d5",
57
- "@tldraw/state-react": "4.2.0-canary.fda2467026d5",
58
- "@tldraw/sync-core": "4.2.0-canary.fda2467026d5",
59
- "@tldraw/utils": "4.2.0-canary.fda2467026d5",
56
+ "@tldraw/state": "4.2.0-next.47462e908ff5",
57
+ "@tldraw/state-react": "4.2.0-next.47462e908ff5",
58
+ "@tldraw/sync-core": "4.2.0-next.47462e908ff5",
59
+ "@tldraw/utils": "4.2.0-next.47462e908ff5",
60
60
  "nanoevents": "^7.0.1",
61
- "tldraw": "4.2.0-canary.fda2467026d5",
61
+ "tldraw": "4.2.0-next.47462e908ff5",
62
62
  "ws": "^8.18.0"
63
63
  },
64
64
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -2,15 +2,7 @@ import { registerTldrawLibraryVersion } from '@tldraw/utils'
2
2
  // eslint-disable-next-line local/no-export-star
3
3
  export * from '@tldraw/sync-core'
4
4
 
5
- export {
6
- useSync,
7
- type RemoteTLStoreWithStatus,
8
- type UseSyncConnectFn,
9
- type UseSyncOptions,
10
- type UseSyncOptionsBase,
11
- type UseSyncOptionsWithConnectFn,
12
- type UseSyncOptionsWithUri,
13
- } from './useSync'
5
+ export { useSync, type RemoteTLStoreWithStatus, type UseSyncOptions } from './useSync'
14
6
  export { useSyncDemo, type UseSyncDemoOptions } from './useSyncDemo'
15
7
 
16
8
  registerTldrawLibraryVersion(
package/src/useSync.ts CHANGED
@@ -3,11 +3,8 @@ import { useAtom } from '@tldraw/state-react'
3
3
  import {
4
4
  ClientWebSocketAdapter,
5
5
  TLCustomMessageHandler,
6
- TLPersistentClientSocket,
7
6
  TLPresenceMode,
8
7
  TLRemoteSyncError,
9
- TLSocketClientSentEvent,
10
- TLSocketServerSentEvent,
11
8
  TLSyncClient,
12
9
  TLSyncErrorCloseEventReason,
13
10
  } from '@tldraw/sync-core'
@@ -171,7 +168,6 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
171
168
  roomId = 'default',
172
169
  assets,
173
170
  onMount,
174
- connect,
175
171
  trackAnalyticsEvent: track,
176
172
  userInfo,
177
173
  getUserPresence: _getUserPresence,
@@ -215,60 +211,32 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
215
211
  }
216
212
  )
217
213
 
218
- let socket: TLPersistentClientSocket<
219
- TLSocketClientSentEvent<TLRecord>,
220
- TLSocketServerSentEvent<TLRecord>
221
- >
222
- if (connect) {
223
- if (uri) {
224
- throw new Error('uri and connect cannot be used together')
225
- }
214
+ const socket = new ClientWebSocketAdapter(async () => {
215
+ const uriString = typeof uri === 'string' ? uri : await uri()
226
216
 
227
- socket = connect({
228
- sessionId: TAB_ID,
229
- storeId,
230
- }) as TLPersistentClientSocket<
231
- TLSocketClientSentEvent<TLRecord>,
232
- TLSocketServerSentEvent<TLRecord>
233
- >
234
- } else if (uri) {
235
- if (connect) {
236
- throw new Error('uri and connect cannot be used together')
217
+ // set sessionId as a query param on the uri
218
+ const withParams = new URL(uriString)
219
+ if (withParams.searchParams.has('sessionId')) {
220
+ throw new Error(
221
+ 'useSync. "sessionId" is a reserved query param name. Please use a different name'
222
+ )
223
+ }
224
+ if (withParams.searchParams.has('storeId')) {
225
+ throw new Error(
226
+ 'useSync. "storeId" is a reserved query param name. Please use a different name'
227
+ )
237
228
  }
238
229
 
239
- socket = new ClientWebSocketAdapter(async () => {
240
- const uriString = typeof uri === 'string' ? uri : await uri()
241
-
242
- // set sessionId as a query param on the uri
243
- const withParams = new URL(uriString)
244
- if (withParams.searchParams.has('sessionId')) {
245
- throw new Error(
246
- 'useSync. "sessionId" is a reserved query param name. Please use a different name'
247
- )
248
- }
249
- if (withParams.searchParams.has('storeId')) {
250
- throw new Error(
251
- 'useSync. "storeId" is a reserved query param name. Please use a different name'
252
- )
253
- }
254
-
255
- withParams.searchParams.set('sessionId', TAB_ID)
256
- withParams.searchParams.set('storeId', storeId)
257
- return withParams.toString()
258
- })
259
- } else {
260
- throw new Error('uri or connect must be provided')
261
- }
230
+ withParams.searchParams.set('sessionId', TAB_ID)
231
+ withParams.searchParams.set('storeId', storeId)
232
+ return withParams.toString()
233
+ })
262
234
 
263
235
  let didCancel = false
264
236
 
265
- function getConnectionStatus() {
266
- return socket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus
267
- }
268
- const collaborationStatusSignal = atom('collaboration status', getConnectionStatus())
269
- const unsubscribeFromConnectionStatus = socket.onStatusChange(() => {
270
- collaborationStatusSignal.set(getConnectionStatus())
271
- })
237
+ const collaborationStatusSignal = computed('collaboration status', () =>
238
+ socket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus
239
+ )
272
240
 
273
241
  const syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')
274
242
 
@@ -302,7 +270,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
302
270
  return 'full'
303
271
  })
304
272
 
305
- const client = new TLSyncClient<TLRecord, TLStore>({
273
+ const client = new TLSyncClient({
306
274
  store,
307
275
  socket,
308
276
  didCancel: () => didCancel,
@@ -353,14 +321,13 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
353
321
 
354
322
  return () => {
355
323
  didCancel = true
356
- unsubscribeFromConnectionStatus()
357
324
  client.close()
358
325
  socket.close()
326
+ setState(null)
359
327
  }
360
328
  }, [
361
329
  assets,
362
330
  onMount,
363
- connect,
364
331
  userAtom,
365
332
  roomId,
366
333
  schema,
@@ -414,7 +381,34 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
414
381
  *
415
382
  * @public
416
383
  */
417
- export interface UseSyncOptionsBase {
384
+ export interface UseSyncOptions {
385
+ /**
386
+ * The WebSocket URI of the multiplayer server for real-time synchronization.
387
+ *
388
+ * Must include the protocol (wss:// for secure, ws:// for local development).
389
+ * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.
390
+ *
391
+ * Can be a static string or a function that returns a URI (useful for dynamic
392
+ * authentication tokens or room routing). The function is called on each
393
+ * connection attempt, allowing for token refresh and dynamic routing.
394
+ *
395
+ * Reserved query parameters `sessionId` and `storeId` are automatically added
396
+ * by the sync system and should not be included in your URI.
397
+ *
398
+ * @example
399
+ * ```ts
400
+ * // Static URI
401
+ * uri: 'wss://myserver.com/sync/room-123'
402
+ *
403
+ * // Dynamic URI with authentication
404
+ * uri: async () => {
405
+ * const token = await getAuthToken()
406
+ * return `wss://myserver.com/sync/room-123?token=${token}`
407
+ * }
408
+ * ```
409
+ */
410
+ uri: string | (() => string | Promise<string>)
411
+
418
412
  /**
419
413
  * User information for multiplayer presence and identification.
420
414
  *
@@ -525,57 +519,3 @@ export interface UseSyncOptionsBase {
525
519
  */
526
520
  getUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null
527
521
  }
528
-
529
- /** @public */
530
- export interface UseSyncOptionsWithUri extends UseSyncOptionsBase {
531
- /**
532
- * The WebSocket URI of the multiplayer server for real-time synchronization.
533
- *
534
- * Must include the protocol (wss:// for secure, ws:// for local development).
535
- * HTTP/HTTPS URLs will be automatically upgraded to WebSocket connections.
536
- *
537
- * Can be a static string or a function that returns a URI (useful for dynamic
538
- * authentication tokens or room routing). The function is called on each
539
- * connection attempt, allowing for token refresh and dynamic routing.
540
- *
541
- * Reserved query parameters `sessionId` and `storeId` are automatically added
542
- * by the sync system and should not be included in your URI.
543
- *
544
- * @example
545
- * ```ts
546
- * // Static URI
547
- * uri: 'wss://myserver.com/sync/room-123'
548
- *
549
- * // Dynamic URI with authentication
550
- * uri: async () => {
551
- * const token = await getAuthToken()
552
- * return `wss://myserver.com/sync/room-123?token=${token}`
553
- * }
554
- * ```
555
- */
556
- uri: string | (() => string | Promise<string>)
557
- connect?: never
558
- }
559
-
560
- /** @public */
561
- export interface UseSyncOptionsWithConnectFn extends UseSyncOptionsBase {
562
- /**
563
- * Create a connection to the server. Mostly you should use {@link UseSyncOptionsWithUri.uri}
564
- * instead, but this is useful if you want to use a custom transport to connect to the server,
565
- * instead of our default websocket-based transport.
566
- */
567
- connect: UseSyncConnectFn
568
- uri?: never
569
- }
570
-
571
- /** @public */
572
- export type UseSyncConnectFn = (query: {
573
- sessionId: string
574
- storeId: string
575
- }) => TLPersistentClientSocket
576
-
577
- /**
578
- * Options for the {@link useSync} hook.
579
- * @public
580
- */
581
- export type UseSyncOptions = UseSyncOptionsWithUri | UseSyncOptionsWithConnectFn