@tldraw/sync-core 5.3.0-internal.fba91ed55f6c → 5.3.0-next.4123e97459a9

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 -154
  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 -27
  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 +10 -160
  14. package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
  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 -154
  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 -27
  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 +10 -160
  30. package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
  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 -3
  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 +3 -61
  40. package/src/lib/TLSyncClient.test.ts +2 -9
  41. package/src/lib/TLSyncClient.ts +3 -15
  42. package/src/lib/TLSyncRoom.ts +10 -294
  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 +2 -144
  50. package/src/test/objectStore.test.ts +0 -630
@@ -41,7 +41,6 @@ export function makePage(id: string, name = id, index = 'a2') {
41
41
  export interface StorageContractFactory {
42
42
  create(opts?: {
43
43
  snapshot?: RoomSnapshot
44
- objectTypes?: readonly string[]
45
44
  onChange?(arg: TLSyncStorageOnChangeCallbackProps): void
46
45
  }): TLSyncStorage<TLRecord>
47
46
  }
@@ -62,84 +61,6 @@ export function registerStorageContractTests(factory: StorageContractFactory) {
62
61
  expect(snapshot.documents.map((d) => d.lastChangedClock)).toEqual([0, 0])
63
62
  })
64
63
 
65
- describe('object-store lane partition', () => {
66
- // the storage doesn't care about record semantics, so 'page' stands in as a
67
- // generic object-lane type here
68
- const createPartitioned = (snapshot?: RoomSnapshot) =>
69
- factory.create({ snapshot, objectTypes: ['page'] })
70
-
71
- it('partitions object-lane records out of getSnapshot into getObjectsSnapshot', () => {
72
- const storage = createPartitioned(
73
- makeContractSnapshot(contractRecords, { documentClock: 5 })
74
- )
75
- const snapshot = storage.getSnapshot!()
76
- expect(snapshot.documents.map((d) => d.state.typeName)).toEqual(['document'])
77
- // clock + schema are unaffected by the partition
78
- expect(snapshot.documentClock).toBe(5)
79
-
80
- expect(storage.getObjectsSnapshot!().map((d) => d.state.typeName)).toEqual(['page'])
81
- })
82
-
83
- it('returns an empty objects snapshot when no objectTypes are configured', () => {
84
- const storage = create(makeContractSnapshot(contractRecords))
85
- expect(storage.getSnapshot!().documents.length).toBe(2)
86
- expect(storage.getObjectsSnapshot!()).toEqual([])
87
- })
88
-
89
- it('routes writes by type and shares the clock, tombstones, and change feed', () => {
90
- const storage = createPartitioned(makeContractSnapshot(contractRecords))
91
- const newPage = makePage('lane')
92
- storage.transaction((txn) => txn.set(newPage.id, newPage))
93
-
94
- // the write landed in the object partition, not the document snapshot
95
- expect(storage.getSnapshot!().documents.map((d) => d.state.id)).toEqual([TLDOCUMENT_ID])
96
- expect(storage.getObjectsSnapshot!().map((d) => d.state.id)).toContain(newPage.id)
97
-
98
- // reads and the change feed span both partitions
99
- storage.transaction((txn) => {
100
- expect(txn.get(newPage.id)).toEqual(newPage)
101
- expect(txn.getChangesSince(0)!.diff.puts[newPage.id]).toEqual(newPage)
102
- })
103
-
104
- // deleting an object-lane record writes a shared tombstone
105
- const clockBeforeDelete = storage.getClock()
106
- storage.transaction((txn) => txn.delete(newPage.id))
107
- expect(storage.getObjectsSnapshot!().map((d) => d.state.id)).not.toContain(newPage.id)
108
- storage.transaction((txn) => {
109
- expect(txn.getChangesSince(clockBeforeDelete)!.diff.deletes).toContain(newPage.id)
110
- })
111
- })
112
-
113
- it('iterates both partitions so schema migrations cover object-lane records', () => {
114
- const storage = createPartitioned(makeContractSnapshot(contractRecords))
115
- storage.transaction((txn) => {
116
- const ids = Array.from(txn.keys()).sort()
117
- expect(ids).toEqual(contractRecords.map((r) => r.id).sort())
118
- expect(
119
- Array.from(txn.values())
120
- .map((r) => r.id)
121
- .sort()
122
- ).toEqual(ids)
123
- expect(
124
- Array.from(txn.entries())
125
- .map(([id]) => id)
126
- .sort()
127
- ).toEqual(ids)
128
- })
129
- })
130
-
131
- it('round-trips through a merged snapshot (persist lanes separately, re-seed together)', () => {
132
- const storage = createPartitioned(makeContractSnapshot(contractRecords))
133
- const merged = {
134
- ...storage.getSnapshot!(),
135
- documents: [...storage.getSnapshot!().documents, ...storage.getObjectsSnapshot!()],
136
- }
137
- const reseeded = createPartitioned(merged)
138
- expect(reseeded.getSnapshot!().documents.map((d) => d.state.typeName)).toEqual(['document'])
139
- expect(reseeded.getObjectsSnapshot!().map((d) => d.state.typeName)).toEqual(['page'])
140
- })
141
- })
142
-
143
64
  describe('transactions', () => {
144
65
  it('[SS2] returns the callback result along with clock metadata', () => {
145
66
  const storage = create(makeContractSnapshot(contractRecords, { documentClock: 7 }))
@@ -2,7 +2,6 @@ import { computed } from '@tldraw/state'
2
2
  import {
3
3
  BaseRecord,
4
4
  RecordId,
5
- SerializedSchema,
6
5
  Store,
7
6
  StoreSchema,
8
7
  UnknownRecord,
@@ -11,7 +10,6 @@ import {
11
10
  createRecordMigrationSequence,
12
11
  createRecordType,
13
12
  } from '@tldraw/store'
14
- import { commentSchemaRecords, createTLSchema } from '@tldraw/tlschema'
15
13
  import { vi, type Mock } from 'vitest'
16
14
  import { RecordOpType, ValueOpType } from '../lib/diff'
17
15
  import { TLSocketServerSentEvent, getTlsyncProtocolVersion } from '../lib/protocol'
@@ -173,13 +171,8 @@ class TestInstance {
173
171
 
174
172
  hasLoaded = false
175
173
 
176
- constructor(
177
- snapshot?: RoomSnapshot,
178
- oldSchema = schemaV1,
179
- newSchema = schemaV2,
180
- roomOpts?: ConstructorParameters<typeof TestServer<RV2>>[2]
181
- ) {
182
- this.server = new TestServer(newSchema, snapshot, roomOpts)
174
+ constructor(snapshot?: RoomSnapshot, oldSchema = schemaV1, newSchema = schemaV2) {
175
+ this.server = new TestServer(newSchema, snapshot)
183
176
  this.oldSocketPair = new TestSocketPair('test_upgrade_old', this.server)
184
177
  this.newSocketPair = new TestSocketPair('test_upgrade_new', this.server)
185
178
 
@@ -225,47 +218,6 @@ class TestInstance {
225
218
  }
226
219
  }
227
220
 
228
- describe('record types unknown to older clients', () => {
229
- // Mirrors a deploy that adds the comment record types: stale tabs still run a schema
230
- // without them, while the server registers them and serves rooms that may contain them. The
231
- // comment types ship guard migrations (retroactive, no `down`), so the server must reject
232
- // stale sessions with CLIENT_TOO_OLD (prompting a refresh) rather than sending them record
233
- // types their store cannot represent.
234
- const schemaWithComments = createTLSchema({ records: commentSchemaRecords })
235
- const schemaWithoutComments = createTLSchema()
236
-
237
- function connectWith(server: TestServer<any>, schema: SerializedSchema) {
238
- const id = 'stale-tab'
239
- const socket = mockSocket()
240
- server.room.handleNewSession({ sessionId: id, socket, meta: undefined, isReadonly: false })
241
- server.room.handleMessage(id, {
242
- type: 'connect',
243
- connectRequestId: 'test',
244
- lastServerClock: 0,
245
- protocolVersion: getTlsyncProtocolVersion(),
246
- schema,
247
- })
248
- return socket
249
- }
250
-
251
- it('[HS3] rejects sessions whose schema predates the comment types', () => {
252
- const server = new TestServer(schemaWithComments as any, undefined, {
253
- objectTypes: ['comment', 'comment-thread'],
254
- })
255
- const socket = connectWith(server, schemaWithoutComments.serialize())
256
- expect(socket.close).toHaveBeenCalledWith(4099, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)
257
- })
258
-
259
- it('[HS3] accepts sessions whose schema registers the comment types', () => {
260
- const server = new TestServer(schemaWithComments as any, undefined, {
261
- objectTypes: ['comment', 'comment-thread'],
262
- })
263
- const socket = connectWith(server, schemaWithComments.serialize())
264
- expect(socket.close).not.toHaveBeenCalled()
265
- expect((socket.sendMessage as Mock).mock.calls[0][0]).toMatchObject({ type: 'connect' })
266
- })
267
- })
268
-
269
221
  it('[MG2] the server can handle receiving v1 stuff from the client', () => {
270
222
  const t = new TestInstance()
271
223
  t.oldSocketPair.connect()
@@ -616,7 +568,6 @@ describe('when the client is too old', () => {
616
568
  schema: schemaV2.serialize(),
617
569
  serverClock: 10,
618
570
  isReadonly: false,
619
- objectAccess: 'write',
620
571
  } satisfies TLSocketServerSentEvent<RV2>)
621
572
 
622
573
  expect(v1SendMessage).toHaveBeenCalledWith({
@@ -628,7 +579,6 @@ describe('when the client is too old', () => {
628
579
  schema: schemaV2.serialize(),
629
580
  serverClock: 10,
630
581
  isReadonly: false,
631
- objectAccess: 'write',
632
582
  } satisfies TLSocketServerSentEvent<RV2>)
633
583
 
634
584
  v2SendMessage.mockClear()
@@ -1150,7 +1100,6 @@ describe('when the client is the same version', () => {
1150
1100
  schema: schemaV2.serialize(),
1151
1101
  serverClock: 10,
1152
1102
  isReadonly: false,
1153
- objectAccess: 'write',
1154
1103
  } satisfies TLSocketServerSentEvent<RV2>)
1155
1104
 
1156
1105
  expect(bSocket.sendMessage).toHaveBeenCalledWith({
@@ -1162,7 +1111,6 @@ describe('when the client is the same version', () => {
1162
1111
  schema: schemaV2.serialize(),
1163
1112
  serverClock: 10,
1164
1113
  isReadonly: false,
1165
- objectAccess: 'write',
1166
1114
  } satisfies TLSocketServerSentEvent<RV2>)
1167
1115
  ;(aSocket.sendMessage as Mock).mockClear()
1168
1116
  ;(bSocket.sendMessage as Mock).mockClear()
@@ -1237,93 +1185,3 @@ describe('when the client is the same version', () => {
1237
1185
  } satisfies TLSocketServerSentEvent<RV2>)
1238
1186
  })
1239
1187
  })
1240
-
1241
- describe('record authorizers see server-schema records', () => {
1242
- function makeInstance(authorizeUser: (args: any) => any) {
1243
- return new TestInstance(undefined, schemaV1, schemaV2, {
1244
- authorizeRecord: { user: authorizeUser },
1245
- })
1246
- }
1247
-
1248
- it('a put from an old client reaches the authorizer up-migrated', () => {
1249
- const seen: any[] = []
1250
- const t = makeInstance((args: any) => {
1251
- seen.push(structuredClone({ type: args.type, prev: args.prev, next: args.next }))
1252
- return args.next
1253
- })
1254
- t.oldSocketPair.connect()
1255
- t.flush()
1256
-
1257
- const user = UserV1.create({ name: 'bob', age: 10 })
1258
- t.oldClient.store.put([user])
1259
- t.flush()
1260
-
1261
- expect(seen).toHaveLength(1)
1262
- expect(seen[0].type).toBe('create')
1263
- expect(seen[0].next).toMatchObject({ name: 'bob', birthdate: null })
1264
- expect(seen[0].next).not.toHaveProperty('age')
1265
- })
1266
-
1267
- it('a stamp applied on create survives — no migration runs after the authorizer', () => {
1268
- const t = makeInstance((args: any) =>
1269
- args.type === 'create' ? { ...args.next, birthdate: '2001-02-03' } : args.next
1270
- )
1271
- t.oldSocketPair.connect()
1272
- t.flush()
1273
-
1274
- const user = UserV1.create({ name: 'bob', age: 10 })
1275
- t.oldClient.store.put([user])
1276
- t.flush()
1277
-
1278
- expect(t.server.storage.documents.get(user.id)?.state).toMatchObject({
1279
- name: 'bob',
1280
- birthdate: '2001-02-03',
1281
- })
1282
- })
1283
-
1284
- it('a patch from an old client reaches the authorizer as the upgraded committed candidate', () => {
1285
- const seen: any[] = []
1286
- const t = makeInstance((args: any) => {
1287
- seen.push(structuredClone({ type: args.type, prev: args.prev, next: args.next }))
1288
- return args.next
1289
- })
1290
- t.oldSocketPair.connect()
1291
- t.newSocketPair.connect()
1292
- t.flush()
1293
-
1294
- const user = UserV2.create({ name: 'bob', birthdate: '2022-01-09' })
1295
- t.newClient.store.put([user])
1296
- t.flush()
1297
- seen.length = 0
1298
-
1299
- t.oldClient.store.update(user.id as any, (u: any) => ({ ...u, name: 'bobby' }))
1300
- t.flush()
1301
-
1302
- expect(seen).toHaveLength(1)
1303
- expect(seen[0].type).toBe('update')
1304
- expect(seen[0].prev).toMatchObject({ name: 'bob', birthdate: '2022-01-09' })
1305
- // The down→patch→up round-trip through the lossy v1 schema resets birthdate to null.
1306
- // The authorizer must see exactly what will be committed — not a preview mixing the
1307
- // server record with the raw v1 diff (which would still show the old birthdate).
1308
- expect(seen[0].next).toMatchObject({ name: 'bobby', birthdate: null })
1309
- expect(seen[0].next).not.toHaveProperty('age')
1310
- })
1311
-
1312
- it('vetoes based on the server-schema record', () => {
1313
- const t = makeInstance((args: any) =>
1314
- args.type === 'update' && args.next.name === 'forged' ? null : args.next
1315
- )
1316
- t.oldSocketPair.connect()
1317
- t.newSocketPair.connect()
1318
- t.flush()
1319
-
1320
- const user = UserV2.create({ name: 'bob', birthdate: '2022-01-09' })
1321
- t.newClient.store.put([user])
1322
- t.flush()
1323
-
1324
- t.oldClient.store.update(user.id as any, (u: any) => ({ ...u, name: 'forged' }))
1325
- t.flush()
1326
-
1327
- expect(t.server.storage.documents.get(user.id)?.state).toMatchObject({ name: 'bob' })
1328
- })
1329
- })