@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.
- package/dist-cjs/index.d.ts +4 -0
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/useSync.js +17 -1
- package/dist-cjs/useSync.js.map +2 -2
- package/dist-esm/index.d.mts +4 -0
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/useSync.mjs +18 -1
- package/dist-esm/useSync.mjs.map +2 -2
- package/package.json +11 -23
- package/src/useSync.ts +24 -1
package/dist-cjs/index.d.ts
CHANGED
|
@@ -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.
|
|
32
|
+
"3.16.0-next.fe14f1b4181f",
|
|
33
33
|
"cjs"
|
|
34
34
|
);
|
|
35
35
|
//# sourceMappingURL=index.js.map
|
package/dist-cjs/useSync.js
CHANGED
|
@@ -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
|
-
}, [
|
|
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
|
() => {
|
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\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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAyC;AACzC,yBAAwB;AACxB,
|
|
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/dist-esm/index.d.mts
CHANGED
|
@@ -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-esm/index.mjs
CHANGED
package/dist-esm/useSync.mjs
CHANGED
|
@@ -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
|
-
}, [
|
|
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
|
() => {
|
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\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": "AAAA,SAAS,MAAM,UAAU,gBAAgB;AACzC,SAAS,eAAe;AACxB;AAAA,EACC;AAAA,
|
|
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.
|
|
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": "
|
|
36
|
-
"test": "yarn run -T
|
|
37
|
-
"test-coverage": "
|
|
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.
|
|
68
|
-
"@tldraw/state-react": "3.16.0-next.
|
|
69
|
-
"@tldraw/sync-core": "3.16.0-next.
|
|
70
|
-
"@tldraw/utils": "3.16.0-next.
|
|
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.
|
|
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
|
-
}, [
|
|
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 */
|