@tldraw/sync-core 5.3.0-canary.fceaae5e9feb → 5.3.0-internal.41291fb579ed

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 (51) hide show
  1. package/dist-cjs/index.d.ts +154 -3
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/InMemorySyncStorage.js +55 -20
  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 +115 -52
  8. package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
  9. package/dist-cjs/lib/TLSocketRoom.js +27 -2
  10. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  11. package/dist-cjs/lib/TLSyncClient.js +6 -1
  12. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  13. package/dist-cjs/lib/TLSyncRoom.js +160 -10
  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 +154 -3
  18. package/dist-esm/index.mjs +1 -1
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/InMemorySyncStorage.mjs +55 -20
  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 +115 -52
  24. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
  25. package/dist-esm/lib/TLSocketRoom.mjs +27 -2
  26. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  27. package/dist-esm/lib/TLSyncClient.mjs +6 -1
  28. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  29. package/dist-esm/lib/TLSyncRoom.mjs +160 -10
  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 +3 -0
  35. package/src/lib/InMemorySyncStorage.ts +74 -21
  36. package/src/lib/RoomSession.ts +7 -2
  37. package/src/lib/SQLiteSyncStorage.test.ts +29 -2
  38. package/src/lib/SQLiteSyncStorage.ts +158 -59
  39. package/src/lib/TLSocketRoom.ts +61 -3
  40. package/src/lib/TLSyncClient.test.ts +9 -2
  41. package/src/lib/TLSyncClient.ts +15 -3
  42. package/src/lib/TLSyncRoom.ts +294 -10
  43. package/src/lib/TLSyncStorage.ts +8 -0
  44. package/src/lib/protocol.ts +17 -0
  45. package/src/test/TLSocketRoom.test.ts +37 -2
  46. package/src/test/TLSyncClientRebase.test.ts +285 -0
  47. package/src/test/TLSyncRoom.test.ts +25 -0
  48. package/src/test/TestServer.ts +14 -4
  49. package/src/test/objectStore.test.ts +630 -0
  50. package/src/test/storageContractSuite.ts +79 -0
  51. package/src/test/upgradeDowngrade.test.ts +144 -2
@@ -11,6 +11,7 @@ import {
11
11
  } from "./TLSyncStorage.mjs";
12
12
  function migrateSqliteSyncStorage(storage, {
13
13
  documentsTable = "documents",
14
+ objectsTable = "objects",
14
15
  tombstonesTable = "tombstones",
15
16
  metadataTable = "metadata"
16
17
  } = {}) {
@@ -58,17 +59,29 @@ function migrateSqliteSyncStorage(storage, {
58
59
  state BLOB NOT NULL,
59
60
  lastChangedClock INTEGER NOT NULL
60
61
  );
61
-
62
+
62
63
  INSERT INTO ${documentsTable}_new (id, state, lastChangedClock)
63
64
  SELECT id, CAST(state AS BLOB), lastChangedClock FROM ${documentsTable};
64
-
65
+
65
66
  DROP TABLE ${documentsTable};
66
-
67
+
67
68
  ALTER TABLE ${documentsTable}_new RENAME TO ${documentsTable};
68
-
69
+
69
70
  CREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);
70
71
  `);
71
72
  }
73
+ if (migrationVersion === 2) {
74
+ migrationVersion++;
75
+ storage.exec(`
76
+ CREATE TABLE ${objectsTable} (
77
+ id TEXT PRIMARY KEY,
78
+ state BLOB NOT NULL,
79
+ lastChangedClock INTEGER NOT NULL
80
+ );
81
+
82
+ CREATE INDEX idx_${objectsTable}_lastChangedClock ON ${objectsTable}(lastChangedClock);
83
+ `);
84
+ }
72
85
  storage.exec(`UPDATE ${metadataTable} SET migrationVersion = ${migrationVersion}`);
73
86
  }
74
87
  const textEncoder = new TextEncoder();
@@ -113,17 +126,50 @@ class SQLiteSyncStorage {
113
126
  // Prepared statements - created once, reused many times
114
127
  stmts;
115
128
  sql;
129
+ /** @internal */
130
+ objectTypes;
116
131
  constructor({
117
132
  sql,
118
133
  snapshot,
134
+ objectTypes,
119
135
  onChange
120
136
  }) {
121
137
  this.sql = sql;
138
+ this.objectTypes = new Set(objectTypes ?? []);
122
139
  const prefix = sql.config?.tablePrefix ?? "";
123
140
  const documentsTable = `${prefix}documents`;
141
+ const objectsTable = `${prefix}objects`;
124
142
  const tombstonesTable = `${prefix}tombstones`;
125
143
  const metadataTable = `${prefix}metadata`;
126
- migrateSqliteSyncStorage(this.sql, { documentsTable, tombstonesTable, metadataTable });
144
+ migrateSqliteSyncStorage(this.sql, {
145
+ documentsTable,
146
+ objectsTable,
147
+ tombstonesTable,
148
+ metadataTable
149
+ });
150
+ const makeRecordTableStmts = (table) => ({
151
+ get: this.sql.prepare(
152
+ `SELECT state FROM ${table} WHERE id = ?`
153
+ ),
154
+ insert: this.sql.prepare(
155
+ `INSERT OR REPLACE INTO ${table} (id, state, lastChangedClock) VALUES (?, ?, ?)`
156
+ ),
157
+ delete: this.sql.prepare(`DELETE FROM ${table} WHERE id = ?`),
158
+ exists: this.sql.prepare(
159
+ `SELECT id FROM ${table} WHERE id = ?`
160
+ ),
161
+ iterate: this.sql.prepare(
162
+ `SELECT state, lastChangedClock FROM ${table}`
163
+ ),
164
+ iterateEntries: this.sql.prepare(
165
+ `SELECT id, state FROM ${table}`
166
+ ),
167
+ iterateKeys: this.sql.prepare(`SELECT id FROM ${table}`),
168
+ iterateValues: this.sql.prepare(`SELECT state FROM ${table}`),
169
+ changedSince: this.sql.prepare(
170
+ `SELECT state FROM ${table} WHERE lastChangedClock > ?`
171
+ )
172
+ });
127
173
  this.stmts = {
128
174
  // Metadata
129
175
  getDocumentClock: this.sql.prepare(
@@ -140,30 +186,9 @@ class SQLiteSyncStorage {
140
186
  incrementDocumentClock: this.sql.prepare(
141
187
  `UPDATE ${metadataTable} SET documentClock = documentClock + 1`
142
188
  ),
143
- // Documents
144
- getDocument: this.sql.prepare(
145
- `SELECT state FROM ${documentsTable} WHERE id = ?`
146
- ),
147
- insertDocument: this.sql.prepare(`INSERT OR REPLACE INTO ${documentsTable} (id, state, lastChangedClock) VALUES (?, ?, ?)`),
148
- deleteDocument: this.sql.prepare(
149
- `DELETE FROM ${documentsTable} WHERE id = ?`
150
- ),
151
- documentExists: this.sql.prepare(
152
- `SELECT id FROM ${documentsTable} WHERE id = ?`
153
- ),
154
- iterateDocuments: this.sql.prepare(
155
- `SELECT state, lastChangedClock FROM ${documentsTable}`
156
- ),
157
- iterateDocumentEntries: this.sql.prepare(
158
- `SELECT id, state FROM ${documentsTable}`
159
- ),
160
- iterateDocumentKeys: this.sql.prepare(`SELECT id FROM ${documentsTable}`),
161
- iterateDocumentValues: this.sql.prepare(
162
- `SELECT state FROM ${documentsTable}`
163
- ),
164
- getDocumentsChangedSince: this.sql.prepare(
165
- `SELECT state FROM ${documentsTable} WHERE lastChangedClock > ?`
166
- ),
189
+ // Record tables (document lane + object-store lane)
190
+ documents: makeRecordTableStmts(documentsTable),
191
+ objects: makeRecordTableStmts(objectsTable),
167
192
  // Tombstones
168
193
  insertTombstone: this.sql.prepare(
169
194
  `INSERT OR REPLACE INTO ${tombstonesTable} (id, clock) VALUES (?, ?)`
@@ -195,10 +220,12 @@ class SQLiteSyncStorage {
195
220
  const tombstoneHistoryStartsAtClock = snapshot.tombstoneHistoryStartsAtClock ?? documentClock;
196
221
  this.sql.exec(`
197
222
  DELETE FROM ${documentsTable};
223
+ DELETE FROM ${objectsTable};
198
224
  DELETE FROM ${tombstonesTable};
199
225
  `);
200
226
  for (const doc of snapshot.documents) {
201
- this.stmts.insertDocument.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock);
227
+ const table = this.objectTypes.has(doc.state.typeName) ? this.stmts.objects : this.stmts.documents;
228
+ table.insert.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock);
202
229
  }
203
230
  if (snapshot.tombstones) {
204
231
  for (const [id, clock] of objectMapEntries(snapshot.tombstones)) {
@@ -210,6 +237,17 @@ class SQLiteSyncStorage {
210
237
  tombstoneHistoryStartsAtClock,
211
238
  JSON.stringify(snapshot.schema)
212
239
  );
240
+ } else {
241
+ for (const typeName of this.objectTypes) {
242
+ const lo = `${typeName}:`;
243
+ const hi = `${typeName};`;
244
+ this.sql.exec(`
245
+ INSERT OR REPLACE INTO ${objectsTable} (id, state, lastChangedClock)
246
+ SELECT id, state, lastChangedClock FROM ${documentsTable}
247
+ WHERE id >= '${lo}' AND id < '${hi}';
248
+ DELETE FROM ${documentsTable} WHERE id >= '${lo}' AND id < '${hi}';
249
+ `);
250
+ }
213
251
  }
214
252
  if (onChange) {
215
253
  this.onChange(onChange);
@@ -287,13 +325,16 @@ class SQLiteSyncStorage {
287
325
  return {
288
326
  tombstoneHistoryStartsAtClock: this._getTombstoneHistoryStartsAtClock(),
289
327
  documentClock: this.getClock(),
290
- documents: Array.from(this._iterateDocuments()),
328
+ documents: Array.from(this._iterateRecords(this.stmts.documents)),
291
329
  tombstones: Object.fromEntries(this._iterateTombstones()),
292
330
  schema: this._getSchema()
293
331
  };
294
332
  }
295
- *_iterateDocuments() {
296
- for (const row of this.stmts.iterateDocuments.iterate()) {
333
+ getObjectsSnapshot() {
334
+ return Array.from(this._iterateRecords(this.stmts.objects));
335
+ }
336
+ *_iterateRecords(table) {
337
+ for (const row of table.iterate.iterate()) {
297
338
  yield { state: decodeState(row.state), lastChangedClock: row.lastChangedClock };
298
339
  }
299
340
  }
@@ -332,9 +373,18 @@ class SQLiteSyncStorageTransaction {
332
373
  }
333
374
  return this._clock;
334
375
  }
376
+ /**
377
+ * Which record table an id belongs to. Record ids are typeName-prefixed (`comment:abc`),
378
+ * so the partition is derivable from the id alone.
379
+ */
380
+ tableFor(id) {
381
+ const sep = id.indexOf(":");
382
+ if (sep === -1) return this.stmts.documents;
383
+ return this.storage.objectTypes.has(id.slice(0, sep)) ? this.stmts.objects : this.stmts.documents;
384
+ }
335
385
  get(id) {
336
386
  this.assertNotClosed();
337
- const row = this.stmts.getDocument.all(id)[0];
387
+ const row = this.tableFor(id).get.all(id)[0];
338
388
  if (!row) return void 0;
339
389
  return decodeState(row.state);
340
390
  }
@@ -343,36 +393,45 @@ class SQLiteSyncStorageTransaction {
343
393
  assert(id === record.id, `Record id mismatch: key does not match record.id`);
344
394
  const clock = this.getNextClock();
345
395
  this.stmts.deleteTombstone.run(id);
346
- this.stmts.insertDocument.run(id, encodeState(record), clock);
396
+ const table = this.storage.objectTypes.has(record.typeName) ? this.stmts.objects : this.stmts.documents;
397
+ table.insert.run(id, encodeState(record), clock);
347
398
  }
348
399
  delete(id) {
349
400
  this.assertNotClosed();
350
- const exists = this.stmts.documentExists.all(id)[0];
401
+ const table = this.tableFor(id);
402
+ const exists = table.exists.all(id)[0];
351
403
  if (!exists) return;
352
404
  const clock = this.getNextClock();
353
- this.stmts.deleteDocument.run(id);
405
+ table.delete.run(id);
354
406
  this.stmts.insertTombstone.run(id, clock);
355
407
  this.storage.pruneTombstones();
356
408
  }
409
+ // iteration spans both record tables so schema migrations cover object-lane records too
357
410
  *entries() {
358
411
  this.assertNotClosed();
359
- for (const row of this.stmts.iterateDocumentEntries.iterate()) {
360
- this.assertNotClosed();
361
- yield [row.id, decodeState(row.state)];
412
+ for (const table of [this.stmts.documents, this.stmts.objects]) {
413
+ for (const row of table.iterateEntries.iterate()) {
414
+ this.assertNotClosed();
415
+ yield [row.id, decodeState(row.state)];
416
+ }
362
417
  }
363
418
  }
364
419
  *keys() {
365
420
  this.assertNotClosed();
366
- for (const row of this.stmts.iterateDocumentKeys.iterate()) {
367
- this.assertNotClosed();
368
- yield row.id;
421
+ for (const table of [this.stmts.documents, this.stmts.objects]) {
422
+ for (const row of table.iterateKeys.iterate()) {
423
+ this.assertNotClosed();
424
+ yield row.id;
425
+ }
369
426
  }
370
427
  }
371
428
  *values() {
372
429
  this.assertNotClosed();
373
- for (const row of this.stmts.iterateDocumentValues.iterate()) {
374
- this.assertNotClosed();
375
- yield decodeState(row.state);
430
+ for (const table of [this.stmts.documents, this.stmts.objects]) {
431
+ for (const row of table.iterateValues.iterate()) {
432
+ this.assertNotClosed();
433
+ yield decodeState(row.state);
434
+ }
376
435
  }
377
436
  }
378
437
  getSchema() {
@@ -393,14 +452,18 @@ class SQLiteSyncStorageTransaction {
393
452
  const diff = { puts: {}, deletes: [] };
394
453
  const wipeAll = sinceClock < this.storage._getTombstoneHistoryStartsAtClock();
395
454
  if (wipeAll) {
396
- for (const row of this.stmts.iterateDocumentValues.iterate()) {
397
- const state = decodeState(row.state);
398
- diff.puts[state.id] = state;
455
+ for (const table of [this.stmts.documents, this.stmts.objects]) {
456
+ for (const row of table.iterateValues.iterate()) {
457
+ const state = decodeState(row.state);
458
+ diff.puts[state.id] = state;
459
+ }
399
460
  }
400
461
  } else {
401
- for (const row of this.stmts.getDocumentsChangedSince.iterate(sinceClock)) {
402
- const state = decodeState(row.state);
403
- diff.puts[state.id] = state;
462
+ for (const table of [this.stmts.documents, this.stmts.objects]) {
463
+ for (const row of table.changedSince.iterate(sinceClock)) {
464
+ const state = decodeState(row.state);
465
+ diff.puts[state.id] = state;
466
+ }
404
467
  }
405
468
  for (const row of this.stmts.getTombstonesChangedSince.iterate(sinceClock)) {
406
469
  diff.deletes.push(row.id);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/SQLiteSyncStorage.ts"],
4
- "sourcesContent": ["import { transaction } from '@tldraw/state'\nimport { SerializedSchema, StoreSnapshot, UnknownRecord } from '@tldraw/store'\nimport { assert, objectMapEntries, throttle } from '@tldraw/utils'\nimport {\n\tcomputeTombstonePruning,\n\tDEFAULT_INITIAL_SNAPSHOT,\n\tMAX_TOMBSTONES,\n} from './InMemorySyncStorage'\nimport { MicrotaskNotifier } from './MicrotaskNotifier'\nimport { RoomSnapshot } from './TLSyncRoom'\nimport {\n\tconvertStoreSnapshotToRoomSnapshot,\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n\tTLSyncStorageGetChangesSinceResult,\n\tTLSyncStorageOnChangeCallbackProps,\n\tTLSyncStorageTransaction,\n\tTLSyncStorageTransactionCallback,\n\tTLSyncStorageTransactionOptions,\n\tTLSyncStorageTransactionResult,\n} from './TLSyncStorage'\n\n/**\n * Valid input value types for SQLite query parameters.\n * These are the types that can be passed as bindings to prepared statements.\n * @public\n */\nexport type TLSqliteInputValue = null | number | bigint | string | Uint8Array\n\n/**\n * Possible output value types returned from SQLite queries.\n * Includes all input types plus Uint8Array for BLOB columns.\n * @public\n */\nexport type TLSqliteOutputValue = null | number | bigint | string | Uint8Array\n\n/**\n * A row returned from a SQLite query, mapping column names to their values.\n * @public\n */\nexport type TLSqliteRow = Record<string, TLSqliteOutputValue>\n\n/**\n * A prepared statement that can be executed multiple times with different bindings.\n * @public\n */\nexport interface TLSyncSqliteStatement<\n\tTResult extends TLSqliteRow | void,\n\tTParams extends TLSqliteInputValue[] = [],\n> {\n\t/** Execute the statement and iterate over results one at a time */\n\titerate(...bindings: TParams): IterableIterator<TResult>\n\t/** Execute the statement and return all results as an array */\n\tall(...bindings: TParams): TResult[]\n\t/** Execute the statement without returning results (for DML) */\n\trun(...bindings: TParams): void\n}\n\n/**\n * Configuration for SQLiteSyncStorage.\n * @public\n */\nexport interface TLSyncSqliteWrapperConfig {\n\t/** Prefix for all table names (default: ''). E.g. 'sync_' creates tables 'sync_documents', 'sync_tombstones', 'sync_metadata' */\n\ttablePrefix?: string\n}\n\n/**\n * Interface for SQLite storage with prepare, exec and transaction capabilities.\n * @public\n */\nexport interface TLSyncSqliteWrapper {\n\t/** Optional configuration for table names. If not provided, defaults are used. */\n\treadonly config?: TLSyncSqliteWrapperConfig\n\t/** Prepare a SQL statement for execution */\n\tprepare<TResult extends TLSqliteRow | void, TParams extends TLSqliteInputValue[] = []>(\n\t\tsql: string\n\t): TLSyncSqliteStatement<TResult, TParams>\n\t/** Execute raw SQL (for DDL, multi-statement scripts) */\n\texec(sql: string): void\n\t/** Execute a callback within a transaction */\n\ttransaction<T>(callback: () => T): T\n}\n\nexport function migrateSqliteSyncStorage(\n\tstorage: TLSyncSqliteWrapper,\n\t{\n\t\tdocumentsTable = 'documents',\n\t\ttombstonesTable = 'tombstones',\n\t\tmetadataTable = 'metadata',\n\t}: { documentsTable?: string; tombstonesTable?: string; metadataTable?: string } = {}\n): void {\n\tlet migrationVersion = 0\n\ttry {\n\t\tconst row = storage\n\t\t\t.prepare<{\n\t\t\t\tmigrationVersion: number\n\t\t\t}>(`SELECT migrationVersion FROM ${metadataTable} LIMIT 1`)\n\t\t\t.all()[0]\n\t\tmigrationVersion = row?.migrationVersion ?? 0\n\t} catch (_e) {\n\t\t// noop\n\t}\n\n\tif (migrationVersion === 0) {\n\t\tmigrationVersion++\n\t\tstorage.exec(`\n\t\t\tCREATE TABLE ${documentsTable} (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tstate BLOB NOT NULL,\n\t\t\t\tlastChangedClock INTEGER NOT NULL\n\t\t\t);\n\n\t\t\tCREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);\n\n\t\t\tCREATE TABLE ${tombstonesTable} (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tclock INTEGER NOT NULL\n\t\t\t);\n\t\t\tCREATE INDEX idx_${tombstonesTable}_clock ON ${tombstonesTable}(clock);\n\n\t\t\t-- This table is used to store the metadata for the sync storage.\n\t\t\t-- There should only be one row in this table.\n\t\t\tCREATE TABLE ${metadataTable} (\n\t\t\t migrationVersion INTEGER NOT NULL,\n\t\t\t\tdocumentClock INTEGER NOT NULL,\n\t\t\t\ttombstoneHistoryStartsAtClock INTEGER NOT NULL,\n\t\t\t\tschema TEXT NOT NULL\n\t\t\t);\n\t\t\t\n\t\t\tINSERT INTO ${metadataTable} (migrationVersion, documentClock, tombstoneHistoryStartsAtClock, schema) VALUES (2, 0, 0, '')\n\t\t`)\n\t\t// Skip migration 2 since we created the table with BLOB already\n\t\tmigrationVersion++\n\t}\n\n\tif (migrationVersion === 1) {\n\t\t// Migration 2: Convert state column from TEXT to BLOB\n\t\t// SQLite doesn't support ALTER COLUMN, so we need to recreate the table\n\t\tmigrationVersion++\n\t\tstorage.exec(`\n\t\t\tCREATE TABLE ${documentsTable}_new (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tstate BLOB NOT NULL,\n\t\t\t\tlastChangedClock INTEGER NOT NULL\n\t\t\t);\n\t\t\t\n\t\t\tINSERT INTO ${documentsTable}_new (id, state, lastChangedClock)\n\t\t\tSELECT id, CAST(state AS BLOB), lastChangedClock FROM ${documentsTable};\n\t\t\t\n\t\t\tDROP TABLE ${documentsTable};\n\t\t\t\n\t\t\tALTER TABLE ${documentsTable}_new RENAME TO ${documentsTable};\n\t\t\t\n\t\t\tCREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);\n\t\t`)\n\t}\n\n\t// add more migrations here if and when needed\n\n\tstorage.exec(`UPDATE ${metadataTable} SET migrationVersion = ${migrationVersion}`)\n}\n\nconst textEncoder = new TextEncoder()\nconst textDecoder = new TextDecoder()\n\nfunction encodeState(state: unknown): Uint8Array {\n\treturn textEncoder.encode(JSON.stringify(state))\n}\n\nfunction decodeState<T>(state: Uint8Array): T {\n\treturn JSON.parse(textDecoder.decode(state))\n}\n\n/**\n * SQLite-based implementation of TLSyncStorage.\n * Stores documents, tombstones, metadata, and clock values in SQLite tables.\n *\n * This storage backend provides persistent synchronization state that survives\n * process restarts, unlike InMemorySyncStorage which loses data when the process ends.\n *\n * @example\n * ```ts\n * // With Cloudflare Durable Objects\n * import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'\n *\n * const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // With Node.js sqlite (Node 22.5+)\n * import { DatabaseSync } from 'node:sqlite'\n * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'\n *\n * const db = new DatabaseSync('sync-state.db')\n * const sql = new NodeSqliteWrapper(db)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // Initialize with an existing snapshot\n * const storage = new SQLiteSyncStorage({ sql, snapshot: existingSnapshot })\n * ```\n *\n * @public\n */\nexport class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {\n\t/**\n\t * Check if the storage has been initialized (has data in the clock table).\n\t * Useful for determining whether to load from an external source on first access.\n\t */\n\tstatic hasBeenInitialized(storage: TLSyncSqliteWrapper): boolean {\n\t\tconst prefix = storage.config?.tablePrefix ?? ''\n\t\ttry {\n\t\t\tconst schema = storage\n\t\t\t\t.prepare<{ schema: string }>(`SELECT schema FROM ${prefix}metadata LIMIT 1`)\n\t\t\t\t.all()[0]?.schema\n\t\t\treturn !!schema\n\t\t} catch (_e) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Get the current document clock value from storage without fully initializing.\n\t * Returns null if storage has not been initialized.\n\t * Useful for comparing storage freshness against external sources.\n\t */\n\tstatic getDocumentClock(storage: TLSyncSqliteWrapper): number | null {\n\t\tconst prefix = storage.config?.tablePrefix ?? ''\n\t\ttry {\n\t\t\tconst row = storage\n\t\t\t\t.prepare<{ documentClock: number }>(`SELECT documentClock FROM ${prefix}metadata LIMIT 1`)\n\t\t\t\t.all()[0]\n\t\t\t// documentClock exists but could be 0, so we check if the storage is initialized\n\t\t\tif (row && SQLiteSyncStorage.hasBeenInitialized(storage)) {\n\t\t\t\treturn row.documentClock\n\t\t\t}\n\t\t\treturn null\n\t\t} catch (_e) {\n\t\t\treturn null\n\t\t}\n\t}\n\n\t// Prepared statements - created once, reused many times\n\tprivate readonly stmts\n\n\tprivate readonly sql: TLSyncSqliteWrapper\n\n\tconstructor({\n\t\tsql,\n\t\tsnapshot,\n\t\tonChange,\n\t}: {\n\t\tsql: TLSyncSqliteWrapper\n\t\tsnapshot?: RoomSnapshot | StoreSnapshot<R>\n\t\tonChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown\n\t}) {\n\t\tthis.sql = sql\n\t\tconst prefix = sql.config?.tablePrefix ?? ''\n\t\tconst documentsTable = `${prefix}documents`\n\t\tconst tombstonesTable = `${prefix}tombstones`\n\t\tconst metadataTable = `${prefix}metadata`\n\n\t\tmigrateSqliteSyncStorage(this.sql, { documentsTable, tombstonesTable, metadataTable })\n\n\t\t// Prepare all statements once\n\t\tthis.stmts = {\n\t\t\t// Metadata\n\t\t\tgetDocumentClock: this.sql.prepare<{ documentClock: number }>(\n\t\t\t\t`SELECT documentClock FROM ${metadataTable} LIMIT 1`\n\t\t\t),\n\t\t\tgetTombstoneHistoryStartsAtClock: this.sql.prepare<{ tombstoneHistoryStartsAtClock: number }>(\n\t\t\t\t`SELECT tombstoneHistoryStartsAtClock FROM ${metadataTable}`\n\t\t\t),\n\t\t\tgetSchema: this.sql.prepare<{ schema: string }>(`SELECT schema FROM ${metadataTable}`),\n\t\t\tsetSchema: this.sql.prepare<void, [schema: string]>(`UPDATE ${metadataTable} SET schema = ?`),\n\t\t\tsetTombstoneHistoryStartsAtClock: this.sql.prepare<void, [clock: number]>(\n\t\t\t\t`UPDATE ${metadataTable} SET tombstoneHistoryStartsAtClock = ?`\n\t\t\t),\n\t\t\tincrementDocumentClock: this.sql.prepare<void>(\n\t\t\t\t`UPDATE ${metadataTable} SET documentClock = documentClock + 1`\n\t\t\t),\n\n\t\t\t// Documents\n\t\t\tgetDocument: this.sql.prepare<{ state: Uint8Array }, [id: string]>(\n\t\t\t\t`SELECT state FROM ${documentsTable} WHERE id = ?`\n\t\t\t),\n\t\t\tinsertDocument: this.sql.prepare<\n\t\t\t\tvoid,\n\t\t\t\t[id: string, state: Uint8Array, lastChangedClock: number]\n\t\t\t>(`INSERT OR REPLACE INTO ${documentsTable} (id, state, lastChangedClock) VALUES (?, ?, ?)`),\n\t\t\tdeleteDocument: this.sql.prepare<void, [id: string]>(\n\t\t\t\t`DELETE FROM ${documentsTable} WHERE id = ?`\n\t\t\t),\n\t\t\tdocumentExists: this.sql.prepare<{ id: string }, [id: string]>(\n\t\t\t\t`SELECT id FROM ${documentsTable} WHERE id = ?`\n\t\t\t),\n\t\t\titerateDocuments: this.sql.prepare<{ state: Uint8Array; lastChangedClock: number }>(\n\t\t\t\t`SELECT state, lastChangedClock FROM ${documentsTable}`\n\t\t\t),\n\t\t\titerateDocumentEntries: this.sql.prepare<{ id: string; state: Uint8Array }>(\n\t\t\t\t`SELECT id, state FROM ${documentsTable}`\n\t\t\t),\n\t\t\titerateDocumentKeys: this.sql.prepare<{ id: string }>(`SELECT id FROM ${documentsTable}`),\n\t\t\titerateDocumentValues: this.sql.prepare<{ state: Uint8Array }>(\n\t\t\t\t`SELECT state FROM ${documentsTable}`\n\t\t\t),\n\t\t\tgetDocumentsChangedSince: this.sql.prepare<{ state: Uint8Array }, [sinceClock: number]>(\n\t\t\t\t`SELECT state FROM ${documentsTable} WHERE lastChangedClock > ?`\n\t\t\t),\n\n\t\t\t// Tombstones\n\t\t\tinsertTombstone: this.sql.prepare<void, [id: string, clock: number]>(\n\t\t\t\t`INSERT OR REPLACE INTO ${tombstonesTable} (id, clock) VALUES (?, ?)`\n\t\t\t),\n\t\t\tdeleteTombstone: this.sql.prepare<void, [id: string]>(\n\t\t\t\t`DELETE FROM ${tombstonesTable} WHERE id = ?`\n\t\t\t),\n\t\t\tdeleteTombstonesBefore: this.sql.prepare<void, [clock: number]>(\n\t\t\t\t`DELETE FROM ${tombstonesTable} WHERE clock < ?`\n\t\t\t),\n\t\t\tcountTombstones: this.sql.prepare<{ count: number }>(\n\t\t\t\t`SELECT count(*) as count FROM ${tombstonesTable}`\n\t\t\t),\n\t\t\titerateTombstones: this.sql.prepare<{ id: string; clock: number }>(\n\t\t\t\t`SELECT id, clock FROM ${tombstonesTable} ORDER BY clock ASC`\n\t\t\t),\n\t\t\tgetTombstonesChangedSince: this.sql.prepare<{ id: string }, [sinceClock: number]>(\n\t\t\t\t`SELECT id FROM ${tombstonesTable} WHERE clock > ?`\n\t\t\t),\n\n\t\t\t// Initial setup (only used when loading a snapshot)\n\t\t\tupdateMetadata: this.sql.prepare<\n\t\t\t\tvoid,\n\t\t\t\t[documentClock: number, tombstoneHistoryStartsAtClock: number, schema: string]\n\t\t\t>(\n\t\t\t\t`UPDATE ${metadataTable} SET documentClock = ?, tombstoneHistoryStartsAtClock = ?, schema = ?`\n\t\t\t),\n\t\t}\n\n\t\t// Check if we already have data\n\t\tconst hasData = SQLiteSyncStorage.hasBeenInitialized(sql)\n\n\t\tif (snapshot || !hasData) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot ?? DEFAULT_INITIAL_SNAPSHOT)\n\n\t\t\tconst documentClock = snapshot.documentClock ?? snapshot.clock ?? 0\n\t\t\tconst tombstoneHistoryStartsAtClock = snapshot.tombstoneHistoryStartsAtClock ?? documentClock\n\n\t\t\t// Clear existing data\n\t\t\tthis.sql.exec(`\n\t\t\t\tDELETE FROM ${documentsTable};\n\t\t\t\tDELETE FROM ${tombstonesTable};\n\t\t\t`)\n\n\t\t\t// Insert documents\n\t\t\tfor (const doc of snapshot.documents) {\n\t\t\t\tthis.stmts.insertDocument.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock)\n\t\t\t}\n\n\t\t\t// Insert tombstones\n\t\t\tif (snapshot.tombstones) {\n\t\t\t\tfor (const [id, clock] of objectMapEntries(snapshot.tombstones)) {\n\t\t\t\t\tthis.stmts.insertTombstone.run(id, clock)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Insert metadata row\n\t\t\tthis.stmts.updateMetadata.run(\n\t\t\t\tdocumentClock,\n\t\t\t\ttombstoneHistoryStartsAtClock,\n\t\t\t\tJSON.stringify(snapshot.schema)\n\t\t\t)\n\t\t}\n\t\tif (onChange) {\n\t\t\tthis.onChange(onChange)\n\t\t}\n\t}\n\n\tprivate notifier = new MicrotaskNotifier<[TLSyncStorageOnChangeCallbackProps]>()\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => void): () => void {\n\t\treturn this.notifier.register(callback)\n\t}\n\n\ttransaction<T>(\n\t\tcallback: TLSyncStorageTransactionCallback<R, T>,\n\t\topts?: TLSyncStorageTransactionOptions\n\t): TLSyncStorageTransactionResult<T, R> {\n\t\tconst clockBefore = this.getClock()\n\t\tconst trackChanges = opts?.emitChanges === 'always'\n\t\treturn this.sql.transaction(() => {\n\t\t\tconst txn = new SQLiteSyncStorageTransaction<R>(this, this.stmts)\n\t\t\tlet result: T\n\t\t\tlet changes: TLSyncForwardDiff<R> | undefined\n\t\t\ttry {\n\t\t\t\tresult = transaction(() => {\n\t\t\t\t\treturn callback(txn)\n\t\t\t\t}) as T\n\t\t\t\tif (trackChanges) {\n\t\t\t\t\tchanges = txn.getChangesSince(clockBefore)?.diff\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\ttxn.close()\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttypeof result === 'object' &&\n\t\t\t\tresult &&\n\t\t\t\t'then' in result &&\n\t\t\t\ttypeof result.then === 'function'\n\t\t\t) {\n\t\t\t\tthrow new Error('Transaction must return a value, not a promise')\n\t\t\t}\n\n\t\t\tconst clockAfter = this.getClock()\n\t\t\tconst didChange = clockAfter > clockBefore\n\t\t\tif (didChange) {\n\t\t\t\tthis.notifier.notify({ id: opts?.id, documentClock: clockAfter })\n\t\t\t}\n\t\t\treturn { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes }\n\t\t})\n\t}\n\n\tgetClock(): number {\n\t\tconst clockRow = this.stmts.getDocumentClock.all()[0]\n\t\treturn clockRow?.documentClock ?? 0\n\t}\n\n\t/** @internal */\n\t_getTombstoneHistoryStartsAtClock(): number {\n\t\tconst clockRow = this.stmts.getTombstoneHistoryStartsAtClock.all()[0]\n\t\treturn clockRow?.tombstoneHistoryStartsAtClock ?? 0\n\t}\n\n\t/** @internal */\n\t_getSchema(): SerializedSchema {\n\t\tconst clockRow = this.stmts.getSchema.all()[0]\n\t\tassert(clockRow, 'Storage not initialized - clock row missing')\n\t\treturn JSON.parse(clockRow.schema)\n\t}\n\n\t/** @internal */\n\t_setSchema(schema: SerializedSchema): void {\n\t\tthis.stmts.setSchema.run(JSON.stringify(schema))\n\t}\n\n\t/** @internal */\n\tpruneTombstones = throttle(\n\t\t() => {\n\t\t\tconst tombstoneCount = this.stmts.countTombstones.all()[0].count as number\n\t\t\tif (tombstoneCount > MAX_TOMBSTONES) {\n\t\t\t\t// Get all tombstones sorted by clock ascending (oldest first)\n\t\t\t\tconst tombstones = this.stmts.iterateTombstones.all()\n\n\t\t\t\tconst result = computeTombstonePruning({ tombstones, documentClock: this.getClock() })\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.stmts.setTombstoneHistoryStartsAtClock.run(result.newTombstoneHistoryStartsAtClock)\n\t\t\t\t\t// Delete all tombstones with clock < newTombstoneHistoryStartsAtClock in one operation.\n\t\t\t\t\t// This works because computeTombstonePruning ensures we never split a clock value.\n\t\t\t\t\tthis.stmts.deleteTombstonesBefore.run(result.newTombstoneHistoryStartsAtClock)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t1000,\n\t\t// prevent this from running synchronously to avoid blocking requests\n\t\t{ leading: false }\n\t)\n\n\tgetSnapshot(): RoomSnapshot {\n\t\treturn {\n\t\t\ttombstoneHistoryStartsAtClock: this._getTombstoneHistoryStartsAtClock(),\n\t\t\tdocumentClock: this.getClock(),\n\t\t\tdocuments: Array.from(this._iterateDocuments()),\n\t\t\ttombstones: Object.fromEntries(this._iterateTombstones()),\n\t\t\tschema: this._getSchema(),\n\t\t}\n\t}\n\tprivate *_iterateDocuments(): IterableIterator<{ state: R; lastChangedClock: number }> {\n\t\tfor (const row of this.stmts.iterateDocuments.iterate()) {\n\t\t\tyield { state: decodeState<R>(row.state), lastChangedClock: row.lastChangedClock }\n\t\t}\n\t}\n\n\tprivate *_iterateTombstones(): IterableIterator<[string, number]> {\n\t\tfor (const row of this.stmts.iterateTombstones.iterate()) {\n\t\t\tyield [row.id, row.clock]\n\t\t}\n\t}\n}\n\n/**\n * Transaction implementation for SQLiteSyncStorage.\n * Provides access to documents, tombstones, and metadata within a transaction.\n *\n * @internal\n */\nclass SQLiteSyncStorageTransaction<R extends UnknownRecord> implements TLSyncStorageTransaction<R> {\n\tprivate _clock: number\n\tprivate _closed = false\n\tprivate _didIncrementClock: boolean = false\n\n\tconstructor(\n\t\tprivate storage: SQLiteSyncStorage<R>,\n\t\tprivate stmts: SQLiteSyncStorage<R>['stmts']\n\t) {\n\t\tthis._clock = this.storage.getClock()\n\t}\n\n\t/** @internal */\n\tclose() {\n\t\tthis._closed = true\n\t}\n\n\tprivate assertNotClosed() {\n\t\tassert(!this._closed, 'Transaction has ended, iterator cannot be consumed')\n\t}\n\n\tgetClock(): number {\n\t\treturn this._clock\n\t}\n\n\tprivate getNextClock(): number {\n\t\tif (!this._didIncrementClock) {\n\t\t\tthis._didIncrementClock = true\n\t\t\tthis.stmts.incrementDocumentClock.run()\n\t\t\tthis._clock = this.storage.getClock()\n\t\t}\n\t\treturn this._clock\n\t}\n\n\tget(id: string): R | undefined {\n\t\tthis.assertNotClosed()\n\t\tconst row = this.stmts.getDocument.all(id)[0]\n\t\tif (!row) return undefined\n\t\treturn decodeState<R>(row.state)\n\t}\n\n\tset(id: string, record: R): void {\n\t\tthis.assertNotClosed()\n\t\tassert(id === record.id, `Record id mismatch: key does not match record.id`)\n\t\tconst clock = this.getNextClock()\n\t\t// Automatically clear tombstone if it exists\n\t\tthis.stmts.deleteTombstone.run(id)\n\t\tthis.stmts.insertDocument.run(id, encodeState(record), clock)\n\t}\n\n\tdelete(id: string): void {\n\t\tthis.assertNotClosed()\n\t\t// Only create a tombstone if the record actually exists\n\t\tconst exists = this.stmts.documentExists.all(id)[0]\n\t\tif (!exists) return\n\t\tconst clock = this.getNextClock()\n\t\tthis.stmts.deleteDocument.run(id)\n\t\tthis.stmts.insertTombstone.run(id, clock)\n\t\tthis.storage.pruneTombstones()\n\t}\n\n\t*entries(): IterableIterator<[string, R]> {\n\t\tthis.assertNotClosed()\n\t\tfor (const row of this.stmts.iterateDocumentEntries.iterate()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield [row.id, decodeState<R>(row.state)]\n\t\t}\n\t}\n\n\t*keys(): IterableIterator<string> {\n\t\tthis.assertNotClosed()\n\t\tfor (const row of this.stmts.iterateDocumentKeys.iterate()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield row.id\n\t\t}\n\t}\n\n\t*values(): IterableIterator<R> {\n\t\tthis.assertNotClosed()\n\t\tfor (const row of this.stmts.iterateDocumentValues.iterate()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield decodeState<R>(row.state)\n\t\t}\n\t}\n\n\tgetSchema(): SerializedSchema {\n\t\tthis.assertNotClosed()\n\t\treturn this.storage._getSchema()\n\t}\n\n\tsetSchema(schema: SerializedSchema): void {\n\t\tthis.assertNotClosed()\n\t\tthis.storage._setSchema(schema)\n\t}\n\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined {\n\t\tthis.assertNotClosed()\n\t\tconst clock = this.storage.getClock()\n\t\tif (sinceClock === clock) return undefined\n\t\tif (sinceClock > clock) {\n\t\t\t// something went wrong, wipe the slate clean\n\t\t\tsinceClock = -1\n\t\t}\n\t\tconst diff: TLSyncForwardDiff<R> = { puts: {}, deletes: [] }\n\t\tconst wipeAll = sinceClock < this.storage._getTombstoneHistoryStartsAtClock()\n\n\t\tif (wipeAll) {\n\t\t\t// If wipeAll, include all documents\n\t\t\tfor (const row of this.stmts.iterateDocumentValues.iterate()) {\n\t\t\t\tconst state = decodeState<R>(row.state)\n\t\t\t\tdiff.puts[state.id] = state\n\t\t\t}\n\t\t} else {\n\t\t\t// Get documents changed since clock\n\t\t\tfor (const row of this.stmts.getDocumentsChangedSince.iterate(sinceClock)) {\n\t\t\t\tconst state = decodeState<R>(row.state)\n\t\t\t\tdiff.puts[state.id] = state\n\t\t\t}\n\t\t\t// When wipeAll, deletes are redundant (full state is in puts). Only include tombstones otherwise.\n\t\t\tfor (const row of this.stmts.getTombstonesChangedSince.iterate(sinceClock)) {\n\t\t\t\tdiff.deletes.push(row.id)\n\t\t\t}\n\t\t}\n\n\t\treturn { diff, wipeAll }\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { transaction } from '@tldraw/state'\nimport { SerializedSchema, StoreSnapshot, UnknownRecord } from '@tldraw/store'\nimport { assert, objectMapEntries, throttle } from '@tldraw/utils'\nimport {\n\tcomputeTombstonePruning,\n\tDEFAULT_INITIAL_SNAPSHOT,\n\tMAX_TOMBSTONES,\n} from './InMemorySyncStorage'\nimport { MicrotaskNotifier } from './MicrotaskNotifier'\nimport { RoomSnapshot } from './TLSyncRoom'\nimport {\n\tconvertStoreSnapshotToRoomSnapshot,\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n\tTLSyncStorageGetChangesSinceResult,\n\tTLSyncStorageOnChangeCallbackProps,\n\tTLSyncStorageTransaction,\n\tTLSyncStorageTransactionCallback,\n\tTLSyncStorageTransactionOptions,\n\tTLSyncStorageTransactionResult,\n} from './TLSyncStorage'\n\n/**\n * Valid input value types for SQLite query parameters.\n * These are the types that can be passed as bindings to prepared statements.\n * @public\n */\nexport type TLSqliteInputValue = null | number | bigint | string | Uint8Array\n\n/**\n * Possible output value types returned from SQLite queries.\n * Includes all input types plus Uint8Array for BLOB columns.\n * @public\n */\nexport type TLSqliteOutputValue = null | number | bigint | string | Uint8Array\n\n/**\n * A row returned from a SQLite query, mapping column names to their values.\n * @public\n */\nexport type TLSqliteRow = Record<string, TLSqliteOutputValue>\n\n/**\n * A prepared statement that can be executed multiple times with different bindings.\n * @public\n */\nexport interface TLSyncSqliteStatement<\n\tTResult extends TLSqliteRow | void,\n\tTParams extends TLSqliteInputValue[] = [],\n> {\n\t/** Execute the statement and iterate over results one at a time */\n\titerate(...bindings: TParams): IterableIterator<TResult>\n\t/** Execute the statement and return all results as an array */\n\tall(...bindings: TParams): TResult[]\n\t/** Execute the statement without returning results (for DML) */\n\trun(...bindings: TParams): void\n}\n\n/**\n * Configuration for SQLiteSyncStorage.\n * @public\n */\nexport interface TLSyncSqliteWrapperConfig {\n\t/** Prefix for all table names (default: ''). E.g. 'sync_' creates tables 'sync_documents', 'sync_tombstones', 'sync_metadata' */\n\ttablePrefix?: string\n}\n\n/**\n * Interface for SQLite storage with prepare, exec and transaction capabilities.\n * @public\n */\nexport interface TLSyncSqliteWrapper {\n\t/** Optional configuration for table names. If not provided, defaults are used. */\n\treadonly config?: TLSyncSqliteWrapperConfig\n\t/** Prepare a SQL statement for execution */\n\tprepare<TResult extends TLSqliteRow | void, TParams extends TLSqliteInputValue[] = []>(\n\t\tsql: string\n\t): TLSyncSqliteStatement<TResult, TParams>\n\t/** Execute raw SQL (for DDL, multi-statement scripts) */\n\texec(sql: string): void\n\t/** Execute a callback within a transaction */\n\ttransaction<T>(callback: () => T): T\n}\n\nexport function migrateSqliteSyncStorage(\n\tstorage: TLSyncSqliteWrapper,\n\t{\n\t\tdocumentsTable = 'documents',\n\t\tobjectsTable = 'objects',\n\t\ttombstonesTable = 'tombstones',\n\t\tmetadataTable = 'metadata',\n\t}: {\n\t\tdocumentsTable?: string\n\t\tobjectsTable?: string\n\t\ttombstonesTable?: string\n\t\tmetadataTable?: string\n\t} = {}\n): void {\n\tlet migrationVersion = 0\n\ttry {\n\t\tconst row = storage\n\t\t\t.prepare<{\n\t\t\t\tmigrationVersion: number\n\t\t\t}>(`SELECT migrationVersion FROM ${metadataTable} LIMIT 1`)\n\t\t\t.all()[0]\n\t\tmigrationVersion = row?.migrationVersion ?? 0\n\t} catch (_e) {\n\t\t// noop\n\t}\n\n\tif (migrationVersion === 0) {\n\t\tmigrationVersion++\n\t\tstorage.exec(`\n\t\t\tCREATE TABLE ${documentsTable} (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tstate BLOB NOT NULL,\n\t\t\t\tlastChangedClock INTEGER NOT NULL\n\t\t\t);\n\n\t\t\tCREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);\n\n\t\t\tCREATE TABLE ${tombstonesTable} (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tclock INTEGER NOT NULL\n\t\t\t);\n\t\t\tCREATE INDEX idx_${tombstonesTable}_clock ON ${tombstonesTable}(clock);\n\n\t\t\t-- This table is used to store the metadata for the sync storage.\n\t\t\t-- There should only be one row in this table.\n\t\t\tCREATE TABLE ${metadataTable} (\n\t\t\t migrationVersion INTEGER NOT NULL,\n\t\t\t\tdocumentClock INTEGER NOT NULL,\n\t\t\t\ttombstoneHistoryStartsAtClock INTEGER NOT NULL,\n\t\t\t\tschema TEXT NOT NULL\n\t\t\t);\n\t\t\t\n\t\t\tINSERT INTO ${metadataTable} (migrationVersion, documentClock, tombstoneHistoryStartsAtClock, schema) VALUES (2, 0, 0, '')\n\t\t`)\n\t\t// Skip migration 2 since we created the table with BLOB already\n\t\tmigrationVersion++\n\t}\n\n\tif (migrationVersion === 1) {\n\t\t// Migration 2: Convert state column from TEXT to BLOB\n\t\t// SQLite doesn't support ALTER COLUMN, so we need to recreate the table\n\t\tmigrationVersion++\n\t\tstorage.exec(`\n\t\t\tCREATE TABLE ${documentsTable}_new (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tstate BLOB NOT NULL,\n\t\t\t\tlastChangedClock INTEGER NOT NULL\n\t\t\t);\n\n\t\t\tINSERT INTO ${documentsTable}_new (id, state, lastChangedClock)\n\t\t\tSELECT id, CAST(state AS BLOB), lastChangedClock FROM ${documentsTable};\n\n\t\t\tDROP TABLE ${documentsTable};\n\n\t\t\tALTER TABLE ${documentsTable}_new RENAME TO ${documentsTable};\n\n\t\t\tCREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);\n\t\t`)\n\t}\n\n\tif (migrationVersion === 2) {\n\t\t// Migration 3: Add the object-store lane table. Object-lane records (e.g. comments) are\n\t\t// partitioned out of the documents table so the document snapshot never contains them.\n\t\t// They share the documents' clock and tombstones.\n\t\tmigrationVersion++\n\t\tstorage.exec(`\n\t\t\tCREATE TABLE ${objectsTable} (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tstate BLOB NOT NULL,\n\t\t\t\tlastChangedClock INTEGER NOT NULL\n\t\t\t);\n\n\t\t\tCREATE INDEX idx_${objectsTable}_lastChangedClock ON ${objectsTable}(lastChangedClock);\n\t\t`)\n\t}\n\n\t// add more migrations here if and when needed\n\n\tstorage.exec(`UPDATE ${metadataTable} SET migrationVersion = ${migrationVersion}`)\n}\n\nconst textEncoder = new TextEncoder()\nconst textDecoder = new TextDecoder()\n\nfunction encodeState(state: unknown): Uint8Array {\n\treturn textEncoder.encode(JSON.stringify(state))\n}\n\nfunction decodeState<T>(state: Uint8Array): T {\n\treturn JSON.parse(textDecoder.decode(state))\n}\n\n/**\n * SQLite-based implementation of TLSyncStorage.\n * Stores documents, tombstones, metadata, and clock values in SQLite tables.\n *\n * This storage backend provides persistent synchronization state that survives\n * process restarts, unlike InMemorySyncStorage which loses data when the process ends.\n *\n * @example\n * ```ts\n * // With Cloudflare Durable Objects\n * import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'\n *\n * const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // With Node.js sqlite (Node 22.5+)\n * import { DatabaseSync } from 'node:sqlite'\n * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'\n *\n * const db = new DatabaseSync('sync-state.db')\n * const sql = new NodeSqliteWrapper(db)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // Initialize with an existing snapshot\n * const storage = new SQLiteSyncStorage({ sql, snapshot: existingSnapshot })\n * ```\n *\n * @public\n */\nexport class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {\n\t/**\n\t * Check if the storage has been initialized (has data in the clock table).\n\t * Useful for determining whether to load from an external source on first access.\n\t */\n\tstatic hasBeenInitialized(storage: TLSyncSqliteWrapper): boolean {\n\t\tconst prefix = storage.config?.tablePrefix ?? ''\n\t\ttry {\n\t\t\tconst schema = storage\n\t\t\t\t.prepare<{ schema: string }>(`SELECT schema FROM ${prefix}metadata LIMIT 1`)\n\t\t\t\t.all()[0]?.schema\n\t\t\treturn !!schema\n\t\t} catch (_e) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Get the current document clock value from storage without fully initializing.\n\t * Returns null if storage has not been initialized.\n\t * Useful for comparing storage freshness against external sources.\n\t */\n\tstatic getDocumentClock(storage: TLSyncSqliteWrapper): number | null {\n\t\tconst prefix = storage.config?.tablePrefix ?? ''\n\t\ttry {\n\t\t\tconst row = storage\n\t\t\t\t.prepare<{ documentClock: number }>(`SELECT documentClock FROM ${prefix}metadata LIMIT 1`)\n\t\t\t\t.all()[0]\n\t\t\t// documentClock exists but could be 0, so we check if the storage is initialized\n\t\t\tif (row && SQLiteSyncStorage.hasBeenInitialized(storage)) {\n\t\t\t\treturn row.documentClock\n\t\t\t}\n\t\t\treturn null\n\t\t} catch (_e) {\n\t\t\treturn null\n\t\t}\n\t}\n\n\t// Prepared statements - created once, reused many times\n\tprivate readonly stmts\n\n\tprivate readonly sql: TLSyncSqliteWrapper\n\n\t/** @internal */\n\treadonly objectTypes: ReadonlySet<string>\n\n\tconstructor({\n\t\tsql,\n\t\tsnapshot,\n\t\tobjectTypes,\n\t\tonChange,\n\t}: {\n\t\tsql: TLSyncSqliteWrapper\n\t\tsnapshot?: RoomSnapshot | StoreSnapshot<R>\n\t\t/**\n\t\t * Record type names stored in the object-store lane. Records of these types are routed to\n\t\t * a separate table: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.\n\t\t * They share the documents' clock, tombstones, and transactions.\n\t\t */\n\t\tobjectTypes?: readonly string[]\n\t\tonChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown\n\t}) {\n\t\tthis.sql = sql\n\t\tthis.objectTypes = new Set(objectTypes ?? [])\n\t\tconst prefix = sql.config?.tablePrefix ?? ''\n\t\tconst documentsTable = `${prefix}documents`\n\t\tconst objectsTable = `${prefix}objects`\n\t\tconst tombstonesTable = `${prefix}tombstones`\n\t\tconst metadataTable = `${prefix}metadata`\n\n\t\tmigrateSqliteSyncStorage(this.sql, {\n\t\t\tdocumentsTable,\n\t\t\tobjectsTable,\n\t\t\ttombstonesTable,\n\t\t\tmetadataTable,\n\t\t})\n\n\t\t// Both record tables have identical shape; prepare the same statements for each.\n\t\tconst makeRecordTableStmts = (table: string) => ({\n\t\t\tget: this.sql.prepare<{ state: Uint8Array }, [id: string]>(\n\t\t\t\t`SELECT state FROM ${table} WHERE id = ?`\n\t\t\t),\n\t\t\tinsert: this.sql.prepare<void, [id: string, state: Uint8Array, lastChangedClock: number]>(\n\t\t\t\t`INSERT OR REPLACE INTO ${table} (id, state, lastChangedClock) VALUES (?, ?, ?)`\n\t\t\t),\n\t\t\tdelete: this.sql.prepare<void, [id: string]>(`DELETE FROM ${table} WHERE id = ?`),\n\t\t\texists: this.sql.prepare<{ id: string }, [id: string]>(\n\t\t\t\t`SELECT id FROM ${table} WHERE id = ?`\n\t\t\t),\n\t\t\titerate: this.sql.prepare<{ state: Uint8Array; lastChangedClock: number }>(\n\t\t\t\t`SELECT state, lastChangedClock FROM ${table}`\n\t\t\t),\n\t\t\titerateEntries: this.sql.prepare<{ id: string; state: Uint8Array }>(\n\t\t\t\t`SELECT id, state FROM ${table}`\n\t\t\t),\n\t\t\titerateKeys: this.sql.prepare<{ id: string }>(`SELECT id FROM ${table}`),\n\t\t\titerateValues: this.sql.prepare<{ state: Uint8Array }>(`SELECT state FROM ${table}`),\n\t\t\tchangedSince: this.sql.prepare<{ state: Uint8Array }, [sinceClock: number]>(\n\t\t\t\t`SELECT state FROM ${table} WHERE lastChangedClock > ?`\n\t\t\t),\n\t\t})\n\n\t\t// Prepare all statements once\n\t\tthis.stmts = {\n\t\t\t// Metadata\n\t\t\tgetDocumentClock: this.sql.prepare<{ documentClock: number }>(\n\t\t\t\t`SELECT documentClock FROM ${metadataTable} LIMIT 1`\n\t\t\t),\n\t\t\tgetTombstoneHistoryStartsAtClock: this.sql.prepare<{ tombstoneHistoryStartsAtClock: number }>(\n\t\t\t\t`SELECT tombstoneHistoryStartsAtClock FROM ${metadataTable}`\n\t\t\t),\n\t\t\tgetSchema: this.sql.prepare<{ schema: string }>(`SELECT schema FROM ${metadataTable}`),\n\t\t\tsetSchema: this.sql.prepare<void, [schema: string]>(`UPDATE ${metadataTable} SET schema = ?`),\n\t\t\tsetTombstoneHistoryStartsAtClock: this.sql.prepare<void, [clock: number]>(\n\t\t\t\t`UPDATE ${metadataTable} SET tombstoneHistoryStartsAtClock = ?`\n\t\t\t),\n\t\t\tincrementDocumentClock: this.sql.prepare<void>(\n\t\t\t\t`UPDATE ${metadataTable} SET documentClock = documentClock + 1`\n\t\t\t),\n\n\t\t\t// Record tables (document lane + object-store lane)\n\t\t\tdocuments: makeRecordTableStmts(documentsTable),\n\t\t\tobjects: makeRecordTableStmts(objectsTable),\n\n\t\t\t// Tombstones\n\t\t\tinsertTombstone: this.sql.prepare<void, [id: string, clock: number]>(\n\t\t\t\t`INSERT OR REPLACE INTO ${tombstonesTable} (id, clock) VALUES (?, ?)`\n\t\t\t),\n\t\t\tdeleteTombstone: this.sql.prepare<void, [id: string]>(\n\t\t\t\t`DELETE FROM ${tombstonesTable} WHERE id = ?`\n\t\t\t),\n\t\t\tdeleteTombstonesBefore: this.sql.prepare<void, [clock: number]>(\n\t\t\t\t`DELETE FROM ${tombstonesTable} WHERE clock < ?`\n\t\t\t),\n\t\t\tcountTombstones: this.sql.prepare<{ count: number }>(\n\t\t\t\t`SELECT count(*) as count FROM ${tombstonesTable}`\n\t\t\t),\n\t\t\titerateTombstones: this.sql.prepare<{ id: string; clock: number }>(\n\t\t\t\t`SELECT id, clock FROM ${tombstonesTable} ORDER BY clock ASC`\n\t\t\t),\n\t\t\tgetTombstonesChangedSince: this.sql.prepare<{ id: string }, [sinceClock: number]>(\n\t\t\t\t`SELECT id FROM ${tombstonesTable} WHERE clock > ?`\n\t\t\t),\n\n\t\t\t// Initial setup (only used when loading a snapshot)\n\t\t\tupdateMetadata: this.sql.prepare<\n\t\t\t\tvoid,\n\t\t\t\t[documentClock: number, tombstoneHistoryStartsAtClock: number, schema: string]\n\t\t\t>(\n\t\t\t\t`UPDATE ${metadataTable} SET documentClock = ?, tombstoneHistoryStartsAtClock = ?, schema = ?`\n\t\t\t),\n\t\t}\n\n\t\t// Check if we already have data\n\t\tconst hasData = SQLiteSyncStorage.hasBeenInitialized(sql)\n\n\t\tif (snapshot || !hasData) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot ?? DEFAULT_INITIAL_SNAPSHOT)\n\n\t\t\tconst documentClock = snapshot.documentClock ?? snapshot.clock ?? 0\n\t\t\tconst tombstoneHistoryStartsAtClock = snapshot.tombstoneHistoryStartsAtClock ?? documentClock\n\n\t\t\t// Clear existing data\n\t\t\tthis.sql.exec(`\n\t\t\t\tDELETE FROM ${documentsTable};\n\t\t\t\tDELETE FROM ${objectsTable};\n\t\t\t\tDELETE FROM ${tombstonesTable};\n\t\t\t`)\n\n\t\t\t// Insert records, routing object-lane types into their partition (a seed snapshot may\n\t\t\t// carry object-lane records merged in, e.g. loaded from a separate persistence lane)\n\t\t\tfor (const doc of snapshot.documents) {\n\t\t\t\tconst table = this.objectTypes.has(doc.state.typeName)\n\t\t\t\t\t? this.stmts.objects\n\t\t\t\t\t: this.stmts.documents\n\t\t\t\ttable.insert.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock)\n\t\t\t}\n\n\t\t\t// Insert tombstones\n\t\t\tif (snapshot.tombstones) {\n\t\t\t\tfor (const [id, clock] of objectMapEntries(snapshot.tombstones)) {\n\t\t\t\t\tthis.stmts.insertTombstone.run(id, clock)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Insert metadata row\n\t\t\tthis.stmts.updateMetadata.run(\n\t\t\t\tdocumentClock,\n\t\t\t\ttombstoneHistoryStartsAtClock,\n\t\t\t\tJSON.stringify(snapshot.schema)\n\t\t\t)\n\t\t} else {\n\t\t\t// One-time sweep: rooms written before the objects table existed may have object-lane\n\t\t\t// records sitting in the documents table. Record ids are typeName-prefixed, so a PK\n\t\t\t// range scan per object type moves them cheaply (no-op once clean).\n\t\t\tfor (const typeName of this.objectTypes) {\n\t\t\t\tconst lo = `${typeName}:`\n\t\t\t\t// ';' is the next code point after ':' \u2014 [lo, hi) covers exactly the `type:` prefix\n\t\t\t\tconst hi = `${typeName};`\n\t\t\t\tthis.sql.exec(`\n\t\t\t\t\tINSERT OR REPLACE INTO ${objectsTable} (id, state, lastChangedClock)\n\t\t\t\t\tSELECT id, state, lastChangedClock FROM ${documentsTable}\n\t\t\t\t\tWHERE id >= '${lo}' AND id < '${hi}';\n\t\t\t\t\tDELETE FROM ${documentsTable} WHERE id >= '${lo}' AND id < '${hi}';\n\t\t\t\t`)\n\t\t\t}\n\t\t}\n\t\tif (onChange) {\n\t\t\tthis.onChange(onChange)\n\t\t}\n\t}\n\n\tprivate notifier = new MicrotaskNotifier<[TLSyncStorageOnChangeCallbackProps]>()\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => void): () => void {\n\t\treturn this.notifier.register(callback)\n\t}\n\n\ttransaction<T>(\n\t\tcallback: TLSyncStorageTransactionCallback<R, T>,\n\t\topts?: TLSyncStorageTransactionOptions\n\t): TLSyncStorageTransactionResult<T, R> {\n\t\tconst clockBefore = this.getClock()\n\t\tconst trackChanges = opts?.emitChanges === 'always'\n\t\treturn this.sql.transaction(() => {\n\t\t\tconst txn = new SQLiteSyncStorageTransaction<R>(this, this.stmts)\n\t\t\tlet result: T\n\t\t\tlet changes: TLSyncForwardDiff<R> | undefined\n\t\t\ttry {\n\t\t\t\tresult = transaction(() => {\n\t\t\t\t\treturn callback(txn)\n\t\t\t\t}) as T\n\t\t\t\tif (trackChanges) {\n\t\t\t\t\tchanges = txn.getChangesSince(clockBefore)?.diff\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\ttxn.close()\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttypeof result === 'object' &&\n\t\t\t\tresult &&\n\t\t\t\t'then' in result &&\n\t\t\t\ttypeof result.then === 'function'\n\t\t\t) {\n\t\t\t\tthrow new Error('Transaction must return a value, not a promise')\n\t\t\t}\n\n\t\t\tconst clockAfter = this.getClock()\n\t\t\tconst didChange = clockAfter > clockBefore\n\t\t\tif (didChange) {\n\t\t\t\tthis.notifier.notify({ id: opts?.id, documentClock: clockAfter })\n\t\t\t}\n\t\t\treturn { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes }\n\t\t})\n\t}\n\n\tgetClock(): number {\n\t\tconst clockRow = this.stmts.getDocumentClock.all()[0]\n\t\treturn clockRow?.documentClock ?? 0\n\t}\n\n\t/** @internal */\n\t_getTombstoneHistoryStartsAtClock(): number {\n\t\tconst clockRow = this.stmts.getTombstoneHistoryStartsAtClock.all()[0]\n\t\treturn clockRow?.tombstoneHistoryStartsAtClock ?? 0\n\t}\n\n\t/** @internal */\n\t_getSchema(): SerializedSchema {\n\t\tconst clockRow = this.stmts.getSchema.all()[0]\n\t\tassert(clockRow, 'Storage not initialized - clock row missing')\n\t\treturn JSON.parse(clockRow.schema)\n\t}\n\n\t/** @internal */\n\t_setSchema(schema: SerializedSchema): void {\n\t\tthis.stmts.setSchema.run(JSON.stringify(schema))\n\t}\n\n\t/** @internal */\n\tpruneTombstones = throttle(\n\t\t() => {\n\t\t\tconst tombstoneCount = this.stmts.countTombstones.all()[0].count as number\n\t\t\tif (tombstoneCount > MAX_TOMBSTONES) {\n\t\t\t\t// Get all tombstones sorted by clock ascending (oldest first)\n\t\t\t\tconst tombstones = this.stmts.iterateTombstones.all()\n\n\t\t\t\tconst result = computeTombstonePruning({ tombstones, documentClock: this.getClock() })\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.stmts.setTombstoneHistoryStartsAtClock.run(result.newTombstoneHistoryStartsAtClock)\n\t\t\t\t\t// Delete all tombstones with clock < newTombstoneHistoryStartsAtClock in one operation.\n\t\t\t\t\t// This works because computeTombstonePruning ensures we never split a clock value.\n\t\t\t\t\tthis.stmts.deleteTombstonesBefore.run(result.newTombstoneHistoryStartsAtClock)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t1000,\n\t\t// prevent this from running synchronously to avoid blocking requests\n\t\t{ leading: false }\n\t)\n\n\tgetSnapshot(): RoomSnapshot {\n\t\t// object-lane records live in their own table, so the document snapshot is\n\t\t// pure-document by construction\n\t\treturn {\n\t\t\ttombstoneHistoryStartsAtClock: this._getTombstoneHistoryStartsAtClock(),\n\t\t\tdocumentClock: this.getClock(),\n\t\t\tdocuments: Array.from(this._iterateRecords(this.stmts.documents)),\n\t\t\ttombstones: Object.fromEntries(this._iterateTombstones()),\n\t\t\tschema: this._getSchema(),\n\t\t}\n\t}\n\n\tgetObjectsSnapshot(): RoomSnapshot['documents'] {\n\t\treturn Array.from(this._iterateRecords(this.stmts.objects))\n\t}\n\n\tprivate *_iterateRecords(\n\t\ttable: SQLiteSyncStorage<R>['stmts']['documents']\n\t): IterableIterator<{ state: R; lastChangedClock: number }> {\n\t\tfor (const row of table.iterate.iterate()) {\n\t\t\tyield { state: decodeState<R>(row.state), lastChangedClock: row.lastChangedClock }\n\t\t}\n\t}\n\n\tprivate *_iterateTombstones(): IterableIterator<[string, number]> {\n\t\tfor (const row of this.stmts.iterateTombstones.iterate()) {\n\t\t\tyield [row.id, row.clock]\n\t\t}\n\t}\n}\n\n/**\n * Transaction implementation for SQLiteSyncStorage.\n * Provides access to documents, tombstones, and metadata within a transaction.\n *\n * @internal\n */\nclass SQLiteSyncStorageTransaction<R extends UnknownRecord> implements TLSyncStorageTransaction<R> {\n\tprivate _clock: number\n\tprivate _closed = false\n\tprivate _didIncrementClock: boolean = false\n\n\tconstructor(\n\t\tprivate storage: SQLiteSyncStorage<R>,\n\t\tprivate stmts: SQLiteSyncStorage<R>['stmts']\n\t) {\n\t\tthis._clock = this.storage.getClock()\n\t}\n\n\t/** @internal */\n\tclose() {\n\t\tthis._closed = true\n\t}\n\n\tprivate assertNotClosed() {\n\t\tassert(!this._closed, 'Transaction has ended, iterator cannot be consumed')\n\t}\n\n\tgetClock(): number {\n\t\treturn this._clock\n\t}\n\n\tprivate getNextClock(): number {\n\t\tif (!this._didIncrementClock) {\n\t\t\tthis._didIncrementClock = true\n\t\t\tthis.stmts.incrementDocumentClock.run()\n\t\t\tthis._clock = this.storage.getClock()\n\t\t}\n\t\treturn this._clock\n\t}\n\n\t/**\n\t * Which record table an id belongs to. Record ids are typeName-prefixed (`comment:abc`),\n\t * so the partition is derivable from the id alone.\n\t */\n\tprivate tableFor(id: string) {\n\t\tconst sep = id.indexOf(':')\n\t\tif (sep === -1) return this.stmts.documents\n\t\treturn this.storage.objectTypes.has(id.slice(0, sep))\n\t\t\t? this.stmts.objects\n\t\t\t: this.stmts.documents\n\t}\n\n\tget(id: string): R | undefined {\n\t\tthis.assertNotClosed()\n\t\tconst row = this.tableFor(id).get.all(id)[0]\n\t\tif (!row) return undefined\n\t\treturn decodeState<R>(row.state)\n\t}\n\n\tset(id: string, record: R): void {\n\t\tthis.assertNotClosed()\n\t\tassert(id === record.id, `Record id mismatch: key does not match record.id`)\n\t\tconst clock = this.getNextClock()\n\t\t// Automatically clear tombstone if it exists\n\t\tthis.stmts.deleteTombstone.run(id)\n\t\t// route by type: object-lane records live in their own table\n\t\tconst table = this.storage.objectTypes.has(record.typeName)\n\t\t\t? this.stmts.objects\n\t\t\t: this.stmts.documents\n\t\ttable.insert.run(id, encodeState(record), clock)\n\t}\n\n\tdelete(id: string): void {\n\t\tthis.assertNotClosed()\n\t\tconst table = this.tableFor(id)\n\t\t// Only create a tombstone if the record actually exists\n\t\tconst exists = table.exists.all(id)[0]\n\t\tif (!exists) return\n\t\tconst clock = this.getNextClock()\n\t\ttable.delete.run(id)\n\t\tthis.stmts.insertTombstone.run(id, clock)\n\t\tthis.storage.pruneTombstones()\n\t}\n\n\t// iteration spans both record tables so schema migrations cover object-lane records too\n\n\t*entries(): IterableIterator<[string, R]> {\n\t\tthis.assertNotClosed()\n\t\tfor (const table of [this.stmts.documents, this.stmts.objects]) {\n\t\t\tfor (const row of table.iterateEntries.iterate()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield [row.id, decodeState<R>(row.state)]\n\t\t\t}\n\t\t}\n\t}\n\n\t*keys(): IterableIterator<string> {\n\t\tthis.assertNotClosed()\n\t\tfor (const table of [this.stmts.documents, this.stmts.objects]) {\n\t\t\tfor (const row of table.iterateKeys.iterate()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield row.id\n\t\t\t}\n\t\t}\n\t}\n\n\t*values(): IterableIterator<R> {\n\t\tthis.assertNotClosed()\n\t\tfor (const table of [this.stmts.documents, this.stmts.objects]) {\n\t\t\tfor (const row of table.iterateValues.iterate()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield decodeState<R>(row.state)\n\t\t\t}\n\t\t}\n\t}\n\n\tgetSchema(): SerializedSchema {\n\t\tthis.assertNotClosed()\n\t\treturn this.storage._getSchema()\n\t}\n\n\tsetSchema(schema: SerializedSchema): void {\n\t\tthis.assertNotClosed()\n\t\tthis.storage._setSchema(schema)\n\t}\n\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined {\n\t\tthis.assertNotClosed()\n\t\tconst clock = this.storage.getClock()\n\t\tif (sinceClock === clock) return undefined\n\t\tif (sinceClock > clock) {\n\t\t\t// something went wrong, wipe the slate clean\n\t\t\tsinceClock = -1\n\t\t}\n\t\tconst diff: TLSyncForwardDiff<R> = { puts: {}, deletes: [] }\n\t\tconst wipeAll = sinceClock < this.storage._getTombstoneHistoryStartsAtClock()\n\n\t\t// both record tables share the clock and tombstones, so the change feed spans both\n\t\tif (wipeAll) {\n\t\t\t// If wipeAll, include all records\n\t\t\tfor (const table of [this.stmts.documents, this.stmts.objects]) {\n\t\t\t\tfor (const row of table.iterateValues.iterate()) {\n\t\t\t\t\tconst state = decodeState<R>(row.state)\n\t\t\t\t\tdiff.puts[state.id] = state\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// Get records changed since clock\n\t\t\tfor (const table of [this.stmts.documents, this.stmts.objects]) {\n\t\t\t\tfor (const row of table.changedSince.iterate(sinceClock)) {\n\t\t\t\t\tconst state = decodeState<R>(row.state)\n\t\t\t\t\tdiff.puts[state.id] = state\n\t\t\t\t}\n\t\t\t}\n\t\t\t// When wipeAll, deletes are redundant (full state is in puts). Only include tombstones otherwise.\n\t\t\tfor (const row of this.stmts.getTombstonesChangedSince.iterate(sinceClock)) {\n\t\t\t\tdiff.deletes.push(row.id)\n\t\t\t}\n\t\t}\n\n\t\treturn { diff, wipeAll }\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -43,7 +43,9 @@ class TLSocketRoom {
43
43
  snapshot: convertStoreSnapshotToRoomSnapshot(
44
44
  // eslint-disable-next-line @typescript-eslint/no-deprecated
45
45
  opts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT
46
- )
46
+ ),
47
+ // keep the storage partition in step with the room's object lane
48
+ objectTypes: opts.objectTypes
47
49
  });
48
50
  if ("onDataChange" in opts && opts.onDataChange) {
49
51
  this.disposables.add(
@@ -54,6 +56,9 @@ class TLSocketRoom {
54
56
  }
55
57
  this.room = new TLSyncRoom({
56
58
  onPresenceChange: opts.onPresenceChange,
59
+ onCommittedChanges: opts.onCommittedChanges,
60
+ objectTypes: opts.objectTypes,
61
+ authorizeRecord: opts.authorizeRecord,
57
62
  schema: opts.schema ?? createTLSchema(),
58
63
  log: opts.log,
59
64
  storage,
@@ -99,6 +104,9 @@ class TLSocketRoom {
99
104
  * - sessionId - Unique identifier for the client session (typically from browser tab)
100
105
  * - socket - WebSocket-like object for client communication
101
106
  * - isReadonly - Whether the client can modify the document (defaults to false)
107
+ * - objectAccess - Write access for object-store lane record types (defaults to 'write').
108
+ * Independent of isReadonly, so a session can be allowed to write objects (e.g. comments)
109
+ * without being allowed to edit the document.
102
110
  * - meta - Additional session metadata (required if SessionMeta is not void)
103
111
  *
104
112
  * @example
@@ -122,7 +130,7 @@ class TLSocketRoom {
122
130
  * ```
123
131
  */
124
132
  handleSocketConnect(opts) {
125
- const { sessionId, socket, isReadonly = false } = opts;
133
+ const { sessionId, socket, isReadonly = false, objectAccess = "write" } = opts;
126
134
  const handleSocketMessage = (event) => this.handleSocketMessage(sessionId, event.data);
127
135
  const handleSocketError = this.handleSocketError.bind(this, sessionId);
128
136
  const handleSocketClose = this.handleSocketClose.bind(this, sessionId);
@@ -138,6 +146,7 @@ class TLSocketRoom {
138
146
  this.room.handleNewSession({
139
147
  sessionId,
140
148
  isReadonly,
149
+ objectAccess,
141
150
  socket: new ServerSocketAdapter({
142
151
  ws: socket,
143
152
  onBeforeSendMessage: this.opts.onBeforeSendMessage ? (message, stringified) => this.opts.onBeforeSendMessage({
@@ -318,6 +327,9 @@ class TLSocketRoom {
318
327
  this.room.handleResumedSession({
319
328
  sessionId,
320
329
  isReadonly: snapshot.isReadonly,
330
+ // snapshots persisted before this field existed fail closed — those sessions predate
331
+ // the record types gated by objectAccess, so they have nothing to write anyway
332
+ objectAccess: snapshot.objectAccess ?? "read",
321
333
  serializedSchema: snapshot.serializedSchema,
322
334
  presenceId: snapshot.presenceId,
323
335
  presenceRecord: snapshot.presenceRecord,
@@ -367,6 +379,7 @@ class TLSocketRoom {
367
379
  return {
368
380
  serializedSchema: session.serializedSchema,
369
381
  isReadonly: session.isReadonly,
382
+ objectAccess: session.objectAccess,
370
383
  presenceId: session.presenceId,
371
384
  presenceRecord,
372
385
  requiresLegacyRejection: session.requiresLegacyRejection,
@@ -441,6 +454,7 @@ class TLSocketRoom {
441
454
  sessionId: session.sessionId,
442
455
  isConnected: session.state === RoomSessionState.Connected,
443
456
  isReadonly: session.isReadonly,
457
+ objectAccess: session.objectAccess,
444
458
  meta: session.meta
445
459
  };
446
460
  });
@@ -470,6 +484,17 @@ class TLSocketRoom {
470
484
  }
471
485
  throw new Error("getCurrentSnapshot is not supported for this storage type");
472
486
  }
487
+ /**
488
+ * Returns a snapshot of the object-store lane (see `objectTypes`), for persisting
489
+ * object-lane records separately from the document. Same entry shape as
490
+ * {@link RoomSnapshot.documents}.
491
+ */
492
+ getCurrentObjectsSnapshot() {
493
+ if (this.storage.getObjectsSnapshot) {
494
+ return this.storage.getObjectsSnapshot();
495
+ }
496
+ throw new Error("getCurrentObjectsSnapshot is not supported for this storage type");
497
+ }
473
498
  /**
474
499
  * Retrieves all presence records from the document store. Presence records
475
500
  * contain ephemeral user state like cursor positions and selections.