@tldraw/sync-core 5.3.0-internal.d205573d66cb → 5.3.0-next.299378752aaf

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.
Files changed (50) hide show
  1. package/dist-cjs/index.d.ts +3 -76
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/InMemorySyncStorage.js +20 -55
  5. package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
  6. package/dist-cjs/lib/RoomSession.js.map +1 -1
  7. package/dist-cjs/lib/SQLiteSyncStorage.js +52 -115
  8. package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
  9. package/dist-cjs/lib/TLSocketRoom.js +2 -26
  10. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  11. package/dist-cjs/lib/TLSyncClient.js +1 -6
  12. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  13. package/dist-cjs/lib/TLSyncRoom.js +5 -43
  14. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  15. package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
  16. package/dist-cjs/lib/protocol.js.map +2 -2
  17. package/dist-esm/index.d.mts +3 -76
  18. package/dist-esm/index.mjs +1 -1
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/InMemorySyncStorage.mjs +20 -55
  21. package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
  22. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  23. package/dist-esm/lib/SQLiteSyncStorage.mjs +52 -115
  24. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
  25. package/dist-esm/lib/TLSocketRoom.mjs +2 -26
  26. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  27. package/dist-esm/lib/TLSyncClient.mjs +1 -6
  28. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  29. package/dist-esm/lib/TLSyncRoom.mjs +5 -43
  30. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  31. package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
  32. package/dist-esm/lib/protocol.mjs.map +2 -2
  33. package/package.json +6 -6
  34. package/src/index.ts +0 -1
  35. package/src/lib/InMemorySyncStorage.ts +21 -74
  36. package/src/lib/RoomSession.ts +2 -7
  37. package/src/lib/SQLiteSyncStorage.test.ts +2 -29
  38. package/src/lib/SQLiteSyncStorage.ts +59 -158
  39. package/src/lib/TLSocketRoom.ts +2 -53
  40. package/src/lib/TLSyncClient.test.ts +2 -9
  41. package/src/lib/TLSyncClient.ts +3 -15
  42. package/src/lib/TLSyncRoom.ts +4 -76
  43. package/src/lib/TLSyncStorage.ts +0 -8
  44. package/src/lib/protocol.ts +0 -17
  45. package/src/test/TLSocketRoom.test.ts +2 -37
  46. package/src/test/TLSyncRoom.test.ts +0 -25
  47. package/src/test/TestServer.ts +4 -14
  48. package/src/test/storageContractSuite.ts +0 -79
  49. package/src/test/upgradeDowngrade.test.ts +0 -47
  50. package/src/test/objectStore.test.ts +0 -387
@@ -3,7 +3,7 @@ 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 { TLObjectStoreAccess, TLSocketServerSentEvent } from './protocol'
6
+ import { TLSocketServerSentEvent } from './protocol'
7
7
  import { RoomSessionState } from './RoomSession'
8
8
  import { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'
9
9
  import { TLSyncErrorCloseEventReason } from './TLSyncClient'
@@ -11,7 +11,6 @@ import { RoomSnapshot, TLSyncRoom } from './TLSyncRoom'
11
11
  import {
12
12
  convertStoreSnapshotToRoomSnapshot,
13
13
  loadSnapshotIntoStorage,
14
- TLSyncForwardDiff,
15
14
  TLSyncStorage,
16
15
  } from './TLSyncStorage'
17
16
 
@@ -72,11 +71,6 @@ export interface TLSyncLog {
72
71
  export interface SessionStateSnapshot {
73
72
  serializedSchema: SerializedSchema
74
73
  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
80
74
  presenceId: string | null
81
75
  presenceRecord: UnknownRecord | null
82
76
  requiresLegacyRejection: boolean
@@ -126,24 +120,6 @@ export interface TLSocketRoomOptions<R extends UnknownRecord, SessionMeta> {
126
120
  }) => void
127
121
  /** @internal */
128
122
  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
123
  /**
148
124
  * When set, the room will call {@link TLSocketRoom.getSessionSnapshot} after
149
125
  * no message activity for a session for 5s and pass the result to this callback.
@@ -254,8 +230,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
254
230
  // eslint-disable-next-line @typescript-eslint/no-deprecated
255
231
  opts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT
256
232
  ),
257
- // keep the storage partition in step with the room's object lane
258
- objectTypes: opts.objectTypes,
259
233
  })
260
234
 
261
235
  // eslint-disable-next-line @typescript-eslint/no-deprecated
@@ -269,8 +243,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
269
243
  }
270
244
  this.room = new TLSyncRoom<R, SessionMeta>({
271
245
  onPresenceChange: opts.onPresenceChange,
272
- onCommittedChanges: opts.onCommittedChanges,
273
- objectTypes: opts.objectTypes,
274
246
  schema: opts.schema ?? (createTLSchema() as any),
275
247
  log: opts.log,
276
248
  storage,
@@ -311,9 +283,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
311
283
  * - sessionId - Unique identifier for the client session (typically from browser tab)
312
284
  * - socket - WebSocket-like object for client communication
313
285
  * - isReadonly - Whether the client can modify the document (defaults to false)
314
- * - objectAccess - Write access for object-store lane record types (defaults to 'write').
315
- * Independent of isReadonly, so a session can be allowed to write objects (e.g. comments)
316
- * without being allowed to edit the document.
317
286
  * - meta - Additional session metadata (required if SessionMeta is not void)
318
287
  *
319
288
  * @example
@@ -341,10 +310,9 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
341
310
  sessionId: string
342
311
  socket: WebSocketMinimal
343
312
  isReadonly?: boolean
344
- objectAccess?: TLObjectStoreAccess
345
313
  } & (SessionMeta extends void ? object : { meta: SessionMeta })
346
314
  ) {
347
- const { sessionId, socket, isReadonly = false, objectAccess = 'write' } = opts
315
+ const { sessionId, socket, isReadonly = false } = opts
348
316
  const handleSocketMessage = (event: MessageEvent) =>
349
317
  this.handleSocketMessage(sessionId, event.data)
350
318
  const handleSocketError = this.handleSocketError.bind(this, sessionId)
@@ -363,7 +331,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
363
331
  this.room.handleNewSession({
364
332
  sessionId,
365
333
  isReadonly,
366
- objectAccess,
367
334
  socket: new ServerSocketAdapter({
368
335
  ws: socket,
369
336
  onBeforeSendMessage: this.opts.onBeforeSendMessage
@@ -573,9 +540,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
573
540
  this.room.handleResumedSession({
574
541
  sessionId,
575
542
  isReadonly: snapshot.isReadonly,
576
- // snapshots persisted before this field existed fail closed — those sessions predate
577
- // the record types gated by objectAccess, so they have nothing to write anyway
578
- objectAccess: snapshot.objectAccess ?? 'read',
579
543
  serializedSchema: snapshot.serializedSchema,
580
544
  presenceId: snapshot.presenceId,
581
545
  presenceRecord: snapshot.presenceRecord,
@@ -631,7 +595,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
631
595
  return {
632
596
  serializedSchema: session.serializedSchema,
633
597
  isReadonly: session.isReadonly,
634
- objectAccess: session.objectAccess,
635
598
  presenceId: session.presenceId,
636
599
  presenceRecord,
637
600
  requiresLegacyRejection: session.requiresLegacyRejection,
@@ -707,7 +670,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
707
670
  sessionId: string
708
671
  isConnected: boolean
709
672
  isReadonly: boolean
710
- objectAccess: TLObjectStoreAccess
711
673
  meta: SessionMeta
712
674
  }> {
713
675
  return [...this.room.sessions.values()].map((session) => {
@@ -715,7 +677,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
715
677
  sessionId: session.sessionId,
716
678
  isConnected: session.state === RoomSessionState.Connected,
717
679
  isReadonly: session.isReadonly,
718
- objectAccess: session.objectAccess,
719
680
  meta: session.meta,
720
681
  }
721
682
  })
@@ -747,18 +708,6 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
747
708
  throw new Error('getCurrentSnapshot is not supported for this storage type')
748
709
  }
749
710
 
750
- /**
751
- * Returns a snapshot of the object-store lane (see `objectTypes`), for persisting
752
- * object-lane records separately from the document. Same entry shape as
753
- * {@link RoomSnapshot.documents}.
754
- */
755
- getCurrentObjectsSnapshot(): RoomSnapshot['documents'] {
756
- if (this.storage.getObjectsSnapshot) {
757
- return this.storage.getObjectsSnapshot()
758
- }
759
- throw new Error('getCurrentObjectsSnapshot is not supported for this storage type')
760
- }
761
-
762
711
  /**
763
712
  * Retrieves all presence records from the document store. Presence records
764
713
  * contain ephemeral user state like cursor positions and selections.
@@ -476,20 +476,13 @@ describe('TLSyncClient', () => {
476
476
  client = createClient()
477
477
  socket.mockServerMessage(createConnectMessage({ isReadonly: false }))
478
478
  expect(client.isConnectedToRoom).toBe(true)
479
- // objectAccess defaults to 'write' when the server doesn't send it
480
- expect(onAfterConnect).toHaveBeenCalledWith(client, {
481
- isReadonly: false,
482
- objectAccess: 'write',
483
- })
479
+ expect(onAfterConnect).toHaveBeenCalledWith(client, { isReadonly: false })
484
480
 
485
481
  // reconnect as readonly
486
482
  socket.mockConnectionStatus('offline')
487
483
  socket.mockConnectionStatus('online')
488
484
  socket.mockServerMessage(createConnectMessage({ isReadonly: true }))
489
- expect(onAfterConnect).toHaveBeenLastCalledWith(client, {
490
- isReadonly: true,
491
- objectAccess: 'write',
492
- })
485
+ expect(onAfterConnect).toHaveBeenLastCalledWith(client, { isReadonly: true })
493
486
  })
494
487
 
495
488
  it('[CL7] pushes the current presence state after connecting', () => {
@@ -24,7 +24,6 @@ import {
24
24
  } from './diff'
25
25
  import { interval } from './interval'
26
26
  import {
27
- TLObjectStoreAccess,
28
27
  TLPushRequest,
29
28
  TLSocketClientSentEvent,
30
29
  TLSocketServerSentDataEvent,
@@ -438,13 +437,8 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
438
437
  * @param self - The TLSyncClient instance that connected
439
438
  * @param details - Connection details
440
439
  * - 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)
443
440
  */
444
- private readonly onAfterConnect?: (
445
- self: this,
446
- details: { isReadonly: boolean; objectAccess: TLObjectStoreAccess }
447
- ) => void
441
+ private readonly onAfterConnect?: (self: this, details: { isReadonly: boolean }) => void
448
442
 
449
443
  private readonly onCustomMessageReceived?: TLCustomMessageHandler
450
444
 
@@ -484,10 +478,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
484
478
  onLoad(self: TLSyncClient<R, S>): void
485
479
  onSyncError(reason: string): void
486
480
  onCustomMessageReceived?: TLCustomMessageHandler
487
- onAfterConnect?(
488
- self: TLSyncClient<R, S>,
489
- details: { isReadonly: boolean; objectAccess: TLObjectStoreAccess }
490
- ): void
481
+ onAfterConnect?(self: TLSyncClient<R, S>, details: { isReadonly: boolean }): void
491
482
  didCancel?(): boolean
492
483
  }) {
493
484
  this.didCancel = config.didCancel
@@ -766,10 +757,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
766
757
  // this.isConnectedToRoom = true
767
758
  // this.store.applyDiff(stashedChanges, false)
768
759
 
769
- this.onAfterConnect?.(this, {
770
- isReadonly: event.isReadonly,
771
- objectAccess: event.objectAccess ?? 'write',
772
- })
760
+ this.onAfterConnect?.(this, { isReadonly: event.isReadonly })
773
761
  const presence = this.presenceState?.get()
774
762
  if (presence) {
775
763
  this.pushPresence(presence)
@@ -31,7 +31,6 @@ import { interval } from './interval'
31
31
  import {
32
32
  getTlsyncProtocolVersion,
33
33
  TLIncompatibilityReason,
34
- TLObjectStoreAccess,
35
34
  TLSocketClientSentEvent,
36
35
  TLSocketServerSentDataEvent,
37
36
  TLSocketServerSentEvent,
@@ -237,43 +236,22 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
237
236
  readonly serializedSchema: SerializedSchema
238
237
 
239
238
  readonly documentTypes: Set<string>
240
- /**
241
- * Record types served by the object-store lane. Object records ride the same wire messages
242
- * as document records but are gated by the session's `objectAccess` instead of `isReadonly`,
243
- * and are excluded from `documentTypes` so hosts can persist them in a separate lane.
244
- */
245
- readonly objectTypes: Set<string>
246
239
  readonly presenceType: RecordType<R, any> | null
247
240
  private log?: TLSyncLog
248
241
  public readonly schema: StoreSchema<R, any>
249
242
  private onPresenceChange?(): void
250
- private onCommittedChanges?(args: { diff: TLSyncForwardDiff<R>; documentClock: number }): void
251
243
  private readonly sessionIdleTimeout: number
252
244
 
253
245
  constructor(opts: {
254
246
  log?: TLSyncLog
255
247
  schema: StoreSchema<R, any>
256
248
  onPresenceChange?(): void
257
- /**
258
- * Called once after a client push commits, with the committed document diff. Fires for
259
- * local and remote pushes. Use this to react to document changes (e.g. persist certain
260
- * record types to a separate lane, or project them to an external store) as soon as they
261
- * commit. Best-effort — do not throw; do not block.
262
- */
263
- onCommittedChanges?(args: { diff: TLSyncForwardDiff<R>; documentClock: number }): void
264
- /**
265
- * Record type names to serve through the object-store lane instead of the document lane.
266
- * Each must be a document-scoped type registered in the schema. Object-lane writes are
267
- * gated per session by `objectAccess` rather than `isReadonly`.
268
- */
269
- objectTypes?: readonly string[]
270
249
  storage: TLSyncStorage<R>
271
250
  clientTimeout?: number
272
251
  }) {
273
252
  this.schema = opts.schema
274
253
  this.log = opts.log
275
254
  this.onPresenceChange = opts.onPresenceChange
276
- this.onCommittedChanges = opts.onCommittedChanges
277
255
  this.storage = opts.storage
278
256
  this.sessionIdleTimeout = opts.clientTimeout ?? SESSION_IDLE_TIMEOUT
279
257
 
@@ -286,20 +264,9 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
286
264
  // do a json serialization cycle to make sure the schema has no 'undefined' values
287
265
  this.serializedSchema = JSON.parse(JSON.stringify(this.schema.serialize()))
288
266
 
289
- this.objectTypes = new Set(opts.objectTypes ?? [])
290
- for (const typeName of this.objectTypes) {
291
- const type = getOwnProperty(this.schema.types, typeName)
292
- assert(type, `TLSyncRoom: object type '${typeName}' is not registered in the schema`)
293
- assert(
294
- type.scope === 'document',
295
- `TLSyncRoom: object type '${typeName}' must have scope 'document', got '${type.scope}'`
296
- )
297
- }
298
-
299
- // object-lane types are partitioned out of the document lane
300
267
  this.documentTypes = new Set(
301
268
  Object.values<RecordType<R, any>>(this.schema.types)
302
- .filter((t) => t.scope === 'document' && !this.objectTypes.has(t.typeName))
269
+ .filter((t) => t.scope === 'document')
303
270
  .map((t) => t.typeName)
304
271
  )
305
272
 
@@ -475,7 +442,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
475
442
  cancellationTime: Date.now(),
476
443
  meta: session.meta,
477
444
  isReadonly: session.isReadonly,
478
- objectAccess: session.objectAccess,
479
445
  requiresLegacyRejection: session.requiresLegacyRejection,
480
446
  supportsStringAppend: session.supportsStringAppend,
481
447
  })
@@ -586,9 +552,8 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
586
552
  socket: TLRoomSocket<R>
587
553
  meta: SessionMeta
588
554
  isReadonly: boolean
589
- objectAccess?: TLObjectStoreAccess
590
555
  }) {
591
- const { sessionId, socket, meta, isReadonly, objectAccess } = opts
556
+ const { sessionId, socket, meta, isReadonly } = opts
592
557
  const existing = this.sessions.get(sessionId)
593
558
  this.sessions.set(sessionId, {
594
559
  state: RoomSessionState.AwaitingConnectMessage,
@@ -598,7 +563,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
598
563
  sessionStartTime: Date.now(),
599
564
  meta,
600
565
  isReadonly: isReadonly ?? false,
601
- objectAccess: objectAccess ?? 'write',
602
566
  // this gets set later during handleConnectMessage
603
567
  requiresLegacyRejection: false,
604
568
  supportsStringAppend: true,
@@ -618,7 +582,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
618
582
  socket: TLRoomSocket<R>
619
583
  meta: SessionMeta
620
584
  isReadonly: boolean
621
- objectAccess?: TLObjectStoreAccess
622
585
  serializedSchema: SerializedSchema
623
586
  presenceId: string | null
624
587
  presenceRecord: UnknownRecord | null
@@ -630,7 +593,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
630
593
  socket,
631
594
  meta,
632
595
  isReadonly,
633
- objectAccess,
634
596
  serializedSchema,
635
597
  presenceId,
636
598
  presenceRecord,
@@ -653,7 +615,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
653
615
  outstandingDataMessages: [],
654
616
  meta,
655
617
  isReadonly,
656
- objectAccess: objectAccess ?? 'write',
657
618
  requiresLegacyRejection,
658
619
  supportsStringAppend,
659
620
  })
@@ -966,7 +927,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
966
927
  supportsStringAppend: session.supportsStringAppend,
967
928
  meta: session.meta,
968
929
  isReadonly: session.isReadonly,
969
- objectAccess: session.objectAccess,
970
930
  requiresLegacyRejection: session.requiresLegacyRejection,
971
931
  })
972
932
  this._unsafe_sendMessage(session.sessionId, msg)
@@ -1017,7 +977,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
1017
977
  serverClock: txn.getClock(),
1018
978
  diff: { ...presenceDiff.value, ...docDiff },
1019
979
  isReadonly: session.isReadonly,
1020
- objectAccess: session.objectAccess,
1021
980
  } satisfies Extract<TLSocketServerSentEvent<R>, { type: 'connect' }>
1022
981
  }) // no id needed because this only reads, no writes.
1023
982
 
@@ -1199,29 +1158,14 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
1199
1158
  }
1200
1159
  }
1201
1160
  }
1202
- // Per-op write gate: document-lane records are gated by `isReadonly`, object-lane
1203
- // records by `objectAccess`. Denied ops are skipped (the client is corrected by the
1204
- // resulting discard/rebase push_result, exactly as whole-diff readonly skips were).
1205
- // Server-initiated pushes (no session) are always allowed.
1206
- const canWrite = (typeName: string) =>
1207
- !session ||
1208
- (this.objectTypes.has(typeName) ? session.objectAccess !== 'read' : !session.isReadonly)
1209
-
1210
- if (message.diff) {
1161
+ if (message.diff && !session?.isReadonly) {
1211
1162
  // The push request was for the document scope.
1212
1163
  for (const [id, op] of objectMapEntriesIterable(message.diff!)) {
1213
1164
  switch (op[0]) {
1214
1165
  case RecordOpType.Put: {
1215
- // The write gate is checked before type validation so that a denied
1216
- // session's ops are skipped without rejection, matching the previous
1217
- // whole-diff readonly behavior.
1218
- if (!canWrite(op[1].typeName)) continue
1219
1166
  // Try to add the document.
1220
1167
  // If we're putting a record with a type that we don't recognize, fail
1221
- if (
1222
- !this.documentTypes.has(op[1].typeName) &&
1223
- !this.objectTypes.has(op[1].typeName)
1224
- ) {
1168
+ if (!this.documentTypes.has(op[1].typeName)) {
1225
1169
  throw new TLSyncError(
1226
1170
  'invalid record',
1227
1171
  TLSyncErrorCloseEventReason.INVALID_RECORD
@@ -1231,10 +1175,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
1231
1175
  break
1232
1176
  }
1233
1177
  case RecordOpType.Patch: {
1234
- const doc = txn.get(id) as R | undefined
1235
- // if it was already deleted, there's no need to apply the patch
1236
- if (!doc) continue
1237
- if (!canWrite(doc.typeName)) continue
1238
1178
  // Try to patch the document. If it fails, stop here.
1239
1179
  patchDocument(txn, docChanges, id, op[1])
1240
1180
  break
@@ -1245,7 +1185,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
1245
1185
  // If the doc was already deleted, don't do anything, no need to propagate a delete op
1246
1186
  continue
1247
1187
  }
1248
- if (!canWrite(doc.typeName)) continue
1249
1188
 
1250
1189
  // Delete the document and propagate the delete op
1251
1190
  // delete automatically creates tombstones
@@ -1333,17 +1272,6 @@ export class TLSyncRoom<R extends UnknownRecord, SessionMeta> {
1333
1272
  this.onPresenceChange?.()
1334
1273
  })
1335
1274
  }
1336
-
1337
- if (result.docChanges.diffs && this.onCommittedChanges) {
1338
- const diff = result.docChanges.diffs.diff
1339
- queueMicrotask(() => {
1340
- try {
1341
- this.onCommittedChanges?.({ diff, documentClock })
1342
- } catch (e) {
1343
- this.log?.error?.('onCommittedChanges threw', e)
1344
- }
1345
- })
1346
- }
1347
1275
  }
1348
1276
 
1349
1277
  /**
@@ -83,15 +83,7 @@ export interface TLSyncStorage<R extends UnknownRecord> {
83
83
 
84
84
  onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void
85
85
 
86
- /** A snapshot of the document lane only — never contains object-store lane records. */
87
86
  getSnapshot?(): RoomSnapshot
88
-
89
- /**
90
- * A snapshot of the object-store lane (see `objectTypes` on the storage), for hosts that
91
- * persist object-lane records separately from the document. Same entry shape as
92
- * `RoomSnapshot['documents']` so a lane can be persisted and re-seeded via a merged snapshot.
93
- */
94
- getObjectsSnapshot?(): RoomSnapshot['documents']
95
87
  }
96
88
 
97
89
  /**
@@ -24,17 +24,6 @@ export function getTlsyncProtocolVersion() {
24
24
  return TLSYNC_PROTOCOL_VERSION
25
25
  }
26
26
 
27
- /**
28
- * Access level for object-store lane record types (see `objectTypes` on the room).
29
- *
30
- * Object-lane records (e.g. comments) are gated by this per-session value instead of the
31
- * document-lane `isReadonly` flag, so a session can be allowed to write objects without
32
- * being allowed to edit the document ("can comment but not edit"), or vice versa.
33
- *
34
- * @public
35
- */
36
- export type TLObjectStoreAccess = 'read' | 'write'
37
-
38
27
  /**
39
28
  * Constants defining the different types of protocol incompatibility reasons.
40
29
  *
@@ -119,12 +108,6 @@ export type TLSocketServerSentEvent<R extends UnknownRecord> =
119
108
  diff: NetworkDiff<R>
120
109
  serverClock: number
121
110
  isReadonly: boolean
122
- /**
123
- * Write access for object-store lane record types, when the room has an object lane.
124
- * Optional and additive: servers without an object lane (or older servers) omit it, and
125
- * older clients ignore it — no protocol version bump needed.
126
- */
127
- objectAccess?: TLObjectStoreAccess
128
111
  }
129
112
  | {
130
113
  type: 'incompatibility_error'
@@ -48,13 +48,8 @@ function createMockSocket(overrides: Partial<WebSocketMinimal> = {}): WebSocketM
48
48
  }
49
49
 
50
50
  // Connect a session and complete the connect handshake
51
- function connectSession(
52
- room: TLSocketRoom<any, any>,
53
- sessionId: string,
54
- socket: WebSocketMinimal,
55
- opts: { objectAccess?: 'read' | 'write' } = {}
56
- ) {
57
- room.handleSocketConnect({ sessionId, socket, ...opts })
51
+ function connectSession(room: TLSocketRoom<any, any>, sessionId: string, socket: WebSocketMinimal) {
52
+ room.handleSocketConnect({ sessionId, socket })
58
53
  const connectRequest = {
59
54
  type: 'connect' as const,
60
55
  connectRequestId: `connect-${sessionId}`,
@@ -1002,41 +997,11 @@ describe('28. TLSocketRoom (SR)', () => {
1002
997
  expect(snapshot).not.toBeNull()
1003
998
  expect(snapshot!.serializedSchema).toBeDefined()
1004
999
  expect(snapshot!.isReadonly).toBe(false)
1005
- expect(snapshot!.objectAccess).toBe('write')
1006
1000
  expect(snapshot!.presenceId).toBeDefined()
1007
1001
  expect(snapshot!.requiresLegacyRejection).toBe(false)
1008
1002
  expect(snapshot!.supportsStringAppend).toBe(true)
1009
1003
  })
1010
1004
 
1011
- it('[SR13] round-trips objectAccess through snapshot and resume', () => {
1012
- const room = new TLSocketRoom({})
1013
- const socket = createMockSocket()
1014
- connectSession(room, 'test', socket, { objectAccess: 'read' })
1015
-
1016
- const snapshot = room.getSessionSnapshot('test')!
1017
- expect(snapshot.objectAccess).toBe('read')
1018
-
1019
- // Simulate hibernation: resume in a new room
1020
- const room2 = new TLSocketRoom({})
1021
- room2.handleSocketResume({ sessionId: 'test', socket: createMockSocket(), snapshot })
1022
- expect(room2.getSessions()[0].objectAccess).toBe('read')
1023
- })
1024
-
1025
- it('[SR13] resumes legacy snapshots without objectAccess as read (fail closed)', () => {
1026
- const room = new TLSocketRoom({})
1027
- const socket = createMockSocket()
1028
- connectSession(room, 'test', socket)
1029
-
1030
- const snapshot = room.getSessionSnapshot('test')!
1031
- // simulate a snapshot persisted before the objectAccess field existed
1032
- delete snapshot.objectAccess
1033
-
1034
- const room2 = new TLSocketRoom({})
1035
- room2.handleSocketResume({ sessionId: 'test', socket: createMockSocket(), snapshot })
1036
- // such sessions predate the record types gated by objectAccess, so they have nothing to write anyway
1037
- expect(room2.getSessions()[0].objectAccess).toBe('read')
1038
- })
1039
-
1040
1005
  it('[SR13] includes presence record when present', () => {
1041
1006
  const store = getStore()
1042
1007
  store.ensureStoreIsUsable()
@@ -188,7 +188,6 @@ function makeRoom(
188
188
  clientTimeout?: number
189
189
  log?: { warn?: Mock; error?: Mock }
190
190
  onPresenceChange?(): void
191
- onCommittedChanges?(args: { diff: any; documentClock: number }): void
192
191
  } = {}
193
192
  ) {
194
193
  const storage = new InMemorySyncStorage<TLRecord>({
@@ -200,7 +199,6 @@ function makeRoom(
200
199
  clientTimeout: opts.clientTimeout,
201
200
  log: opts.log,
202
201
  onPresenceChange: opts.onPresenceChange,
203
- onCommittedChanges: opts.onCommittedChanges,
204
202
  })
205
203
  disposables.push(() => room.close())
206
204
  return { storage, room }
@@ -636,29 +634,6 @@ describe('22. Room construction (RC)', () => {
636
634
  expect(clientBMessages[0].data[0].type).toBe('patch')
637
635
  })
638
636
 
639
- it('fires onCommittedChanges once with the committed document diff after a push', async () => {
640
- const onCommittedChanges = vi.fn()
641
- const { room } = makeRoom({ onCommittedChanges })
642
- connectSession(room, 'session-a')
643
-
644
- const newPage = makePage('committed_page', 'Committed Page')
645
- room.handleMessage('session-a', {
646
- type: 'push',
647
- clientClock: 1,
648
- diff: {
649
- [newPage.id]: ['put', newPage],
650
- },
651
- } as TLPushRequest<TLRecord>)
652
-
653
- // the tap fires in a microtask, like onPresenceChange
654
- await Promise.resolve()
655
-
656
- expect(onCommittedChanges).toHaveBeenCalledTimes(1)
657
- const arg = onCommittedChanges.mock.calls[0][0]
658
- expect(arg.diff.puts[newPage.id]).toBeTruthy()
659
- expect(typeof arg.documentClock).toBe('number')
660
- })
661
-
662
637
  it('[RC4] handles multiple rapid external changes', async () => {
663
638
  const { room, storage } = makeRoom()
664
639
  const socket = connectSession(room, 'test-session')
@@ -1,18 +1,12 @@
1
1
  import { StoreSchema, UnknownRecord } from '@tldraw/store'
2
2
  import { InMemorySyncStorage } from '../lib/InMemorySyncStorage'
3
- import { TLObjectStoreAccess } from '../lib/protocol'
4
3
  import { RoomSnapshot, TLSyncRoom } from '../lib/TLSyncRoom'
5
4
  import { TestSocketPair } from './TestSocketPair'
6
5
 
7
- type TestRoomOptions<R extends UnknownRecord> = Omit<
8
- ConstructorParameters<typeof TLSyncRoom<R, undefined>>[0],
9
- 'schema' | 'storage'
10
- >
11
-
12
6
  export class TestServer<R extends UnknownRecord, P = unknown> {
13
7
  room: TLSyncRoom<R, undefined>
14
8
  storage: InMemorySyncStorage<R>
15
- constructor(schema: StoreSchema<R, P>, snapshot?: RoomSnapshot, roomOpts?: TestRoomOptions<R>) {
9
+ constructor(schema: StoreSchema<R, P>, snapshot?: RoomSnapshot) {
16
10
  // Use provided snapshot or create an empty one with the current schema
17
11
  this.storage = new InMemorySyncStorage<R>({
18
12
  snapshot: snapshot ?? {
@@ -22,19 +16,15 @@ export class TestServer<R extends UnknownRecord, P = unknown> {
22
16
  schema: schema.serialize(),
23
17
  },
24
18
  })
25
- this.room = new TLSyncRoom<R, undefined>({ schema, storage: this.storage, ...roomOpts })
19
+ this.room = new TLSyncRoom<R, undefined>({ schema, storage: this.storage })
26
20
  }
27
21
 
28
- connect(
29
- socketPair: TestSocketPair<R>,
30
- opts?: { isReadonly?: boolean; objectAccess?: TLObjectStoreAccess }
31
- ): void {
22
+ connect(socketPair: TestSocketPair<R>): void {
32
23
  this.room.handleNewSession({
33
24
  sessionId: socketPair.id,
34
25
  socket: socketPair.roomSocket,
35
26
  meta: undefined,
36
- isReadonly: opts?.isReadonly ?? false,
37
- objectAccess: opts?.objectAccess,
27
+ isReadonly: false,
38
28
  })
39
29
 
40
30
  socketPair.clientSocket.connectionStatus = 'online'