@tldraw/sync 3.16.0-next.df90ce0ff566 → 3.16.0-next.eafb52d15064
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.js +1 -1
- package/dist-cjs/useSync.js +9 -1
- package/dist-cjs/useSync.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/useSync.mjs +9 -1
- package/dist-esm/useSync.mjs.map +2 -2
- package/package.json +6 -6
- package/src/useSync.ts +11 -0
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.eafb52d15064",
|
|
33
33
|
"cjs"
|
|
34
34
|
);
|
|
35
35
|
//# sourceMappingURL=index.js.map
|
package/dist-cjs/useSync.js
CHANGED
|
@@ -105,6 +105,13 @@ function useSync(opts) {
|
|
|
105
105
|
id: import_tldraw.InstancePresenceRecordType.createId(store.id)
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
+
const otherUserPresences = store.query.ids("instance_presence", () => ({
|
|
109
|
+
userId: { neq: userPreferences.get().id }
|
|
110
|
+
}));
|
|
111
|
+
const presenceMode = (0, import_tldraw.computed)("presenceMode", () => {
|
|
112
|
+
if (otherUserPresences.get().size === 0) return "solo";
|
|
113
|
+
return "full";
|
|
114
|
+
});
|
|
108
115
|
const client = new import_sync_core.TLSyncClient({
|
|
109
116
|
store,
|
|
110
117
|
socket,
|
|
@@ -141,7 +148,8 @@ function useSync(opts) {
|
|
|
141
148
|
store.ensureStoreIsUsable();
|
|
142
149
|
});
|
|
143
150
|
},
|
|
144
|
-
presence
|
|
151
|
+
presence,
|
|
152
|
+
presenceMode
|
|
145
153
|
});
|
|
146
154
|
return () => {
|
|
147
155
|
didCancel = true;
|
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\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 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})\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\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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",
|
|
6
6
|
"names": ["client"]
|
|
7
7
|
}
|
package/dist-esm/index.mjs
CHANGED
package/dist-esm/useSync.mjs
CHANGED
|
@@ -101,6 +101,13 @@ function useSync(opts) {
|
|
|
101
101
|
id: InstancePresenceRecordType.createId(store.id)
|
|
102
102
|
});
|
|
103
103
|
});
|
|
104
|
+
const otherUserPresences = store.query.ids("instance_presence", () => ({
|
|
105
|
+
userId: { neq: userPreferences.get().id }
|
|
106
|
+
}));
|
|
107
|
+
const presenceMode = computed("presenceMode", () => {
|
|
108
|
+
if (otherUserPresences.get().size === 0) return "solo";
|
|
109
|
+
return "full";
|
|
110
|
+
});
|
|
104
111
|
const client = new TLSyncClient({
|
|
105
112
|
store,
|
|
106
113
|
socket,
|
|
@@ -137,7 +144,8 @@ function useSync(opts) {
|
|
|
137
144
|
store.ensureStoreIsUsable();
|
|
138
145
|
});
|
|
139
146
|
},
|
|
140
|
-
presence
|
|
147
|
+
presence,
|
|
148
|
+
presenceMode
|
|
141
149
|
});
|
|
142
150
|
return () => {
|
|
143
151
|
didCancel = true;
|
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\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 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})\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\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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",
|
|
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.eafb52d15064",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw GB Ltd.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -64,12 +64,12 @@
|
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"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.
|
|
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",
|
|
71
71
|
"nanoevents": "^7.0.1",
|
|
72
|
-
"tldraw": "3.16.0-next.
|
|
72
|
+
"tldraw": "3.16.0-next.eafb52d15064",
|
|
73
73
|
"ws": "^8.18.0"
|
|
74
74
|
},
|
|
75
75
|
"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
|
+
TLPresenceMode,
|
|
5
6
|
TLRemoteSyncError,
|
|
6
7
|
TLSyncClient,
|
|
7
8
|
TLSyncErrorCloseEventReason,
|
|
@@ -165,6 +166,15 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
|
|
|
165
166
|
})
|
|
166
167
|
})
|
|
167
168
|
|
|
169
|
+
const otherUserPresences = store.query.ids('instance_presence', () => ({
|
|
170
|
+
userId: { neq: userPreferences.get().id },
|
|
171
|
+
}))
|
|
172
|
+
|
|
173
|
+
const presenceMode = computed<TLPresenceMode>('presenceMode', () => {
|
|
174
|
+
if (otherUserPresences.get().size === 0) return 'solo'
|
|
175
|
+
return 'full'
|
|
176
|
+
})
|
|
177
|
+
|
|
168
178
|
const client = new TLSyncClient({
|
|
169
179
|
store,
|
|
170
180
|
socket,
|
|
@@ -210,6 +220,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
|
|
|
210
220
|
})
|
|
211
221
|
},
|
|
212
222
|
presence,
|
|
223
|
+
presenceMode,
|
|
213
224
|
})
|
|
214
225
|
|
|
215
226
|
return () => {
|