@tldraw/sync-core 5.3.0-canary.fa3355c81e86 → 5.3.0-internal.c5c7f1d817d0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +154 -3
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/InMemorySyncStorage.js +55 -20
- package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/SQLiteSyncStorage.js +115 -52
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +27 -2
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +6 -1
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +160 -10
- package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
- package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
- package/dist-cjs/lib/protocol.js.map +2 -2
- package/dist-esm/index.d.mts +154 -3
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/InMemorySyncStorage.mjs +55 -20
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/SQLiteSyncStorage.mjs +115 -52
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +27 -2
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +6 -1
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +160 -10
- package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
- package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs.map +2 -2
- package/package.json +6 -6
- package/src/index.ts +3 -0
- package/src/lib/InMemorySyncStorage.ts +74 -21
- package/src/lib/RoomSession.ts +7 -2
- package/src/lib/SQLiteSyncStorage.test.ts +29 -2
- package/src/lib/SQLiteSyncStorage.ts +158 -59
- package/src/lib/TLSocketRoom.ts +61 -3
- package/src/lib/TLSyncClient.test.ts +9 -2
- package/src/lib/TLSyncClient.ts +15 -3
- package/src/lib/TLSyncRoom.ts +294 -10
- package/src/lib/TLSyncStorage.ts +8 -0
- package/src/lib/protocol.ts +17 -0
- package/src/test/TLSocketRoom.test.ts +37 -2
- package/src/test/TLSyncRoom.test.ts +25 -0
- package/src/test/TestServer.ts +14 -4
- package/src/test/objectStore.test.ts +630 -0
- package/src/test/storageContractSuite.ts +79 -0
- package/src/test/upgradeDowngrade.test.ts +144 -2
|
@@ -29,6 +29,7 @@ var import_MicrotaskNotifier = require("./MicrotaskNotifier");
|
|
|
29
29
|
var import_TLSyncStorage = require("./TLSyncStorage");
|
|
30
30
|
function migrateSqliteSyncStorage(storage, {
|
|
31
31
|
documentsTable = "documents",
|
|
32
|
+
objectsTable = "objects",
|
|
32
33
|
tombstonesTable = "tombstones",
|
|
33
34
|
metadataTable = "metadata"
|
|
34
35
|
} = {}) {
|
|
@@ -76,17 +77,29 @@ function migrateSqliteSyncStorage(storage, {
|
|
|
76
77
|
state BLOB NOT NULL,
|
|
77
78
|
lastChangedClock INTEGER NOT NULL
|
|
78
79
|
);
|
|
79
|
-
|
|
80
|
+
|
|
80
81
|
INSERT INTO ${documentsTable}_new (id, state, lastChangedClock)
|
|
81
82
|
SELECT id, CAST(state AS BLOB), lastChangedClock FROM ${documentsTable};
|
|
82
|
-
|
|
83
|
+
|
|
83
84
|
DROP TABLE ${documentsTable};
|
|
84
|
-
|
|
85
|
+
|
|
85
86
|
ALTER TABLE ${documentsTable}_new RENAME TO ${documentsTable};
|
|
86
|
-
|
|
87
|
+
|
|
87
88
|
CREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);
|
|
88
89
|
`);
|
|
89
90
|
}
|
|
91
|
+
if (migrationVersion === 2) {
|
|
92
|
+
migrationVersion++;
|
|
93
|
+
storage.exec(`
|
|
94
|
+
CREATE TABLE ${objectsTable} (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
state BLOB NOT NULL,
|
|
97
|
+
lastChangedClock INTEGER NOT NULL
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE INDEX idx_${objectsTable}_lastChangedClock ON ${objectsTable}(lastChangedClock);
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
90
103
|
storage.exec(`UPDATE ${metadataTable} SET migrationVersion = ${migrationVersion}`);
|
|
91
104
|
}
|
|
92
105
|
const textEncoder = new TextEncoder();
|
|
@@ -131,17 +144,50 @@ class SQLiteSyncStorage {
|
|
|
131
144
|
// Prepared statements - created once, reused many times
|
|
132
145
|
stmts;
|
|
133
146
|
sql;
|
|
147
|
+
/** @internal */
|
|
148
|
+
objectTypes;
|
|
134
149
|
constructor({
|
|
135
150
|
sql,
|
|
136
151
|
snapshot,
|
|
152
|
+
objectTypes,
|
|
137
153
|
onChange
|
|
138
154
|
}) {
|
|
139
155
|
this.sql = sql;
|
|
156
|
+
this.objectTypes = new Set(objectTypes ?? []);
|
|
140
157
|
const prefix = sql.config?.tablePrefix ?? "";
|
|
141
158
|
const documentsTable = `${prefix}documents`;
|
|
159
|
+
const objectsTable = `${prefix}objects`;
|
|
142
160
|
const tombstonesTable = `${prefix}tombstones`;
|
|
143
161
|
const metadataTable = `${prefix}metadata`;
|
|
144
|
-
migrateSqliteSyncStorage(this.sql, {
|
|
162
|
+
migrateSqliteSyncStorage(this.sql, {
|
|
163
|
+
documentsTable,
|
|
164
|
+
objectsTable,
|
|
165
|
+
tombstonesTable,
|
|
166
|
+
metadataTable
|
|
167
|
+
});
|
|
168
|
+
const makeRecordTableStmts = (table) => ({
|
|
169
|
+
get: this.sql.prepare(
|
|
170
|
+
`SELECT state FROM ${table} WHERE id = ?`
|
|
171
|
+
),
|
|
172
|
+
insert: this.sql.prepare(
|
|
173
|
+
`INSERT OR REPLACE INTO ${table} (id, state, lastChangedClock) VALUES (?, ?, ?)`
|
|
174
|
+
),
|
|
175
|
+
delete: this.sql.prepare(`DELETE FROM ${table} WHERE id = ?`),
|
|
176
|
+
exists: this.sql.prepare(
|
|
177
|
+
`SELECT id FROM ${table} WHERE id = ?`
|
|
178
|
+
),
|
|
179
|
+
iterate: this.sql.prepare(
|
|
180
|
+
`SELECT state, lastChangedClock FROM ${table}`
|
|
181
|
+
),
|
|
182
|
+
iterateEntries: this.sql.prepare(
|
|
183
|
+
`SELECT id, state FROM ${table}`
|
|
184
|
+
),
|
|
185
|
+
iterateKeys: this.sql.prepare(`SELECT id FROM ${table}`),
|
|
186
|
+
iterateValues: this.sql.prepare(`SELECT state FROM ${table}`),
|
|
187
|
+
changedSince: this.sql.prepare(
|
|
188
|
+
`SELECT state FROM ${table} WHERE lastChangedClock > ?`
|
|
189
|
+
)
|
|
190
|
+
});
|
|
145
191
|
this.stmts = {
|
|
146
192
|
// Metadata
|
|
147
193
|
getDocumentClock: this.sql.prepare(
|
|
@@ -158,30 +204,9 @@ class SQLiteSyncStorage {
|
|
|
158
204
|
incrementDocumentClock: this.sql.prepare(
|
|
159
205
|
`UPDATE ${metadataTable} SET documentClock = documentClock + 1`
|
|
160
206
|
),
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
),
|
|
165
|
-
insertDocument: this.sql.prepare(`INSERT OR REPLACE INTO ${documentsTable} (id, state, lastChangedClock) VALUES (?, ?, ?)`),
|
|
166
|
-
deleteDocument: this.sql.prepare(
|
|
167
|
-
`DELETE FROM ${documentsTable} WHERE id = ?`
|
|
168
|
-
),
|
|
169
|
-
documentExists: this.sql.prepare(
|
|
170
|
-
`SELECT id FROM ${documentsTable} WHERE id = ?`
|
|
171
|
-
),
|
|
172
|
-
iterateDocuments: this.sql.prepare(
|
|
173
|
-
`SELECT state, lastChangedClock FROM ${documentsTable}`
|
|
174
|
-
),
|
|
175
|
-
iterateDocumentEntries: this.sql.prepare(
|
|
176
|
-
`SELECT id, state FROM ${documentsTable}`
|
|
177
|
-
),
|
|
178
|
-
iterateDocumentKeys: this.sql.prepare(`SELECT id FROM ${documentsTable}`),
|
|
179
|
-
iterateDocumentValues: this.sql.prepare(
|
|
180
|
-
`SELECT state FROM ${documentsTable}`
|
|
181
|
-
),
|
|
182
|
-
getDocumentsChangedSince: this.sql.prepare(
|
|
183
|
-
`SELECT state FROM ${documentsTable} WHERE lastChangedClock > ?`
|
|
184
|
-
),
|
|
207
|
+
// Record tables (document lane + object-store lane)
|
|
208
|
+
documents: makeRecordTableStmts(documentsTable),
|
|
209
|
+
objects: makeRecordTableStmts(objectsTable),
|
|
185
210
|
// Tombstones
|
|
186
211
|
insertTombstone: this.sql.prepare(
|
|
187
212
|
`INSERT OR REPLACE INTO ${tombstonesTable} (id, clock) VALUES (?, ?)`
|
|
@@ -213,10 +238,12 @@ class SQLiteSyncStorage {
|
|
|
213
238
|
const tombstoneHistoryStartsAtClock = snapshot.tombstoneHistoryStartsAtClock ?? documentClock;
|
|
214
239
|
this.sql.exec(`
|
|
215
240
|
DELETE FROM ${documentsTable};
|
|
241
|
+
DELETE FROM ${objectsTable};
|
|
216
242
|
DELETE FROM ${tombstonesTable};
|
|
217
243
|
`);
|
|
218
244
|
for (const doc of snapshot.documents) {
|
|
219
|
-
this.
|
|
245
|
+
const table = this.objectTypes.has(doc.state.typeName) ? this.stmts.objects : this.stmts.documents;
|
|
246
|
+
table.insert.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock);
|
|
220
247
|
}
|
|
221
248
|
if (snapshot.tombstones) {
|
|
222
249
|
for (const [id, clock] of (0, import_utils.objectMapEntries)(snapshot.tombstones)) {
|
|
@@ -228,6 +255,17 @@ class SQLiteSyncStorage {
|
|
|
228
255
|
tombstoneHistoryStartsAtClock,
|
|
229
256
|
JSON.stringify(snapshot.schema)
|
|
230
257
|
);
|
|
258
|
+
} else {
|
|
259
|
+
for (const typeName of this.objectTypes) {
|
|
260
|
+
const lo = `${typeName}:`;
|
|
261
|
+
const hi = `${typeName};`;
|
|
262
|
+
this.sql.exec(`
|
|
263
|
+
INSERT OR REPLACE INTO ${objectsTable} (id, state, lastChangedClock)
|
|
264
|
+
SELECT id, state, lastChangedClock FROM ${documentsTable}
|
|
265
|
+
WHERE id >= '${lo}' AND id < '${hi}';
|
|
266
|
+
DELETE FROM ${documentsTable} WHERE id >= '${lo}' AND id < '${hi}';
|
|
267
|
+
`);
|
|
268
|
+
}
|
|
231
269
|
}
|
|
232
270
|
if (onChange) {
|
|
233
271
|
this.onChange(onChange);
|
|
@@ -305,13 +343,16 @@ class SQLiteSyncStorage {
|
|
|
305
343
|
return {
|
|
306
344
|
tombstoneHistoryStartsAtClock: this._getTombstoneHistoryStartsAtClock(),
|
|
307
345
|
documentClock: this.getClock(),
|
|
308
|
-
documents: Array.from(this.
|
|
346
|
+
documents: Array.from(this._iterateRecords(this.stmts.documents)),
|
|
309
347
|
tombstones: Object.fromEntries(this._iterateTombstones()),
|
|
310
348
|
schema: this._getSchema()
|
|
311
349
|
};
|
|
312
350
|
}
|
|
313
|
-
|
|
314
|
-
|
|
351
|
+
getObjectsSnapshot() {
|
|
352
|
+
return Array.from(this._iterateRecords(this.stmts.objects));
|
|
353
|
+
}
|
|
354
|
+
*_iterateRecords(table) {
|
|
355
|
+
for (const row of table.iterate.iterate()) {
|
|
315
356
|
yield { state: decodeState(row.state), lastChangedClock: row.lastChangedClock };
|
|
316
357
|
}
|
|
317
358
|
}
|
|
@@ -350,9 +391,18 @@ class SQLiteSyncStorageTransaction {
|
|
|
350
391
|
}
|
|
351
392
|
return this._clock;
|
|
352
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Which record table an id belongs to. Record ids are typeName-prefixed (`comment:abc`),
|
|
396
|
+
* so the partition is derivable from the id alone.
|
|
397
|
+
*/
|
|
398
|
+
tableFor(id) {
|
|
399
|
+
const sep = id.indexOf(":");
|
|
400
|
+
if (sep === -1) return this.stmts.documents;
|
|
401
|
+
return this.storage.objectTypes.has(id.slice(0, sep)) ? this.stmts.objects : this.stmts.documents;
|
|
402
|
+
}
|
|
353
403
|
get(id) {
|
|
354
404
|
this.assertNotClosed();
|
|
355
|
-
const row = this.
|
|
405
|
+
const row = this.tableFor(id).get.all(id)[0];
|
|
356
406
|
if (!row) return void 0;
|
|
357
407
|
return decodeState(row.state);
|
|
358
408
|
}
|
|
@@ -361,36 +411,45 @@ class SQLiteSyncStorageTransaction {
|
|
|
361
411
|
(0, import_utils.assert)(id === record.id, `Record id mismatch: key does not match record.id`);
|
|
362
412
|
const clock = this.getNextClock();
|
|
363
413
|
this.stmts.deleteTombstone.run(id);
|
|
364
|
-
this.
|
|
414
|
+
const table = this.storage.objectTypes.has(record.typeName) ? this.stmts.objects : this.stmts.documents;
|
|
415
|
+
table.insert.run(id, encodeState(record), clock);
|
|
365
416
|
}
|
|
366
417
|
delete(id) {
|
|
367
418
|
this.assertNotClosed();
|
|
368
|
-
const
|
|
419
|
+
const table = this.tableFor(id);
|
|
420
|
+
const exists = table.exists.all(id)[0];
|
|
369
421
|
if (!exists) return;
|
|
370
422
|
const clock = this.getNextClock();
|
|
371
|
-
|
|
423
|
+
table.delete.run(id);
|
|
372
424
|
this.stmts.insertTombstone.run(id, clock);
|
|
373
425
|
this.storage.pruneTombstones();
|
|
374
426
|
}
|
|
427
|
+
// iteration spans both record tables so schema migrations cover object-lane records too
|
|
375
428
|
*entries() {
|
|
376
429
|
this.assertNotClosed();
|
|
377
|
-
for (const
|
|
378
|
-
|
|
379
|
-
|
|
430
|
+
for (const table of [this.stmts.documents, this.stmts.objects]) {
|
|
431
|
+
for (const row of table.iterateEntries.iterate()) {
|
|
432
|
+
this.assertNotClosed();
|
|
433
|
+
yield [row.id, decodeState(row.state)];
|
|
434
|
+
}
|
|
380
435
|
}
|
|
381
436
|
}
|
|
382
437
|
*keys() {
|
|
383
438
|
this.assertNotClosed();
|
|
384
|
-
for (const
|
|
385
|
-
|
|
386
|
-
|
|
439
|
+
for (const table of [this.stmts.documents, this.stmts.objects]) {
|
|
440
|
+
for (const row of table.iterateKeys.iterate()) {
|
|
441
|
+
this.assertNotClosed();
|
|
442
|
+
yield row.id;
|
|
443
|
+
}
|
|
387
444
|
}
|
|
388
445
|
}
|
|
389
446
|
*values() {
|
|
390
447
|
this.assertNotClosed();
|
|
391
|
-
for (const
|
|
392
|
-
|
|
393
|
-
|
|
448
|
+
for (const table of [this.stmts.documents, this.stmts.objects]) {
|
|
449
|
+
for (const row of table.iterateValues.iterate()) {
|
|
450
|
+
this.assertNotClosed();
|
|
451
|
+
yield decodeState(row.state);
|
|
452
|
+
}
|
|
394
453
|
}
|
|
395
454
|
}
|
|
396
455
|
getSchema() {
|
|
@@ -411,14 +470,18 @@ class SQLiteSyncStorageTransaction {
|
|
|
411
470
|
const diff = { puts: {}, deletes: [] };
|
|
412
471
|
const wipeAll = sinceClock < this.storage._getTombstoneHistoryStartsAtClock();
|
|
413
472
|
if (wipeAll) {
|
|
414
|
-
for (const
|
|
415
|
-
const
|
|
416
|
-
|
|
473
|
+
for (const table of [this.stmts.documents, this.stmts.objects]) {
|
|
474
|
+
for (const row of table.iterateValues.iterate()) {
|
|
475
|
+
const state = decodeState(row.state);
|
|
476
|
+
diff.puts[state.id] = state;
|
|
477
|
+
}
|
|
417
478
|
}
|
|
418
479
|
} else {
|
|
419
|
-
for (const
|
|
420
|
-
const
|
|
421
|
-
|
|
480
|
+
for (const table of [this.stmts.documents, this.stmts.objects]) {
|
|
481
|
+
for (const row of table.changedSince.iterate(sinceClock)) {
|
|
482
|
+
const state = decodeState(row.state);
|
|
483
|
+
diff.puts[state.id] = state;
|
|
484
|
+
}
|
|
422
485
|
}
|
|
423
486
|
for (const row of this.stmts.getTombstonesChangedSince.iterate(sinceClock)) {
|
|
424
487
|
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": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA4B;AAE5B,mBAAmD;AACnD,iCAIO;AACP,+BAAkC;AAElC,2BAUO;AAgEA,SAAS,yBACf,SACA;AAAA,EACC,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,gBAAgB;AACjB,
|
|
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
|
}
|
|
@@ -63,7 +63,9 @@ class TLSocketRoom {
|
|
|
63
63
|
snapshot: (0, import_TLSyncStorage.convertStoreSnapshotToRoomSnapshot)(
|
|
64
64
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
65
65
|
opts.initialSnapshot ?? import_InMemorySyncStorage.DEFAULT_INITIAL_SNAPSHOT
|
|
66
|
-
)
|
|
66
|
+
),
|
|
67
|
+
// keep the storage partition in step with the room's object lane
|
|
68
|
+
objectTypes: opts.objectTypes
|
|
67
69
|
});
|
|
68
70
|
if ("onDataChange" in opts && opts.onDataChange) {
|
|
69
71
|
this.disposables.add(
|
|
@@ -74,6 +76,9 @@ class TLSocketRoom {
|
|
|
74
76
|
}
|
|
75
77
|
this.room = new import_TLSyncRoom.TLSyncRoom({
|
|
76
78
|
onPresenceChange: opts.onPresenceChange,
|
|
79
|
+
onCommittedChanges: opts.onCommittedChanges,
|
|
80
|
+
objectTypes: opts.objectTypes,
|
|
81
|
+
authorizeRecord: opts.authorizeRecord,
|
|
77
82
|
schema: opts.schema ?? (0, import_tlschema.createTLSchema)(),
|
|
78
83
|
log: opts.log,
|
|
79
84
|
storage,
|
|
@@ -119,6 +124,9 @@ class TLSocketRoom {
|
|
|
119
124
|
* - sessionId - Unique identifier for the client session (typically from browser tab)
|
|
120
125
|
* - socket - WebSocket-like object for client communication
|
|
121
126
|
* - isReadonly - Whether the client can modify the document (defaults to false)
|
|
127
|
+
* - objectAccess - Write access for object-store lane record types (defaults to 'write').
|
|
128
|
+
* Independent of isReadonly, so a session can be allowed to write objects (e.g. comments)
|
|
129
|
+
* without being allowed to edit the document.
|
|
122
130
|
* - meta - Additional session metadata (required if SessionMeta is not void)
|
|
123
131
|
*
|
|
124
132
|
* @example
|
|
@@ -142,7 +150,7 @@ class TLSocketRoom {
|
|
|
142
150
|
* ```
|
|
143
151
|
*/
|
|
144
152
|
handleSocketConnect(opts) {
|
|
145
|
-
const { sessionId, socket, isReadonly = false } = opts;
|
|
153
|
+
const { sessionId, socket, isReadonly = false, objectAccess = "write" } = opts;
|
|
146
154
|
const handleSocketMessage = (event) => this.handleSocketMessage(sessionId, event.data);
|
|
147
155
|
const handleSocketError = this.handleSocketError.bind(this, sessionId);
|
|
148
156
|
const handleSocketClose = this.handleSocketClose.bind(this, sessionId);
|
|
@@ -158,6 +166,7 @@ class TLSocketRoom {
|
|
|
158
166
|
this.room.handleNewSession({
|
|
159
167
|
sessionId,
|
|
160
168
|
isReadonly,
|
|
169
|
+
objectAccess,
|
|
161
170
|
socket: new import_ServerSocketAdapter.ServerSocketAdapter({
|
|
162
171
|
ws: socket,
|
|
163
172
|
onBeforeSendMessage: this.opts.onBeforeSendMessage ? (message, stringified) => this.opts.onBeforeSendMessage({
|
|
@@ -338,6 +347,9 @@ class TLSocketRoom {
|
|
|
338
347
|
this.room.handleResumedSession({
|
|
339
348
|
sessionId,
|
|
340
349
|
isReadonly: snapshot.isReadonly,
|
|
350
|
+
// snapshots persisted before this field existed fail closed — those sessions predate
|
|
351
|
+
// the record types gated by objectAccess, so they have nothing to write anyway
|
|
352
|
+
objectAccess: snapshot.objectAccess ?? "read",
|
|
341
353
|
serializedSchema: snapshot.serializedSchema,
|
|
342
354
|
presenceId: snapshot.presenceId,
|
|
343
355
|
presenceRecord: snapshot.presenceRecord,
|
|
@@ -387,6 +399,7 @@ class TLSocketRoom {
|
|
|
387
399
|
return {
|
|
388
400
|
serializedSchema: session.serializedSchema,
|
|
389
401
|
isReadonly: session.isReadonly,
|
|
402
|
+
objectAccess: session.objectAccess,
|
|
390
403
|
presenceId: session.presenceId,
|
|
391
404
|
presenceRecord,
|
|
392
405
|
requiresLegacyRejection: session.requiresLegacyRejection,
|
|
@@ -461,6 +474,7 @@ class TLSocketRoom {
|
|
|
461
474
|
sessionId: session.sessionId,
|
|
462
475
|
isConnected: session.state === import_RoomSession.RoomSessionState.Connected,
|
|
463
476
|
isReadonly: session.isReadonly,
|
|
477
|
+
objectAccess: session.objectAccess,
|
|
464
478
|
meta: session.meta
|
|
465
479
|
};
|
|
466
480
|
});
|
|
@@ -490,6 +504,17 @@ class TLSocketRoom {
|
|
|
490
504
|
}
|
|
491
505
|
throw new Error("getCurrentSnapshot is not supported for this storage type");
|
|
492
506
|
}
|
|
507
|
+
/**
|
|
508
|
+
* Returns a snapshot of the object-store lane (see `objectTypes`), for persisting
|
|
509
|
+
* object-lane records separately from the document. Same entry shape as
|
|
510
|
+
* {@link RoomSnapshot.documents}.
|
|
511
|
+
*/
|
|
512
|
+
getCurrentObjectsSnapshot() {
|
|
513
|
+
if (this.storage.getObjectsSnapshot) {
|
|
514
|
+
return this.storage.getObjectsSnapshot();
|
|
515
|
+
}
|
|
516
|
+
throw new Error("getCurrentObjectsSnapshot is not supported for this storage type");
|
|
517
|
+
}
|
|
493
518
|
/**
|
|
494
519
|
* Retrieves all presence records from the document store. Presence records
|
|
495
520
|
* contain ephemeral user state like cursor positions and selections.
|