@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
@@ -117,14 +117,6 @@ export const DEFAULT_INITIAL_SNAPSHOT = {
117
117
  export class InMemorySyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {
118
118
  /** @internal */
119
119
  documents: AtomMap<string, { state: R; lastChangedClock: number }>
120
- /**
121
- * Object-store lane records, partitioned out of `documents` so they never appear in the
122
- * document snapshot. They share the same clock, tombstones, and transactions as documents.
123
- * @internal
124
- */
125
- objects: AtomMap<string, { state: R; lastChangedClock: number }>
126
- /** @internal */
127
- readonly objectTypes: ReadonlySet<string>
128
120
  /** @internal */
129
121
  tombstones: AtomMap<string, number>
130
122
  /** @internal */
@@ -141,38 +133,22 @@ export class InMemorySyncStorage<R extends UnknownRecord> implements TLSyncStora
141
133
 
142
134
  constructor({
143
135
  snapshot = DEFAULT_INITIAL_SNAPSHOT,
144
- objectTypes,
145
136
  onChange,
146
137
  }: {
147
138
  snapshot?: RoomSnapshot
148
- /**
149
- * Record type names stored in the object-store lane. Records of these types are routed to
150
- * a separate partition: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.
151
- */
152
- objectTypes?: readonly string[]
153
139
  onChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown
154
140
  } = {}) {
155
- this.objectTypes = new Set(objectTypes ?? [])
156
141
  const maxClockValue = Math.max(
157
142
  0,
158
143
  ...Object.values(snapshot.tombstones ?? {}),
159
144
  ...Object.values(snapshot.documents.map((d) => d.lastChangedClock))
160
145
  )
161
- // route snapshot entries into their partitions (a seed snapshot may carry object-lane
162
- // records merged in, e.g. loaded from a separate persistence lane)
163
- const toEntry = (
164
- d: RoomSnapshot['documents'][number]
165
- ): [string, { state: R; lastChangedClock: number }] => [
166
- d.state.id,
167
- { state: devFreeze(d.state) as R, lastChangedClock: d.lastChangedClock },
168
- ]
169
146
  this.documents = new AtomMap(
170
147
  'room documents',
171
- snapshot.documents.filter((d) => !this.objectTypes.has(d.state.typeName)).map(toEntry)
172
- )
173
- this.objects = new AtomMap(
174
- 'room objects',
175
- snapshot.documents.filter((d) => this.objectTypes.has(d.state.typeName)).map(toEntry)
148
+ snapshot.documents.map((d) => [
149
+ d.state.id,
150
+ { state: devFreeze(d.state) as R, lastChangedClock: d.lastChangedClock },
151
+ ])
176
152
  )
177
153
  const documentClock = Math.max(maxClockValue, snapshot.documentClock ?? snapshot.clock ?? 0)
178
154
 
@@ -273,8 +249,6 @@ export class InMemorySyncStorage<R extends UnknownRecord> implements TLSyncStora
273
249
  )
274
250
 
275
251
  getSnapshot(): RoomSnapshot {
276
- // object-lane records live in their own partition, so the document snapshot is
277
- // pure-document by construction
278
252
  return {
279
253
  tombstoneHistoryStartsAtClock: this.tombstoneHistoryStartsAtClock.get(),
280
254
  documentClock: this.documentClock.get(),
@@ -283,10 +257,6 @@ export class InMemorySyncStorage<R extends UnknownRecord> implements TLSyncStora
283
257
  schema: this.schema.get(),
284
258
  }
285
259
  }
286
-
287
- getObjectsSnapshot(): RoomSnapshot['documents'] {
288
- return Array.from(this.objects.values())
289
- }
290
260
  }
291
261
 
292
262
  /**
@@ -327,16 +297,9 @@ class InMemorySyncStorageTransaction<
327
297
  return this._clock
328
298
  }
329
299
 
330
- /** The partition a record with this id currently lives in, if any. */
331
- private mapContaining(id: string) {
332
- if (this.storage.documents.has(id)) return this.storage.documents
333
- if (this.storage.objects.has(id)) return this.storage.objects
334
- return undefined
335
- }
336
-
337
300
  get(id: string): R | undefined {
338
301
  this.assertNotClosed()
339
- return (this.storage.documents.get(id) ?? this.storage.objects.get(id))?.state
302
+ return this.storage.documents.get(id)?.state
340
303
  }
341
304
 
342
305
  set(id: string, record: R): void {
@@ -347,11 +310,7 @@ class InMemorySyncStorageTransaction<
347
310
  if (this.storage.tombstones.has(id)) {
348
311
  this.storage.tombstones.delete(id)
349
312
  }
350
- // route by type: object-lane records live in their own partition
351
- const map = this.storage.objectTypes.has(record.typeName)
352
- ? this.storage.objects
353
- : this.storage.documents
354
- map.set(id, {
313
+ this.storage.documents.set(id, {
355
314
  state: devFreeze(record) as R,
356
315
  lastChangedClock: clock,
357
316
  })
@@ -360,43 +319,34 @@ class InMemorySyncStorageTransaction<
360
319
  delete(id: string): void {
361
320
  this.assertNotClosed()
362
321
  // Only create a tombstone if the record actually exists
363
- const map = this.mapContaining(id)
364
- if (!map) return
322
+ if (!this.storage.documents.has(id)) return
365
323
  const clock = this.getNextClock()
366
- map.delete(id)
324
+ this.storage.documents.delete(id)
367
325
  this.storage.tombstones.set(id, clock)
368
326
  this.storage.pruneTombstones()
369
327
  }
370
328
 
371
- // iteration spans both partitions so schema migrations cover object-lane records too
372
-
373
329
  *entries(): IterableIterator<[string, R]> {
374
330
  this.assertNotClosed()
375
- for (const map of [this.storage.documents, this.storage.objects]) {
376
- for (const [id, record] of map.entries()) {
377
- this.assertNotClosed()
378
- yield [id, record.state]
379
- }
331
+ for (const [id, record] of this.storage.documents.entries()) {
332
+ this.assertNotClosed()
333
+ yield [id, record.state]
380
334
  }
381
335
  }
382
336
 
383
337
  *keys(): IterableIterator<string> {
384
338
  this.assertNotClosed()
385
- for (const map of [this.storage.documents, this.storage.objects]) {
386
- for (const key of map.keys()) {
387
- this.assertNotClosed()
388
- yield key
389
- }
339
+ for (const key of this.storage.documents.keys()) {
340
+ this.assertNotClosed()
341
+ yield key
390
342
  }
391
343
  }
392
344
 
393
345
  *values(): IterableIterator<R> {
394
346
  this.assertNotClosed()
395
- for (const map of [this.storage.documents, this.storage.objects]) {
396
- for (const record of map.values()) {
397
- this.assertNotClosed()
398
- yield record.state
399
- }
347
+ for (const record of this.storage.documents.values()) {
348
+ this.assertNotClosed()
349
+ yield record.state
400
350
  }
401
351
  }
402
352
 
@@ -420,13 +370,10 @@ class InMemorySyncStorageTransaction<
420
370
  }
421
371
  const diff: TLSyncForwardDiff<R> = { puts: {}, deletes: [] }
422
372
  const wipeAll = sinceClock < this.storage.tombstoneHistoryStartsAtClock.get()
423
- // both partitions share the clock and tombstones, so the change feed spans both
424
- for (const map of [this.storage.documents, this.storage.objects]) {
425
- for (const doc of map.values()) {
426
- if (wipeAll || doc.lastChangedClock > sinceClock) {
427
- // For historical changes, we don't have "from" state, so use added
428
- diff.puts[doc.state.id] = doc.state as R
429
- }
373
+ for (const doc of this.storage.documents.values()) {
374
+ if (wipeAll || doc.lastChangedClock > sinceClock) {
375
+ // For historical changes, we don't have "from" state, so use added
376
+ diff.puts[doc.state.id] = doc.state as R
430
377
  }
431
378
  }
432
379
  if (!wipeAll) {
@@ -1,5 +1,5 @@
1
1
  import { SerializedSchema, UnknownRecord } from '@tldraw/store'
2
- import { TLObjectStoreAccess, TLSocketServerSentDataEvent } from './protocol'
2
+ import { TLSocketServerSentDataEvent } from './protocol'
3
3
  import { TLRoomSocket } from './TLSyncRoom'
4
4
 
5
5
  /**
@@ -78,13 +78,8 @@ export interface RoomSessionBase<R extends UnknownRecord, Meta> {
78
78
  socket: TLRoomSocket<R>
79
79
  /** Custom metadata associated with this session */
80
80
  meta: Meta
81
- /** Whether this session has read-only permissions for document-lane records */
81
+ /** Whether this session has read-only permissions */
82
82
  isReadonly: boolean
83
- /**
84
- * Write access for object-store lane record types (see `objectTypes` on the room).
85
- * Deliberately independent of `isReadonly` so "can comment but not edit" is expressible.
86
- */
87
- objectAccess: TLObjectStoreAccess
88
83
  /** Whether this session requires legacy protocol rejection handling */
89
84
  requiresLegacyRejection: boolean
90
85
  /** Whether this session supports string append operations */
@@ -38,33 +38,6 @@ describe('SQLiteSyncStorage', () => {
38
38
  ).toEqual(newPage)
39
39
  })
40
40
 
41
- it('[SQ1] sweeps pre-partition object-lane rows out of the documents table on reopen', () => {
42
- // a database written before the objects table existed: object-lane records (here
43
- // 'page' stands in) live in the documents table
44
- const sql = createWrapper()
45
- const storage = new SQLiteSyncStorage<TLRecord>({
46
- sql,
47
- snapshot: makeContractSnapshot(contractRecords),
48
- })
49
- const extraPage = makePage('legacy')
50
- storage.transaction((txn) => txn.set(extraPage.id, extraPage))
51
- expect(storage.getSnapshot().documents).toHaveLength(3)
52
-
53
- // reopening with objectTypes moves the rows into the objects partition
54
- const reopened = new SQLiteSyncStorage<TLRecord>({ sql, objectTypes: ['page'] })
55
- expect(reopened.getSnapshot().documents.map((d) => d.state.typeName)).toEqual(['document'])
56
- expect(
57
- reopened
58
- .getObjectsSnapshot()
59
- .map((d) => d.state.id)
60
- .sort()
61
- ).toEqual([contractRecords[1].id, extraPage.id].sort())
62
- // lastChangedClock survives the move
63
- expect(
64
- reopened.getObjectsSnapshot().find((d) => d.state.id === extraPage.id)?.lastChangedClock
65
- ).toBe(1)
66
- })
67
-
68
41
  it('[SQ1] honors the table prefix', () => {
69
42
  const sql = createWrapper({ tablePrefix: 'tldraw_' })
70
43
  const storage = new SQLiteSyncStorage<TLRecord>({
@@ -254,13 +227,13 @@ describe('SQLiteSyncStorage', () => {
254
227
  expect(storage.getSnapshot().documents.length).toBe(3)
255
228
  })
256
229
 
257
- it('[SQ6] fresh databases start at migration version 3', () => {
230
+ it('[SQ6] fresh databases start at migration version 2', () => {
258
231
  const sql = createWrapper()
259
232
  new SQLiteSyncStorage<TLRecord>({ sql, snapshot: makeContractSnapshot(contractRecords) })
260
233
  const row = sql
261
234
  .prepare<{ migrationVersion: number }>('SELECT migrationVersion FROM metadata')
262
235
  .all()[0]
263
- expect(row?.migrationVersion).toBe(3)
236
+ expect(row?.migrationVersion).toBe(2)
264
237
  })
265
238
  })
266
239
  })
@@ -86,15 +86,9 @@ export function migrateSqliteSyncStorage(
86
86
  storage: TLSyncSqliteWrapper,
87
87
  {
88
88
  documentsTable = 'documents',
89
- objectsTable = 'objects',
90
89
  tombstonesTable = 'tombstones',
91
90
  metadataTable = 'metadata',
92
- }: {
93
- documentsTable?: string
94
- objectsTable?: string
95
- tombstonesTable?: string
96
- metadataTable?: string
97
- } = {}
91
+ }: { documentsTable?: string; tombstonesTable?: string; metadataTable?: string } = {}
98
92
  ): void {
99
93
  let migrationVersion = 0
100
94
  try {
@@ -150,34 +144,18 @@ export function migrateSqliteSyncStorage(
150
144
  state BLOB NOT NULL,
151
145
  lastChangedClock INTEGER NOT NULL
152
146
  );
153
-
147
+
154
148
  INSERT INTO ${documentsTable}_new (id, state, lastChangedClock)
155
149
  SELECT id, CAST(state AS BLOB), lastChangedClock FROM ${documentsTable};
156
-
150
+
157
151
  DROP TABLE ${documentsTable};
158
-
152
+
159
153
  ALTER TABLE ${documentsTable}_new RENAME TO ${documentsTable};
160
-
154
+
161
155
  CREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);
162
156
  `)
163
157
  }
164
158
 
165
- if (migrationVersion === 2) {
166
- // Migration 3: Add the object-store lane table. Object-lane records (e.g. comments) are
167
- // partitioned out of the documents table so the document snapshot never contains them.
168
- // They share the documents' clock and tombstones.
169
- migrationVersion++
170
- storage.exec(`
171
- CREATE TABLE ${objectsTable} (
172
- id TEXT PRIMARY KEY,
173
- state BLOB NOT NULL,
174
- lastChangedClock INTEGER NOT NULL
175
- );
176
-
177
- CREATE INDEX idx_${objectsTable}_lastChangedClock ON ${objectsTable}(lastChangedClock);
178
- `)
179
- }
180
-
181
159
  // add more migrations here if and when needed
182
160
 
183
161
  storage.exec(`UPDATE ${metadataTable} SET migrationVersion = ${migrationVersion}`)
@@ -272,64 +250,22 @@ export class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage
272
250
 
273
251
  private readonly sql: TLSyncSqliteWrapper
274
252
 
275
- /** @internal */
276
- readonly objectTypes: ReadonlySet<string>
277
-
278
253
  constructor({
279
254
  sql,
280
255
  snapshot,
281
- objectTypes,
282
256
  onChange,
283
257
  }: {
284
258
  sql: TLSyncSqliteWrapper
285
259
  snapshot?: RoomSnapshot | StoreSnapshot<R>
286
- /**
287
- * Record type names stored in the object-store lane. Records of these types are routed to
288
- * a separate table: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.
289
- * They share the documents' clock, tombstones, and transactions.
290
- */
291
- objectTypes?: readonly string[]
292
260
  onChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown
293
261
  }) {
294
262
  this.sql = sql
295
- this.objectTypes = new Set(objectTypes ?? [])
296
263
  const prefix = sql.config?.tablePrefix ?? ''
297
264
  const documentsTable = `${prefix}documents`
298
- const objectsTable = `${prefix}objects`
299
265
  const tombstonesTable = `${prefix}tombstones`
300
266
  const metadataTable = `${prefix}metadata`
301
267
 
302
- migrateSqliteSyncStorage(this.sql, {
303
- documentsTable,
304
- objectsTable,
305
- tombstonesTable,
306
- metadataTable,
307
- })
308
-
309
- // Both record tables have identical shape; prepare the same statements for each.
310
- const makeRecordTableStmts = (table: string) => ({
311
- get: this.sql.prepare<{ state: Uint8Array }, [id: string]>(
312
- `SELECT state FROM ${table} WHERE id = ?`
313
- ),
314
- insert: this.sql.prepare<void, [id: string, state: Uint8Array, lastChangedClock: number]>(
315
- `INSERT OR REPLACE INTO ${table} (id, state, lastChangedClock) VALUES (?, ?, ?)`
316
- ),
317
- delete: this.sql.prepare<void, [id: string]>(`DELETE FROM ${table} WHERE id = ?`),
318
- exists: this.sql.prepare<{ id: string }, [id: string]>(
319
- `SELECT id FROM ${table} WHERE id = ?`
320
- ),
321
- iterate: this.sql.prepare<{ state: Uint8Array; lastChangedClock: number }>(
322
- `SELECT state, lastChangedClock FROM ${table}`
323
- ),
324
- iterateEntries: this.sql.prepare<{ id: string; state: Uint8Array }>(
325
- `SELECT id, state FROM ${table}`
326
- ),
327
- iterateKeys: this.sql.prepare<{ id: string }>(`SELECT id FROM ${table}`),
328
- iterateValues: this.sql.prepare<{ state: Uint8Array }>(`SELECT state FROM ${table}`),
329
- changedSince: this.sql.prepare<{ state: Uint8Array }, [sinceClock: number]>(
330
- `SELECT state FROM ${table} WHERE lastChangedClock > ?`
331
- ),
332
- })
268
+ migrateSqliteSyncStorage(this.sql, { documentsTable, tombstonesTable, metadataTable })
333
269
 
334
270
  // Prepare all statements once
335
271
  this.stmts = {
@@ -349,9 +285,33 @@ export class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage
349
285
  `UPDATE ${metadataTable} SET documentClock = documentClock + 1`
350
286
  ),
351
287
 
352
- // Record tables (document lane + object-store lane)
353
- documents: makeRecordTableStmts(documentsTable),
354
- objects: makeRecordTableStmts(objectsTable),
288
+ // Documents
289
+ getDocument: this.sql.prepare<{ state: Uint8Array }, [id: string]>(
290
+ `SELECT state FROM ${documentsTable} WHERE id = ?`
291
+ ),
292
+ insertDocument: this.sql.prepare<
293
+ void,
294
+ [id: string, state: Uint8Array, lastChangedClock: number]
295
+ >(`INSERT OR REPLACE INTO ${documentsTable} (id, state, lastChangedClock) VALUES (?, ?, ?)`),
296
+ deleteDocument: this.sql.prepare<void, [id: string]>(
297
+ `DELETE FROM ${documentsTable} WHERE id = ?`
298
+ ),
299
+ documentExists: this.sql.prepare<{ id: string }, [id: string]>(
300
+ `SELECT id FROM ${documentsTable} WHERE id = ?`
301
+ ),
302
+ iterateDocuments: this.sql.prepare<{ state: Uint8Array; lastChangedClock: number }>(
303
+ `SELECT state, lastChangedClock FROM ${documentsTable}`
304
+ ),
305
+ iterateDocumentEntries: this.sql.prepare<{ id: string; state: Uint8Array }>(
306
+ `SELECT id, state FROM ${documentsTable}`
307
+ ),
308
+ iterateDocumentKeys: this.sql.prepare<{ id: string }>(`SELECT id FROM ${documentsTable}`),
309
+ iterateDocumentValues: this.sql.prepare<{ state: Uint8Array }>(
310
+ `SELECT state FROM ${documentsTable}`
311
+ ),
312
+ getDocumentsChangedSince: this.sql.prepare<{ state: Uint8Array }, [sinceClock: number]>(
313
+ `SELECT state FROM ${documentsTable} WHERE lastChangedClock > ?`
314
+ ),
355
315
 
356
316
  // Tombstones
357
317
  insertTombstone: this.sql.prepare<void, [id: string, clock: number]>(
@@ -394,17 +354,12 @@ export class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage
394
354
  // Clear existing data
395
355
  this.sql.exec(`
396
356
  DELETE FROM ${documentsTable};
397
- DELETE FROM ${objectsTable};
398
357
  DELETE FROM ${tombstonesTable};
399
358
  `)
400
359
 
401
- // Insert records, routing object-lane types into their partition (a seed snapshot may
402
- // carry object-lane records merged in, e.g. loaded from a separate persistence lane)
360
+ // Insert documents
403
361
  for (const doc of snapshot.documents) {
404
- const table = this.objectTypes.has(doc.state.typeName)
405
- ? this.stmts.objects
406
- : this.stmts.documents
407
- table.insert.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock)
362
+ this.stmts.insertDocument.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock)
408
363
  }
409
364
 
410
365
  // Insert tombstones
@@ -420,21 +375,6 @@ export class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage
420
375
  tombstoneHistoryStartsAtClock,
421
376
  JSON.stringify(snapshot.schema)
422
377
  )
423
- } else {
424
- // One-time sweep: rooms written before the objects table existed may have object-lane
425
- // records sitting in the documents table. Record ids are typeName-prefixed, so a PK
426
- // range scan per object type moves them cheaply (no-op once clean).
427
- for (const typeName of this.objectTypes) {
428
- const lo = `${typeName}:`
429
- // ';' is the next code point after ':' — [lo, hi) covers exactly the `type:` prefix
430
- const hi = `${typeName};`
431
- this.sql.exec(`
432
- INSERT OR REPLACE INTO ${objectsTable} (id, state, lastChangedClock)
433
- SELECT id, state, lastChangedClock FROM ${documentsTable}
434
- WHERE id >= '${lo}' AND id < '${hi}';
435
- DELETE FROM ${documentsTable} WHERE id >= '${lo}' AND id < '${hi}';
436
- `)
437
- }
438
378
  }
439
379
  if (onChange) {
440
380
  this.onChange(onChange)
@@ -530,25 +470,16 @@ export class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage
530
470
  )
531
471
 
532
472
  getSnapshot(): RoomSnapshot {
533
- // object-lane records live in their own table, so the document snapshot is
534
- // pure-document by construction
535
473
  return {
536
474
  tombstoneHistoryStartsAtClock: this._getTombstoneHistoryStartsAtClock(),
537
475
  documentClock: this.getClock(),
538
- documents: Array.from(this._iterateRecords(this.stmts.documents)),
476
+ documents: Array.from(this._iterateDocuments()),
539
477
  tombstones: Object.fromEntries(this._iterateTombstones()),
540
478
  schema: this._getSchema(),
541
479
  }
542
480
  }
543
-
544
- getObjectsSnapshot(): RoomSnapshot['documents'] {
545
- return Array.from(this._iterateRecords(this.stmts.objects))
546
- }
547
-
548
- private *_iterateRecords(
549
- table: SQLiteSyncStorage<R>['stmts']['documents']
550
- ): IterableIterator<{ state: R; lastChangedClock: number }> {
551
- for (const row of table.iterate.iterate()) {
481
+ private *_iterateDocuments(): IterableIterator<{ state: R; lastChangedClock: number }> {
482
+ for (const row of this.stmts.iterateDocuments.iterate()) {
552
483
  yield { state: decodeState<R>(row.state), lastChangedClock: row.lastChangedClock }
553
484
  }
554
485
  }
@@ -600,21 +531,9 @@ class SQLiteSyncStorageTransaction<R extends UnknownRecord> implements TLSyncSto
600
531
  return this._clock
601
532
  }
602
533
 
603
- /**
604
- * Which record table an id belongs to. Record ids are typeName-prefixed (`comment:abc`),
605
- * so the partition is derivable from the id alone.
606
- */
607
- private tableFor(id: string) {
608
- const sep = id.indexOf(':')
609
- if (sep === -1) return this.stmts.documents
610
- return this.storage.objectTypes.has(id.slice(0, sep))
611
- ? this.stmts.objects
612
- : this.stmts.documents
613
- }
614
-
615
534
  get(id: string): R | undefined {
616
535
  this.assertNotClosed()
617
- const row = this.tableFor(id).get.all(id)[0]
536
+ const row = this.stmts.getDocument.all(id)[0]
618
537
  if (!row) return undefined
619
538
  return decodeState<R>(row.state)
620
539
  }
@@ -625,54 +544,41 @@ class SQLiteSyncStorageTransaction<R extends UnknownRecord> implements TLSyncSto
625
544
  const clock = this.getNextClock()
626
545
  // Automatically clear tombstone if it exists
627
546
  this.stmts.deleteTombstone.run(id)
628
- // route by type: object-lane records live in their own table
629
- const table = this.storage.objectTypes.has(record.typeName)
630
- ? this.stmts.objects
631
- : this.stmts.documents
632
- table.insert.run(id, encodeState(record), clock)
547
+ this.stmts.insertDocument.run(id, encodeState(record), clock)
633
548
  }
634
549
 
635
550
  delete(id: string): void {
636
551
  this.assertNotClosed()
637
- const table = this.tableFor(id)
638
552
  // Only create a tombstone if the record actually exists
639
- const exists = table.exists.all(id)[0]
553
+ const exists = this.stmts.documentExists.all(id)[0]
640
554
  if (!exists) return
641
555
  const clock = this.getNextClock()
642
- table.delete.run(id)
556
+ this.stmts.deleteDocument.run(id)
643
557
  this.stmts.insertTombstone.run(id, clock)
644
558
  this.storage.pruneTombstones()
645
559
  }
646
560
 
647
- // iteration spans both record tables so schema migrations cover object-lane records too
648
-
649
561
  *entries(): IterableIterator<[string, R]> {
650
562
  this.assertNotClosed()
651
- for (const table of [this.stmts.documents, this.stmts.objects]) {
652
- for (const row of table.iterateEntries.iterate()) {
653
- this.assertNotClosed()
654
- yield [row.id, decodeState<R>(row.state)]
655
- }
563
+ for (const row of this.stmts.iterateDocumentEntries.iterate()) {
564
+ this.assertNotClosed()
565
+ yield [row.id, decodeState<R>(row.state)]
656
566
  }
657
567
  }
658
568
 
659
569
  *keys(): IterableIterator<string> {
660
570
  this.assertNotClosed()
661
- for (const table of [this.stmts.documents, this.stmts.objects]) {
662
- for (const row of table.iterateKeys.iterate()) {
663
- this.assertNotClosed()
664
- yield row.id
665
- }
571
+ for (const row of this.stmts.iterateDocumentKeys.iterate()) {
572
+ this.assertNotClosed()
573
+ yield row.id
666
574
  }
667
575
  }
668
576
 
669
577
  *values(): IterableIterator<R> {
670
578
  this.assertNotClosed()
671
- for (const table of [this.stmts.documents, this.stmts.objects]) {
672
- for (const row of table.iterateValues.iterate()) {
673
- this.assertNotClosed()
674
- yield decodeState<R>(row.state)
675
- }
579
+ for (const row of this.stmts.iterateDocumentValues.iterate()) {
580
+ this.assertNotClosed()
581
+ yield decodeState<R>(row.state)
676
582
  }
677
583
  }
678
584
 
@@ -697,22 +603,17 @@ class SQLiteSyncStorageTransaction<R extends UnknownRecord> implements TLSyncSto
697
603
  const diff: TLSyncForwardDiff<R> = { puts: {}, deletes: [] }
698
604
  const wipeAll = sinceClock < this.storage._getTombstoneHistoryStartsAtClock()
699
605
 
700
- // both record tables share the clock and tombstones, so the change feed spans both
701
606
  if (wipeAll) {
702
- // If wipeAll, include all records
703
- for (const table of [this.stmts.documents, this.stmts.objects]) {
704
- for (const row of table.iterateValues.iterate()) {
705
- const state = decodeState<R>(row.state)
706
- diff.puts[state.id] = state
707
- }
607
+ // If wipeAll, include all documents
608
+ for (const row of this.stmts.iterateDocumentValues.iterate()) {
609
+ const state = decodeState<R>(row.state)
610
+ diff.puts[state.id] = state
708
611
  }
709
612
  } else {
710
- // Get records changed since clock
711
- for (const table of [this.stmts.documents, this.stmts.objects]) {
712
- for (const row of table.changedSince.iterate(sinceClock)) {
713
- const state = decodeState<R>(row.state)
714
- diff.puts[state.id] = state
715
- }
613
+ // Get documents changed since clock
614
+ for (const row of this.stmts.getDocumentsChangedSince.iterate(sinceClock)) {
615
+ const state = decodeState<R>(row.state)
616
+ diff.puts[state.id] = state
716
617
  }
717
618
  // When wipeAll, deletes are redundant (full state is in puts). Only include tombstones otherwise.
718
619
  for (const row of this.stmts.getTombstonesChangedSince.iterate(sinceClock)) {