@tldraw/sync-core 5.4.0-next.ef99ab47e5ce → 5.5.0-canary.022e164c8709

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 (41) hide show
  1. package/dist-cjs/index.d.ts +17 -13
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/NodeSqliteWrapper.js +1 -1
  4. package/dist-cjs/lib/NodeSqliteWrapper.js.map +2 -2
  5. package/dist-cjs/lib/SQLiteSyncStorage.js +15 -7
  6. package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
  7. package/dist-cjs/lib/TLSyncClient.js +20 -1
  8. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  9. package/dist-cjs/lib/TLSyncRoom.js +30 -3
  10. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  11. package/dist-cjs/lib/TLSyncStorage.js +1 -1
  12. package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
  13. package/dist-cjs/lib/chunk.js +10 -1
  14. package/dist-cjs/lib/chunk.js.map +2 -2
  15. package/dist-esm/index.d.mts +17 -13
  16. package/dist-esm/index.mjs +1 -1
  17. package/dist-esm/lib/NodeSqliteWrapper.mjs +1 -1
  18. package/dist-esm/lib/NodeSqliteWrapper.mjs.map +2 -2
  19. package/dist-esm/lib/SQLiteSyncStorage.mjs +15 -7
  20. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
  21. package/dist-esm/lib/TLSyncClient.mjs +20 -1
  22. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  23. package/dist-esm/lib/TLSyncRoom.mjs +30 -3
  24. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  25. package/dist-esm/lib/TLSyncStorage.mjs +1 -1
  26. package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
  27. package/dist-esm/lib/chunk.mjs +10 -1
  28. package/dist-esm/lib/chunk.mjs.map +2 -2
  29. package/package.json +14 -7
  30. package/src/lib/NodeSqliteWrapper.test.ts +29 -0
  31. package/src/lib/NodeSqliteWrapper.ts +4 -1
  32. package/src/lib/SQLiteSyncStorage.test.ts +60 -1
  33. package/src/lib/SQLiteSyncStorage.ts +22 -14
  34. package/src/lib/TLSyncClient.test.ts +64 -6
  35. package/src/lib/TLSyncClient.ts +38 -2
  36. package/src/lib/TLSyncRoom.ts +39 -3
  37. package/src/lib/TLSyncStorage.test.ts +37 -0
  38. package/src/lib/TLSyncStorage.ts +4 -2
  39. package/src/lib/chunk.test.ts +34 -0
  40. package/src/lib/chunk.ts +20 -1
  41. package/src/test/TLSyncRoom.test.ts +105 -0
@@ -16,7 +16,6 @@ import { TLDocument } from 'tldraw';
16
16
  import { TLPage } from 'tldraw';
17
17
  import { TLRecord } from '@tldraw/tlschema';
18
18
  import { TLStoreSnapshot } from '@tldraw/tlschema';
19
- import { TLStoreSnapshot as TLStoreSnapshot_2 } from 'tldraw';
20
19
  import { UnknownRecord } from '@tldraw/store';
21
20
 
22
21
  /* Excluded from this release type: AppendOp */
@@ -116,7 +115,7 @@ export declare class InMemorySyncStorage<R extends UnknownRecord> implements TLS
116
115
  /* Excluded from this release type: tombstoneHistoryStartsAtClock */
117
116
  private notifier;
118
117
  onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void;
119
- constructor({ snapshot, objectTypes, onChange }?: {
118
+ constructor({ snapshot, objectTypes, onChange, }?: {
120
119
  /**
121
120
  * Record type names stored in the object-store lane. Records of these types are routed to
122
121
  * a separate partition: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.
@@ -206,7 +205,7 @@ export declare class JsonChunkAssembler {
206
205
  * @param schema - The current schema
207
206
  * @param snapshot - The snapshot to load
208
207
  */
209
- export declare function loadSnapshotIntoStorage<R extends UnknownRecord>(txn: TLSyncStorageTransaction<R>, schema: StoreSchema<R, any>, snapshot: RoomSnapshot | TLStoreSnapshot_2): void;
208
+ export declare function loadSnapshotIntoStorage<R extends UnknownRecord>(txn: TLSyncStorageTransaction<R>, schema: StoreSchema<R, any>, snapshot: RoomSnapshot | TLStoreSnapshot): void;
210
209
 
211
210
  /* Excluded from this release type: MinimalDocStore */
212
211
 
@@ -453,7 +452,7 @@ export declare class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyn
453
452
  private readonly stmts;
454
453
  private readonly sql;
455
454
  /* Excluded from this release type: objectTypes */
456
- constructor({ sql, snapshot, objectTypes, onChange }: {
455
+ constructor({ sql, snapshot, objectTypes, onChange, }: {
457
456
  /**
458
457
  * Record type names stored in the object-store lane. Records of these types are routed to
459
458
  * a separate table: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.
@@ -1433,6 +1432,11 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
1433
1432
  /** The last clock time from the most recent server update */
1434
1433
  private lastServerClock;
1435
1434
  private lastServerInteractionTimestamp;
1435
+ /**
1436
+ * Send time of the oldest outstanding ping; answered iff `lastServerInteractionTimestamp >= it`
1437
+ * (any server message counts). Null until the first ping after (re)connect.
1438
+ */
1439
+ private firstUnansweredPingAt;
1436
1440
  /** The queue of in-flight push requests that have not yet been acknowledged by the server */
1437
1441
  private pendingPushRequests;
1438
1442
  private unsentChanges;
@@ -1614,23 +1618,23 @@ export declare const TLSyncErrorCloseEventCode: 4099;
1614
1618
  */
1615
1619
  export declare const TLSyncErrorCloseEventReason: {
1616
1620
  /** Client exceeded rate limits */
1617
- readonly RATE_LIMITED: "RATE_LIMITED";
1621
+ readonly RATE_LIMITED: 'RATE_LIMITED';
1618
1622
  /** Client protocol version too old */
1619
- readonly CLIENT_TOO_OLD: "CLIENT_TOO_OLD";
1623
+ readonly CLIENT_TOO_OLD: 'CLIENT_TOO_OLD';
1620
1624
  /** Client sent invalid or corrupted record data */
1621
- readonly INVALID_RECORD: "INVALID_RECORD";
1625
+ readonly INVALID_RECORD: 'INVALID_RECORD';
1622
1626
  /** Room has reached maximum capacity */
1623
- readonly ROOM_FULL: "ROOM_FULL";
1627
+ readonly ROOM_FULL: 'ROOM_FULL';
1624
1628
  /** Room or resource not found */
1625
- readonly NOT_FOUND: "NOT_FOUND";
1629
+ readonly NOT_FOUND: 'NOT_FOUND';
1626
1630
  /** Server protocol version too old */
1627
- readonly SERVER_TOO_OLD: "SERVER_TOO_OLD";
1631
+ readonly SERVER_TOO_OLD: 'SERVER_TOO_OLD';
1628
1632
  /** Unexpected server error occurred */
1629
- readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
1633
+ readonly UNKNOWN_ERROR: 'UNKNOWN_ERROR';
1630
1634
  /** User authentication required or invalid */
1631
- readonly NOT_AUTHENTICATED: "NOT_AUTHENTICATED";
1635
+ readonly NOT_AUTHENTICATED: 'NOT_AUTHENTICATED';
1632
1636
  /** User lacks permission to access the room */
1633
- readonly FORBIDDEN: "FORBIDDEN";
1637
+ readonly FORBIDDEN: 'FORBIDDEN';
1634
1638
  };
1635
1639
 
1636
1640
  /**
package/dist-cjs/index.js CHANGED
@@ -61,7 +61,7 @@ var import_TLSyncRoom = require("./lib/TLSyncRoom");
61
61
  var import_TLSyncStorage = require("./lib/TLSyncStorage");
62
62
  (0, import_utils.registerTldrawLibraryVersion)(
63
63
  "@tldraw/sync-core",
64
- "5.4.0-next.ef99ab47e5ce",
64
+ "5.5.0-canary.022e164c8709",
65
65
  "cjs"
66
66
  );
67
67
  //# sourceMappingURL=index.js.map
@@ -39,11 +39,11 @@ class NodeSqliteWrapper {
39
39
  let result;
40
40
  try {
41
41
  result = callback();
42
+ this.db.exec("COMMIT");
42
43
  } catch (e) {
43
44
  this.db.exec("ROLLBACK");
44
45
  throw e;
45
46
  }
46
- this.db.exec("COMMIT");
47
47
  return result;
48
48
  }
49
49
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/NodeSqliteWrapper.ts"],
4
- "sourcesContent": ["import {\n\ttype TLSqliteInputValue,\n\ttype TLSqliteRow,\n\ttype TLSyncSqliteStatement,\n\ttype TLSyncSqliteWrapper,\n\ttype TLSyncSqliteWrapperConfig,\n} from './SQLiteSyncStorage'\n\n/**\n * Minimal interface for a synchronous SQLite database.\n *\n * This interface is compatible with:\n * - `node:sqlite` DatabaseSync (Node.js 22.5+)\n * - `better-sqlite3` Database\n *\n * Any SQLite library that provides synchronous `exec` and `prepare` methods\n * with the signatures below can be used with {@link NodeSqliteWrapper}.\n *\n * @public\n */\nexport interface SyncSqliteDatabase {\n\t/** Execute raw SQL without returning results */\n\texec(sql: string): void\n\t/** Prepare a statement for execution */\n\tprepare(sql: string): {\n\t\titerate(...params: unknown[]): IterableIterator<unknown>\n\t\tall(...params: unknown[]): unknown[]\n\t\trun(...params: unknown[]): unknown\n\t}\n}\n\n/**\n * A wrapper around synchronous SQLite databases that implements TLSyncSqliteWrapper.\n * Works with both `node:sqlite` DatabaseSync (Node.js 22.5+) and `better-sqlite3` Database.\n *\n * Use this wrapper with SQLiteSyncStorage to persist tldraw sync state to a SQLite database\n * in Node.js environments.\n *\n * @example\n * ```ts\n * // With node:sqlite (Node.js 22.5+)\n * import { DatabaseSync } from 'node:sqlite'\n * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'\n *\n * const db = new DatabaseSync(':memory:')\n * const sql = new NodeSqliteWrapper(db)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // With better-sqlite3\n * import Database from 'better-sqlite3'\n * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'\n *\n * const db = new Database(':memory:')\n * const sql = new NodeSqliteWrapper(db)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // With table prefix to avoid conflicts with other tables\n * const sql = new NodeSqliteWrapper(db, { tablePrefix: 'tldraw_' })\n * // Creates tables: tldraw_documents, tldraw_tombstones, tldraw_metadata\n * ```\n *\n * @public\n */\nexport class NodeSqliteWrapper implements TLSyncSqliteWrapper {\n\tconstructor(\n\t\tprivate db: SyncSqliteDatabase,\n\t\tpublic config?: TLSyncSqliteWrapperConfig\n\t) {}\n\n\texec(sql: string): void {\n\t\tthis.db.exec(sql)\n\t}\n\n\tprepare<\n\t\tTResult extends TLSqliteRow | void = void,\n\t\tTParams extends TLSqliteInputValue[] = TLSqliteInputValue[],\n\t>(sql: string): TLSyncSqliteStatement<TResult, TParams> {\n\t\treturn this.db.prepare(sql) as unknown as TLSyncSqliteStatement<TResult, TParams>\n\t}\n\n\ttransaction<T>(callback: () => T): T {\n\t\tthis.db.exec('BEGIN')\n\t\tlet result: T\n\t\ttry {\n\t\t\tresult = callback()\n\t\t} catch (e) {\n\t\t\tthis.db.exec('ROLLBACK')\n\t\t\tthrow e\n\t\t}\n\t\tthis.db.exec('COMMIT')\n\t\treturn result\n\t}\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqEO,MAAM,kBAAiD;AAAA,EAC7D,YACS,IACD,QACN;AAFO;AACD;AAAA,EACL;AAAA,EAFM;AAAA,EACD;AAAA,EAGR,KAAK,KAAmB;AACvB,SAAK,GAAG,KAAK,GAAG;AAAA,EACjB;AAAA,EAEA,QAGE,KAAsD;AACvD,WAAO,KAAK,GAAG,QAAQ,GAAG;AAAA,EAC3B;AAAA,EAEA,YAAe,UAAsB;AACpC,SAAK,GAAG,KAAK,OAAO;AACpB,QAAI;AACJ,QAAI;AACH,eAAS,SAAS;AAAA,IACnB,SAAS,GAAG;AACX,WAAK,GAAG,KAAK,UAAU;AACvB,YAAM;AAAA,IACP;AACA,SAAK,GAAG,KAAK,QAAQ;AACrB,WAAO;AAAA,EACR;AACD;",
4
+ "sourcesContent": ["import {\n\ttype TLSqliteInputValue,\n\ttype TLSqliteRow,\n\ttype TLSyncSqliteStatement,\n\ttype TLSyncSqliteWrapper,\n\ttype TLSyncSqliteWrapperConfig,\n} from './SQLiteSyncStorage'\n\n/**\n * Minimal interface for a synchronous SQLite database.\n *\n * This interface is compatible with:\n * - `node:sqlite` DatabaseSync (Node.js 22.5+)\n * - `better-sqlite3` Database\n *\n * Any SQLite library that provides synchronous `exec` and `prepare` methods\n * with the signatures below can be used with {@link NodeSqliteWrapper}.\n *\n * @public\n */\nexport interface SyncSqliteDatabase {\n\t/** Execute raw SQL without returning results */\n\texec(sql: string): void\n\t/** Prepare a statement for execution */\n\tprepare(sql: string): {\n\t\titerate(...params: unknown[]): IterableIterator<unknown>\n\t\tall(...params: unknown[]): unknown[]\n\t\trun(...params: unknown[]): unknown\n\t}\n}\n\n/**\n * A wrapper around synchronous SQLite databases that implements TLSyncSqliteWrapper.\n * Works with both `node:sqlite` DatabaseSync (Node.js 22.5+) and `better-sqlite3` Database.\n *\n * Use this wrapper with SQLiteSyncStorage to persist tldraw sync state to a SQLite database\n * in Node.js environments.\n *\n * @example\n * ```ts\n * // With node:sqlite (Node.js 22.5+)\n * import { DatabaseSync } from 'node:sqlite'\n * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'\n *\n * const db = new DatabaseSync(':memory:')\n * const sql = new NodeSqliteWrapper(db)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // With better-sqlite3\n * import Database from 'better-sqlite3'\n * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'\n *\n * const db = new Database(':memory:')\n * const sql = new NodeSqliteWrapper(db)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // With table prefix to avoid conflicts with other tables\n * const sql = new NodeSqliteWrapper(db, { tablePrefix: 'tldraw_' })\n * // Creates tables: tldraw_documents, tldraw_tombstones, tldraw_metadata\n * ```\n *\n * @public\n */\nexport class NodeSqliteWrapper implements TLSyncSqliteWrapper {\n\tconstructor(\n\t\tprivate db: SyncSqliteDatabase,\n\t\tpublic config?: TLSyncSqliteWrapperConfig\n\t) {}\n\n\texec(sql: string): void {\n\t\tthis.db.exec(sql)\n\t}\n\n\tprepare<\n\t\tTResult extends TLSqliteRow | void = void,\n\t\tTParams extends TLSqliteInputValue[] = TLSqliteInputValue[],\n\t>(sql: string): TLSyncSqliteStatement<TResult, TParams> {\n\t\treturn this.db.prepare(sql) as unknown as TLSyncSqliteStatement<TResult, TParams>\n\t}\n\n\ttransaction<T>(callback: () => T): T {\n\t\tthis.db.exec('BEGIN')\n\t\tlet result: T\n\t\ttry {\n\t\t\tresult = callback()\n\t\t\t// COMMIT itself can fail (SQLITE_BUSY from another connection, I/O errors); without a\n\t\t\t// ROLLBACK the connection would stay inside the transaction and every later BEGIN\n\t\t\t// would throw \"cannot start a transaction within a transaction\"\n\t\t\tthis.db.exec('COMMIT')\n\t\t} catch (e) {\n\t\t\tthis.db.exec('ROLLBACK')\n\t\t\tthrow e\n\t\t}\n\t\treturn result\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAqEO,MAAM,kBAAiD;AAAA,EAC7D,YACS,IACD,QACN;AAFO;AACD;AAAA,EACL;AAAA,EAFM;AAAA,EACD;AAAA,EAGR,KAAK,KAAmB;AACvB,SAAK,GAAG,KAAK,GAAG;AAAA,EACjB;AAAA,EAEA,QAGE,KAAsD;AACvD,WAAO,KAAK,GAAG,QAAQ,GAAG;AAAA,EAC3B;AAAA,EAEA,YAAe,UAAsB;AACpC,SAAK,GAAG,KAAK,OAAO;AACpB,QAAI;AACJ,QAAI;AACH,eAAS,SAAS;AAIlB,WAAK,GAAG,KAAK,QAAQ;AAAA,IACtB,SAAS,GAAG;AACX,WAAK,GAAG,KAAK,UAAU;AACvB,YAAM;AAAA,IACP;AACA,WAAO;AAAA,EACR;AACD;",
6
6
  "names": []
7
7
  }
@@ -328,14 +328,22 @@ class SQLiteSyncStorage {
328
328
  /** @internal */
329
329
  pruneTombstones = (0, import_utils.throttle)(
330
330
  () => {
331
- const tombstoneCount = this.stmts.countTombstones.all()[0].count;
332
- if (tombstoneCount > import_InMemorySyncStorage.MAX_TOMBSTONES) {
333
- const tombstones = this.stmts.iterateTombstones.all();
334
- const result = (0, import_InMemorySyncStorage.computeTombstonePruning)({ tombstones, documentClock: this.getClock() });
335
- if (result) {
336
- this.stmts.setTombstoneHistoryStartsAtClock.run(result.newTombstoneHistoryStartsAtClock);
337
- this.stmts.deleteTombstonesBefore.run(result.newTombstoneHistoryStartsAtClock);
331
+ try {
332
+ const tombstoneCount = this.stmts.countTombstones.all()[0].count;
333
+ if (tombstoneCount > import_InMemorySyncStorage.MAX_TOMBSTONES) {
334
+ const tombstones = this.stmts.iterateTombstones.all();
335
+ const result = (0, import_InMemorySyncStorage.computeTombstonePruning)({ tombstones, documentClock: this.getClock() });
336
+ if (result) {
337
+ this.sql.transaction(() => {
338
+ this.stmts.setTombstoneHistoryStartsAtClock.run(
339
+ result.newTombstoneHistoryStartsAtClock
340
+ );
341
+ this.stmts.deleteTombstonesBefore.run(result.newTombstoneHistoryStartsAtClock);
342
+ });
343
+ }
338
344
  }
345
+ } catch (e) {
346
+ console.error("Failed to prune tombstones", e);
339
347
  }
340
348
  },
341
349
  1e3,
@@ -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\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\tgetWithClock: this.sql.prepare<{ state: Uint8Array; lastChangedClock: number }, [id: string]>(\n\t\t\t\t`SELECT state, lastChangedClock 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\tgetObjectsByIds(ids: Iterable<string>): RoomSnapshot['documents'] {\n\t\tconst result: RoomSnapshot['documents'] = []\n\t\tconst seen = new Set<string>()\n\t\tfor (const id of ids) {\n\t\t\tif (seen.has(id)) continue\n\t\t\tseen.add(id)\n\t\t\tconst row = this.stmts.objects.getWithClock.all(id)[0]\n\t\t\tif (!row) continue\n\t\t\tresult.push({ state: decodeState<R>(row.state), lastChangedClock: row.lastChangedClock })\n\t\t}\n\t\treturn result\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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",
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\tgetWithClock: this.sql.prepare<{ state: Uint8Array; lastChangedClock: number }, [id: string]>(\n\t\t\t\t`SELECT state, lastChangedClock 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\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\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\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\t// Runs from a timer, so a host that closed the database right after the last delete\n\t\t\t// (e.g. `room.close(); db.close()` in onSessionRemoved) would otherwise get an uncaught\n\t\t\t// throw from here; pruning is best-effort and simply runs again on the next delete.\n\t\t\ttry {\n\t\t\t\tconst tombstoneCount = this.stmts.countTombstones.all()[0].count as number\n\t\t\t\tif (tombstoneCount > MAX_TOMBSTONES) {\n\t\t\t\t\t// Get all tombstones sorted by clock ascending (oldest first)\n\t\t\t\t\tconst tombstones = this.stmts.iterateTombstones.all()\n\n\t\t\t\t\tconst result = computeTombstonePruning({ tombstones, documentClock: this.getClock() })\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\tthis.sql.transaction(() => {\n\t\t\t\t\t\t\tthis.stmts.setTombstoneHistoryStartsAtClock.run(\n\t\t\t\t\t\t\t\tresult.newTombstoneHistoryStartsAtClock\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t// Delete all tombstones with clock < newTombstoneHistoryStartsAtClock in one operation.\n\t\t\t\t\t\t\t// This works because computeTombstonePruning ensures we never split a clock value.\n\t\t\t\t\t\t\tthis.stmts.deleteTombstonesBefore.run(result.newTombstoneHistoryStartsAtClock)\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error('Failed to prune tombstones', e)\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\tgetObjectsByIds(ids: Iterable<string>): RoomSnapshot['documents'] {\n\t\tconst result: RoomSnapshot['documents'] = []\n\t\tconst seen = new Set<string>()\n\t\tfor (const id of ids) {\n\t\t\tif (seen.has(id)) continue\n\t\t\tseen.add(id)\n\t\t\tconst row = this.stmts.objects.getWithClock.all(id)[0]\n\t\t\tif (!row) continue\n\t\t\tresult.push({ state: decodeState<R>(row.state), lastChangedClock: row.lastChangedClock })\n\t\t}\n\t\treturn result\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
  }
@@ -62,6 +62,7 @@ class TLSyncError extends Error {
62
62
  }
63
63
  const PING_INTERVAL = 5e3;
64
64
  const MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION = PING_INTERVAL * 2;
65
+ const PONG_TIMEOUT = PING_INTERVAL * 2;
65
66
  function getPresenceOp(lastPushedPresenceState, nextPresence) {
66
67
  if (!lastPushedPresenceState && nextPresence) {
67
68
  return [import_diff.RecordOpType.Put, nextPresence];
@@ -77,6 +78,11 @@ class TLSyncClient {
77
78
  /** The last clock time from the most recent server update */
78
79
  lastServerClock = -1;
79
80
  lastServerInteractionTimestamp = Date.now();
81
+ /**
82
+ * Send time of the oldest outstanding ping; answered iff `lastServerInteractionTimestamp >= it`
83
+ * (any server message counts). Null until the first ping after (re)connect.
84
+ */
85
+ firstUnansweredPingAt = null;
80
86
  /** The queue of in-flight push requests that have not yet been acknowledged by the server */
81
87
  pendingPushRequests = [];
82
88
  unsentChanges = { nextDiff: void 0, nextPresence: void 0 };
@@ -247,6 +253,9 @@ class TLSyncClient {
247
253
  this.debug("ping loop", { isConnectedToRoom: this.isConnectedToRoom });
248
254
  if (!this.isConnectedToRoom) return;
249
255
  try {
256
+ if (this.firstUnansweredPingAt === null || this.lastServerInteractionTimestamp >= this.firstUnansweredPingAt) {
257
+ this.firstUnansweredPingAt = Date.now();
258
+ }
250
259
  this.socket.sendMessage({ type: "ping" });
251
260
  } catch (error) {
252
261
  console.warn("ping failed, resetting", error);
@@ -263,7 +272,16 @@ class TLSyncClient {
263
272
  this.debug("health check passed", { timeSinceLastServerInteraction });
264
273
  return;
265
274
  }
266
- console.warn(`Haven't heard from the server in a while, resetting connection...`);
275
+ const firstUnansweredPingAt = this.firstUnansweredPingAt;
276
+ const pingOverdue = firstUnansweredPingAt !== null && this.lastServerInteractionTimestamp < firstUnansweredPingAt && Date.now() - firstUnansweredPingAt >= PONG_TIMEOUT;
277
+ if (!pingOverdue) {
278
+ this.debug("health check stale but no overdue ping", { timeSinceLastServerInteraction });
279
+ return;
280
+ }
281
+ console.warn(`Haven't heard from the server in a while, resetting connection...`, {
282
+ timeSinceLastServerInteraction,
283
+ pingOutstandingMs: Date.now() - firstUnansweredPingAt
284
+ });
267
285
  this.resetConnection();
268
286
  }, PING_INTERVAL * 2)
269
287
  );
@@ -317,6 +335,7 @@ class TLSyncClient {
317
335
  }
318
336
  this.lastPushedPresenceState = null;
319
337
  this.isConnectedToRoom = false;
338
+ this.firstUnansweredPingAt = null;
320
339
  this.pendingPushRequests = [];
321
340
  this.incomingDiffBuffer = [];
322
341
  this.unsentChanges.nextDiff = void 0;