@tldraw/sync 4.6.0-next.5a871ec02ff3 → 4.6.0-next.6594d48ace27
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 +34 -42
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +1 -1
- package/dist-cjs/useSync.js +44 -32
- package/dist-cjs/useSync.js.map +2 -2
- package/dist-cjs/useSyncDemo.js.map +1 -1
- package/dist-esm/index.d.mts +34 -42
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +1 -1
- package/dist-esm/useSync.mjs +53 -35
- package/dist-esm/useSync.mjs.map +2 -2
- package/dist-esm/useSyncDemo.mjs.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +1 -1
- package/src/useSync.ts +94 -76
- 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,33 @@ 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
|
+
TLThemes,
|
|
24
|
+
TLUser,
|
|
25
|
+
TLUserStore,
|
|
26
|
+
UserRecordType,
|
|
27
27
|
computed,
|
|
28
|
+
createCachedUserResolve,
|
|
29
|
+
createPresenceStateDerivation,
|
|
30
|
+
registerColorsFromThemes,
|
|
31
|
+
registerFontsFromThemes,
|
|
32
|
+
resolveThemes,
|
|
28
33
|
createTLStore,
|
|
34
|
+
createUserId,
|
|
29
35
|
defaultUserPreferences,
|
|
36
|
+
defaultUserStore,
|
|
30
37
|
getDefaultUserPresence,
|
|
31
38
|
getUserPreferences,
|
|
32
39
|
uniqueId,
|
|
33
40
|
useEvent,
|
|
34
41
|
useReactiveEvent,
|
|
35
42
|
useRefState,
|
|
36
|
-
useShallowObjectIdentity,
|
|
37
43
|
useTLSchemaFromUtils,
|
|
38
44
|
useValue,
|
|
39
45
|
} from 'tldraw'
|
|
@@ -96,7 +102,7 @@ export type RemoteTLStoreWithStatus = Exclude<
|
|
|
96
102
|
* @param opts - Configuration options for multiplayer synchronization
|
|
97
103
|
* - `uri` - WebSocket server URI (string or async function returning URI)
|
|
98
104
|
* - `assets` - Asset store for blob storage (required for production use)
|
|
99
|
-
* - `
|
|
105
|
+
* - `users` - User store for identity, presence and attribution
|
|
100
106
|
* - `getUserPresence` - Optional function to customize presence data
|
|
101
107
|
* - `onCustomMessageReceived` - Handler for custom socket messages
|
|
102
108
|
* - `roomId` - Room identifier for analytics (internal use)
|
|
@@ -112,10 +118,13 @@ export type RemoteTLStoreWithStatus = Exclude<
|
|
|
112
118
|
* const store = useSync({
|
|
113
119
|
* uri: 'wss://myserver.com/sync/room-123',
|
|
114
120
|
* assets: myAssetStore,
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
121
|
+
* users: {
|
|
122
|
+
* currentUser: computed('current-user', () => ({
|
|
123
|
+
* id: createUserId('user-1'),
|
|
124
|
+
* name: 'Alice',
|
|
125
|
+
* color: '#ff0000',
|
|
126
|
+
* meta: {},
|
|
127
|
+
* })),
|
|
119
128
|
* }
|
|
120
129
|
* })
|
|
121
130
|
*
|
|
@@ -133,19 +142,15 @@ export type RemoteTLStoreWithStatus = Exclude<
|
|
|
133
142
|
*
|
|
134
143
|
* @example
|
|
135
144
|
* ```tsx
|
|
136
|
-
* // Dynamic authentication with
|
|
137
|
-
* import { atom } from '@tldraw/state'
|
|
138
|
-
*
|
|
145
|
+
* // Dynamic authentication with user store
|
|
139
146
|
* function AuthenticatedApp() {
|
|
140
|
-
* const currentUser = atom('user', { id: 'user-1', name: 'Alice', color: '#ff0000' })
|
|
141
|
-
*
|
|
142
147
|
* const store = useSync({
|
|
143
148
|
* uri: async () => {
|
|
144
149
|
* const token = await getAuthToken()
|
|
145
150
|
* return `wss://myserver.com/sync/room-123?token=${token}`
|
|
146
151
|
* },
|
|
147
152
|
* assets: authenticatedAssetStore,
|
|
148
|
-
*
|
|
153
|
+
* users: myUserStore,
|
|
149
154
|
* getUserPresence: (store, user) => {
|
|
150
155
|
* return {
|
|
151
156
|
* userId: user.id,
|
|
@@ -170,52 +175,76 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
|
|
|
170
175
|
uri,
|
|
171
176
|
roomId = 'default',
|
|
172
177
|
assets,
|
|
178
|
+
users: _users,
|
|
173
179
|
onMount,
|
|
174
180
|
connect,
|
|
175
181
|
trackAnalyticsEvent: track,
|
|
176
|
-
userInfo,
|
|
177
182
|
getUserPresence: _getUserPresence,
|
|
178
183
|
onCustomMessageReceived: _onCustomMessageReceived,
|
|
184
|
+
themes,
|
|
179
185
|
...schemaOpts
|
|
180
186
|
} = opts
|
|
181
187
|
|
|
182
188
|
// This line will throw a type error if we add any new options to the useSync hook but we don't destructure them
|
|
183
189
|
// 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
|
|
190
|
+
// is allowed to be unstable
|
|
185
191
|
const __never__: never = 0 as any as keyof Omit<typeof schemaOpts, keyof TLStoreSchemaOptions>
|
|
186
192
|
|
|
193
|
+
const resolvedThemes = resolveThemes(themes)
|
|
194
|
+
registerColorsFromThemes(resolvedThemes)
|
|
195
|
+
registerFontsFromThemes(resolvedThemes)
|
|
187
196
|
const schema = useTLSchemaFromUtils(schemaOpts)
|
|
188
197
|
|
|
189
|
-
const prefs = useShallowObjectIdentity(userInfo)
|
|
190
198
|
const getUserPresence = useReactiveEvent(
|
|
191
199
|
(_getUserPresence ?? getDefaultUserPresence) as typeof getDefaultUserPresence
|
|
192
200
|
)
|
|
193
201
|
const onCustomMessageReceived = useEvent(_onCustomMessageReceived ?? defaultCustomMessageHandler)
|
|
194
202
|
|
|
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
203
|
useEffect(() => {
|
|
205
204
|
const storeId = uniqueId()
|
|
206
205
|
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
const users: Required<TLUserStore> = _users
|
|
207
|
+
? {
|
|
208
|
+
currentUser: _users.currentUser,
|
|
209
|
+
resolve:
|
|
210
|
+
_users.resolve ??
|
|
211
|
+
createCachedUserResolve((userId) => {
|
|
212
|
+
const current = _users.currentUser.get()
|
|
213
|
+
return current && current.id === createUserId(userId) ? current : null
|
|
214
|
+
}),
|
|
216
215
|
}
|
|
217
|
-
|
|
218
|
-
|
|
216
|
+
: {
|
|
217
|
+
currentUser: defaultUserStore.currentUser,
|
|
218
|
+
resolve: createCachedUserResolve((userId) => {
|
|
219
|
+
const current = defaultUserStore.currentUser.get()
|
|
220
|
+
if (current && current.id === createUserId(userId)) return current
|
|
221
|
+
const presences = store.query.records('instance_presence').get()
|
|
222
|
+
const match = presences.find((p) => p.userId === createUserId(userId))
|
|
223
|
+
if (match) {
|
|
224
|
+
return UserRecordType.create({
|
|
225
|
+
id: createUserId(userId),
|
|
226
|
+
name: match.userName,
|
|
227
|
+
color: match.color,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
return null
|
|
231
|
+
}),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// This always returns a non-null user for presence display, falling back
|
|
235
|
+
// to anonymous user preferences. The store receives the raw `users` object
|
|
236
|
+
// (where currentUser may return null), so attribution via
|
|
237
|
+
// getAttributionUserId() correctly returns null for anonymous sessions.
|
|
238
|
+
const currentUser = computed<TLUser>('currentUser', () => {
|
|
239
|
+
const user = users.currentUser.get()
|
|
240
|
+
if (user) return user
|
|
241
|
+
const prefs = getUserPreferences()
|
|
242
|
+
return UserRecordType.create({
|
|
243
|
+
id: createUserId(prefs.id),
|
|
244
|
+
name: prefs.name ?? '',
|
|
245
|
+
color: prefs.color ?? defaultUserPreferences.color,
|
|
246
|
+
})
|
|
247
|
+
})
|
|
219
248
|
|
|
220
249
|
let socket: TLPersistentClientSocket<
|
|
221
250
|
TLSocketClientSentEvent<TLRecord>,
|
|
@@ -278,6 +307,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
|
|
|
278
307
|
id: storeId,
|
|
279
308
|
schema,
|
|
280
309
|
assets,
|
|
310
|
+
users,
|
|
281
311
|
onMount,
|
|
282
312
|
collaboration: {
|
|
283
313
|
status: collaborationStatusSignal,
|
|
@@ -285,18 +315,12 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
|
|
|
285
315
|
},
|
|
286
316
|
})
|
|
287
317
|
|
|
288
|
-
const presence =
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return InstancePresenceRecordType.create({
|
|
293
|
-
...presenceState,
|
|
294
|
-
id: InstancePresenceRecordType.createId(store.id),
|
|
295
|
-
})
|
|
296
|
-
})
|
|
318
|
+
const presence = createPresenceStateDerivation(currentUser, {
|
|
319
|
+
getUserPresence,
|
|
320
|
+
})(store)
|
|
297
321
|
|
|
298
322
|
const otherUserPresences = store.query.ids('instance_presence', () => ({
|
|
299
|
-
userId: { neq:
|
|
323
|
+
userId: { neq: currentUser.get().id },
|
|
300
324
|
}))
|
|
301
325
|
|
|
302
326
|
const presenceMode = computed<TLPresenceMode>('presenceMode', () => {
|
|
@@ -363,7 +387,7 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
|
|
|
363
387
|
assets,
|
|
364
388
|
onMount,
|
|
365
389
|
connect,
|
|
366
|
-
|
|
390
|
+
_users,
|
|
367
391
|
roomId,
|
|
368
392
|
schema,
|
|
369
393
|
setState,
|
|
@@ -401,10 +425,8 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
|
|
|
401
425
|
* const syncOptions: UseSyncOptions = {
|
|
402
426
|
* uri: 'wss://myserver.com/sync/room-123',
|
|
403
427
|
* assets: myAssetStore,
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* name: 'Alice',
|
|
407
|
-
* color: '#ff0000'
|
|
428
|
+
* users: {
|
|
429
|
+
* currentUser: myCurrentUserSignal,
|
|
408
430
|
* },
|
|
409
431
|
* getUserPresence: (store, user) => ({
|
|
410
432
|
* userId: user.id,
|
|
@@ -418,27 +440,11 @@ export function useSync(opts: UseSyncOptions & TLStoreSchemaOptions): RemoteTLSt
|
|
|
418
440
|
*/
|
|
419
441
|
export interface UseSyncOptionsBase {
|
|
420
442
|
/**
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
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
|
-
* ```
|
|
443
|
+
* Named theme definitions. When provided, custom color names are automatically
|
|
444
|
+
* registered before the store is constructed so persisted data with those
|
|
445
|
+
* colors passes validation on load.
|
|
440
446
|
*/
|
|
441
|
-
|
|
447
|
+
themes?: Partial<TLThemes>
|
|
442
448
|
|
|
443
449
|
/**
|
|
444
450
|
* Asset store implementation for handling file uploads and storage.
|
|
@@ -466,6 +472,18 @@ export interface UseSyncOptionsBase {
|
|
|
466
472
|
*/
|
|
467
473
|
assets: TLAssetStore
|
|
468
474
|
|
|
475
|
+
/**
|
|
476
|
+
* User store for identity, presence and attribution.
|
|
477
|
+
*
|
|
478
|
+
* Both methods return reactive {@link @tldraw/state#Signal | Signals}.
|
|
479
|
+
* `currentUser` provides the current user's identity (used for
|
|
480
|
+
* both presence broadcasting and shape attribution) and optionally
|
|
481
|
+
* `resolve(userId)` looks up other users by ID. If not provided,
|
|
482
|
+
* a default implementation backed by localStorage user preferences is
|
|
483
|
+
* used, with `resolve` falling back to presence records in the store.
|
|
484
|
+
*/
|
|
485
|
+
users?: TLUserStore
|
|
486
|
+
|
|
469
487
|
/**
|
|
470
488
|
* Handler for receiving custom messages sent through the multiplayer connection.
|
|
471
489
|
*
|
|
@@ -525,7 +543,7 @@ export interface UseSyncOptionsBase {
|
|
|
525
543
|
* }
|
|
526
544
|
* ```
|
|
527
545
|
*/
|
|
528
|
-
getUserPresence?(store: TLStore, user:
|
|
546
|
+
getUserPresence?(store: TLStore, user: TLUser): TLPresenceStateInfo | null
|
|
529
547
|
}
|
|
530
548
|
|
|
531
549
|
/** @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
|
/**
|