@tldraw/sync 3.16.0-next.eafb52d15064 → 3.16.0-next.fe14f1b4181f

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.
@@ -117,6 +117,10 @@ export declare interface UseSyncOptions {
117
117
  * Note that storing base64 blobs inline in JSON is very inefficient and will cause performance issues quickly with large images and videos.
118
118
  */
119
119
  assets: TLAssetStore;
120
+ /**
121
+ * A handler for custom socket messages.
122
+ */
123
+ onCustomMessageReceived?(data: any): void;
120
124
  /* Excluded from this release type: onMount */
121
125
  /* Excluded from this release type: roomId */
122
126
  /* Excluded from this release type: trackAnalyticsEvent */
package/dist-cjs/index.js CHANGED
@@ -29,7 +29,7 @@ var import_useSync = require("./useSync");
29
29
  var import_useSyncDemo = require("./useSyncDemo");
30
30
  (0, import_utils.registerTldrawLibraryVersion)(
31
31
  "@tldraw/sync",
32
- "3.16.0-next.eafb52d15064",
32
+ "3.16.0-next.fe14f1b4181f",
33
33
  "cjs"
34
34
  );
35
35
  //# sourceMappingURL=index.js.map
@@ -27,6 +27,8 @@ var import_sync_core = require("@tldraw/sync-core");
27
27
  var import_react = require("react");
28
28
  var import_tldraw = require("tldraw");
29
29
  const MULTIPLAYER_EVENT_NAME = "multiplayer.client";
30
+ const defaultCustomMessageHandler = () => {
31
+ };
30
32
  function useSync(opts) {
31
33
  const [state, setState] = (0, import_tldraw.useRefState)(null);
32
34
  const {
@@ -37,12 +39,14 @@ function useSync(opts) {
37
39
  trackAnalyticsEvent: track,
38
40
  userInfo,
39
41
  getUserPresence: _getUserPresence,
42
+ onCustomMessageReceived: _onCustomMessageReceived,
40
43
  ...schemaOpts
41
44
  } = opts;
42
45
  const __never__ = 0;
43
46
  const schema = (0, import_tldraw.useTLSchemaFromUtils)(schemaOpts);
44
47
  const prefs = (0, import_tldraw.useShallowObjectIdentity)(userInfo);
45
48
  const getUserPresence = (0, import_tldraw.useReactiveEvent)(_getUserPresence ?? import_tldraw.getDefaultUserPresence);
49
+ const onCustomMessageReceived = (0, import_tldraw.useEvent)(_onCustomMessageReceived ?? defaultCustomMessageHandler);
46
50
  const userAtom = (0, import_state_react.useAtom)(
47
51
  "userAtom",
48
52
  prefs
@@ -148,6 +152,7 @@ function useSync(opts) {
148
152
  store.ensureStoreIsUsable();
149
153
  });
150
154
  },
155
+ onCustomMessageReceived,
151
156
  presence,
152
157
  presenceMode
153
158
  });
@@ -157,7 +162,18 @@ function useSync(opts) {
157
162
  socket.close();
158
163
  setState(null);
159
164
  };
160
- }, [assets, onMount, userAtom, roomId, schema, setState, track, uri, getUserPresence]);
165
+ }, [
166
+ assets,
167
+ onMount,
168
+ userAtom,
169
+ roomId,
170
+ schema,
171
+ setState,
172
+ track,
173
+ uri,
174
+ getUserPresence,
175
+ onCustomMessageReceived
176
+ ]);
161
177
  return (0, import_tldraw.useValue)(
162
178
  "remote synced store",
163
179
  () => {
@@ -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\tTLPresenceMode,\n\tTLRemoteSyncError,\n\tTLSyncClient,\n\tTLSyncErrorCloseEventReason,\n} from '@tldraw/sync-core'\nimport { useEffect } from 'react'\nimport {\n\tEditor,\n\tInstancePresenceRecordType,\n\tSignal,\n\tTAB_ID,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLRecord,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLStoreWithStatus,\n\tcomputed,\n\tcreateTLStore,\n\tdefaultUserPreferences,\n\tgetDefaultUserPresence,\n\tgetUserPreferences,\n\tuniqueId,\n\tuseReactiveEvent,\n\tuseRefState,\n\tuseShallowObjectIdentity,\n\tuseTLSchemaFromUtils,\n\tuseValue,\n} from 'tldraw'\n\nconst MULTIPLAYER_EVENT_NAME = 'multiplayer.client'\n\n/** @public */\nexport type RemoteTLStoreWithStatus = Exclude<\n\tTLStoreWithStatus,\n\t{ status: 'synced-local' } | { status: 'not-synced' }\n>\n\n/**\n * useSync creates a store that is synced with a multiplayer server.\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 * To enable external blob storage, you should also pass in an `assets` object that implements the {@link tldraw#TLAssetStore} interface.\n * If you don't do this, adding large images and videos to rooms will cause performance issues at serialization boundaries.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSync({\n * uri: 'wss://myapp.com/sync/my-test-room',\n * assets: myAssetStore\n * })\n * return <Tldraw store={store} />\n * }\n *\n * ```\n * @param opts - Options for the multiplayer sync store. See {@link UseSyncOptions} and {@link tldraw#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus {\n\tconst [state, setState] = useRefState<{\n\t\treadyClient?: TLSyncClient<TLRecord, TLStore>\n\t\terror?: Error\n\t} | null>(null)\n\tconst {\n\t\turi,\n\t\troomId = 'default',\n\t\tassets,\n\t\tonMount,\n\t\ttrackAnalyticsEvent: track,\n\t\tuserInfo,\n\t\tgetUserPresence: _getUserPresence,\n\t\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\n\tconst userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(\n\t\t'userAtom',\n\t\tprefs\n\t)\n\n\tuseEffect(() => {\n\t\tuserAtom.set(prefs)\n\t}, [prefs, userAtom])\n\n\tuseEffect(() => {\n\t\tconst storeId = uniqueId()\n\n\t\tconst userPreferences = computed<{ id: string; color: string; name: string }>(\n\t\t\t'userPreferences',\n\t\t\t() => {\n\t\t\t\tconst userStuff = userAtom.get()\n\t\t\t\tconst user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()\n\t\t\t\treturn {\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tcolor: user.color ?? defaultUserPreferences.color,\n\t\t\t\t\tname: user.name ?? defaultUserPreferences.name,\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\tconst socket = new ClientWebSocketAdapter(async () => {\n\t\t\tconst uriString = typeof uri === 'string' ? uri : await uri()\n\n\t\t\t// set sessionId as a query param on the uri\n\t\t\tconst withParams = new URL(uriString)\n\t\t\tif (withParams.searchParams.has('sessionId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"sessionId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (withParams.searchParams.has('storeId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"storeId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\n\t\t\twithParams.searchParams.set('sessionId', TAB_ID)\n\t\t\twithParams.searchParams.set('storeId', storeId)\n\t\t\treturn withParams.toString()\n\t\t})\n\n\t\tlet didCancel = false\n\n\t\tconst collaborationStatusSignal = computed('collaboration status', () =>\n\t\t\tsocket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus\n\t\t)\n\n\t\tconst syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')\n\n\t\tconst store = createTLStore({\n\t\t\tid: storeId,\n\t\t\tschema,\n\t\t\tassets,\n\t\t\tonMount,\n\t\t\tcollaboration: {\n\t\t\t\tstatus: collaborationStatusSignal,\n\t\t\t\tmode: syncMode,\n\t\t\t},\n\t\t})\n\n\t\tconst presence = computed('instancePresence', () => {\n\t\t\tconst presenceState = getUserPresence(store, userPreferences.get())\n\t\t\tif (!presenceState) return null\n\n\t\t\treturn InstancePresenceRecordType.create({\n\t\t\t\t...presenceState,\n\t\t\t\tid: InstancePresenceRecordType.createId(store.id),\n\t\t\t})\n\t\t})\n\n\t\tconst otherUserPresences = store.query.ids('instance_presence', () => ({\n\t\t\tuserId: { neq: userPreferences.get().id },\n\t\t}))\n\n\t\tconst presenceMode = computed<TLPresenceMode>('presenceMode', () => {\n\t\t\tif (otherUserPresences.get().size === 0) return 'solo'\n\t\t\treturn 'full'\n\t\t})\n\n\t\tconst client = new TLSyncClient({\n\t\t\tstore,\n\t\t\tsocket,\n\t\t\tdidCancel: () => didCancel,\n\t\t\tonLoad(client) {\n\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })\n\t\t\t\tsetState({ readyClient: client })\n\t\t\t},\n\t\t\tonSyncError(reason) {\n\t\t\t\tconsole.error('sync error', reason)\n\n\t\t\t\tswitch (reason) {\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_FOUND:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.FORBIDDEN:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'forbidden', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'not-authenticated', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.RATE_LIMITED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'rate-limited', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error:' + reason, roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsetState({ error: new TLRemoteSyncError(reason) })\n\t\t\t\tsocket.close()\n\t\t\t},\n\t\t\tonAfterConnect(_, { isReadonly }) {\n\t\t\t\ttransact(() => {\n\t\t\t\t\tsyncMode.set(isReadonly ? 'readonly' : 'readwrite')\n\t\t\t\t\t// if the server crashes and loses all data it can return an empty document\n\t\t\t\t\t// when it comes back up. This is a safety check to make sure that if something like\n\t\t\t\t\t// that happens, it won't render the app broken and require a restart. The user will\n\t\t\t\t\t// most likely lose all their changes though since they'll have been working with pages\n\t\t\t\t\t// that won't exist. There's certainly something we can do to make this better.\n\t\t\t\t\t// but the likelihood of this happening is very low and maybe not worth caring about beyond this.\n\t\t\t\t\tstore.ensureStoreIsUsable()\n\t\t\t\t})\n\t\t\t},\n\t\t\tpresence,\n\t\t\tpresenceMode,\n\t\t})\n\n\t\treturn () => {\n\t\t\tdidCancel = true\n\t\t\tclient.close()\n\t\t\tsocket.close()\n\t\t\tsetState(null)\n\t\t}\n\t}, [assets, onMount, userAtom, roomId, schema, setState, track, uri, getUserPresence])\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 * Options for the {@link useSync} hook.\n * @public\n */\nexport interface UseSyncOptions {\n\t/**\n\t * The URI of the multiplayer server. This must include the protocol,\n\t *\n\t * e.g. `wss://server.example.com/my-room` or `ws://localhost:5858/my-room`.\n\t *\n\t * Note that the protocol can also be `https` or `http` and it will upgrade to a websocket\n\t * connection.\n\t *\n\t * Optionally, you can pass a function which will be called each time a connection is\n\t * established to get the URI. This is useful if you need to include e.g. a short-lived session\n\t * token for authentication.\n\t */\n\turi: string | (() => string | Promise<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\t/**\n\t * The asset store for blob storage. See {@link tldraw#TLAssetStore}.\n\t *\n\t * If you don't have time to implement blob storage and just want to get started, you can use the inline base64 asset store. {@link tldraw#inlineBase64AssetStore}\n\t * Note that storing base64 blobs inline in JSON is very inefficient and will cause performance issues quickly with large images and videos.\n\t */\n\tassets: TLAssetStore\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. The\n\t * result of this function will be synchronized across all clients to display presence\n\t * indicators such as cursors. See {@link @tldraw/tlschema#getDefaultUserPresence} for\n\t * the default implementation of this function.\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { atom, isSignal, transact } from '@tldraw/state'\nimport { useAtom } from '@tldraw/state-react'\nimport {\n\tClientWebSocketAdapter,\n\tTLCustomMessageHandler,\n\tTLPresenceMode,\n\tTLRemoteSyncError,\n\tTLSyncClient,\n\tTLSyncErrorCloseEventReason,\n} from '@tldraw/sync-core'\nimport { useEffect } from 'react'\nimport {\n\tEditor,\n\tInstancePresenceRecordType,\n\tSignal,\n\tTAB_ID,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLRecord,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLStoreWithStatus,\n\tcomputed,\n\tcreateTLStore,\n\tdefaultUserPreferences,\n\tgetDefaultUserPresence,\n\tgetUserPreferences,\n\tuniqueId,\n\tuseEvent,\n\tuseReactiveEvent,\n\tuseRefState,\n\tuseShallowObjectIdentity,\n\tuseTLSchemaFromUtils,\n\tuseValue,\n} from 'tldraw'\n\nconst MULTIPLAYER_EVENT_NAME = 'multiplayer.client'\n\nconst defaultCustomMessageHandler: TLCustomMessageHandler = () => {}\n\n/** @public */\nexport type RemoteTLStoreWithStatus = Exclude<\n\tTLStoreWithStatus,\n\t{ status: 'synced-local' } | { status: 'not-synced' }\n>\n\n/**\n * useSync creates a store that is synced with a multiplayer server.\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 * To enable external blob storage, you should also pass in an `assets` object that implements the {@link tldraw#TLAssetStore} interface.\n * If you don't do this, adding large images and videos to rooms will cause performance issues at serialization boundaries.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSync({\n * uri: 'wss://myapp.com/sync/my-test-room',\n * assets: myAssetStore\n * })\n * return <Tldraw store={store} />\n * }\n *\n * ```\n * @param opts - Options for the multiplayer sync store. See {@link UseSyncOptions} and {@link tldraw#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus {\n\tconst [state, setState] = useRefState<{\n\t\treadyClient?: TLSyncClient<TLRecord, TLStore>\n\t\terror?: Error\n\t} | null>(null)\n\tconst {\n\t\turi,\n\t\troomId = 'default',\n\t\tassets,\n\t\tonMount,\n\t\ttrackAnalyticsEvent: track,\n\t\tuserInfo,\n\t\tgetUserPresence: _getUserPresence,\n\t\tonCustomMessageReceived: _onCustomMessageReceived,\n\t\t...schemaOpts\n\t} = opts\n\n\t// This line will throw a type error if we add any new options to the useSync hook but we don't destructure them\n\t// This is required because otherwise the useTLSchemaFromUtils might return a new schema on every render if the newly-added option\n\t// is allowed to be unstable (e.g. userInfo)\n\tconst __never__: never = 0 as any as keyof Omit<typeof schemaOpts, keyof TLStoreSchemaOptions>\n\n\tconst schema = useTLSchemaFromUtils(schemaOpts)\n\n\tconst prefs = useShallowObjectIdentity(userInfo)\n\tconst getUserPresence = useReactiveEvent(_getUserPresence ?? getDefaultUserPresence)\n\tconst onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)\n\n\tconst userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(\n\t\t'userAtom',\n\t\tprefs\n\t)\n\n\tuseEffect(() => {\n\t\tuserAtom.set(prefs)\n\t}, [prefs, userAtom])\n\n\tuseEffect(() => {\n\t\tconst storeId = uniqueId()\n\n\t\tconst userPreferences = computed<{ id: string; color: string; name: string }>(\n\t\t\t'userPreferences',\n\t\t\t() => {\n\t\t\t\tconst userStuff = userAtom.get()\n\t\t\t\tconst user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()\n\t\t\t\treturn {\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tcolor: user.color ?? defaultUserPreferences.color,\n\t\t\t\t\tname: user.name ?? defaultUserPreferences.name,\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\tconst socket = new ClientWebSocketAdapter(async () => {\n\t\t\tconst uriString = typeof uri === 'string' ? uri : await uri()\n\n\t\t\t// set sessionId as a query param on the uri\n\t\t\tconst withParams = new URL(uriString)\n\t\t\tif (withParams.searchParams.has('sessionId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"sessionId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (withParams.searchParams.has('storeId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"storeId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\n\t\t\twithParams.searchParams.set('sessionId', TAB_ID)\n\t\t\twithParams.searchParams.set('storeId', storeId)\n\t\t\treturn withParams.toString()\n\t\t})\n\n\t\tlet didCancel = false\n\n\t\tconst collaborationStatusSignal = computed('collaboration status', () =>\n\t\t\tsocket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus\n\t\t)\n\n\t\tconst syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')\n\n\t\tconst store = createTLStore({\n\t\t\tid: storeId,\n\t\t\tschema,\n\t\t\tassets,\n\t\t\tonMount,\n\t\t\tcollaboration: {\n\t\t\t\tstatus: collaborationStatusSignal,\n\t\t\t\tmode: syncMode,\n\t\t\t},\n\t\t})\n\n\t\tconst presence = computed('instancePresence', () => {\n\t\t\tconst presenceState = getUserPresence(store, userPreferences.get())\n\t\t\tif (!presenceState) return null\n\n\t\t\treturn InstancePresenceRecordType.create({\n\t\t\t\t...presenceState,\n\t\t\t\tid: InstancePresenceRecordType.createId(store.id),\n\t\t\t})\n\t\t})\n\n\t\tconst otherUserPresences = store.query.ids('instance_presence', () => ({\n\t\t\tuserId: { neq: userPreferences.get().id },\n\t\t}))\n\n\t\tconst presenceMode = computed<TLPresenceMode>('presenceMode', () => {\n\t\t\tif (otherUserPresences.get().size === 0) return 'solo'\n\t\t\treturn 'full'\n\t\t})\n\n\t\tconst client = new TLSyncClient({\n\t\t\tstore,\n\t\t\tsocket,\n\t\t\tdidCancel: () => didCancel,\n\t\t\tonLoad(client) {\n\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })\n\t\t\t\tsetState({ readyClient: client })\n\t\t\t},\n\t\t\tonSyncError(reason) {\n\t\t\t\tconsole.error('sync error', reason)\n\n\t\t\t\tswitch (reason) {\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_FOUND:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.FORBIDDEN:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'forbidden', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'not-authenticated', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.RATE_LIMITED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'rate-limited', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error:' + reason, roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsetState({ error: new TLRemoteSyncError(reason) })\n\t\t\t\tsocket.close()\n\t\t\t},\n\t\t\tonAfterConnect(_, { isReadonly }) {\n\t\t\t\ttransact(() => {\n\t\t\t\t\tsyncMode.set(isReadonly ? 'readonly' : 'readwrite')\n\t\t\t\t\t// if the server crashes and loses all data it can return an empty document\n\t\t\t\t\t// when it comes back up. This is a safety check to make sure that if something like\n\t\t\t\t\t// that happens, it won't render the app broken and require a restart. The user will\n\t\t\t\t\t// most likely lose all their changes though since they'll have been working with pages\n\t\t\t\t\t// that won't exist. There's certainly something we can do to make this better.\n\t\t\t\t\t// but the likelihood of this happening is very low and maybe not worth caring about beyond this.\n\t\t\t\t\tstore.ensureStoreIsUsable()\n\t\t\t\t})\n\t\t\t},\n\t\t\tonCustomMessageReceived,\n\t\t\tpresence,\n\t\t\tpresenceMode,\n\t\t})\n\n\t\treturn () => {\n\t\t\tdidCancel = true\n\t\t\tclient.close()\n\t\t\tsocket.close()\n\t\t\tsetState(null)\n\t\t}\n\t}, [\n\t\tassets,\n\t\tonMount,\n\t\tuserAtom,\n\t\troomId,\n\t\tschema,\n\t\tsetState,\n\t\ttrack,\n\t\turi,\n\t\tgetUserPresence,\n\t\tonCustomMessageReceived,\n\t])\n\n\treturn useValue<RemoteTLStoreWithStatus>(\n\t\t'remote synced store',\n\t\t() => {\n\t\t\tif (!state) return { status: 'loading' }\n\t\t\tif (state.error) return { status: 'error', error: state.error }\n\t\t\tif (!state.readyClient) return { status: 'loading' }\n\t\t\tconst connectionStatus = state.readyClient.socket.connectionStatus\n\t\t\treturn {\n\t\t\t\tstatus: 'synced-remote',\n\t\t\t\tconnectionStatus: connectionStatus === 'error' ? 'offline' : connectionStatus,\n\t\t\t\tstore: state.readyClient.store,\n\t\t\t}\n\t\t},\n\t\t[state]\n\t)\n}\n\n/**\n * Options for the {@link useSync} hook.\n * @public\n */\nexport interface UseSyncOptions {\n\t/**\n\t * The URI of the multiplayer server. This must include the protocol,\n\t *\n\t * e.g. `wss://server.example.com/my-room` or `ws://localhost:5858/my-room`.\n\t *\n\t * Note that the protocol can also be `https` or `http` and it will upgrade to a websocket\n\t * connection.\n\t *\n\t * Optionally, you can pass a function which will be called each time a connection is\n\t * established to get the URI. This is useful if you need to include e.g. a short-lived session\n\t * token for authentication.\n\t */\n\turi: string | (() => string | Promise<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\t/**\n\t * The asset store for blob storage. See {@link tldraw#TLAssetStore}.\n\t *\n\t * If you don't have time to implement blob storage and just want to get started, you can use the inline base64 asset store. {@link tldraw#inlineBase64AssetStore}\n\t * Note that storing base64 blobs inline in JSON is very inefficient and will cause performance issues quickly with large images and videos.\n\t */\n\tassets: TLAssetStore\n\n\t/**\n\t * A handler for custom socket messages.\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. The\n\t * result of this function will be synchronized across all clients to display presence\n\t * indicators such as cursors. See {@link @tldraw/tlschema#getDefaultUserPresence} for\n\t * the default implementation of this function.\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["client"]
7
7
  }
@@ -117,6 +117,10 @@ export declare interface UseSyncOptions {
117
117
  * Note that storing base64 blobs inline in JSON is very inefficient and will cause performance issues quickly with large images and videos.
118
118
  */
119
119
  assets: TLAssetStore;
120
+ /**
121
+ * A handler for custom socket messages.
122
+ */
123
+ onCustomMessageReceived?(data: any): void;
120
124
  /* Excluded from this release type: onMount */
121
125
  /* Excluded from this release type: roomId */
122
126
  /* Excluded from this release type: trackAnalyticsEvent */
@@ -4,7 +4,7 @@ import { useSync } from "./useSync.mjs";
4
4
  import { useSyncDemo } from "./useSyncDemo.mjs";
5
5
  registerTldrawLibraryVersion(
6
6
  "@tldraw/sync",
7
- "3.16.0-next.eafb52d15064",
7
+ "3.16.0-next.fe14f1b4181f",
8
8
  "esm"
9
9
  );
10
10
  export {
@@ -16,6 +16,7 @@ import {
16
16
  getDefaultUserPresence,
17
17
  getUserPreferences,
18
18
  uniqueId,
19
+ useEvent,
19
20
  useReactiveEvent,
20
21
  useRefState,
21
22
  useShallowObjectIdentity,
@@ -23,6 +24,8 @@ import {
23
24
  useValue
24
25
  } from "tldraw";
25
26
  const MULTIPLAYER_EVENT_NAME = "multiplayer.client";
27
+ const defaultCustomMessageHandler = () => {
28
+ };
26
29
  function useSync(opts) {
27
30
  const [state, setState] = useRefState(null);
28
31
  const {
@@ -33,12 +36,14 @@ function useSync(opts) {
33
36
  trackAnalyticsEvent: track,
34
37
  userInfo,
35
38
  getUserPresence: _getUserPresence,
39
+ onCustomMessageReceived: _onCustomMessageReceived,
36
40
  ...schemaOpts
37
41
  } = opts;
38
42
  const __never__ = 0;
39
43
  const schema = useTLSchemaFromUtils(schemaOpts);
40
44
  const prefs = useShallowObjectIdentity(userInfo);
41
45
  const getUserPresence = useReactiveEvent(_getUserPresence ?? getDefaultUserPresence);
46
+ const onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler);
42
47
  const userAtom = useAtom(
43
48
  "userAtom",
44
49
  prefs
@@ -144,6 +149,7 @@ function useSync(opts) {
144
149
  store.ensureStoreIsUsable();
145
150
  });
146
151
  },
152
+ onCustomMessageReceived,
147
153
  presence,
148
154
  presenceMode
149
155
  });
@@ -153,7 +159,18 @@ function useSync(opts) {
153
159
  socket.close();
154
160
  setState(null);
155
161
  };
156
- }, [assets, onMount, userAtom, roomId, schema, setState, track, uri, getUserPresence]);
162
+ }, [
163
+ assets,
164
+ onMount,
165
+ userAtom,
166
+ roomId,
167
+ schema,
168
+ setState,
169
+ track,
170
+ uri,
171
+ getUserPresence,
172
+ onCustomMessageReceived
173
+ ]);
157
174
  return useValue(
158
175
  "remote synced store",
159
176
  () => {
@@ -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\tTLPresenceMode,\n\tTLRemoteSyncError,\n\tTLSyncClient,\n\tTLSyncErrorCloseEventReason,\n} from '@tldraw/sync-core'\nimport { useEffect } from 'react'\nimport {\n\tEditor,\n\tInstancePresenceRecordType,\n\tSignal,\n\tTAB_ID,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLRecord,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLStoreWithStatus,\n\tcomputed,\n\tcreateTLStore,\n\tdefaultUserPreferences,\n\tgetDefaultUserPresence,\n\tgetUserPreferences,\n\tuniqueId,\n\tuseReactiveEvent,\n\tuseRefState,\n\tuseShallowObjectIdentity,\n\tuseTLSchemaFromUtils,\n\tuseValue,\n} from 'tldraw'\n\nconst MULTIPLAYER_EVENT_NAME = 'multiplayer.client'\n\n/** @public */\nexport type RemoteTLStoreWithStatus = Exclude<\n\tTLStoreWithStatus,\n\t{ status: 'synced-local' } | { status: 'not-synced' }\n>\n\n/**\n * useSync creates a store that is synced with a multiplayer server.\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 * To enable external blob storage, you should also pass in an `assets` object that implements the {@link tldraw#TLAssetStore} interface.\n * If you don't do this, adding large images and videos to rooms will cause performance issues at serialization boundaries.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSync({\n * uri: 'wss://myapp.com/sync/my-test-room',\n * assets: myAssetStore\n * })\n * return <Tldraw store={store} />\n * }\n *\n * ```\n * @param opts - Options for the multiplayer sync store. See {@link UseSyncOptions} and {@link tldraw#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus {\n\tconst [state, setState] = useRefState<{\n\t\treadyClient?: TLSyncClient<TLRecord, TLStore>\n\t\terror?: Error\n\t} | null>(null)\n\tconst {\n\t\turi,\n\t\troomId = 'default',\n\t\tassets,\n\t\tonMount,\n\t\ttrackAnalyticsEvent: track,\n\t\tuserInfo,\n\t\tgetUserPresence: _getUserPresence,\n\t\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\n\tconst userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(\n\t\t'userAtom',\n\t\tprefs\n\t)\n\n\tuseEffect(() => {\n\t\tuserAtom.set(prefs)\n\t}, [prefs, userAtom])\n\n\tuseEffect(() => {\n\t\tconst storeId = uniqueId()\n\n\t\tconst userPreferences = computed<{ id: string; color: string; name: string }>(\n\t\t\t'userPreferences',\n\t\t\t() => {\n\t\t\t\tconst userStuff = userAtom.get()\n\t\t\t\tconst user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()\n\t\t\t\treturn {\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tcolor: user.color ?? defaultUserPreferences.color,\n\t\t\t\t\tname: user.name ?? defaultUserPreferences.name,\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\tconst socket = new ClientWebSocketAdapter(async () => {\n\t\t\tconst uriString = typeof uri === 'string' ? uri : await uri()\n\n\t\t\t// set sessionId as a query param on the uri\n\t\t\tconst withParams = new URL(uriString)\n\t\t\tif (withParams.searchParams.has('sessionId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"sessionId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (withParams.searchParams.has('storeId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"storeId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\n\t\t\twithParams.searchParams.set('sessionId', TAB_ID)\n\t\t\twithParams.searchParams.set('storeId', storeId)\n\t\t\treturn withParams.toString()\n\t\t})\n\n\t\tlet didCancel = false\n\n\t\tconst collaborationStatusSignal = computed('collaboration status', () =>\n\t\t\tsocket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus\n\t\t)\n\n\t\tconst syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')\n\n\t\tconst store = createTLStore({\n\t\t\tid: storeId,\n\t\t\tschema,\n\t\t\tassets,\n\t\t\tonMount,\n\t\t\tcollaboration: {\n\t\t\t\tstatus: collaborationStatusSignal,\n\t\t\t\tmode: syncMode,\n\t\t\t},\n\t\t})\n\n\t\tconst presence = computed('instancePresence', () => {\n\t\t\tconst presenceState = getUserPresence(store, userPreferences.get())\n\t\t\tif (!presenceState) return null\n\n\t\t\treturn InstancePresenceRecordType.create({\n\t\t\t\t...presenceState,\n\t\t\t\tid: InstancePresenceRecordType.createId(store.id),\n\t\t\t})\n\t\t})\n\n\t\tconst otherUserPresences = store.query.ids('instance_presence', () => ({\n\t\t\tuserId: { neq: userPreferences.get().id },\n\t\t}))\n\n\t\tconst presenceMode = computed<TLPresenceMode>('presenceMode', () => {\n\t\t\tif (otherUserPresences.get().size === 0) return 'solo'\n\t\t\treturn 'full'\n\t\t})\n\n\t\tconst client = new TLSyncClient({\n\t\t\tstore,\n\t\t\tsocket,\n\t\t\tdidCancel: () => didCancel,\n\t\t\tonLoad(client) {\n\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })\n\t\t\t\tsetState({ readyClient: client })\n\t\t\t},\n\t\t\tonSyncError(reason) {\n\t\t\t\tconsole.error('sync error', reason)\n\n\t\t\t\tswitch (reason) {\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_FOUND:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.FORBIDDEN:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'forbidden', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'not-authenticated', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.RATE_LIMITED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'rate-limited', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error:' + reason, roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsetState({ error: new TLRemoteSyncError(reason) })\n\t\t\t\tsocket.close()\n\t\t\t},\n\t\t\tonAfterConnect(_, { isReadonly }) {\n\t\t\t\ttransact(() => {\n\t\t\t\t\tsyncMode.set(isReadonly ? 'readonly' : 'readwrite')\n\t\t\t\t\t// if the server crashes and loses all data it can return an empty document\n\t\t\t\t\t// when it comes back up. This is a safety check to make sure that if something like\n\t\t\t\t\t// that happens, it won't render the app broken and require a restart. The user will\n\t\t\t\t\t// most likely lose all their changes though since they'll have been working with pages\n\t\t\t\t\t// that won't exist. There's certainly something we can do to make this better.\n\t\t\t\t\t// but the likelihood of this happening is very low and maybe not worth caring about beyond this.\n\t\t\t\t\tstore.ensureStoreIsUsable()\n\t\t\t\t})\n\t\t\t},\n\t\t\tpresence,\n\t\t\tpresenceMode,\n\t\t})\n\n\t\treturn () => {\n\t\t\tdidCancel = true\n\t\t\tclient.close()\n\t\t\tsocket.close()\n\t\t\tsetState(null)\n\t\t}\n\t}, [assets, onMount, userAtom, roomId, schema, setState, track, uri, getUserPresence])\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 * Options for the {@link useSync} hook.\n * @public\n */\nexport interface UseSyncOptions {\n\t/**\n\t * The URI of the multiplayer server. This must include the protocol,\n\t *\n\t * e.g. `wss://server.example.com/my-room` or `ws://localhost:5858/my-room`.\n\t *\n\t * Note that the protocol can also be `https` or `http` and it will upgrade to a websocket\n\t * connection.\n\t *\n\t * Optionally, you can pass a function which will be called each time a connection is\n\t * established to get the URI. This is useful if you need to include e.g. a short-lived session\n\t * token for authentication.\n\t */\n\turi: string | (() => string | Promise<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\t/**\n\t * The asset store for blob storage. See {@link tldraw#TLAssetStore}.\n\t *\n\t * If you don't have time to implement blob storage and just want to get started, you can use the inline base64 asset store. {@link tldraw#inlineBase64AssetStore}\n\t * Note that storing base64 blobs inline in JSON is very inefficient and will cause performance issues quickly with large images and videos.\n\t */\n\tassets: TLAssetStore\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. The\n\t * result of this function will be synchronized across all clients to display presence\n\t * indicators such as cursors. See {@link @tldraw/tlschema#getDefaultUserPresence} for\n\t * the default implementation of this function.\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { atom, isSignal, transact } from '@tldraw/state'\nimport { useAtom } from '@tldraw/state-react'\nimport {\n\tClientWebSocketAdapter,\n\tTLCustomMessageHandler,\n\tTLPresenceMode,\n\tTLRemoteSyncError,\n\tTLSyncClient,\n\tTLSyncErrorCloseEventReason,\n} from '@tldraw/sync-core'\nimport { useEffect } from 'react'\nimport {\n\tEditor,\n\tInstancePresenceRecordType,\n\tSignal,\n\tTAB_ID,\n\tTLAssetStore,\n\tTLPresenceStateInfo,\n\tTLPresenceUserInfo,\n\tTLRecord,\n\tTLStore,\n\tTLStoreSchemaOptions,\n\tTLStoreWithStatus,\n\tcomputed,\n\tcreateTLStore,\n\tdefaultUserPreferences,\n\tgetDefaultUserPresence,\n\tgetUserPreferences,\n\tuniqueId,\n\tuseEvent,\n\tuseReactiveEvent,\n\tuseRefState,\n\tuseShallowObjectIdentity,\n\tuseTLSchemaFromUtils,\n\tuseValue,\n} from 'tldraw'\n\nconst MULTIPLAYER_EVENT_NAME = 'multiplayer.client'\n\nconst defaultCustomMessageHandler: TLCustomMessageHandler = () => {}\n\n/** @public */\nexport type RemoteTLStoreWithStatus = Exclude<\n\tTLStoreWithStatus,\n\t{ status: 'synced-local' } | { status: 'not-synced' }\n>\n\n/**\n * useSync creates a store that is synced with a multiplayer server.\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 * To enable external blob storage, you should also pass in an `assets` object that implements the {@link tldraw#TLAssetStore} interface.\n * If you don't do this, adding large images and videos to rooms will cause performance issues at serialization boundaries.\n *\n * @example\n * ```tsx\n * function MyApp() {\n * const store = useSync({\n * uri: 'wss://myapp.com/sync/my-test-room',\n * assets: myAssetStore\n * })\n * return <Tldraw store={store} />\n * }\n *\n * ```\n * @param opts - Options for the multiplayer sync store. See {@link UseSyncOptions} and {@link tldraw#TLStoreSchemaOptions}.\n *\n * @public\n */\nexport function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLStoreWithStatus {\n\tconst [state, setState] = useRefState<{\n\t\treadyClient?: TLSyncClient<TLRecord, TLStore>\n\t\terror?: Error\n\t} | null>(null)\n\tconst {\n\t\turi,\n\t\troomId = 'default',\n\t\tassets,\n\t\tonMount,\n\t\ttrackAnalyticsEvent: track,\n\t\tuserInfo,\n\t\tgetUserPresence: _getUserPresence,\n\t\tonCustomMessageReceived: _onCustomMessageReceived,\n\t\t...schemaOpts\n\t} = opts\n\n\t// This line will throw a type error if we add any new options to the useSync hook but we don't destructure them\n\t// This is required because otherwise the useTLSchemaFromUtils might return a new schema on every render if the newly-added option\n\t// is allowed to be unstable (e.g. userInfo)\n\tconst __never__: never = 0 as any as keyof Omit<typeof schemaOpts, keyof TLStoreSchemaOptions>\n\n\tconst schema = useTLSchemaFromUtils(schemaOpts)\n\n\tconst prefs = useShallowObjectIdentity(userInfo)\n\tconst getUserPresence = useReactiveEvent(_getUserPresence ?? getDefaultUserPresence)\n\tconst onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)\n\n\tconst userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(\n\t\t'userAtom',\n\t\tprefs\n\t)\n\n\tuseEffect(() => {\n\t\tuserAtom.set(prefs)\n\t}, [prefs, userAtom])\n\n\tuseEffect(() => {\n\t\tconst storeId = uniqueId()\n\n\t\tconst userPreferences = computed<{ id: string; color: string; name: string }>(\n\t\t\t'userPreferences',\n\t\t\t() => {\n\t\t\t\tconst userStuff = userAtom.get()\n\t\t\t\tconst user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()\n\t\t\t\treturn {\n\t\t\t\t\tid: user.id,\n\t\t\t\t\tcolor: user.color ?? defaultUserPreferences.color,\n\t\t\t\t\tname: user.name ?? defaultUserPreferences.name,\n\t\t\t\t}\n\t\t\t}\n\t\t)\n\n\t\tconst socket = new ClientWebSocketAdapter(async () => {\n\t\t\tconst uriString = typeof uri === 'string' ? uri : await uri()\n\n\t\t\t// set sessionId as a query param on the uri\n\t\t\tconst withParams = new URL(uriString)\n\t\t\tif (withParams.searchParams.has('sessionId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"sessionId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\t\t\tif (withParams.searchParams.has('storeId')) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'useSync. \"storeId\" is a reserved query param name. Please use a different name'\n\t\t\t\t)\n\t\t\t}\n\n\t\t\twithParams.searchParams.set('sessionId', TAB_ID)\n\t\t\twithParams.searchParams.set('storeId', storeId)\n\t\t\treturn withParams.toString()\n\t\t})\n\n\t\tlet didCancel = false\n\n\t\tconst collaborationStatusSignal = computed('collaboration status', () =>\n\t\t\tsocket.connectionStatus === 'error' ? 'offline' : socket.connectionStatus\n\t\t)\n\n\t\tconst syncMode = atom('sync mode', 'readwrite' as 'readonly' | 'readwrite')\n\n\t\tconst store = createTLStore({\n\t\t\tid: storeId,\n\t\t\tschema,\n\t\t\tassets,\n\t\t\tonMount,\n\t\t\tcollaboration: {\n\t\t\t\tstatus: collaborationStatusSignal,\n\t\t\t\tmode: syncMode,\n\t\t\t},\n\t\t})\n\n\t\tconst presence = computed('instancePresence', () => {\n\t\t\tconst presenceState = getUserPresence(store, userPreferences.get())\n\t\t\tif (!presenceState) return null\n\n\t\t\treturn InstancePresenceRecordType.create({\n\t\t\t\t...presenceState,\n\t\t\t\tid: InstancePresenceRecordType.createId(store.id),\n\t\t\t})\n\t\t})\n\n\t\tconst otherUserPresences = store.query.ids('instance_presence', () => ({\n\t\t\tuserId: { neq: userPreferences.get().id },\n\t\t}))\n\n\t\tconst presenceMode = computed<TLPresenceMode>('presenceMode', () => {\n\t\t\tif (otherUserPresences.get().size === 0) return 'solo'\n\t\t\treturn 'full'\n\t\t})\n\n\t\tconst client = new TLSyncClient({\n\t\t\tstore,\n\t\t\tsocket,\n\t\t\tdidCancel: () => didCancel,\n\t\t\tonLoad(client) {\n\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'load', roomId })\n\t\t\t\tsetState({ readyClient: client })\n\t\t\t},\n\t\t\tonSyncError(reason) {\n\t\t\t\tconsole.error('sync error', reason)\n\n\t\t\t\tswitch (reason) {\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_FOUND:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'room-not-found', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.FORBIDDEN:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'forbidden', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'not-authenticated', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tcase TLSyncErrorCloseEventReason.RATE_LIMITED:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'rate-limited', roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t\tdefault:\n\t\t\t\t\t\ttrack?.(MULTIPLAYER_EVENT_NAME, { name: 'sync-error:' + reason, roomId })\n\t\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsetState({ error: new TLRemoteSyncError(reason) })\n\t\t\t\tsocket.close()\n\t\t\t},\n\t\t\tonAfterConnect(_, { isReadonly }) {\n\t\t\t\ttransact(() => {\n\t\t\t\t\tsyncMode.set(isReadonly ? 'readonly' : 'readwrite')\n\t\t\t\t\t// if the server crashes and loses all data it can return an empty document\n\t\t\t\t\t// when it comes back up. This is a safety check to make sure that if something like\n\t\t\t\t\t// that happens, it won't render the app broken and require a restart. The user will\n\t\t\t\t\t// most likely lose all their changes though since they'll have been working with pages\n\t\t\t\t\t// that won't exist. There's certainly something we can do to make this better.\n\t\t\t\t\t// but the likelihood of this happening is very low and maybe not worth caring about beyond this.\n\t\t\t\t\tstore.ensureStoreIsUsable()\n\t\t\t\t})\n\t\t\t},\n\t\t\tonCustomMessageReceived,\n\t\t\tpresence,\n\t\t\tpresenceMode,\n\t\t})\n\n\t\treturn () => {\n\t\t\tdidCancel = true\n\t\t\tclient.close()\n\t\t\tsocket.close()\n\t\t\tsetState(null)\n\t\t}\n\t}, [\n\t\tassets,\n\t\tonMount,\n\t\tuserAtom,\n\t\troomId,\n\t\tschema,\n\t\tsetState,\n\t\ttrack,\n\t\turi,\n\t\tgetUserPresence,\n\t\tonCustomMessageReceived,\n\t])\n\n\treturn useValue<RemoteTLStoreWithStatus>(\n\t\t'remote synced store',\n\t\t() => {\n\t\t\tif (!state) return { status: 'loading' }\n\t\t\tif (state.error) return { status: 'error', error: state.error }\n\t\t\tif (!state.readyClient) return { status: 'loading' }\n\t\t\tconst connectionStatus = state.readyClient.socket.connectionStatus\n\t\t\treturn {\n\t\t\t\tstatus: 'synced-remote',\n\t\t\t\tconnectionStatus: connectionStatus === 'error' ? 'offline' : connectionStatus,\n\t\t\t\tstore: state.readyClient.store,\n\t\t\t}\n\t\t},\n\t\t[state]\n\t)\n}\n\n/**\n * Options for the {@link useSync} hook.\n * @public\n */\nexport interface UseSyncOptions {\n\t/**\n\t * The URI of the multiplayer server. This must include the protocol,\n\t *\n\t * e.g. `wss://server.example.com/my-room` or `ws://localhost:5858/my-room`.\n\t *\n\t * Note that the protocol can also be `https` or `http` and it will upgrade to a websocket\n\t * connection.\n\t *\n\t * Optionally, you can pass a function which will be called each time a connection is\n\t * established to get the URI. This is useful if you need to include e.g. a short-lived session\n\t * token for authentication.\n\t */\n\turi: string | (() => string | Promise<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\t/**\n\t * The asset store for blob storage. See {@link tldraw#TLAssetStore}.\n\t *\n\t * If you don't have time to implement blob storage and just want to get started, you can use the inline base64 asset store. {@link tldraw#inlineBase64AssetStore}\n\t * Note that storing base64 blobs inline in JSON is very inefficient and will cause performance issues quickly with large images and videos.\n\t */\n\tassets: TLAssetStore\n\n\t/**\n\t * A handler for custom socket messages.\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. The\n\t * result of this function will be synchronized across all clients to display presence\n\t * indicators such as cursors. See {@link @tldraw/tlschema#getDefaultUserPresence} for\n\t * the default implementation of this function.\n\t */\n\tgetUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["client"]
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync react bindings).",
4
- "version": "3.16.0-next.eafb52d15064",
4
+ "version": "3.16.0-next.fe14f1b4181f",
5
5
  "author": {
6
6
  "name": "tldraw GB Ltd.",
7
7
  "email": "hello@tldraw.com"
@@ -32,15 +32,16 @@
32
32
  "src"
33
33
  ],
34
34
  "scripts": {
35
- "test-ci": "lazy inherit",
36
- "test": "yarn run -T jest",
37
- "test-coverage": "lazy inherit",
35
+ "test-ci": "yarn run -T vitest run --passWithNoTests",
36
+ "test": "yarn run -T vitest --passWithNoTests",
37
+ "test-coverage": "yarn run -T vitest run --coverage --passWithNoTests",
38
38
  "lint": "yarn run -T tsx ../../internal/scripts/lint.ts",
39
39
  "build": "yarn run -T tsx ../../internal/scripts/build-package.ts",
40
40
  "build-api": "yarn run -T tsx ../../internal/scripts/build-api.ts",
41
41
  "prepack": "yarn run -T tsx ../../internal/scripts/prepack.ts",
42
42
  "postpack": "../../internal/scripts/postpack.sh",
43
- "pack-tarball": "yarn pack"
43
+ "pack-tarball": "yarn pack",
44
+ "context": "yarn run -T tsx ../../internal/scripts/context.ts"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@types/react": "^18.3.18",
@@ -50,26 +51,13 @@
50
51
  "uuid-by-string": "^4.0.0",
51
52
  "uuid-readable": "^0.0.2"
52
53
  },
53
- "jest": {
54
- "preset": "../../internal/config/jest/node/jest-preset.js",
55
- "testEnvironment": "../../../packages/utils/patchedJestJsDom.js",
56
- "moduleNameMapper": {
57
- "^~(.*)": "<rootDir>/src/$1"
58
- },
59
- "transformIgnorePatterns": [
60
- "ignore everything. swc is fast enough to transform everything"
61
- ],
62
- "setupFiles": [
63
- "./setupJest.js"
64
- ]
65
- },
66
54
  "dependencies": {
67
- "@tldraw/state": "3.16.0-next.eafb52d15064",
68
- "@tldraw/state-react": "3.16.0-next.eafb52d15064",
69
- "@tldraw/sync-core": "3.16.0-next.eafb52d15064",
70
- "@tldraw/utils": "3.16.0-next.eafb52d15064",
55
+ "@tldraw/state": "3.16.0-next.fe14f1b4181f",
56
+ "@tldraw/state-react": "3.16.0-next.fe14f1b4181f",
57
+ "@tldraw/sync-core": "3.16.0-next.fe14f1b4181f",
58
+ "@tldraw/utils": "3.16.0-next.fe14f1b4181f",
71
59
  "nanoevents": "^7.0.1",
72
- "tldraw": "3.16.0-next.eafb52d15064",
60
+ "tldraw": "3.16.0-next.fe14f1b4181f",
73
61
  "ws": "^8.18.0"
74
62
  },
75
63
  "peerDependencies": {
package/src/useSync.ts CHANGED
@@ -2,6 +2,7 @@ import { atom, isSignal, transact } from '@tldraw/state'
2
2
  import { useAtom } from '@tldraw/state-react'
3
3
  import {
4
4
  ClientWebSocketAdapter,
5
+ TLCustomMessageHandler,
5
6
  TLPresenceMode,
6
7
  TLRemoteSyncError,
7
8
  TLSyncClient,
@@ -26,6 +27,7 @@ import {
26
27
  getDefaultUserPresence,
27
28
  getUserPreferences,
28
29
  uniqueId,
30
+ useEvent,
29
31
  useReactiveEvent,
30
32
  useRefState,
31
33
  useShallowObjectIdentity,
@@ -35,6 +37,8 @@ import {
35
37
 
36
38
  const MULTIPLAYER_EVENT_NAME = 'multiplayer.client'
37
39
 
40
+ const defaultCustomMessageHandler: TLCustomMessageHandler = () => {}
41
+
38
42
  /** @public */
39
43
  export type RemoteTLStoreWithStatus = Exclude<
40
44
  TLStoreWithStatus,
@@ -78,6 +82,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
78
82
  trackAnalyticsEvent: track,
79
83
  userInfo,
80
84
  getUserPresence: _getUserPresence,
85
+ onCustomMessageReceived: _onCustomMessageReceived,
81
86
  ...schemaOpts
82
87
  } = opts
83
88
 
@@ -90,6 +95,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
90
95
 
91
96
  const prefs = useShallowObjectIdentity(userInfo)
92
97
  const getUserPresence = useReactiveEvent(_getUserPresence ?? getDefaultUserPresence)
98
+ const onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)
93
99
 
94
100
  const userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(
95
101
  'userAtom',
@@ -219,6 +225,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
219
225
  store.ensureStoreIsUsable()
220
226
  })
221
227
  },
228
+ onCustomMessageReceived,
222
229
  presence,
223
230
  presenceMode,
224
231
  })
@@ -229,7 +236,18 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
229
236
  socket.close()
230
237
  setState(null)
231
238
  }
232
- }, [assets, onMount, userAtom, roomId, schema, setState, track, uri, getUserPresence])
239
+ }, [
240
+ assets,
241
+ onMount,
242
+ userAtom,
243
+ roomId,
244
+ schema,
245
+ setState,
246
+ track,
247
+ uri,
248
+ getUserPresence,
249
+ onCustomMessageReceived,
250
+ ])
233
251
 
234
252
  return useValue<RemoteTLStoreWithStatus>(
235
253
  'remote synced store',
@@ -280,6 +298,11 @@ export interface UseSyncOptions {
280
298
  */
281
299
  assets: TLAssetStore
282
300
 
301
+ /**
302
+ * A handler for custom socket messages.
303
+ */
304
+ onCustomMessageReceived?(data: any): void
305
+
283
306
  /** @internal */
284
307
  onMount?(editor: Editor): void
285
308
  /** @internal used for analytics only, we should refactor this away */