@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.
- package/dist-cjs/index.d.ts +3 -154
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/InMemorySyncStorage.js +20 -55
- package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/SQLiteSyncStorage.js +52 -115
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +2 -27
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +1 -6
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +10 -160
- 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 +3 -154
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/InMemorySyncStorage.mjs +20 -55
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/SQLiteSyncStorage.mjs +52 -115
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +2 -27
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +1 -6
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +10 -160
- 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 +0 -3
- package/src/lib/InMemorySyncStorage.ts +21 -74
- package/src/lib/RoomSession.ts +2 -7
- package/src/lib/SQLiteSyncStorage.test.ts +2 -29
- package/src/lib/SQLiteSyncStorage.ts +59 -158
- package/src/lib/TLSocketRoom.ts +3 -61
- package/src/lib/TLSyncClient.test.ts +2 -9
- package/src/lib/TLSyncClient.ts +3 -15
- package/src/lib/TLSyncRoom.ts +10 -294
- package/src/lib/TLSyncStorage.ts +0 -8
- package/src/lib/protocol.ts +0 -17
- package/src/test/TLSocketRoom.test.ts +2 -37
- package/src/test/TLSyncRoom.test.ts +0 -25
- package/src/test/TestServer.ts +4 -14
- package/src/test/storageContractSuite.ts +0 -79
- package/src/test/upgradeDowngrade.test.ts +2 -144
- package/src/test/objectStore.test.ts +0 -630
|
@@ -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.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
if (!map) return
|
|
322
|
+
if (!this.storage.documents.has(id)) return
|
|
365
323
|
const clock = this.getNextClock()
|
|
366
|
-
|
|
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
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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) {
|
package/src/lib/RoomSession.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SerializedSchema, UnknownRecord } from '@tldraw/store'
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
476
|
+
documents: Array.from(this._iterateDocuments()),
|
|
539
477
|
tombstones: Object.fromEntries(this._iterateTombstones()),
|
|
540
478
|
schema: this._getSchema(),
|
|
541
479
|
}
|
|
542
480
|
}
|
|
543
|
-
|
|
544
|
-
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
553
|
+
const exists = this.stmts.documentExists.all(id)[0]
|
|
640
554
|
if (!exists) return
|
|
641
555
|
const clock = this.getNextClock()
|
|
642
|
-
|
|
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
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
662
|
-
|
|
663
|
-
|
|
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
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
703
|
-
for (const
|
|
704
|
-
|
|
705
|
-
|
|
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
|
|
711
|
-
for (const
|
|
712
|
-
|
|
713
|
-
|
|
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)) {
|