@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/dist-cjs/index.d.ts +29 -44
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/useSync.js +40 -32
- package/dist-cjs/useSync.js.map +2 -2
- package/dist-cjs/useSyncDemo.js.map +1 -1
- package/dist-esm/index.d.mts +29 -44
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/useSync.mjs +46 -35
- package/dist-esm/useSync.mjs.map +2 -2
- package/dist-esm/useSyncDemo.mjs.map +1 -1
- package/package.json +7 -7
- package/src/useSync.ts +82 -79
- package/src/useSyncDemo.ts +6 -6
package/src/useSync.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { atom,
|
|
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
|
-
* - `
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 =
|
|
289
|
-
|
|
290
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
*
|
|
405
|
-
*
|
|
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:
|
|
531
|
+
getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null
|
|
529
532
|
}
|
|
530
533
|
|
|
531
534
|
/** @public */
|
package/src/useSyncDemo.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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:
|
|
43
|
+
getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|