@tldraw/sync 4.3.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +2 -2
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/useSync.js.map +1 -1
- package/dist-cjs/useSyncDemo.js.map +1 -1
- package/dist-esm/index.d.mts +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/useSync.mjs.map +1 -1
- package/dist-esm/useSyncDemo.mjs.map +1 -1
- package/package.json +6 -6
- package/src/useSync.ts +1 -1
- package/src/useSyncDemo.ts +1 -1
package/dist-cjs/index.d.ts
CHANGED
|
@@ -155,7 +155,7 @@ export declare type UseSyncConnectFn = (query: {
|
|
|
155
155
|
* }
|
|
156
156
|
* ```
|
|
157
157
|
*
|
|
158
|
-
* @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link tldraw#TLStoreSchemaOptions}.
|
|
158
|
+
* @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.
|
|
159
159
|
*
|
|
160
160
|
* @public
|
|
161
161
|
*/
|
|
@@ -246,7 +246,7 @@ export declare interface UseSyncOptionsBase {
|
|
|
246
246
|
*
|
|
247
247
|
* The asset store must implement upload (for new files) and resolve
|
|
248
248
|
* (for displaying existing files) methods. For prototyping, you can use
|
|
249
|
-
* {@link tldraw#inlineBase64AssetStore} but this is not recommended for production.
|
|
249
|
+
* {@link @tldraw/editor#inlineBase64AssetStore} but this is not recommended for production.
|
|
250
250
|
*
|
|
251
251
|
* @example
|
|
252
252
|
* ```ts
|
package/dist-cjs/index.js
CHANGED
package/dist-cjs/useSync.js.map
CHANGED
|
@@ -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"],
|
|
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/editor#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
5
|
"mappings": "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",
|
|
6
6
|
"names": ["client"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/useSyncDemo.ts"],
|
|
4
|
-
"sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tSignal,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tclamp,\n\tdefaultBindingUtils,\n\tdefaultShapeUtils,\n\tgetHashForString,\n\tuniqueId,\n\tuseShallowObjectIdentity,\n} from 'tldraw'\nimport { RemoteTLStoreWithStatus, useSync } from './useSync'\n\n/** @public */\nexport interface UseSyncDemoOptions {\n\t/**\n\t * The room ID to sync with. Make sure the room ID is unique. The namespace is shared by\n\t * everyone using the demo server. Consider prefixing it with your company or project name.\n\t */\n\troomId: string\n\t/**\n\t * A signal that contains the user information needed for multiplayer features.\n\t * This should be synchronized with the `userPreferences` configuration for the main `<Tldraw />` component.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\n\n\t/** @internal */\n\thost?: string\n\n\t/**\n\t * {@inheritdoc UseSyncOptions.getUserPresence}\n\t * @public\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n\n/**\n * Safely accesses environment variables across different bundling environments.\n *\n * Depending on the environment this package is used in, process.env may not be available. This function\n * wraps `process.env` accesses in a try/catch to prevent runtime errors in environments where process\n * is not defined.\n *\n * The reason that this is just a try/catch and not a dynamic check e.g. `process &&\n * process.env[key]` is that many bundlers implement `process.env.WHATEVER` using compile-time\n * string replacement, rather than actually creating a runtime implementation of a `process` object.\n *\n * @param cb - Callback function that accesses an environment variable\n * @returns The environment variable value if available, otherwise undefined\n * @internal\n */\nfunction getEnv(cb: () => string | undefined): string | undefined {\n\ttry {\n\t\treturn cb()\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'\nconst IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'\n\n/**\n * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.\n *\n * The store can be passed directly into the `<Tldraw />` component to enable multiplayer features.\n * It will handle loading states, and enable multiplayer UX like user cursors and following.\n *\n * All data on the demo server is\n *\n * - Deleted after a day or so.\n * - Publicly accessible to anyone who knows the room ID. Use your company name as a prefix to help avoid collisions, or generate UUIDs for maximum privacy.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSyncDemo({roomId: 'my-app-test-room'})\n * return <Tldraw store={store} />\n * }\n * ```\n *\n * @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link tldraw#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSyncDemo(\n\toptions: UseSyncDemoOptions & TLStoreSchemaOptions\n): RemoteTLStoreWithStatus {\n\tconst { roomId, host = DEMO_WORKER, ..._syncOpts } = options\n\tconst assets = useMemo(() => createDemoAssetStore(host), [host])\n\n\tconst syncOpts = useShallowObjectIdentity(_syncOpts)\n\tconst syncOptsWithDefaults = useMemo(() => {\n\t\tif ('schema' in syncOpts && syncOpts.schema) return syncOpts\n\n\t\treturn {\n\t\t\t...syncOpts,\n\t\t\tshapeUtils:\n\t\t\t\t'shapeUtils' in syncOpts\n\t\t\t\t\t? [...defaultShapeUtils, ...(syncOpts.shapeUtils ?? [])]\n\t\t\t\t\t: defaultShapeUtils,\n\t\t\tbindingUtils:\n\t\t\t\t'bindingUtils' in syncOpts\n\t\t\t\t\t? [...defaultBindingUtils, ...(syncOpts.bindingUtils ?? [])]\n\t\t\t\t\t: defaultBindingUtils,\n\t\t}\n\t}, [syncOpts])\n\n\treturn useSync({\n\t\turi: `${host}/connect/${encodeURIComponent(roomId)}`,\n\t\troomId,\n\t\tassets,\n\t\tonMount: useCallback(\n\t\t\t(editor: Editor) => {\n\t\t\t\teditor.registerExternalAssetHandler('url', async ({ url }) => {\n\t\t\t\t\treturn await createAssetFromUrlUsingDemoServer(host, url)\n\t\t\t\t})\n\t\t\t},\n\t\t\t[host]\n\t\t),\n\t\t...syncOptsWithDefaults,\n\t})\n}\n\n/**\n * Determines whether file uploads should be disabled for a given host.\n *\n * Uploads are disabled for production tldraw domains to prevent abuse of the demo server\n * infrastructure. This includes tldraw.com and tldraw.xyz domains and their subdomains.\n *\n * @param host - The host URL to check for upload restrictions\n * @returns True if uploads should be disabled, false otherwise\n * @internal\n */\nfunction shouldDisallowUploads(host: string) {\n\tconst disallowedHosts = ['tldraw.com', 'tldraw.xyz']\n\treturn disallowedHosts.some(\n\t\t(disallowedHost) => host === disallowedHost || host.endsWith(`.${disallowedHost}`)\n\t)\n}\n\n/**\n * Creates an asset store implementation optimized for the tldraw demo server.\n *\n * This asset store handles file uploads to the demo server and provides intelligent\n * asset resolution with automatic image optimization based on network conditions,\n * screen density, and display size. It includes safeguards to prevent uploads to\n * production domains and optimizes images through the tldraw image processing service.\n *\n * @param host - The demo server host URL for file uploads and asset resolution\n * @returns A TLAssetStore implementation with upload and resolve capabilities\n * @example\n * ```ts\n * const assetStore = createDemoAssetStore('https://demo.tldraw.xyz')\n *\n * // Upload a file\n * const result = await assetStore.upload(asset, file)\n * console.log('Uploaded to:', result.src)\n *\n * // Resolve optimized asset URL\n * const optimizedUrl = assetStore.resolve(imageAsset, {\n * steppedScreenScale: 1.5,\n * dpr: 2,\n * networkEffectiveType: '4g'\n * })\n * ```\n * @internal\n */\nfunction createDemoAssetStore(host: string): TLAssetStore {\n\treturn {\n\t\tupload: async (_asset, file) => {\n\t\t\tif (shouldDisallowUploads(host)) {\n\t\t\t\talert('Uploading images is disabled in this demo.')\n\t\t\t\tthrow new Error('Uploading images is disabled in this demo.')\n\t\t\t}\n\t\t\tconst id = uniqueId()\n\n\t\t\tconst objectName = `${id}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tawait fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tif (!asset.props.src) return null\n\n\t\t\t// We don't deal with videos at the moment.\n\t\t\tif (asset.type === 'video') return asset.props.src\n\n\t\t\t// Assert it's an image to make TS happy.\n\t\t\tif (asset.type !== 'image') return null\n\n\t\t\t// Don't try to transform data: URLs, yikes.\n\t\t\tif (!asset.props.src.startsWith('http:') && !asset.props.src.startsWith('https:'))\n\t\t\t\treturn asset.props.src\n\n\t\t\tif (context.shouldResolveToOriginal) return asset.props.src\n\n\t\t\t// Don't try to transform animated images.\n\t\t\tif (MediaHelpers.isAnimatedImageType(asset?.props.mimeType) || asset.props.isAnimated)\n\t\t\t\treturn asset.props.src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset?.props.mimeType)) return asset.props.src\n\n\t\t\tconst url = new URL(asset.props.src)\n\n\t\t\t// we only transform images that are hosted on domains we control\n\t\t\tconst isTldrawImage =\n\t\t\t\turl.origin === host || /\\.tldraw\\.(?:com|xyz|dev|workers\\.dev)$/.test(url.host)\n\n\t\t\tif (!isTldrawImage) return asset.props.src\n\n\t\t\t// Assets that are under a certain file size aren't worth transforming (and incurring cost).\n\t\t\t// We still send them through the image worker to get them optimized though.\n\t\t\tconst { fileSize = 0 } = asset.props\n\t\t\tconst isWorthResizing = fileSize >= 1024 * 1024 * 1.5\n\n\t\t\tif (isWorthResizing) {\n\t\t\t\t// N.B. navigator.connection is only available in certain browsers (mainly Blink-based browsers)\n\t\t\t\t// 4g is as high the 'effectiveType' goes and we can pick a lower effective image quality for slower connections.\n\t\t\t\tconst networkCompensation =\n\t\t\t\t\t!context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5\n\n\t\t\t\tconst width = Math.ceil(\n\t\t\t\t\tMath.min(\n\t\t\t\t\t\tasset.props.w *\n\t\t\t\t\t\t\tclamp(context.steppedScreenScale, 1 / 32, 1) *\n\t\t\t\t\t\t\tnetworkCompensation *\n\t\t\t\t\t\t\tcontext.dpr,\n\t\t\t\t\t\tasset.props.w\n\t\t\t\t\t)\n\t\t\t\t)\n\n\t\t\t\turl.searchParams.set('w', width.toString())\n\t\t\t}\n\n\t\t\tconst newUrl = `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t\treturn newUrl\n\t\t},\n\t}\n}\n\n/**\n * Creates a bookmark asset by fetching metadata from a URL using the demo server.\n *\n * This function uses the demo server's bookmark unfurling service to extract metadata\n * like title, description, favicon, and preview image from a given URL. If the metadata\n * fetch fails, it returns a blank bookmark asset with just the URL.\n *\n * @param host - The demo server host URL to use for bookmark unfurling\n * @param url - The URL to create a bookmark asset from\n * @returns A promise that resolves to a TLAsset of type 'bookmark' with extracted metadata\n * @example\n * ```ts\n * const asset = await createAssetFromUrlUsingDemoServer(\n * 'https://demo.tldraw.xyz',\n * 'https://example.com'\n * )\n *\n * console.log(asset.props.title) // \"Example Domain\"\n * console.log(asset.props.description) // \"This domain is for use in illustrative examples...\"\n * ```\n * @internal\n */\nasync function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {\n\tconst urlHash = getHashForString(url)\n\ttry {\n\t\t// First, try to get the meta data from our endpoint\n\t\tconst fetchUrl = new URL(`${host}/bookmarks/unfurl`)\n\t\tfetchUrl.searchParams.set('url', url)\n\n\t\tconst meta = (await (await fetch(fetchUrl, { method: 'POST' })).json()) as {\n\t\t\tdescription?: string\n\t\t\timage?: string\n\t\t\tfavicon?: string\n\t\t\ttitle?: string\n\t\t} | null\n\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: meta?.description ?? '',\n\t\t\t\timage: meta?.image ?? '',\n\t\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\t\ttitle: meta?.title ?? '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t} catch (error) {\n\t\t// Otherwise, fallback to a blank bookmark\n\t\tconsole.error(error)\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: '',\n\t\t\t\timage: '',\n\t\t\t\tfavicon: '',\n\t\t\t\ttitle: '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t}\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tSignal,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tclamp,\n\tdefaultBindingUtils,\n\tdefaultShapeUtils,\n\tgetHashForString,\n\tuniqueId,\n\tuseShallowObjectIdentity,\n} from 'tldraw'\nimport { RemoteTLStoreWithStatus, useSync } from './useSync'\n\n/** @public */\nexport interface UseSyncDemoOptions {\n\t/**\n\t * The room ID to sync with. Make sure the room ID is unique. The namespace is shared by\n\t * everyone using the demo server. Consider prefixing it with your company or project name.\n\t */\n\troomId: string\n\t/**\n\t * A signal that contains the user information needed for multiplayer features.\n\t * This should be synchronized with the `userPreferences` configuration for the main `<Tldraw />` component.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\n\n\t/** @internal */\n\thost?: string\n\n\t/**\n\t * {@inheritdoc UseSyncOptions.getUserPresence}\n\t * @public\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n\n/**\n * Safely accesses environment variables across different bundling environments.\n *\n * Depending on the environment this package is used in, process.env may not be available. This function\n * wraps `process.env` accesses in a try/catch to prevent runtime errors in environments where process\n * is not defined.\n *\n * The reason that this is just a try/catch and not a dynamic check e.g. `process &&\n * process.env[key]` is that many bundlers implement `process.env.WHATEVER` using compile-time\n * string replacement, rather than actually creating a runtime implementation of a `process` object.\n *\n * @param cb - Callback function that accesses an environment variable\n * @returns The environment variable value if available, otherwise undefined\n * @internal\n */\nfunction getEnv(cb: () => string | undefined): string | undefined {\n\ttry {\n\t\treturn cb()\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'\nconst IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'\n\n/**\n * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.\n *\n * The store can be passed directly into the `<Tldraw />` component to enable multiplayer features.\n * It will handle loading states, and enable multiplayer UX like user cursors and following.\n *\n * All data on the demo server is\n *\n * - Deleted after a day or so.\n * - Publicly accessible to anyone who knows the room ID. Use your company name as a prefix to help avoid collisions, or generate UUIDs for maximum privacy.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSyncDemo({roomId: 'my-app-test-room'})\n * return <Tldraw store={store} />\n * }\n * ```\n *\n * @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSyncDemo(\n\toptions: UseSyncDemoOptions & TLStoreSchemaOptions\n): RemoteTLStoreWithStatus {\n\tconst { roomId, host = DEMO_WORKER, ..._syncOpts } = options\n\tconst assets = useMemo(() => createDemoAssetStore(host), [host])\n\n\tconst syncOpts = useShallowObjectIdentity(_syncOpts)\n\tconst syncOptsWithDefaults = useMemo(() => {\n\t\tif ('schema' in syncOpts && syncOpts.schema) return syncOpts\n\n\t\treturn {\n\t\t\t...syncOpts,\n\t\t\tshapeUtils:\n\t\t\t\t'shapeUtils' in syncOpts\n\t\t\t\t\t? [...defaultShapeUtils, ...(syncOpts.shapeUtils ?? [])]\n\t\t\t\t\t: defaultShapeUtils,\n\t\t\tbindingUtils:\n\t\t\t\t'bindingUtils' in syncOpts\n\t\t\t\t\t? [...defaultBindingUtils, ...(syncOpts.bindingUtils ?? [])]\n\t\t\t\t\t: defaultBindingUtils,\n\t\t}\n\t}, [syncOpts])\n\n\treturn useSync({\n\t\turi: `${host}/connect/${encodeURIComponent(roomId)}`,\n\t\troomId,\n\t\tassets,\n\t\tonMount: useCallback(\n\t\t\t(editor: Editor) => {\n\t\t\t\teditor.registerExternalAssetHandler('url', async ({ url }) => {\n\t\t\t\t\treturn await createAssetFromUrlUsingDemoServer(host, url)\n\t\t\t\t})\n\t\t\t},\n\t\t\t[host]\n\t\t),\n\t\t...syncOptsWithDefaults,\n\t})\n}\n\n/**\n * Determines whether file uploads should be disabled for a given host.\n *\n * Uploads are disabled for production tldraw domains to prevent abuse of the demo server\n * infrastructure. This includes tldraw.com and tldraw.xyz domains and their subdomains.\n *\n * @param host - The host URL to check for upload restrictions\n * @returns True if uploads should be disabled, false otherwise\n * @internal\n */\nfunction shouldDisallowUploads(host: string) {\n\tconst disallowedHosts = ['tldraw.com', 'tldraw.xyz']\n\treturn disallowedHosts.some(\n\t\t(disallowedHost) => host === disallowedHost || host.endsWith(`.${disallowedHost}`)\n\t)\n}\n\n/**\n * Creates an asset store implementation optimized for the tldraw demo server.\n *\n * This asset store handles file uploads to the demo server and provides intelligent\n * asset resolution with automatic image optimization based on network conditions,\n * screen density, and display size. It includes safeguards to prevent uploads to\n * production domains and optimizes images through the tldraw image processing service.\n *\n * @param host - The demo server host URL for file uploads and asset resolution\n * @returns A TLAssetStore implementation with upload and resolve capabilities\n * @example\n * ```ts\n * const assetStore = createDemoAssetStore('https://demo.tldraw.xyz')\n *\n * // Upload a file\n * const result = await assetStore.upload(asset, file)\n * console.log('Uploaded to:', result.src)\n *\n * // Resolve optimized asset URL\n * const optimizedUrl = assetStore.resolve(imageAsset, {\n * steppedScreenScale: 1.5,\n * dpr: 2,\n * networkEffectiveType: '4g'\n * })\n * ```\n * @internal\n */\nfunction createDemoAssetStore(host: string): TLAssetStore {\n\treturn {\n\t\tupload: async (_asset, file) => {\n\t\t\tif (shouldDisallowUploads(host)) {\n\t\t\t\talert('Uploading images is disabled in this demo.')\n\t\t\t\tthrow new Error('Uploading images is disabled in this demo.')\n\t\t\t}\n\t\t\tconst id = uniqueId()\n\n\t\t\tconst objectName = `${id}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tawait fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tif (!asset.props.src) return null\n\n\t\t\t// We don't deal with videos at the moment.\n\t\t\tif (asset.type === 'video') return asset.props.src\n\n\t\t\t// Assert it's an image to make TS happy.\n\t\t\tif (asset.type !== 'image') return null\n\n\t\t\t// Don't try to transform data: URLs, yikes.\n\t\t\tif (!asset.props.src.startsWith('http:') && !asset.props.src.startsWith('https:'))\n\t\t\t\treturn asset.props.src\n\n\t\t\tif (context.shouldResolveToOriginal) return asset.props.src\n\n\t\t\t// Don't try to transform animated images.\n\t\t\tif (MediaHelpers.isAnimatedImageType(asset?.props.mimeType) || asset.props.isAnimated)\n\t\t\t\treturn asset.props.src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset?.props.mimeType)) return asset.props.src\n\n\t\t\tconst url = new URL(asset.props.src)\n\n\t\t\t// we only transform images that are hosted on domains we control\n\t\t\tconst isTldrawImage =\n\t\t\t\turl.origin === host || /\\.tldraw\\.(?:com|xyz|dev|workers\\.dev)$/.test(url.host)\n\n\t\t\tif (!isTldrawImage) return asset.props.src\n\n\t\t\t// Assets that are under a certain file size aren't worth transforming (and incurring cost).\n\t\t\t// We still send them through the image worker to get them optimized though.\n\t\t\tconst { fileSize = 0 } = asset.props\n\t\t\tconst isWorthResizing = fileSize >= 1024 * 1024 * 1.5\n\n\t\t\tif (isWorthResizing) {\n\t\t\t\t// N.B. navigator.connection is only available in certain browsers (mainly Blink-based browsers)\n\t\t\t\t// 4g is as high the 'effectiveType' goes and we can pick a lower effective image quality for slower connections.\n\t\t\t\tconst networkCompensation =\n\t\t\t\t\t!context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5\n\n\t\t\t\tconst width = Math.ceil(\n\t\t\t\t\tMath.min(\n\t\t\t\t\t\tasset.props.w *\n\t\t\t\t\t\t\tclamp(context.steppedScreenScale, 1 / 32, 1) *\n\t\t\t\t\t\t\tnetworkCompensation *\n\t\t\t\t\t\t\tcontext.dpr,\n\t\t\t\t\t\tasset.props.w\n\t\t\t\t\t)\n\t\t\t\t)\n\n\t\t\t\turl.searchParams.set('w', width.toString())\n\t\t\t}\n\n\t\t\tconst newUrl = `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t\treturn newUrl\n\t\t},\n\t}\n}\n\n/**\n * Creates a bookmark asset by fetching metadata from a URL using the demo server.\n *\n * This function uses the demo server's bookmark unfurling service to extract metadata\n * like title, description, favicon, and preview image from a given URL. If the metadata\n * fetch fails, it returns a blank bookmark asset with just the URL.\n *\n * @param host - The demo server host URL to use for bookmark unfurling\n * @param url - The URL to create a bookmark asset from\n * @returns A promise that resolves to a TLAsset of type 'bookmark' with extracted metadata\n * @example\n * ```ts\n * const asset = await createAssetFromUrlUsingDemoServer(\n * 'https://demo.tldraw.xyz',\n * 'https://example.com'\n * )\n *\n * console.log(asset.props.title) // \"Example Domain\"\n * console.log(asset.props.description) // \"This domain is for use in illustrative examples...\"\n * ```\n * @internal\n */\nasync function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {\n\tconst urlHash = getHashForString(url)\n\ttry {\n\t\t// First, try to get the meta data from our endpoint\n\t\tconst fetchUrl = new URL(`${host}/bookmarks/unfurl`)\n\t\tfetchUrl.searchParams.set('url', url)\n\n\t\tconst meta = (await (await fetch(fetchUrl, { method: 'POST' })).json()) as {\n\t\t\tdescription?: string\n\t\t\timage?: string\n\t\t\tfavicon?: string\n\t\t\ttitle?: string\n\t\t} | null\n\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: meta?.description ?? '',\n\t\t\t\timage: meta?.image ?? '',\n\t\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\t\ttitle: meta?.title ?? '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t} catch (error) {\n\t\t// Otherwise, fallback to a blank bookmark\n\t\tconsole.error(error)\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: '',\n\t\t\t\timage: '',\n\t\t\t\tfavicon: '',\n\t\t\t\ttitle: '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t}\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAqC;AACrC,oBAiBO;AACP,qBAAiD;AAyCjD,SAAS,OAAO,IAAkD;AACjE,MAAI;AACH,WAAO,GAAG;AAAA,EACX,QAAQ;AACP,WAAO;AAAA,EACR;AACD;AAEA,MAAM,cAAc,OAAO,MAAM,yBAA2B,KAAK;AACjE,MAAM,eAAe,OAAO,MAAM,QAAQ,IAAI,gBAAgB,KAAK;AAyB5D,SAAS,YACf,SAC0B;AAC1B,QAAM,EAAE,QAAQ,OAAO,aAAa,GAAG,UAAU,IAAI;AACrD,QAAM,aAAS,sBAAQ,MAAM,qBAAqB,IAAI,GAAG,CAAC,IAAI,CAAC;AAE/D,QAAM,eAAW,wCAAyB,SAAS;AACnD,QAAM,2BAAuB,sBAAQ,MAAM;AAC1C,QAAI,YAAY,YAAY,SAAS,OAAQ,QAAO;AAEpD,WAAO;AAAA,MACN,GAAG;AAAA,MACH,YACC,gBAAgB,WACb,CAAC,GAAG,iCAAmB,GAAI,SAAS,cAAc,CAAC,CAAE,IACrD;AAAA,MACJ,cACC,kBAAkB,WACf,CAAC,GAAG,mCAAqB,GAAI,SAAS,gBAAgB,CAAC,CAAE,IACzD;AAAA,IACL;AAAA,EACD,GAAG,CAAC,QAAQ,CAAC;AAEb,aAAO,wBAAQ;AAAA,IACd,KAAK,GAAG,IAAI,YAAY,mBAAmB,MAAM,CAAC;AAAA,IAClD;AAAA,IACA;AAAA,IACA,aAAS;AAAA,MACR,CAAC,WAAmB;AACnB,eAAO,6BAA6B,OAAO,OAAO,EAAE,IAAI,MAAM;AAC7D,iBAAO,MAAM,kCAAkC,MAAM,GAAG;AAAA,QACzD,CAAC;AAAA,MACF;AAAA,MACA,CAAC,IAAI;AAAA,IACN;AAAA,IACA,GAAG;AAAA,EACJ,CAAC;AACF;AAYA,SAAS,sBAAsB,MAAc;AAC5C,QAAM,kBAAkB,CAAC,cAAc,YAAY;AACnD,SAAO,gBAAgB;AAAA,IACtB,CAAC,mBAAmB,SAAS,kBAAkB,KAAK,SAAS,IAAI,cAAc,EAAE;AAAA,EAClF;AACD;AA6BA,SAAS,qBAAqB,MAA4B;AACzD,SAAO;AAAA,IACN,QAAQ,OAAO,QAAQ,SAAS;AAC/B,UAAI,sBAAsB,IAAI,GAAG;AAChC,cAAM,4CAA4C;AAClD,cAAM,IAAI,MAAM,4CAA4C;AAAA,MAC7D;AACA,YAAM,SAAK,wBAAS;AAEpB,YAAM,aAAa,GAAG,EAAE,IAAI,KAAK,IAAI,GAAG,QAAQ,OAAO,GAAG;AAC1D,YAAM,MAAM,GAAG,IAAI,YAAY,UAAU;AAEzC,YAAM,MAAM,KAAK;AAAA,QAChB,QAAQ;AAAA,QACR,MAAM;AAAA,MACP,CAAC;AAED,aAAO,EAAE,KAAK,IAAI;AAAA,IACnB;AAAA,IAEA,QAAQ,OAAO,SAAS;AACvB,UAAI,CAAC,MAAM,MAAM,IAAK,QAAO;AAG7B,UAAI,MAAM,SAAS,QAAS,QAAO,MAAM,MAAM;AAG/C,UAAI,MAAM,SAAS,QAAS,QAAO;AAGnC,UAAI,CAAC,MAAM,MAAM,IAAI,WAAW,OAAO,KAAK,CAAC,MAAM,MAAM,IAAI,WAAW,QAAQ;AAC/E,eAAO,MAAM,MAAM;AAEpB,UAAI,QAAQ,wBAAyB,QAAO,MAAM,MAAM;AAGxD,UAAI,2BAAa,oBAAoB,OAAO,MAAM,QAAQ,KAAK,MAAM,MAAM;AAC1E,eAAO,MAAM,MAAM;AAGpB,UAAI,2BAAa,kBAAkB,OAAO,MAAM,QAAQ,EAAG,QAAO,MAAM,MAAM;AAE9E,YAAM,MAAM,IAAI,IAAI,MAAM,MAAM,GAAG;AAGnC,YAAM,gBACL,IAAI,WAAW,QAAQ,0CAA0C,KAAK,IAAI,IAAI;AAE/E,UAAI,CAAC,cAAe,QAAO,MAAM,MAAM;AAIvC,YAAM,EAAE,WAAW,EAAE,IAAI,MAAM;AAC/B,YAAM,kBAAkB,YAAY,OAAO,OAAO;AAElD,UAAI,iBAAiB;AAGpB,cAAM,sBACL,CAAC,QAAQ,wBAAwB,QAAQ,yBAAyB,OAAO,IAAI;AAE9E,cAAM,QAAQ,KAAK;AAAA,UAClB,KAAK;AAAA,YACJ,MAAM,MAAM,QACX,qBAAM,QAAQ,oBAAoB,IAAI,IAAI,CAAC,IAC3C,sBACA,QAAQ;AAAA,YACT,MAAM,MAAM;AAAA,UACb;AAAA,QACD;AAEA,YAAI,aAAa,IAAI,KAAK,MAAM,SAAS,CAAC;AAAA,MAC3C;AAEA,YAAM,SAAS,GAAG,YAAY,IAAI,IAAI,IAAI,IAAI,IAAI,SAAS,EAAE,MAAM,IAAI,OAAO,SAAS,CAAC,CAAC;AACzF,aAAO;AAAA,IACR;AAAA,EACD;AACD;AAwBA,eAAe,kCAAkC,MAAc,KAA+B;AAC7F,QAAM,cAAU,gCAAiB,GAAG;AACpC,MAAI;AAEH,UAAM,WAAW,IAAI,IAAI,GAAG,IAAI,mBAAmB;AACnD,aAAS,aAAa,IAAI,OAAO,GAAG;AAEpC,UAAM,OAAQ,OAAO,MAAM,MAAM,UAAU,EAAE,QAAQ,OAAO,CAAC,GAAG,KAAK;AAOrE,WAAO;AAAA,MACN,IAAI,8BAAgB,SAAS,OAAO;AAAA,MACpC,UAAU;AAAA,MACV,MAAM;AAAA,MACN,OAAO;AAAA,QACN,KAAK;AAAA,QACL,aAAa,MAAM,eAAe;AAAA,QAClC,OAAO,MAAM,SAAS;AAAA,QACtB,SAAS,MAAM,WAAW;AAAA,QAC1B,OAAO,MAAM,SAAS;AAAA,MACvB;AAAA,MACA,MAAM,CAAC;AAAA,IACR;AAAA,EACD,SAAS,OAAO;AAEf,YAAQ,MAAM,KAAK;AACnB,WAAO;AAAA,MACN,IAAI,8BAAgB,SAAS,OAAO;AAAA,MACpC,UAAU;AAAA,MACV,MAAM;AAAA,MACN,OAAO;AAAA,QACN,KAAK;AAAA,QACL,aAAa;AAAA,QACb,OAAO;AAAA,QACP,SAAS;AAAA,QACT,OAAO;AAAA,MACR;AAAA,MACA,MAAM,CAAC;AAAA,IACR;AAAA,EACD;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.d.mts
CHANGED
|
@@ -155,7 +155,7 @@ export declare type UseSyncConnectFn = (query: {
|
|
|
155
155
|
* }
|
|
156
156
|
* ```
|
|
157
157
|
*
|
|
158
|
-
* @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link tldraw#TLStoreSchemaOptions}.
|
|
158
|
+
* @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.
|
|
159
159
|
*
|
|
160
160
|
* @public
|
|
161
161
|
*/
|
|
@@ -246,7 +246,7 @@ export declare interface UseSyncOptionsBase {
|
|
|
246
246
|
*
|
|
247
247
|
* The asset store must implement upload (for new files) and resolve
|
|
248
248
|
* (for displaying existing files) methods. For prototyping, you can use
|
|
249
|
-
* {@link tldraw#inlineBase64AssetStore} but this is not recommended for production.
|
|
249
|
+
* {@link @tldraw/editor#inlineBase64AssetStore} but this is not recommended for production.
|
|
250
250
|
*
|
|
251
251
|
* @example
|
|
252
252
|
* ```ts
|
package/dist-esm/index.mjs
CHANGED
package/dist-esm/useSync.mjs.map
CHANGED
|
@@ -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"],
|
|
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/editor#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
5
|
"mappings": "AAAA,SAAS,MAAM,UAAU,gBAAgB;AACzC,SAAS,eAAe;AACxB;AAAA,EACC;AAAA,EAIA;AAAA,EAGA;AAAA,EACA;AAAA,OACM;AACP,SAAS,iBAAiB;AAC1B;AAAA,EAEC;AAAA,EAEA;AAAA,EAQA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACM;AAEP,MAAM,yBAAyB;AAE/B,MAAM,8BAAsD,MAAM;AAAC;AAyH5D,SAAS,QAAQ,MAAsE;AAC7F,QAAM,CAAC,OAAO,QAAQ,IAAI,YAGhB,IAAI;AACd,QAAM;AAAA,IACL;AAAA,IACA,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA;AAAA,IACA,qBAAqB;AAAA,IACrB;AAAA,IACA,iBAAiB;AAAA,IACjB,yBAAyB;AAAA,IACzB,GAAG;AAAA,EACJ,IAAI;AAKJ,QAAM,YAAmB;AAEzB,QAAM,SAAS,qBAAqB,UAAU;AAE9C,QAAM,QAAQ,yBAAyB,QAAQ;AAC/C,QAAM,kBAAkB,iBAAiB,oBAAoB,sBAAsB;AACnF,QAAM,0BAA0B,SAAS,4BAA4B,2BAA2B;AAEhG,QAAM,WAAW;AAAA,IAChB;AAAA,IACA;AAAA,EACD;AAEA,YAAU,MAAM;AACf,aAAS,IAAI,KAAK;AAAA,EACnB,GAAG,CAAC,OAAO,QAAQ,CAAC;AAEpB,YAAU,MAAM;AACf,UAAM,UAAU,SAAS;AAEzB,UAAM,kBAAkB;AAAA,MACvB;AAAA,MACA,MAAM;AACL,cAAM,YAAY,SAAS,IAAI;AAC/B,cAAM,QAAQ,SAAS,SAAS,IAAI,UAAU,IAAI,IAAI,cAAc,mBAAmB;AACvF,eAAO;AAAA,UACN,IAAI,KAAK;AAAA,UACT,OAAO,KAAK,SAAS,uBAAuB;AAAA,UAC5C,MAAM,KAAK,QAAQ,uBAAuB;AAAA,QAC3C;AAAA,MACD;AAAA,IACD;AAEA,QAAI;AAIJ,QAAI,SAAS;AACZ,UAAI,KAAK;AACR,cAAM,IAAI,MAAM,yCAAyC;AAAA,MAC1D;AAEA,eAAS,QAAQ;AAAA,QAChB,WAAW;AAAA,QACX;AAAA,MACD,CAAC;AAAA,IAIF,WAAW,KAAK;AACf,UAAI,SAAS;AACZ,cAAM,IAAI,MAAM,yCAAyC;AAAA,MAC1D;AAEA,eAAS,IAAI,uBAAuB,YAAY;AAC/C,cAAM,YAAY,OAAO,QAAQ,WAAW,MAAM,MAAM,IAAI;AAG5D,cAAM,aAAa,IAAI,IAAI,SAAS;AACpC,YAAI,WAAW,aAAa,IAAI,WAAW,GAAG;AAC7C,gBAAM,IAAI;AAAA,YACT;AAAA,UACD;AAAA,QACD;AACA,YAAI,WAAW,aAAa,IAAI,SAAS,GAAG;AAC3C,gBAAM,IAAI;AAAA,YACT;AAAA,UACD;AAAA,QACD;AAEA,mBAAW,aAAa,IAAI,aAAa,MAAM;AAC/C,mBAAW,aAAa,IAAI,WAAW,OAAO;AAC9C,eAAO,WAAW,SAAS;AAAA,MAC5B,CAAC;AAAA,IACF,OAAO;AACN,YAAM,IAAI,MAAM,iCAAiC;AAAA,IAClD;AAEA,QAAI,YAAY;AAEhB,aAAS,sBAAsB;AAC9B,aAAO,OAAO,qBAAqB,UAAU,YAAY,OAAO;AAAA,IACjE;AACA,UAAM,4BAA4B,KAAK,wBAAwB,oBAAoB,CAAC;AACpF,UAAM,kCAAkC,OAAO,eAAe,MAAM;AACnE,gCAA0B,IAAI,oBAAoB,CAAC;AAAA,IACpD,CAAC;AAED,UAAM,WAAW,KAAK,aAAa,WAAuC;AAE1E,UAAM,QAAQ,cAAc;AAAA,MAC3B,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,eAAe;AAAA,QACd,QAAQ;AAAA,QACR,MAAM;AAAA,MACP;AAAA,IACD,CAAC;AAED,UAAM,WAAW,SAAS,oBAAoB,MAAM;AACnD,YAAM,gBAAgB,gBAAgB,OAAO,gBAAgB,IAAI,CAAC;AAClE,UAAI,CAAC,cAAe,QAAO;AAE3B,aAAO,2BAA2B,OAAO;AAAA,QACxC,GAAG;AAAA,QACH,IAAI,2BAA2B,SAAS,MAAM,EAAE;AAAA,MACjD,CAAC;AAAA,IACF,CAAC;AAED,UAAM,qBAAqB,MAAM,MAAM,IAAI,qBAAqB,OAAO;AAAA,MACtE,QAAQ,EAAE,KAAK,gBAAgB,IAAI,EAAE,GAAG;AAAA,IACzC,EAAE;AAEF,UAAM,eAAe,SAAyB,gBAAgB,MAAM;AACnE,UAAI,mBAAmB,IAAI,EAAE,SAAS,EAAG,QAAO;AAChD,aAAO;AAAA,IACR,CAAC;AAED,UAAM,SAAS,IAAI,aAAgC;AAAA,MAClD;AAAA,MACA;AAAA,MACA,WAAW,MAAM;AAAA,MACjB,OAAOA,SAAQ;AACd,gBAAQ,wBAAwB,EAAE,MAAM,QAAQ,OAAO,CAAC;AACxD,iBAAS,EAAE,aAAaA,QAAO,CAAC;AAAA,MACjC;AAAA,MACA,YAAY,QAAQ;AACnB,gBAAQ,MAAM,cAAc,MAAM;AAElC,gBAAQ,QAAQ;AAAA,UACf,KAAK,4BAA4B;AAChC,oBAAQ,wBAAwB,EAAE,MAAM,kBAAkB,OAAO,CAAC;AAClE;AAAA,UACD,KAAK,4BAA4B;AAChC,oBAAQ,wBAAwB,EAAE,MAAM,aAAa,OAAO,CAAC;AAC7D;AAAA,UACD,KAAK,4BAA4B;AAChC,oBAAQ,wBAAwB,EAAE,MAAM,qBAAqB,OAAO,CAAC;AACrE;AAAA,UACD,KAAK,4BAA4B;AAChC,oBAAQ,wBAAwB,EAAE,MAAM,gBAAgB,OAAO,CAAC;AAChE;AAAA,UACD;AACC,oBAAQ,wBAAwB,EAAE,MAAM,gBAAgB,QAAQ,OAAO,CAAC;AACxE;AAAA,QACF;AAEA,iBAAS,EAAE,OAAO,IAAI,kBAAkB,MAAM,EAAE,CAAC;AACjD,eAAO,MAAM;AAAA,MACd;AAAA,MACA,eAAe,GAAG,EAAE,WAAW,GAAG;AACjC,iBAAS,MAAM;AACd,mBAAS,IAAI,aAAa,aAAa,WAAW;AAOlD,gBAAM,oBAAoB;AAAA,QAC3B,CAAC;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACD,CAAC;AAED,WAAO,MAAM;AACZ,kBAAY;AACZ,sCAAgC;AAChC,aAAO,MAAM;AACb,aAAO,MAAM;AAAA,IACd;AAAA,EACD,GAAG;AAAA,IACF;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD,CAAC;AAED,SAAO;AAAA,IACN;AAAA,IACA,MAAM;AACL,UAAI,CAAC,MAAO,QAAO,EAAE,QAAQ,UAAU;AACvC,UAAI,MAAM,MAAO,QAAO,EAAE,QAAQ,SAAS,OAAO,MAAM,MAAM;AAC9D,UAAI,CAAC,MAAM,YAAa,QAAO,EAAE,QAAQ,UAAU;AACnD,YAAM,mBAAmB,MAAM,YAAY,OAAO;AAClD,aAAO;AAAA,QACN,QAAQ;AAAA,QACR,kBAAkB,qBAAqB,UAAU,YAAY;AAAA,QAC7D,OAAO,MAAM,YAAY;AAAA,MAC1B;AAAA,IACD;AAAA,IACA,CAAC,KAAK;AAAA,EACP;AACD;",
|
|
6
6
|
"names": ["client"]
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/useSyncDemo.ts"],
|
|
4
|
-
"sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tSignal,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tclamp,\n\tdefaultBindingUtils,\n\tdefaultShapeUtils,\n\tgetHashForString,\n\tuniqueId,\n\tuseShallowObjectIdentity,\n} from 'tldraw'\nimport { RemoteTLStoreWithStatus, useSync } from './useSync'\n\n/** @public */\nexport interface UseSyncDemoOptions {\n\t/**\n\t * The room ID to sync with. Make sure the room ID is unique. The namespace is shared by\n\t * everyone using the demo server. Consider prefixing it with your company or project name.\n\t */\n\troomId: string\n\t/**\n\t * A signal that contains the user information needed for multiplayer features.\n\t * This should be synchronized with the `userPreferences` configuration for the main `<Tldraw />` component.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\n\n\t/** @internal */\n\thost?: string\n\n\t/**\n\t * {@inheritdoc UseSyncOptions.getUserPresence}\n\t * @public\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n\n/**\n * Safely accesses environment variables across different bundling environments.\n *\n * Depending on the environment this package is used in, process.env may not be available. This function\n * wraps `process.env` accesses in a try/catch to prevent runtime errors in environments where process\n * is not defined.\n *\n * The reason that this is just a try/catch and not a dynamic check e.g. `process &&\n * process.env[key]` is that many bundlers implement `process.env.WHATEVER` using compile-time\n * string replacement, rather than actually creating a runtime implementation of a `process` object.\n *\n * @param cb - Callback function that accesses an environment variable\n * @returns The environment variable value if available, otherwise undefined\n * @internal\n */\nfunction getEnv(cb: () => string | undefined): string | undefined {\n\ttry {\n\t\treturn cb()\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'\nconst IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'\n\n/**\n * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.\n *\n * The store can be passed directly into the `<Tldraw />` component to enable multiplayer features.\n * It will handle loading states, and enable multiplayer UX like user cursors and following.\n *\n * All data on the demo server is\n *\n * - Deleted after a day or so.\n * - Publicly accessible to anyone who knows the room ID. Use your company name as a prefix to help avoid collisions, or generate UUIDs for maximum privacy.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSyncDemo({roomId: 'my-app-test-room'})\n * return <Tldraw store={store} />\n * }\n * ```\n *\n * @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link tldraw#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSyncDemo(\n\toptions: UseSyncDemoOptions & TLStoreSchemaOptions\n): RemoteTLStoreWithStatus {\n\tconst { roomId, host = DEMO_WORKER, ..._syncOpts } = options\n\tconst assets = useMemo(() => createDemoAssetStore(host), [host])\n\n\tconst syncOpts = useShallowObjectIdentity(_syncOpts)\n\tconst syncOptsWithDefaults = useMemo(() => {\n\t\tif ('schema' in syncOpts && syncOpts.schema) return syncOpts\n\n\t\treturn {\n\t\t\t...syncOpts,\n\t\t\tshapeUtils:\n\t\t\t\t'shapeUtils' in syncOpts\n\t\t\t\t\t? [...defaultShapeUtils, ...(syncOpts.shapeUtils ?? [])]\n\t\t\t\t\t: defaultShapeUtils,\n\t\t\tbindingUtils:\n\t\t\t\t'bindingUtils' in syncOpts\n\t\t\t\t\t? [...defaultBindingUtils, ...(syncOpts.bindingUtils ?? [])]\n\t\t\t\t\t: defaultBindingUtils,\n\t\t}\n\t}, [syncOpts])\n\n\treturn useSync({\n\t\turi: `${host}/connect/${encodeURIComponent(roomId)}`,\n\t\troomId,\n\t\tassets,\n\t\tonMount: useCallback(\n\t\t\t(editor: Editor) => {\n\t\t\t\teditor.registerExternalAssetHandler('url', async ({ url }) => {\n\t\t\t\t\treturn await createAssetFromUrlUsingDemoServer(host, url)\n\t\t\t\t})\n\t\t\t},\n\t\t\t[host]\n\t\t),\n\t\t...syncOptsWithDefaults,\n\t})\n}\n\n/**\n * Determines whether file uploads should be disabled for a given host.\n *\n * Uploads are disabled for production tldraw domains to prevent abuse of the demo server\n * infrastructure. This includes tldraw.com and tldraw.xyz domains and their subdomains.\n *\n * @param host - The host URL to check for upload restrictions\n * @returns True if uploads should be disabled, false otherwise\n * @internal\n */\nfunction shouldDisallowUploads(host: string) {\n\tconst disallowedHosts = ['tldraw.com', 'tldraw.xyz']\n\treturn disallowedHosts.some(\n\t\t(disallowedHost) => host === disallowedHost || host.endsWith(`.${disallowedHost}`)\n\t)\n}\n\n/**\n * Creates an asset store implementation optimized for the tldraw demo server.\n *\n * This asset store handles file uploads to the demo server and provides intelligent\n * asset resolution with automatic image optimization based on network conditions,\n * screen density, and display size. It includes safeguards to prevent uploads to\n * production domains and optimizes images through the tldraw image processing service.\n *\n * @param host - The demo server host URL for file uploads and asset resolution\n * @returns A TLAssetStore implementation with upload and resolve capabilities\n * @example\n * ```ts\n * const assetStore = createDemoAssetStore('https://demo.tldraw.xyz')\n *\n * // Upload a file\n * const result = await assetStore.upload(asset, file)\n * console.log('Uploaded to:', result.src)\n *\n * // Resolve optimized asset URL\n * const optimizedUrl = assetStore.resolve(imageAsset, {\n * steppedScreenScale: 1.5,\n * dpr: 2,\n * networkEffectiveType: '4g'\n * })\n * ```\n * @internal\n */\nfunction createDemoAssetStore(host: string): TLAssetStore {\n\treturn {\n\t\tupload: async (_asset, file) => {\n\t\t\tif (shouldDisallowUploads(host)) {\n\t\t\t\talert('Uploading images is disabled in this demo.')\n\t\t\t\tthrow new Error('Uploading images is disabled in this demo.')\n\t\t\t}\n\t\t\tconst id = uniqueId()\n\n\t\t\tconst objectName = `${id}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tawait fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tif (!asset.props.src) return null\n\n\t\t\t// We don't deal with videos at the moment.\n\t\t\tif (asset.type === 'video') return asset.props.src\n\n\t\t\t// Assert it's an image to make TS happy.\n\t\t\tif (asset.type !== 'image') return null\n\n\t\t\t// Don't try to transform data: URLs, yikes.\n\t\t\tif (!asset.props.src.startsWith('http:') && !asset.props.src.startsWith('https:'))\n\t\t\t\treturn asset.props.src\n\n\t\t\tif (context.shouldResolveToOriginal) return asset.props.src\n\n\t\t\t// Don't try to transform animated images.\n\t\t\tif (MediaHelpers.isAnimatedImageType(asset?.props.mimeType) || asset.props.isAnimated)\n\t\t\t\treturn asset.props.src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset?.props.mimeType)) return asset.props.src\n\n\t\t\tconst url = new URL(asset.props.src)\n\n\t\t\t// we only transform images that are hosted on domains we control\n\t\t\tconst isTldrawImage =\n\t\t\t\turl.origin === host || /\\.tldraw\\.(?:com|xyz|dev|workers\\.dev)$/.test(url.host)\n\n\t\t\tif (!isTldrawImage) return asset.props.src\n\n\t\t\t// Assets that are under a certain file size aren't worth transforming (and incurring cost).\n\t\t\t// We still send them through the image worker to get them optimized though.\n\t\t\tconst { fileSize = 0 } = asset.props\n\t\t\tconst isWorthResizing = fileSize >= 1024 * 1024 * 1.5\n\n\t\t\tif (isWorthResizing) {\n\t\t\t\t// N.B. navigator.connection is only available in certain browsers (mainly Blink-based browsers)\n\t\t\t\t// 4g is as high the 'effectiveType' goes and we can pick a lower effective image quality for slower connections.\n\t\t\t\tconst networkCompensation =\n\t\t\t\t\t!context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5\n\n\t\t\t\tconst width = Math.ceil(\n\t\t\t\t\tMath.min(\n\t\t\t\t\t\tasset.props.w *\n\t\t\t\t\t\t\tclamp(context.steppedScreenScale, 1 / 32, 1) *\n\t\t\t\t\t\t\tnetworkCompensation *\n\t\t\t\t\t\t\tcontext.dpr,\n\t\t\t\t\t\tasset.props.w\n\t\t\t\t\t)\n\t\t\t\t)\n\n\t\t\t\turl.searchParams.set('w', width.toString())\n\t\t\t}\n\n\t\t\tconst newUrl = `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t\treturn newUrl\n\t\t},\n\t}\n}\n\n/**\n * Creates a bookmark asset by fetching metadata from a URL using the demo server.\n *\n * This function uses the demo server's bookmark unfurling service to extract metadata\n * like title, description, favicon, and preview image from a given URL. If the metadata\n * fetch fails, it returns a blank bookmark asset with just the URL.\n *\n * @param host - The demo server host URL to use for bookmark unfurling\n * @param url - The URL to create a bookmark asset from\n * @returns A promise that resolves to a TLAsset of type 'bookmark' with extracted metadata\n * @example\n * ```ts\n * const asset = await createAssetFromUrlUsingDemoServer(\n * 'https://demo.tldraw.xyz',\n * 'https://example.com'\n * )\n *\n * console.log(asset.props.title) // \"Example Domain\"\n * console.log(asset.props.description) // \"This domain is for use in illustrative examples...\"\n * ```\n * @internal\n */\nasync function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {\n\tconst urlHash = getHashForString(url)\n\ttry {\n\t\t// First, try to get the meta data from our endpoint\n\t\tconst fetchUrl = new URL(`${host}/bookmarks/unfurl`)\n\t\tfetchUrl.searchParams.set('url', url)\n\n\t\tconst meta = (await (await fetch(fetchUrl, { method: 'POST' })).json()) as {\n\t\t\tdescription?: string\n\t\t\timage?: string\n\t\t\tfavicon?: string\n\t\t\ttitle?: string\n\t\t} | null\n\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: meta?.description ?? '',\n\t\t\t\timage: meta?.image ?? '',\n\t\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\t\ttitle: meta?.title ?? '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t} catch (error) {\n\t\t// Otherwise, fallback to a blank bookmark\n\t\tconsole.error(error)\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: '',\n\t\t\t\timage: '',\n\t\t\t\tfavicon: '',\n\t\t\t\ttitle: '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t}\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { useCallback, useMemo } from 'react'\nimport {\n\tAssetRecordType,\n\tEditor,\n\tMediaHelpers,\n\tSignal,\n\tTLAsset,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tclamp,\n\tdefaultBindingUtils,\n\tdefaultShapeUtils,\n\tgetHashForString,\n\tuniqueId,\n\tuseShallowObjectIdentity,\n} from 'tldraw'\nimport { RemoteTLStoreWithStatus, useSync } from './useSync'\n\n/** @public */\nexport interface UseSyncDemoOptions {\n\t/**\n\t * The room ID to sync with. Make sure the room ID is unique. The namespace is shared by\n\t * everyone using the demo server. Consider prefixing it with your company or project name.\n\t */\n\troomId: string\n\t/**\n\t * A signal that contains the user information needed for multiplayer features.\n\t * This should be synchronized with the `userPreferences` configuration for the main `<Tldraw />` component.\n\t * If not provided, a default implementation based on localStorage will be used.\n\t */\n\tuserInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>\n\n\t/** @internal */\n\thost?: string\n\n\t/**\n\t * {@inheritdoc UseSyncOptions.getUserPresence}\n\t * @public\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n\n/**\n * Safely accesses environment variables across different bundling environments.\n *\n * Depending on the environment this package is used in, process.env may not be available. This function\n * wraps `process.env` accesses in a try/catch to prevent runtime errors in environments where process\n * is not defined.\n *\n * The reason that this is just a try/catch and not a dynamic check e.g. `process &&\n * process.env[key]` is that many bundlers implement `process.env.WHATEVER` using compile-time\n * string replacement, rather than actually creating a runtime implementation of a `process` object.\n *\n * @param cb - Callback function that accesses an environment variable\n * @returns The environment variable value if available, otherwise undefined\n * @internal\n */\nfunction getEnv(cb: () => string | undefined): string | undefined {\n\ttry {\n\t\treturn cb()\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\nconst DEMO_WORKER = getEnv(() => process.env.TLDRAW_BEMO_URL) ?? 'https://demo.tldraw.xyz'\nconst IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://images.tldraw.xyz'\n\n/**\n * Creates a tldraw store synced with a multiplayer room hosted on tldraw's demo server `https://demo.tldraw.xyz`.\n *\n * The store can be passed directly into the `<Tldraw />` component to enable multiplayer features.\n * It will handle loading states, and enable multiplayer UX like user cursors and following.\n *\n * All data on the demo server is\n *\n * - Deleted after a day or so.\n * - Publicly accessible to anyone who knows the room ID. Use your company name as a prefix to help avoid collisions, or generate UUIDs for maximum privacy.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSyncDemo({roomId: 'my-app-test-room'})\n * return <Tldraw store={store} />\n * }\n * ```\n *\n * @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSyncDemo(\n\toptions: UseSyncDemoOptions & TLStoreSchemaOptions\n): RemoteTLStoreWithStatus {\n\tconst { roomId, host = DEMO_WORKER, ..._syncOpts } = options\n\tconst assets = useMemo(() => createDemoAssetStore(host), [host])\n\n\tconst syncOpts = useShallowObjectIdentity(_syncOpts)\n\tconst syncOptsWithDefaults = useMemo(() => {\n\t\tif ('schema' in syncOpts && syncOpts.schema) return syncOpts\n\n\t\treturn {\n\t\t\t...syncOpts,\n\t\t\tshapeUtils:\n\t\t\t\t'shapeUtils' in syncOpts\n\t\t\t\t\t? [...defaultShapeUtils, ...(syncOpts.shapeUtils ?? [])]\n\t\t\t\t\t: defaultShapeUtils,\n\t\t\tbindingUtils:\n\t\t\t\t'bindingUtils' in syncOpts\n\t\t\t\t\t? [...defaultBindingUtils, ...(syncOpts.bindingUtils ?? [])]\n\t\t\t\t\t: defaultBindingUtils,\n\t\t}\n\t}, [syncOpts])\n\n\treturn useSync({\n\t\turi: `${host}/connect/${encodeURIComponent(roomId)}`,\n\t\troomId,\n\t\tassets,\n\t\tonMount: useCallback(\n\t\t\t(editor: Editor) => {\n\t\t\t\teditor.registerExternalAssetHandler('url', async ({ url }) => {\n\t\t\t\t\treturn await createAssetFromUrlUsingDemoServer(host, url)\n\t\t\t\t})\n\t\t\t},\n\t\t\t[host]\n\t\t),\n\t\t...syncOptsWithDefaults,\n\t})\n}\n\n/**\n * Determines whether file uploads should be disabled for a given host.\n *\n * Uploads are disabled for production tldraw domains to prevent abuse of the demo server\n * infrastructure. This includes tldraw.com and tldraw.xyz domains and their subdomains.\n *\n * @param host - The host URL to check for upload restrictions\n * @returns True if uploads should be disabled, false otherwise\n * @internal\n */\nfunction shouldDisallowUploads(host: string) {\n\tconst disallowedHosts = ['tldraw.com', 'tldraw.xyz']\n\treturn disallowedHosts.some(\n\t\t(disallowedHost) => host === disallowedHost || host.endsWith(`.${disallowedHost}`)\n\t)\n}\n\n/**\n * Creates an asset store implementation optimized for the tldraw demo server.\n *\n * This asset store handles file uploads to the demo server and provides intelligent\n * asset resolution with automatic image optimization based on network conditions,\n * screen density, and display size. It includes safeguards to prevent uploads to\n * production domains and optimizes images through the tldraw image processing service.\n *\n * @param host - The demo server host URL for file uploads and asset resolution\n * @returns A TLAssetStore implementation with upload and resolve capabilities\n * @example\n * ```ts\n * const assetStore = createDemoAssetStore('https://demo.tldraw.xyz')\n *\n * // Upload a file\n * const result = await assetStore.upload(asset, file)\n * console.log('Uploaded to:', result.src)\n *\n * // Resolve optimized asset URL\n * const optimizedUrl = assetStore.resolve(imageAsset, {\n * steppedScreenScale: 1.5,\n * dpr: 2,\n * networkEffectiveType: '4g'\n * })\n * ```\n * @internal\n */\nfunction createDemoAssetStore(host: string): TLAssetStore {\n\treturn {\n\t\tupload: async (_asset, file) => {\n\t\t\tif (shouldDisallowUploads(host)) {\n\t\t\t\talert('Uploading images is disabled in this demo.')\n\t\t\t\tthrow new Error('Uploading images is disabled in this demo.')\n\t\t\t}\n\t\t\tconst id = uniqueId()\n\n\t\t\tconst objectName = `${id}-${file.name}`.replace(/\\W/g, '-')\n\t\t\tconst url = `${host}/uploads/${objectName}`\n\n\t\t\tawait fetch(url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\tbody: file,\n\t\t\t})\n\n\t\t\treturn { src: url }\n\t\t},\n\n\t\tresolve(asset, context) {\n\t\t\tif (!asset.props.src) return null\n\n\t\t\t// We don't deal with videos at the moment.\n\t\t\tif (asset.type === 'video') return asset.props.src\n\n\t\t\t// Assert it's an image to make TS happy.\n\t\t\tif (asset.type !== 'image') return null\n\n\t\t\t// Don't try to transform data: URLs, yikes.\n\t\t\tif (!asset.props.src.startsWith('http:') && !asset.props.src.startsWith('https:'))\n\t\t\t\treturn asset.props.src\n\n\t\t\tif (context.shouldResolveToOriginal) return asset.props.src\n\n\t\t\t// Don't try to transform animated images.\n\t\t\tif (MediaHelpers.isAnimatedImageType(asset?.props.mimeType) || asset.props.isAnimated)\n\t\t\t\treturn asset.props.src\n\n\t\t\t// Don't try to transform vector images.\n\t\t\tif (MediaHelpers.isVectorImageType(asset?.props.mimeType)) return asset.props.src\n\n\t\t\tconst url = new URL(asset.props.src)\n\n\t\t\t// we only transform images that are hosted on domains we control\n\t\t\tconst isTldrawImage =\n\t\t\t\turl.origin === host || /\\.tldraw\\.(?:com|xyz|dev|workers\\.dev)$/.test(url.host)\n\n\t\t\tif (!isTldrawImage) return asset.props.src\n\n\t\t\t// Assets that are under a certain file size aren't worth transforming (and incurring cost).\n\t\t\t// We still send them through the image worker to get them optimized though.\n\t\t\tconst { fileSize = 0 } = asset.props\n\t\t\tconst isWorthResizing = fileSize >= 1024 * 1024 * 1.5\n\n\t\t\tif (isWorthResizing) {\n\t\t\t\t// N.B. navigator.connection is only available in certain browsers (mainly Blink-based browsers)\n\t\t\t\t// 4g is as high the 'effectiveType' goes and we can pick a lower effective image quality for slower connections.\n\t\t\t\tconst networkCompensation =\n\t\t\t\t\t!context.networkEffectiveType || context.networkEffectiveType === '4g' ? 1 : 0.5\n\n\t\t\t\tconst width = Math.ceil(\n\t\t\t\t\tMath.min(\n\t\t\t\t\t\tasset.props.w *\n\t\t\t\t\t\t\tclamp(context.steppedScreenScale, 1 / 32, 1) *\n\t\t\t\t\t\t\tnetworkCompensation *\n\t\t\t\t\t\t\tcontext.dpr,\n\t\t\t\t\t\tasset.props.w\n\t\t\t\t\t)\n\t\t\t\t)\n\n\t\t\t\turl.searchParams.set('w', width.toString())\n\t\t\t}\n\n\t\t\tconst newUrl = `${IMAGE_WORKER}/${url.host}/${url.toString().slice(url.origin.length + 1)}`\n\t\t\treturn newUrl\n\t\t},\n\t}\n}\n\n/**\n * Creates a bookmark asset by fetching metadata from a URL using the demo server.\n *\n * This function uses the demo server's bookmark unfurling service to extract metadata\n * like title, description, favicon, and preview image from a given URL. If the metadata\n * fetch fails, it returns a blank bookmark asset with just the URL.\n *\n * @param host - The demo server host URL to use for bookmark unfurling\n * @param url - The URL to create a bookmark asset from\n * @returns A promise that resolves to a TLAsset of type 'bookmark' with extracted metadata\n * @example\n * ```ts\n * const asset = await createAssetFromUrlUsingDemoServer(\n * 'https://demo.tldraw.xyz',\n * 'https://example.com'\n * )\n *\n * console.log(asset.props.title) // \"Example Domain\"\n * console.log(asset.props.description) // \"This domain is for use in illustrative examples...\"\n * ```\n * @internal\n */\nasync function createAssetFromUrlUsingDemoServer(host: string, url: string): Promise<TLAsset> {\n\tconst urlHash = getHashForString(url)\n\ttry {\n\t\t// First, try to get the meta data from our endpoint\n\t\tconst fetchUrl = new URL(`${host}/bookmarks/unfurl`)\n\t\tfetchUrl.searchParams.set('url', url)\n\n\t\tconst meta = (await (await fetch(fetchUrl, { method: 'POST' })).json()) as {\n\t\t\tdescription?: string\n\t\t\timage?: string\n\t\t\tfavicon?: string\n\t\t\ttitle?: string\n\t\t} | null\n\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: meta?.description ?? '',\n\t\t\t\timage: meta?.image ?? '',\n\t\t\t\tfavicon: meta?.favicon ?? '',\n\t\t\t\ttitle: meta?.title ?? '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t} catch (error) {\n\t\t// Otherwise, fallback to a blank bookmark\n\t\tconsole.error(error)\n\t\treturn {\n\t\t\tid: AssetRecordType.createId(urlHash),\n\t\t\ttypeName: 'asset',\n\t\t\ttype: 'bookmark',\n\t\t\tprops: {\n\t\t\t\tsrc: url,\n\t\t\t\tdescription: '',\n\t\t\t\timage: '',\n\t\t\t\tfavicon: '',\n\t\t\t\ttitle: '',\n\t\t\t},\n\t\t\tmeta: {},\n\t\t}\n\t}\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
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.3.
|
|
4
|
+
"version": "4.3.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw GB Ltd.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
"vitest": "^3.2.4"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@tldraw/state": "4.3.
|
|
58
|
-
"@tldraw/state-react": "4.3.
|
|
59
|
-
"@tldraw/sync-core": "4.3.
|
|
60
|
-
"@tldraw/utils": "4.3.
|
|
57
|
+
"@tldraw/state": "4.3.1",
|
|
58
|
+
"@tldraw/state-react": "4.3.1",
|
|
59
|
+
"@tldraw/sync-core": "4.3.1",
|
|
60
|
+
"@tldraw/utils": "4.3.1",
|
|
61
61
|
"nanoevents": "^7.0.1",
|
|
62
|
-
"tldraw": "4.3.
|
|
62
|
+
"tldraw": "4.3.1",
|
|
63
63
|
"ws": "^8.18.0"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
package/src/useSync.ts
CHANGED
|
@@ -447,7 +447,7 @@ export interface UseSyncOptionsBase {
|
|
|
447
447
|
*
|
|
448
448
|
* The asset store must implement upload (for new files) and resolve
|
|
449
449
|
* (for displaying existing files) methods. For prototyping, you can use
|
|
450
|
-
* {@link tldraw#inlineBase64AssetStore} but this is not recommended for production.
|
|
450
|
+
* {@link @tldraw/editor#inlineBase64AssetStore} but this is not recommended for production.
|
|
451
451
|
*
|
|
452
452
|
* @example
|
|
453
453
|
* ```ts
|
package/src/useSyncDemo.ts
CHANGED
|
@@ -88,7 +88,7 @@ const IMAGE_WORKER = getEnv(() => process.env.TLDRAW_IMAGE_URL) ?? 'https://imag
|
|
|
88
88
|
* }
|
|
89
89
|
* ```
|
|
90
90
|
*
|
|
91
|
-
* @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link tldraw#TLStoreSchemaOptions}.
|
|
91
|
+
* @param options - Options for the multiplayer demo sync store. See {@link UseSyncDemoOptions} and {@link @tldraw/editor#TLStoreSchemaOptions}.
|
|
92
92
|
*
|
|
93
93
|
* @public
|
|
94
94
|
*/
|