@tldraw/sync-core 4.2.2 → 4.2.3

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 (84) hide show
  1. package/dist-cjs/index.d.ts +58 -483
  2. package/dist-cjs/index.js +3 -13
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/RoomSession.js.map +1 -1
  5. package/dist-cjs/lib/TLSocketRoom.js +69 -117
  6. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  7. package/dist-cjs/lib/TLSyncClient.js +0 -7
  8. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  9. package/dist-cjs/lib/TLSyncRoom.js +688 -357
  10. package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
  11. package/dist-cjs/lib/chunk.js +2 -2
  12. package/dist-cjs/lib/chunk.js.map +1 -1
  13. package/dist-esm/index.d.mts +58 -483
  14. package/dist-esm/index.mjs +5 -20
  15. package/dist-esm/index.mjs.map +2 -2
  16. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  17. package/dist-esm/lib/TLSocketRoom.mjs +70 -121
  18. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  19. package/dist-esm/lib/TLSyncClient.mjs +0 -7
  20. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  21. package/dist-esm/lib/TLSyncRoom.mjs +702 -370
  22. package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
  23. package/dist-esm/lib/chunk.mjs +2 -2
  24. package/dist-esm/lib/chunk.mjs.map +1 -1
  25. package/package.json +11 -12
  26. package/src/index.ts +3 -32
  27. package/src/lib/ClientWebSocketAdapter.test.ts +0 -3
  28. package/src/lib/RoomSession.test.ts +0 -1
  29. package/src/lib/RoomSession.ts +0 -2
  30. package/src/lib/TLSocketRoom.ts +114 -228
  31. package/src/lib/TLSyncClient.ts +0 -12
  32. package/src/lib/TLSyncRoom.ts +913 -473
  33. package/src/lib/chunk.ts +2 -2
  34. package/src/test/FuzzEditor.ts +5 -4
  35. package/src/test/TLSocketRoom.test.ts +49 -255
  36. package/src/test/TLSyncRoom.test.ts +534 -1024
  37. package/src/test/TestServer.ts +1 -12
  38. package/src/test/customMessages.test.ts +1 -1
  39. package/src/test/presenceMode.test.ts +6 -6
  40. package/src/test/pruneTombstones.test.ts +178 -0
  41. package/src/test/syncFuzz.test.ts +4 -2
  42. package/src/test/upgradeDowngrade.test.ts +8 -290
  43. package/src/test/validation.test.ts +10 -15
  44. package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js +0 -55
  45. package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js.map +0 -7
  46. package/dist-cjs/lib/InMemorySyncStorage.js +0 -287
  47. package/dist-cjs/lib/InMemorySyncStorage.js.map +0 -7
  48. package/dist-cjs/lib/MicrotaskNotifier.js +0 -50
  49. package/dist-cjs/lib/MicrotaskNotifier.js.map +0 -7
  50. package/dist-cjs/lib/NodeSqliteWrapper.js +0 -48
  51. package/dist-cjs/lib/NodeSqliteWrapper.js.map +0 -7
  52. package/dist-cjs/lib/SQLiteSyncStorage.js +0 -428
  53. package/dist-cjs/lib/SQLiteSyncStorage.js.map +0 -7
  54. package/dist-cjs/lib/TLSyncStorage.js +0 -76
  55. package/dist-cjs/lib/TLSyncStorage.js.map +0 -7
  56. package/dist-cjs/lib/recordDiff.js +0 -52
  57. package/dist-cjs/lib/recordDiff.js.map +0 -7
  58. package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs +0 -35
  59. package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs.map +0 -7
  60. package/dist-esm/lib/InMemorySyncStorage.mjs +0 -272
  61. package/dist-esm/lib/InMemorySyncStorage.mjs.map +0 -7
  62. package/dist-esm/lib/MicrotaskNotifier.mjs +0 -30
  63. package/dist-esm/lib/MicrotaskNotifier.mjs.map +0 -7
  64. package/dist-esm/lib/NodeSqliteWrapper.mjs +0 -28
  65. package/dist-esm/lib/NodeSqliteWrapper.mjs.map +0 -7
  66. package/dist-esm/lib/SQLiteSyncStorage.mjs +0 -414
  67. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +0 -7
  68. package/dist-esm/lib/TLSyncStorage.mjs +0 -56
  69. package/dist-esm/lib/TLSyncStorage.mjs.map +0 -7
  70. package/dist-esm/lib/recordDiff.mjs +0 -32
  71. package/dist-esm/lib/recordDiff.mjs.map +0 -7
  72. package/src/lib/DurableObjectSqliteSyncWrapper.ts +0 -95
  73. package/src/lib/InMemorySyncStorage.ts +0 -387
  74. package/src/lib/MicrotaskNotifier.test.ts +0 -429
  75. package/src/lib/MicrotaskNotifier.ts +0 -38
  76. package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
  77. package/src/lib/NodeSqliteSyncWrapper.test.ts +0 -272
  78. package/src/lib/NodeSqliteWrapper.ts +0 -99
  79. package/src/lib/SQLiteSyncStorage.ts +0 -627
  80. package/src/lib/TLSyncStorage.ts +0 -216
  81. package/src/lib/computeTombstonePruning.test.ts +0 -352
  82. package/src/lib/recordDiff.ts +0 -73
  83. package/src/test/InMemorySyncStorage.test.ts +0 -1684
  84. package/src/test/SQLiteSyncStorage.test.ts +0 -1378
@@ -1,627 +0,0 @@
1
- import { transaction } from '@tldraw/state'
2
- import { SerializedSchema, StoreSnapshot, UnknownRecord } from '@tldraw/store'
3
- import { assert, objectMapEntries, throttle } from '@tldraw/utils'
4
- import {
5
- computeTombstonePruning,
6
- DEFAULT_INITIAL_SNAPSHOT,
7
- MAX_TOMBSTONES,
8
- } from './InMemorySyncStorage'
9
- import { MicrotaskNotifier } from './MicrotaskNotifier'
10
- import { RoomSnapshot } from './TLSyncRoom'
11
- import {
12
- convertStoreSnapshotToRoomSnapshot,
13
- TLSyncForwardDiff,
14
- TLSyncStorage,
15
- TLSyncStorageGetChangesSinceResult,
16
- TLSyncStorageOnChangeCallbackProps,
17
- TLSyncStorageTransaction,
18
- TLSyncStorageTransactionCallback,
19
- TLSyncStorageTransactionOptions,
20
- TLSyncStorageTransactionResult,
21
- } from './TLSyncStorage'
22
-
23
- /**
24
- * Valid input value types for SQLite query parameters.
25
- * These are the types that can be passed as bindings to prepared statements.
26
- * @public
27
- */
28
- export type TLSqliteInputValue = null | number | bigint | string | Uint8Array
29
-
30
- /**
31
- * Possible output value types returned from SQLite queries.
32
- * Includes all input types plus Uint8Array for BLOB columns.
33
- * @public
34
- */
35
- export type TLSqliteOutputValue = null | number | bigint | string | Uint8Array
36
-
37
- /**
38
- * A row returned from a SQLite query, mapping column names to their values.
39
- * @public
40
- */
41
- export type TLSqliteRow = Record<string, TLSqliteOutputValue>
42
-
43
- /**
44
- * A prepared statement that can be executed multiple times with different bindings.
45
- * @public
46
- */
47
- export interface TLSyncSqliteStatement<
48
- TResult extends TLSqliteRow | void,
49
- TParams extends TLSqliteInputValue[] = [],
50
- > {
51
- /** Execute the statement and iterate over results one at a time */
52
- iterate(...bindings: TParams): IterableIterator<TResult>
53
- /** Execute the statement and return all results as an array */
54
- all(...bindings: TParams): TResult[]
55
- /** Execute the statement without returning results (for DML) */
56
- run(...bindings: TParams): void
57
- }
58
-
59
- /**
60
- * Configuration for SQLiteSyncStorage.
61
- * @public
62
- */
63
- export interface TLSyncSqliteWrapperConfig {
64
- /** Prefix for all table names (default: ''). E.g. 'sync_' creates tables 'sync_documents', 'sync_tombstones', 'sync_metadata' */
65
- tablePrefix?: string
66
- }
67
-
68
- /**
69
- * Interface for SQLite storage with prepare, exec and transaction capabilities.
70
- * @public
71
- */
72
- export interface TLSyncSqliteWrapper {
73
- /** Optional configuration for table names. If not provided, defaults are used. */
74
- readonly config?: TLSyncSqliteWrapperConfig
75
- /** Prepare a SQL statement for execution */
76
- prepare<TResult extends TLSqliteRow | void, TParams extends TLSqliteInputValue[] = []>(
77
- sql: string
78
- ): TLSyncSqliteStatement<TResult, TParams>
79
- /** Execute raw SQL (for DDL, multi-statement scripts) */
80
- exec(sql: string): void
81
- /** Execute a callback within a transaction */
82
- transaction<T>(callback: () => T): T
83
- }
84
-
85
- export function migrateSqliteSyncStorage(
86
- storage: TLSyncSqliteWrapper,
87
- {
88
- documentsTable = 'documents',
89
- tombstonesTable = 'tombstones',
90
- metadataTable = 'metadata',
91
- }: { documentsTable?: string; tombstonesTable?: string; metadataTable?: string } = {}
92
- ): void {
93
- let migrationVersion = 0
94
- try {
95
- const row = storage
96
- .prepare<{
97
- migrationVersion: number
98
- }>(`SELECT migrationVersion FROM ${metadataTable} LIMIT 1`)
99
- .all()[0]
100
- migrationVersion = row?.migrationVersion ?? 0
101
- } catch (_e) {
102
- // noop
103
- }
104
-
105
- if (migrationVersion === 0) {
106
- migrationVersion++
107
- storage.exec(`
108
- CREATE TABLE ${documentsTable} (
109
- id TEXT PRIMARY KEY,
110
- state BLOB NOT NULL,
111
- lastChangedClock INTEGER NOT NULL
112
- );
113
-
114
- CREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);
115
-
116
- CREATE TABLE ${tombstonesTable} (
117
- id TEXT PRIMARY KEY,
118
- clock INTEGER NOT NULL
119
- );
120
- CREATE INDEX idx_${tombstonesTable}_clock ON ${tombstonesTable}(clock);
121
-
122
- -- This table is used to store the metadata for the sync storage.
123
- -- There should only be one row in this table.
124
- CREATE TABLE ${metadataTable} (
125
- migrationVersion INTEGER NOT NULL,
126
- documentClock INTEGER NOT NULL,
127
- tombstoneHistoryStartsAtClock INTEGER NOT NULL,
128
- schema TEXT NOT NULL
129
- );
130
-
131
- INSERT INTO ${metadataTable} (migrationVersion, documentClock, tombstoneHistoryStartsAtClock, schema) VALUES (2, 0, 0, '')
132
- `)
133
- // Skip migration 2 since we created the table with BLOB already
134
- migrationVersion++
135
- }
136
-
137
- if (migrationVersion === 1) {
138
- // Migration 2: Convert state column from TEXT to BLOB
139
- // SQLite doesn't support ALTER COLUMN, so we need to recreate the table
140
- migrationVersion++
141
- storage.exec(`
142
- CREATE TABLE ${documentsTable}_new (
143
- id TEXT PRIMARY KEY,
144
- state BLOB NOT NULL,
145
- lastChangedClock INTEGER NOT NULL
146
- );
147
-
148
- INSERT INTO ${documentsTable}_new (id, state, lastChangedClock)
149
- SELECT id, CAST(state AS BLOB), lastChangedClock FROM ${documentsTable};
150
-
151
- DROP TABLE ${documentsTable};
152
-
153
- ALTER TABLE ${documentsTable}_new RENAME TO ${documentsTable};
154
-
155
- CREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);
156
- `)
157
- }
158
-
159
- // add more migrations here if and when needed
160
-
161
- storage.exec(`UPDATE ${metadataTable} SET migrationVersion = ${migrationVersion}`)
162
- }
163
-
164
- const textEncoder = new TextEncoder()
165
- const textDecoder = new TextDecoder()
166
-
167
- function encodeState(state: unknown): Uint8Array {
168
- return textEncoder.encode(JSON.stringify(state))
169
- }
170
-
171
- function decodeState<T>(state: Uint8Array): T {
172
- return JSON.parse(textDecoder.decode(state))
173
- }
174
-
175
- /**
176
- * SQLite-based implementation of TLSyncStorage.
177
- * Stores documents, tombstones, metadata, and clock values in SQLite tables.
178
- *
179
- * This storage backend provides persistent synchronization state that survives
180
- * process restarts, unlike InMemorySyncStorage which loses data when the process ends.
181
- *
182
- * @example
183
- * ```ts
184
- * // With Cloudflare Durable Objects
185
- * import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'
186
- *
187
- * const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)
188
- * const storage = new SQLiteSyncStorage({ sql })
189
- * ```
190
- *
191
- * @example
192
- * ```ts
193
- * // With Node.js sqlite (Node 22.5+)
194
- * import { DatabaseSync } from 'node:sqlite'
195
- * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'
196
- *
197
- * const db = new DatabaseSync('sync-state.db')
198
- * const sql = new NodeSqliteWrapper(db)
199
- * const storage = new SQLiteSyncStorage({ sql })
200
- * ```
201
- *
202
- * @example
203
- * ```ts
204
- * // Initialize with an existing snapshot
205
- * const storage = new SQLiteSyncStorage({ sql, snapshot: existingSnapshot })
206
- * ```
207
- *
208
- * @public
209
- */
210
- export class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {
211
- /**
212
- * Check if the storage has been initialized (has data in the clock table).
213
- * Useful for determining whether to load from an external source on first access.
214
- */
215
- static hasBeenInitialized(storage: TLSyncSqliteWrapper): boolean {
216
- const prefix = storage.config?.tablePrefix ?? ''
217
- try {
218
- const schema = storage
219
- .prepare<{ schema: string }>(`SELECT schema FROM ${prefix}metadata LIMIT 1`)
220
- .all()[0]?.schema
221
- return !!schema
222
- } catch (_e) {
223
- return false
224
- }
225
- }
226
-
227
- /**
228
- * Get the current document clock value from storage without fully initializing.
229
- * Returns null if storage has not been initialized.
230
- * Useful for comparing storage freshness against external sources.
231
- */
232
- static getDocumentClock(storage: TLSyncSqliteWrapper): number | null {
233
- const prefix = storage.config?.tablePrefix ?? ''
234
- try {
235
- const row = storage
236
- .prepare<{ documentClock: number }>(`SELECT documentClock FROM ${prefix}metadata LIMIT 1`)
237
- .all()[0]
238
- // documentClock exists but could be 0, so we check if the storage is initialized
239
- if (row && SQLiteSyncStorage.hasBeenInitialized(storage)) {
240
- return row.documentClock
241
- }
242
- return null
243
- } catch (_e) {
244
- return null
245
- }
246
- }
247
-
248
- // Prepared statements - created once, reused many times
249
- private readonly stmts
250
-
251
- private readonly sql: TLSyncSqliteWrapper
252
-
253
- constructor({
254
- sql,
255
- snapshot,
256
- onChange,
257
- }: {
258
- sql: TLSyncSqliteWrapper
259
- snapshot?: RoomSnapshot | StoreSnapshot<R>
260
- onChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown
261
- }) {
262
- this.sql = sql
263
- const prefix = sql.config?.tablePrefix ?? ''
264
- const documentsTable = `${prefix}documents`
265
- const tombstonesTable = `${prefix}tombstones`
266
- const metadataTable = `${prefix}metadata`
267
-
268
- migrateSqliteSyncStorage(this.sql, { documentsTable, tombstonesTable, metadataTable })
269
-
270
- // Prepare all statements once
271
- this.stmts = {
272
- // Metadata
273
- getDocumentClock: this.sql.prepare<{ documentClock: number }>(
274
- `SELECT documentClock FROM ${metadataTable} LIMIT 1`
275
- ),
276
- getTombstoneHistoryStartsAtClock: this.sql.prepare<{ tombstoneHistoryStartsAtClock: number }>(
277
- `SELECT tombstoneHistoryStartsAtClock FROM ${metadataTable}`
278
- ),
279
- getSchema: this.sql.prepare<{ schema: string }>(`SELECT schema FROM ${metadataTable}`),
280
- setSchema: this.sql.prepare<void, [schema: string]>(`UPDATE ${metadataTable} SET schema = ?`),
281
- setTombstoneHistoryStartsAtClock: this.sql.prepare<void, [clock: number]>(
282
- `UPDATE ${metadataTable} SET tombstoneHistoryStartsAtClock = ?`
283
- ),
284
- incrementDocumentClock: this.sql.prepare<void>(
285
- `UPDATE ${metadataTable} SET documentClock = documentClock + 1`
286
- ),
287
-
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
- ),
315
-
316
- // Tombstones
317
- insertTombstone: this.sql.prepare<void, [id: string, clock: number]>(
318
- `INSERT OR REPLACE INTO ${tombstonesTable} (id, clock) VALUES (?, ?)`
319
- ),
320
- deleteTombstone: this.sql.prepare<void, [id: string]>(
321
- `DELETE FROM ${tombstonesTable} WHERE id = ?`
322
- ),
323
- deleteTombstonesBefore: this.sql.prepare<void, [clock: number]>(
324
- `DELETE FROM ${tombstonesTable} WHERE clock < ?`
325
- ),
326
- countTombstones: this.sql.prepare<{ count: number }>(
327
- `SELECT count(*) as count FROM ${tombstonesTable}`
328
- ),
329
- iterateTombstones: this.sql.prepare<{ id: string; clock: number }>(
330
- `SELECT id, clock FROM ${tombstonesTable} ORDER BY clock ASC`
331
- ),
332
- getTombstonesChangedSince: this.sql.prepare<{ id: string }, [sinceClock: number]>(
333
- `SELECT id FROM ${tombstonesTable} WHERE clock > ?`
334
- ),
335
-
336
- // Initial setup (only used when loading a snapshot)
337
- updateMetadata: this.sql.prepare<
338
- void,
339
- [documentClock: number, tombstoneHistoryStartsAtClock: number, schema: string]
340
- >(
341
- `UPDATE ${metadataTable} SET documentClock = ?, tombstoneHistoryStartsAtClock = ?, schema = ?`
342
- ),
343
- }
344
-
345
- // Check if we already have data
346
- const hasData = SQLiteSyncStorage.hasBeenInitialized(sql)
347
-
348
- if (snapshot || !hasData) {
349
- snapshot = convertStoreSnapshotToRoomSnapshot(snapshot ?? DEFAULT_INITIAL_SNAPSHOT)
350
-
351
- const documentClock = snapshot.documentClock ?? snapshot.clock ?? 0
352
- const tombstoneHistoryStartsAtClock = snapshot.tombstoneHistoryStartsAtClock ?? documentClock
353
-
354
- // Clear existing data
355
- this.sql.exec(`
356
- DELETE FROM ${documentsTable};
357
- DELETE FROM ${tombstonesTable};
358
- `)
359
-
360
- // Insert documents
361
- for (const doc of snapshot.documents) {
362
- this.stmts.insertDocument.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock)
363
- }
364
-
365
- // Insert tombstones
366
- if (snapshot.tombstones) {
367
- for (const [id, clock] of objectMapEntries(snapshot.tombstones)) {
368
- this.stmts.insertTombstone.run(id, clock)
369
- }
370
- }
371
-
372
- // Insert metadata row
373
- this.stmts.updateMetadata.run(
374
- documentClock,
375
- tombstoneHistoryStartsAtClock,
376
- JSON.stringify(snapshot.schema)
377
- )
378
- }
379
- if (onChange) {
380
- this.onChange(onChange)
381
- }
382
- }
383
-
384
- private notifier = new MicrotaskNotifier<[TLSyncStorageOnChangeCallbackProps]>()
385
- onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => void): () => void {
386
- return this.notifier.register(callback)
387
- }
388
-
389
- transaction<T>(
390
- callback: TLSyncStorageTransactionCallback<R, T>,
391
- opts?: TLSyncStorageTransactionOptions
392
- ): TLSyncStorageTransactionResult<T, R> {
393
- const clockBefore = this.getClock()
394
- const trackChanges = opts?.emitChanges === 'always'
395
- return this.sql.transaction(() => {
396
- const txn = new SQLiteSyncStorageTransaction<R>(this, this.stmts)
397
- let result: T
398
- let changes: TLSyncForwardDiff<R> | undefined
399
- try {
400
- result = transaction(() => {
401
- return callback(txn)
402
- }) as T
403
- if (trackChanges) {
404
- changes = txn.getChangesSince(clockBefore)?.diff
405
- }
406
- } finally {
407
- txn.close()
408
- }
409
- if (
410
- typeof result === 'object' &&
411
- result &&
412
- 'then' in result &&
413
- typeof result.then === 'function'
414
- ) {
415
- throw new Error('Transaction must return a value, not a promise')
416
- }
417
-
418
- const clockAfter = this.getClock()
419
- const didChange = clockAfter > clockBefore
420
- if (didChange) {
421
- this.notifier.notify({ id: opts?.id, documentClock: clockAfter })
422
- }
423
- return { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes }
424
- })
425
- }
426
-
427
- getClock(): number {
428
- const clockRow = this.stmts.getDocumentClock.all()[0]
429
- return clockRow?.documentClock ?? 0
430
- }
431
-
432
- /** @internal */
433
- _getTombstoneHistoryStartsAtClock(): number {
434
- const clockRow = this.stmts.getTombstoneHistoryStartsAtClock.all()[0]
435
- return clockRow?.tombstoneHistoryStartsAtClock ?? 0
436
- }
437
-
438
- /** @internal */
439
- _getSchema(): SerializedSchema {
440
- const clockRow = this.stmts.getSchema.all()[0]
441
- assert(clockRow, 'Storage not initialized - clock row missing')
442
- return JSON.parse(clockRow.schema)
443
- }
444
-
445
- /** @internal */
446
- _setSchema(schema: SerializedSchema): void {
447
- this.stmts.setSchema.run(JSON.stringify(schema))
448
- }
449
-
450
- /** @internal */
451
- pruneTombstones = throttle(
452
- () => {
453
- const tombstoneCount = this.stmts.countTombstones.all()[0].count as number
454
- if (tombstoneCount > MAX_TOMBSTONES) {
455
- // Get all tombstones sorted by clock ascending (oldest first)
456
- const tombstones = this.stmts.iterateTombstones.all()
457
-
458
- const result = computeTombstonePruning({ tombstones, documentClock: this.getClock() })
459
- if (result) {
460
- this.stmts.setTombstoneHistoryStartsAtClock.run(result.newTombstoneHistoryStartsAtClock)
461
- // Delete all tombstones with clock < newTombstoneHistoryStartsAtClock in one operation.
462
- // This works because computeTombstonePruning ensures we never split a clock value.
463
- this.stmts.deleteTombstonesBefore.run(result.newTombstoneHistoryStartsAtClock)
464
- }
465
- }
466
- },
467
- 1000,
468
- // prevent this from running synchronously to avoid blocking requests
469
- { leading: false }
470
- )
471
-
472
- getSnapshot(): RoomSnapshot {
473
- return {
474
- tombstoneHistoryStartsAtClock: this._getTombstoneHistoryStartsAtClock(),
475
- documentClock: this.getClock(),
476
- documents: Array.from(this._iterateDocuments()),
477
- tombstones: Object.fromEntries(this._iterateTombstones()),
478
- schema: this._getSchema(),
479
- }
480
- }
481
- private *_iterateDocuments(): IterableIterator<{ state: R; lastChangedClock: number }> {
482
- for (const row of this.stmts.iterateDocuments.iterate()) {
483
- yield { state: decodeState<R>(row.state), lastChangedClock: row.lastChangedClock }
484
- }
485
- }
486
-
487
- private *_iterateTombstones(): IterableIterator<[string, number]> {
488
- for (const row of this.stmts.iterateTombstones.iterate()) {
489
- yield [row.id, row.clock]
490
- }
491
- }
492
- }
493
-
494
- /**
495
- * Transaction implementation for SQLiteSyncStorage.
496
- * Provides access to documents, tombstones, and metadata within a transaction.
497
- *
498
- * @internal
499
- */
500
- class SQLiteSyncStorageTransaction<R extends UnknownRecord> implements TLSyncStorageTransaction<R> {
501
- private _clock: number
502
- private _closed = false
503
- private _didIncrementClock: boolean = false
504
-
505
- constructor(
506
- private storage: SQLiteSyncStorage<R>,
507
- private stmts: SQLiteSyncStorage<R>['stmts']
508
- ) {
509
- this._clock = this.storage.getClock()
510
- }
511
-
512
- /** @internal */
513
- close() {
514
- this._closed = true
515
- }
516
-
517
- private assertNotClosed() {
518
- assert(!this._closed, 'Transaction has ended, iterator cannot be consumed')
519
- }
520
-
521
- getClock(): number {
522
- return this._clock
523
- }
524
-
525
- private getNextClock(): number {
526
- if (!this._didIncrementClock) {
527
- this._didIncrementClock = true
528
- this.stmts.incrementDocumentClock.run()
529
- this._clock = this.storage.getClock()
530
- }
531
- return this._clock
532
- }
533
-
534
- get(id: string): R | undefined {
535
- this.assertNotClosed()
536
- const row = this.stmts.getDocument.all(id)[0]
537
- if (!row) return undefined
538
- return decodeState<R>(row.state)
539
- }
540
-
541
- set(id: string, record: R): void {
542
- this.assertNotClosed()
543
- assert(id === record.id, `Record id mismatch: key does not match record.id`)
544
- const clock = this.getNextClock()
545
- // Automatically clear tombstone if it exists
546
- this.stmts.deleteTombstone.run(id)
547
- this.stmts.insertDocument.run(id, encodeState(record), clock)
548
- }
549
-
550
- delete(id: string): void {
551
- this.assertNotClosed()
552
- // Only create a tombstone if the record actually exists
553
- const exists = this.stmts.documentExists.all(id)[0]
554
- if (!exists) return
555
- const clock = this.getNextClock()
556
- this.stmts.deleteDocument.run(id)
557
- this.stmts.insertTombstone.run(id, clock)
558
- this.storage.pruneTombstones()
559
- }
560
-
561
- *entries(): IterableIterator<[string, R]> {
562
- this.assertNotClosed()
563
- for (const row of this.stmts.iterateDocumentEntries.iterate()) {
564
- this.assertNotClosed()
565
- yield [row.id, decodeState<R>(row.state)]
566
- }
567
- }
568
-
569
- *keys(): IterableIterator<string> {
570
- this.assertNotClosed()
571
- for (const row of this.stmts.iterateDocumentKeys.iterate()) {
572
- this.assertNotClosed()
573
- yield row.id
574
- }
575
- }
576
-
577
- *values(): IterableIterator<R> {
578
- this.assertNotClosed()
579
- for (const row of this.stmts.iterateDocumentValues.iterate()) {
580
- this.assertNotClosed()
581
- yield decodeState<R>(row.state)
582
- }
583
- }
584
-
585
- getSchema(): SerializedSchema {
586
- this.assertNotClosed()
587
- return this.storage._getSchema()
588
- }
589
-
590
- setSchema(schema: SerializedSchema): void {
591
- this.assertNotClosed()
592
- this.storage._setSchema(schema)
593
- }
594
-
595
- getChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined {
596
- this.assertNotClosed()
597
- const clock = this.storage.getClock()
598
- if (sinceClock === clock) return undefined
599
- if (sinceClock > clock) {
600
- // something went wrong, wipe the slate clean
601
- sinceClock = -1
602
- }
603
- const diff: TLSyncForwardDiff<R> = { puts: {}, deletes: [] }
604
- const wipeAll = sinceClock < this.storage._getTombstoneHistoryStartsAtClock()
605
-
606
- if (wipeAll) {
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
611
- }
612
- } else {
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
617
- }
618
- }
619
-
620
- // Get tombstones changed since clock
621
- for (const row of this.stmts.getTombstonesChangedSince.iterate(sinceClock)) {
622
- diff.deletes.push(row.id)
623
- }
624
-
625
- return { diff, wipeAll }
626
- }
627
- }