@tldraw/sync-core 5.3.0-canary.fa3355c81e86 → 5.3.0-internal.1640468db8fd
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 +154 -3
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/InMemorySyncStorage.js +55 -20
- package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/SQLiteSyncStorage.js +115 -52
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +27 -2
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +6 -1
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +160 -10
- package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
- package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
- package/dist-cjs/lib/protocol.js.map +2 -2
- package/dist-esm/index.d.mts +154 -3
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/InMemorySyncStorage.mjs +55 -20
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/SQLiteSyncStorage.mjs +115 -52
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +27 -2
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +6 -1
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +160 -10
- package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
- package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs.map +2 -2
- package/package.json +6 -6
- package/src/index.ts +3 -0
- package/src/lib/InMemorySyncStorage.ts +74 -21
- package/src/lib/RoomSession.ts +7 -2
- package/src/lib/SQLiteSyncStorage.test.ts +29 -2
- package/src/lib/SQLiteSyncStorage.ts +158 -59
- package/src/lib/TLSocketRoom.ts +61 -3
- package/src/lib/TLSyncClient.test.ts +9 -2
- package/src/lib/TLSyncClient.ts +15 -3
- package/src/lib/TLSyncRoom.ts +294 -10
- package/src/lib/TLSyncStorage.ts +8 -0
- package/src/lib/protocol.ts +17 -0
- package/src/test/TLSocketRoom.test.ts +37 -2
- package/src/test/TLSyncRoom.test.ts +25 -0
- package/src/test/TestServer.ts +14 -4
- package/src/test/objectStore.test.ts +630 -0
- package/src/test/storageContractSuite.ts +79 -0
- package/src/test/upgradeDowngrade.test.ts +144 -2
package/src/lib/TLSocketRoom.ts
CHANGED
|
@@ -3,14 +3,15 @@ import { createTLSchema, TLInstancePresence, TLStoreSnapshot } from '@tldraw/tls
|
|
|
3
3
|
import { getOwnProperty, hasOwnProperty, isEqual, structuredClone } from '@tldraw/utils'
|
|
4
4
|
import { JsonChunkAssembler } from './chunk'
|
|
5
5
|
import { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './InMemorySyncStorage'
|
|
6
|
-
import { TLSocketServerSentEvent } from './protocol'
|
|
6
|
+
import { TLObjectStoreAccess, TLSocketServerSentEvent } from './protocol'
|
|
7
7
|
import { RoomSessionState } from './RoomSession'
|
|
8
8
|
import { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'
|
|
9
9
|
import { TLSyncErrorCloseEventReason } from './TLSyncClient'
|
|
10
|
-
import { RoomSnapshot, TLSyncRoom } from './TLSyncRoom'
|
|
10
|
+
import { RoomSnapshot, TLRecordAuthorizers, TLSyncRoom } from './TLSyncRoom'
|
|
11
11
|
import {
|
|
12
12
|
convertStoreSnapshotToRoomSnapshot,
|
|
13
13
|
loadSnapshotIntoStorage,
|
|
14
|
+
TLSyncForwardDiff,
|
|
14
15
|
TLSyncStorage,
|
|
15
16
|
} from './TLSyncStorage'
|
|
16
17
|
|
|
@@ -71,6 +72,11 @@ export interface TLSyncLog {
|
|
|
71
72
|
export interface SessionStateSnapshot {
|
|
72
73
|
serializedSchema: SerializedSchema
|
|
73
74
|
isReadonly: boolean
|
|
75
|
+
/**
|
|
76
|
+
* Write access for the record types listed in `objectTypes`. Optional so snapshots persisted
|
|
77
|
+
* before this field existed resume cleanly (they fail closed to 'read').
|
|
78
|
+
*/
|
|
79
|
+
objectAccess?: TLObjectStoreAccess
|
|
74
80
|
presenceId: string | null
|
|
75
81
|
presenceRecord: UnknownRecord | null
|
|
76
82
|
requiresLegacyRejection: boolean
|
|
@@ -120,6 +126,30 @@ export interface TLSocketRoomOptions<R extends UnknownRecord, SessionMeta> {
|
|
|
120
126
|
}) => void
|
|
121
127
|
/** @internal */
|
|
122
128
|
onPresenceChange?(): void
|
|
129
|
+
/**
|
|
130
|
+
* Called once after a client push commits, with the committed document diff. Use to react to
|
|
131
|
+
* document changes as soon as they commit — e.g. project certain record types to an external
|
|
132
|
+
* store. Best-effort: do not throw and do not block.
|
|
133
|
+
*
|
|
134
|
+
* Only fires for client pushes. Server-initiated changes — `updateStore`, `loadSnapshot`, or
|
|
135
|
+
* writing to the storage directly — do not trigger it, so anything mirroring room state through
|
|
136
|
+
* this callback must handle those paths itself.
|
|
137
|
+
*/
|
|
138
|
+
// eslint-disable-next-line tldraw/method-signature-style
|
|
139
|
+
onCommittedChanges?: (args: { diff: TLSyncForwardDiff<R>; documentClock: number }) => void
|
|
140
|
+
/**
|
|
141
|
+
* Record type names to serve through the object-store lane instead of the document lane.
|
|
142
|
+
* Each must be a document-scoped type registered in the schema. Object-lane writes are gated
|
|
143
|
+
* per session by `objectAccess` (see {@link TLSocketRoom.handleSocketConnect}) rather than
|
|
144
|
+
* `isReadonly`, so e.g. a session can be allowed to comment without being allowed to edit.
|
|
145
|
+
*/
|
|
146
|
+
objectTypes?: readonly string[]
|
|
147
|
+
/**
|
|
148
|
+
* Per-type authorizers for client record writes (create, update, delete): veto writes the
|
|
149
|
+
* session isn't allowed to make, or rewrite the record on create — e.g. force a comment's
|
|
150
|
+
* `authorId` to the signed-in user. See {@link TLRecordAuthorizers}.
|
|
151
|
+
*/
|
|
152
|
+
authorizeRecord?: TLRecordAuthorizers<R, SessionMeta>
|
|
123
153
|
/**
|
|
124
154
|
* When set, the room will call {@link TLSocketRoom.getSessionSnapshot} after
|
|
125
155
|
* no message activity for a session for 5s and pass the result to this callback.
|
|
@@ -230,6 +260,8 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
230
260
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
231
261
|
opts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT
|
|
232
262
|
),
|
|
263
|
+
// keep the storage partition in step with the room's object lane
|
|
264
|
+
objectTypes: opts.objectTypes,
|
|
233
265
|
})
|
|
234
266
|
|
|
235
267
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
@@ -243,6 +275,9 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
243
275
|
}
|
|
244
276
|
this.room = new TLSyncRoom<R, SessionMeta>({
|
|
245
277
|
onPresenceChange: opts.onPresenceChange,
|
|
278
|
+
onCommittedChanges: opts.onCommittedChanges,
|
|
279
|
+
objectTypes: opts.objectTypes,
|
|
280
|
+
authorizeRecord: opts.authorizeRecord,
|
|
246
281
|
schema: opts.schema ?? (createTLSchema() as any),
|
|
247
282
|
log: opts.log,
|
|
248
283
|
storage,
|
|
@@ -283,6 +318,9 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
283
318
|
* - sessionId - Unique identifier for the client session (typically from browser tab)
|
|
284
319
|
* - socket - WebSocket-like object for client communication
|
|
285
320
|
* - isReadonly - Whether the client can modify the document (defaults to false)
|
|
321
|
+
* - objectAccess - Write access for object-store lane record types (defaults to 'write').
|
|
322
|
+
* Independent of isReadonly, so a session can be allowed to write objects (e.g. comments)
|
|
323
|
+
* without being allowed to edit the document.
|
|
286
324
|
* - meta - Additional session metadata (required if SessionMeta is not void)
|
|
287
325
|
*
|
|
288
326
|
* @example
|
|
@@ -310,9 +348,10 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
310
348
|
sessionId: string
|
|
311
349
|
socket: WebSocketMinimal
|
|
312
350
|
isReadonly?: boolean
|
|
351
|
+
objectAccess?: TLObjectStoreAccess
|
|
313
352
|
} & (SessionMeta extends void ? object : { meta: SessionMeta })
|
|
314
353
|
) {
|
|
315
|
-
const { sessionId, socket, isReadonly = false } = opts
|
|
354
|
+
const { sessionId, socket, isReadonly = false, objectAccess = 'write' } = opts
|
|
316
355
|
const handleSocketMessage = (event: MessageEvent) =>
|
|
317
356
|
this.handleSocketMessage(sessionId, event.data)
|
|
318
357
|
const handleSocketError = this.handleSocketError.bind(this, sessionId)
|
|
@@ -331,6 +370,7 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
331
370
|
this.room.handleNewSession({
|
|
332
371
|
sessionId,
|
|
333
372
|
isReadonly,
|
|
373
|
+
objectAccess,
|
|
334
374
|
socket: new ServerSocketAdapter({
|
|
335
375
|
ws: socket,
|
|
336
376
|
onBeforeSendMessage: this.opts.onBeforeSendMessage
|
|
@@ -540,6 +580,9 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
540
580
|
this.room.handleResumedSession({
|
|
541
581
|
sessionId,
|
|
542
582
|
isReadonly: snapshot.isReadonly,
|
|
583
|
+
// snapshots persisted before this field existed fail closed — those sessions predate
|
|
584
|
+
// the record types gated by objectAccess, so they have nothing to write anyway
|
|
585
|
+
objectAccess: snapshot.objectAccess ?? 'read',
|
|
543
586
|
serializedSchema: snapshot.serializedSchema,
|
|
544
587
|
presenceId: snapshot.presenceId,
|
|
545
588
|
presenceRecord: snapshot.presenceRecord,
|
|
@@ -595,6 +638,7 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
595
638
|
return {
|
|
596
639
|
serializedSchema: session.serializedSchema,
|
|
597
640
|
isReadonly: session.isReadonly,
|
|
641
|
+
objectAccess: session.objectAccess,
|
|
598
642
|
presenceId: session.presenceId,
|
|
599
643
|
presenceRecord,
|
|
600
644
|
requiresLegacyRejection: session.requiresLegacyRejection,
|
|
@@ -670,6 +714,7 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
670
714
|
sessionId: string
|
|
671
715
|
isConnected: boolean
|
|
672
716
|
isReadonly: boolean
|
|
717
|
+
objectAccess: TLObjectStoreAccess
|
|
673
718
|
meta: SessionMeta
|
|
674
719
|
}> {
|
|
675
720
|
return [...this.room.sessions.values()].map((session) => {
|
|
@@ -677,6 +722,7 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
677
722
|
sessionId: session.sessionId,
|
|
678
723
|
isConnected: session.state === RoomSessionState.Connected,
|
|
679
724
|
isReadonly: session.isReadonly,
|
|
725
|
+
objectAccess: session.objectAccess,
|
|
680
726
|
meta: session.meta,
|
|
681
727
|
}
|
|
682
728
|
})
|
|
@@ -708,6 +754,18 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
708
754
|
throw new Error('getCurrentSnapshot is not supported for this storage type')
|
|
709
755
|
}
|
|
710
756
|
|
|
757
|
+
/**
|
|
758
|
+
* Returns a snapshot of the object-store lane (see `objectTypes`), for persisting
|
|
759
|
+
* object-lane records separately from the document. Same entry shape as
|
|
760
|
+
* {@link RoomSnapshot.documents}.
|
|
761
|
+
*/
|
|
762
|
+
getCurrentObjectsSnapshot(): RoomSnapshot['documents'] {
|
|
763
|
+
if (this.storage.getObjectsSnapshot) {
|
|
764
|
+
return this.storage.getObjectsSnapshot()
|
|
765
|
+
}
|
|
766
|
+
throw new Error('getCurrentObjectsSnapshot is not supported for this storage type')
|
|
767
|
+
}
|
|
768
|
+
|
|
711
769
|
/**
|
|
712
770
|
* Retrieves all presence records from the document store. Presence records
|
|
713
771
|
* contain ephemeral user state like cursor positions and selections.
|
|
@@ -476,13 +476,20 @@ describe('TLSyncClient', () => {
|
|
|
476
476
|
client = createClient()
|
|
477
477
|
socket.mockServerMessage(createConnectMessage({ isReadonly: false }))
|
|
478
478
|
expect(client.isConnectedToRoom).toBe(true)
|
|
479
|
-
|
|
479
|
+
// objectAccess defaults to 'write' when the server doesn't send it
|
|
480
|
+
expect(onAfterConnect).toHaveBeenCalledWith(client, {
|
|
481
|
+
isReadonly: false,
|
|
482
|
+
objectAccess: 'write',
|
|
483
|
+
})
|
|
480
484
|
|
|
481
485
|
// reconnect as readonly
|
|
482
486
|
socket.mockConnectionStatus('offline')
|
|
483
487
|
socket.mockConnectionStatus('online')
|
|
484
488
|
socket.mockServerMessage(createConnectMessage({ isReadonly: true }))
|
|
485
|
-
expect(onAfterConnect).toHaveBeenLastCalledWith(client, {
|
|
489
|
+
expect(onAfterConnect).toHaveBeenLastCalledWith(client, {
|
|
490
|
+
isReadonly: true,
|
|
491
|
+
objectAccess: 'write',
|
|
492
|
+
})
|
|
486
493
|
})
|
|
487
494
|
|
|
488
495
|
it('[CL7] pushes the current presence state after connecting', () => {
|
package/src/lib/TLSyncClient.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from './diff'
|
|
25
25
|
import { interval } from './interval'
|
|
26
26
|
import {
|
|
27
|
+
TLObjectStoreAccess,
|
|
27
28
|
TLPushRequest,
|
|
28
29
|
TLSocketClientSentEvent,
|
|
29
30
|
TLSocketServerSentDataEvent,
|
|
@@ -437,8 +438,13 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
|
|
437
438
|
* @param self - The TLSyncClient instance that connected
|
|
438
439
|
* @param details - Connection details
|
|
439
440
|
* - isReadonly - Whether the connection is in read-only mode
|
|
441
|
+
* - objectAccess - Write access for object-store lane record types (defaults to 'write'
|
|
442
|
+
* when the server doesn't send it, e.g. older servers or rooms with no object lane)
|
|
440
443
|
*/
|
|
441
|
-
private readonly onAfterConnect?: (
|
|
444
|
+
private readonly onAfterConnect?: (
|
|
445
|
+
self: this,
|
|
446
|
+
details: { isReadonly: boolean; objectAccess: TLObjectStoreAccess }
|
|
447
|
+
) => void
|
|
442
448
|
|
|
443
449
|
private readonly onCustomMessageReceived?: TLCustomMessageHandler
|
|
444
450
|
|
|
@@ -478,7 +484,10 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
|
|
478
484
|
onLoad(self: TLSyncClient<R, S>): void
|
|
479
485
|
onSyncError(reason: string): void
|
|
480
486
|
onCustomMessageReceived?: TLCustomMessageHandler
|
|
481
|
-
onAfterConnect?(
|
|
487
|
+
onAfterConnect?(
|
|
488
|
+
self: TLSyncClient<R, S>,
|
|
489
|
+
details: { isReadonly: boolean; objectAccess: TLObjectStoreAccess }
|
|
490
|
+
): void
|
|
482
491
|
didCancel?(): boolean
|
|
483
492
|
}) {
|
|
484
493
|
this.didCancel = config.didCancel
|
|
@@ -757,7 +766,10 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
|
|
757
766
|
// this.isConnectedToRoom = true
|
|
758
767
|
// this.store.applyDiff(stashedChanges, false)
|
|
759
768
|
|
|
760
|
-
this.onAfterConnect?.(this, {
|
|
769
|
+
this.onAfterConnect?.(this, {
|
|
770
|
+
isReadonly: event.isReadonly,
|
|
771
|
+
objectAccess: event.objectAccess ?? 'write',
|
|
772
|
+
})
|
|
761
773
|
const presence = this.presenceState?.get()
|
|
762
774
|
if (presence) {
|
|
763
775
|
this.pushPresence(presence)
|