@tldraw/sync 4.6.0-next.1f489710ee41 → 4.6.0-next.4dde09fa17ab

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/src/useSync.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { atom, isSignal, transact } from '@tldraw/state'
2
- import { useAtom } from '@tldraw/state-react'
1
+ import { atom, transact } from '@tldraw/state'
3
2
  import {
4
3
  ClientWebSocketAdapter,
5
4
  TLCustomMessageHandler,
@@ -14,26 +13,29 @@ import {
14
13
  import { useEffect } from 'react'
15
14
  import {
16
15
  Editor,
17
- InstancePresenceRecordType,
18
- Signal,
19
16
  TAB_ID,
20
17
  TLAssetStore,
21
18
  TLPresenceStateInfo,
22
- TLPresenceUserInfo,
23
19
  TLRecord,
24
20
  TLStore,
25
21
  TLStoreSchemaOptions,
26
22
  TLStoreWithStatus,
23
+ TLUser,
24
+ TLUserStore,
25
+ UserRecordType,
27
26
  computed,
27
+ createCachedUserResolve,
28
+ createPresenceStateDerivation,
28
29
  createTLStore,
30
+ createUserId,
29
31
  defaultUserPreferences,
32
+ defaultUserStore,
30
33
  getDefaultUserPresence,
31
34
  getUserPreferences,
32
35
  uniqueId,
33
36
  useEvent,
34
37
  useReactiveEvent,
35
38
  useRefState,
36
- useShallowObjectIdentity,
37
39
  useTLSchemaFromUtils,
38
40
  useValue,
39
41
  } from 'tldraw'
@@ -96,7 +98,7 @@ export type RemoteTLStoreWithStatus = Exclude<
96
98
  * @param opts - Configuration options for multiplayer synchronization
97
99
  * - `uri` - WebSocket server URI (string or async function returning URI)
98
100
  * - `assets` - Asset store for blob storage (required for production use)
99
- * - `userInfo` - User information for presence system (can be reactive signal)
101
+ * - `users` - User store for identity, presence and attribution
100
102
  * - `getUserPresence` - Optional function to customize presence data
101
103
  * - `onCustomMessageReceived` - Handler for custom socket messages
102
104
  * - `roomId` - Room identifier for analytics (internal use)
@@ -112,10 +114,13 @@ export type RemoteTLStoreWithStatus = Exclude<
112
114
  * const store = useSync({
113
115
  * uri: 'wss://myserver.com/sync/room-123',
114
116
  * assets: myAssetStore,
115
- * userInfo: {
116
- * id: 'user-1',
117
- * name: 'Alice',
118
- * color: '#ff0000'
117
+ * users: {
118
+ * currentUser: computed('current-user', () => ({
119
+ * id: createUserId('user-1'),
120
+ * name: 'Alice',
121
+ * color: '#ff0000',
122
+ * meta: {},
123
+ * })),
119
124
  * }
120
125
  * })
121
126
  *
@@ -133,19 +138,15 @@ export type RemoteTLStoreWithStatus = Exclude<
133
138
  *
134
139
  * @example
135
140
  * ```tsx
136
- * // Dynamic authentication with reactive user info
137
- * import { atom } from '@tldraw/state'
138
- *
141
+ * // Dynamic authentication with user store
139
142
  * function AuthenticatedApp() {
140
- * const currentUser = atom('user', { id: 'user-1', name: 'Alice', color: '#ff0000' })
141
- *
142
143
  * const store = useSync({
143
144
  * uri: async () => {
144
145
  * const token = await getAuthToken()
145
146
  * return `wss://myserver.com/sync/room-123?token=${token}`
146
147
  * },
147
148
  * assets: authenticatedAssetStore,
148
- * userInfo: currentUser, // Reactive signal
149
+ * users: myUserStore,
149
150
  * getUserPresence: (store, user) => {
150
151
  * return {
151
152
  * userId: user.id,
@@ -170,10 +171,10 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
170
171
  uri,
171
172
  roomId = 'default',
172
173
  assets,
174
+ users: _users,
173
175
  onMount,
174
176
  connect,
175
177
  trackAnalyticsEvent: track,
176
- userInfo,
177
178
  getUserPresence: _getUserPresence,
178
179
  onCustomMessageReceived: _onCustomMessageReceived,
179
180
  ...schemaOpts
@@ -181,41 +182,61 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
181
182
 
182
183
  // This line will throw a type error if we add any new options to the useSync hook but we don't destructure them
183
184
  // This is required because otherwise the useTLSchemaFromUtils might return a new schema on every render if the newly-added option
184
- // is allowed to be unstable (e.g. userInfo)
185
+ // is allowed to be unstable
185
186
  const __never__: never = 0 as any as keyof Omit<typeof schemaOpts, keyof TLStoreSchemaOptions>
186
187
 
187
188
  const schema = useTLSchemaFromUtils(schemaOpts)
188
189
 
189
- const prefs = useShallowObjectIdentity(userInfo)
190
190
  const getUserPresence = useReactiveEvent(
191
191
  (_getUserPresence ?? getDefaultUserPresence) as typeof getDefaultUserPresence
192
192
  )
193
193
  const onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)
194
194
 
195
- const userAtom = useAtom<TLPresenceUserInfo | Signal<TLPresenceUserInfo> | undefined>(
196
- 'userAtom',
197
- prefs
198
- )
199
-
200
- useEffect(() => {
201
- userAtom.set(prefs)
202
- }, [prefs, userAtom])
203
-
204
195
  useEffect(() => {
205
196
  const storeId = uniqueId()
206
197
 
207
- const userPreferences = computed<{ id: string; color: string; name: string }>(
208
- 'userPreferences',
209
- () => {
210
- const userStuff = userAtom.get()
211
- const user = (isSignal(userStuff) ? userStuff.get() : userStuff) ?? getUserPreferences()
212
- return {
213
- id: user.id,
214
- color: user.color ?? defaultUserPreferences.color,
215
- name: user.name ?? defaultUserPreferences.name,
198
+ const users: Required<TLUserStore> = _users
199
+ ? {
200
+ currentUser: _users.currentUser,
201
+ resolve:
202
+ _users.resolve ??
203
+ createCachedUserResolve((userId) => {
204
+ const current = _users.currentUser.get()
205
+ return current && current.id === createUserId(userId) ? current : null
206
+ }),
216
207
  }
217
- }
218
- )
208
+ : {
209
+ currentUser: defaultUserStore.currentUser,
210
+ resolve: createCachedUserResolve((userId) => {
211
+ const current = defaultUserStore.currentUser.get()
212
+ if (current && current.id === createUserId(userId)) return current
213
+ const presences = store.query.records('instance_presence').get()
214
+ const match = presences.find((p) => p.userId === createUserId(userId))
215
+ if (match) {
216
+ return UserRecordType.create({
217
+ id: createUserId(userId),
218
+ name: match.userName,
219
+ color: match.color,
220
+ })
221
+ }
222
+ return null
223
+ }),
224
+ }
225
+
226
+ // This always returns a non-null user for presence display, falling back
227
+ // to anonymous user preferences. The store receives the raw `users` object
228
+ // (where currentUser may return null), so attribution via
229
+ // getAttributionUserId() correctly returns null for anonymous sessions.
230
+ const currentUser = computed<TLUser>('currentUser', () => {
231
+ const user = users.currentUser.get()
232
+ if (user) return user
233
+ const prefs = getUserPreferences()
234
+ return UserRecordType.create({
235
+ id: createUserId(prefs.id),
236
+ name: prefs.name ?? '',
237
+ color: prefs.color ?? defaultUserPreferences.color,
238
+ })
239
+ })
219
240
 
220
241
  let socket: TLPersistentClientSocket<
221
242
  TLSocketClientSentEvent<TLRecord>,
@@ -278,6 +299,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
278
299
  id: storeId,
279
300
  schema,
280
301
  assets,
302
+ users,
281
303
  onMount,
282
304
  collaboration: {
283
305
  status: collaborationStatusSignal,
@@ -285,18 +307,12 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
285
307
  },
286
308
  })
287
309
 
288
- const presence = computed('instancePresence', () => {
289
- const presenceState = getUserPresence(store, userPreferences.get())
290
- if (!presenceState) return null
291
-
292
- return InstancePresenceRecordType.create({
293
- ...presenceState,
294
- id: InstancePresenceRecordType.createId(store.id),
295
- })
296
- })
310
+ const presence = createPresenceStateDerivation(currentUser, {
311
+ getUserPresence,
312
+ })(store)
297
313
 
298
314
  const otherUserPresences = store.query.ids('instance_presence', () => ({
299
- userId: { neq: userPreferences.get().id },
315
+ userId: { neq: currentUser.get().id },
300
316
  }))
301
317
 
302
318
  const presenceMode = computed<TLPresenceMode>('presenceMode', () => {
@@ -363,7 +379,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
363
379
  assets,
364
380
  onMount,
365
381
  connect,
366
- userAtom,
382
+ _users,
367
383
  roomId,
368
384
  schema,
369
385
  setState,
@@ -401,10 +417,8 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
401
417
  * const syncOptions: UseSyncOptions = {
402
418
  * uri: 'wss://myserver.com/sync/room-123',
403
419
  * assets: myAssetStore,
404
- * userInfo: {
405
- * id: 'user-1',
406
- * name: 'Alice',
407
- * color: '#ff0000'
420
+ * users: {
421
+ * currentUser: myCurrentUserSignal,
408
422
  * },
409
423
  * getUserPresence: (store, user) => ({
410
424
  * userId: user.id,
@@ -417,29 +431,6 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
417
431
  * @public
418
432
  */
419
433
  export interface UseSyncOptionsBase {
420
- /**
421
- * User information for multiplayer presence and identification.
422
- *
423
- * Can be a static object or a reactive signal that updates when user
424
- * information changes. The presence system automatically updates when
425
- * reactive signals change, allowing real-time user profile updates.
426
- *
427
- * Should be synchronized with the `userPreferences` prop of the main
428
- * Tldraw component for consistent user experience. If not provided,
429
- * defaults to localStorage-based user preferences.
430
- *
431
- * @example
432
- * ```ts
433
- * // Static user info
434
- * userInfo: { id: 'user-123', name: 'Alice', color: '#ff0000' }
435
- *
436
- * // Reactive user info
437
- * const userSignal = atom('user', { id: 'user-123', name: 'Alice', color: '#ff0000' })
438
- * userInfo: userSignal
439
- * ```
440
- */
441
- userInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>
442
-
443
434
  /**
444
435
  * Asset store implementation for handling file uploads and storage.
445
436
  *
@@ -466,6 +457,18 @@ export interface UseSyncOptionsBase {
466
457
  */
467
458
  assets: TLAssetStore
468
459
 
460
+ /**
461
+ * User store for identity, presence and attribution.
462
+ *
463
+ * Both methods return reactive {@link @tldraw/state#Signal | Signals}.
464
+ * `currentUser` provides the current user's identity (used for
465
+ * both presence broadcasting and shape attribution) and optionally
466
+ * `resolve(userId)` looks up other users by ID. If not provided,
467
+ * a default implementation backed by localStorage user preferences is
468
+ * used, with `resolve` falling back to presence records in the store.
469
+ */
470
+ users?: TLUserStore
471
+
469
472
  /**
470
473
  * Handler for receiving custom messages sent through the multiplayer connection.
471
474
  *
@@ -525,7 +528,7 @@ export interface UseSyncOptionsBase {
525
528
  * }
526
529
  * ```
527
530
  */
528
- getUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null
531
+ getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null
529
532
  }
530
533
 
531
534
  /** @public */
@@ -3,13 +3,13 @@ import {
3
3
  AssetRecordType,
4
4
  Editor,
5
5
  MediaHelpers,
6
- Signal,
7
6
  TLAsset,
8
7
  TLAssetStore,
9
8
  TLPresenceStateInfo,
10
- TLPresenceUserInfo,
11
9
  TLStore,
12
10
  TLStoreSchemaOptions,
11
+ TLUser,
12
+ TLUserStore,
13
13
  clamp,
14
14
  defaultBindingUtils,
15
15
  defaultShapeUtils,
@@ -26,12 +26,12 @@ export interface UseSyncDemoOptions {
26
26
  * everyone using the demo server. Consider prefixing it with your company or project name.
27
27
  */
28
28
  roomId: string
29
+
29
30
  /**
30
- * A signal that contains the user information needed for multiplayer features.
31
- * This should be synchronized with the `userPreferences` configuration for the main `<Tldraw />` component.
31
+ * User store for identity, presence and attribution.
32
32
  * If not provided, a default implementation based on localStorage will be used.
33
33
  */
34
- userInfo?: TLPresenceUserInfo | Signal<TLPresenceUserInfo>
34
+ users?: TLUserStore
35
35
 
36
36
  /** @internal */
37
37
  host?: string
@@ -40,7 +40,7 @@ export interface UseSyncDemoOptions {
40
40
  * {@inheritdoc UseSyncOptions.getUserPresence}
41
41
  * @public
42
42
  */
43
- getUserPresence?(store: TLStore, user: TLPresenceUserInfo): TLPresenceStateInfo | null
43
+ getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null
44
44
  }
45
45
 
46
46
  /**