@tldraw/sync-core 5.4.0-next.ef99ab47e5ce → 5.5.0-next.6d9e51e9f3f7
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 +17 -13
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/NodeSqliteWrapper.js +1 -1
- package/dist-cjs/lib/NodeSqliteWrapper.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +20 -1
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncStorage.js +1 -1
- package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
- package/dist-esm/index.d.mts +17 -13
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/NodeSqliteWrapper.mjs +1 -1
- package/dist-esm/lib/NodeSqliteWrapper.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +20 -1
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncStorage.mjs +1 -1
- package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
- package/package.json +14 -7
- package/src/lib/NodeSqliteWrapper.test.ts +29 -0
- package/src/lib/NodeSqliteWrapper.ts +4 -1
- package/src/lib/TLSyncClient.test.ts +64 -6
- package/src/lib/TLSyncClient.ts +38 -2
- package/src/lib/TLSyncStorage.test.ts +37 -0
- package/src/lib/TLSyncStorage.ts +4 -2
package/dist-cjs/index.d.ts
CHANGED
|
@@ -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 |
|
|
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:
|
|
1621
|
+
readonly RATE_LIMITED: 'RATE_LIMITED';
|
|
1618
1622
|
/** Client protocol version too old */
|
|
1619
|
-
readonly CLIENT_TOO_OLD:
|
|
1623
|
+
readonly CLIENT_TOO_OLD: 'CLIENT_TOO_OLD';
|
|
1620
1624
|
/** Client sent invalid or corrupted record data */
|
|
1621
|
-
readonly INVALID_RECORD:
|
|
1625
|
+
readonly INVALID_RECORD: 'INVALID_RECORD';
|
|
1622
1626
|
/** Room has reached maximum capacity */
|
|
1623
|
-
readonly ROOM_FULL:
|
|
1627
|
+
readonly ROOM_FULL: 'ROOM_FULL';
|
|
1624
1628
|
/** Room or resource not found */
|
|
1625
|
-
readonly NOT_FOUND:
|
|
1629
|
+
readonly NOT_FOUND: 'NOT_FOUND';
|
|
1626
1630
|
/** Server protocol version too old */
|
|
1627
|
-
readonly SERVER_TOO_OLD:
|
|
1631
|
+
readonly SERVER_TOO_OLD: 'SERVER_TOO_OLD';
|
|
1628
1632
|
/** Unexpected server error occurred */
|
|
1629
|
-
readonly UNKNOWN_ERROR:
|
|
1633
|
+
readonly UNKNOWN_ERROR: 'UNKNOWN_ERROR';
|
|
1630
1634
|
/** User authentication required or invalid */
|
|
1631
|
-
readonly NOT_AUTHENTICATED:
|
|
1635
|
+
readonly NOT_AUTHENTICATED: 'NOT_AUTHENTICATED';
|
|
1632
1636
|
/** User lacks permission to access the room */
|
|
1633
|
-
readonly 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.
|
|
64
|
+
"5.5.0-next.6d9e51e9f3f7",
|
|
65
65
|
"cjs"
|
|
66
66
|
);
|
|
67
67
|
//# sourceMappingURL=index.js.map
|
|
@@ -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
|
|
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,
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/TLSyncClient.ts"],
|
|
4
|
-
"sourcesContent": ["import { Signal, react, transact } from '@tldraw/state'\nimport {\n\tRecordId,\n\tRecordsDiff,\n\tStore,\n\tUnknownRecord,\n\treverseRecordsDiff,\n\tsquashRecordDiffsMutable,\n} from '@tldraw/store'\nimport {\n\tFpsScheduler,\n\texhaustiveSwitchError,\n\tisEqual,\n\tobjectMapEntries,\n\tuniqueId,\n} from '@tldraw/utils'\nimport {\n\tNetworkDiff,\n\tObjectDiff,\n\tRecordOpType,\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n} from './diff'\nimport { interval } from './interval'\nimport {\n\tTLObjectStoreAccess,\n\tTLPushRequest,\n\tTLSocketClientSentEvent,\n\tTLSocketServerSentDataEvent,\n\tTLSocketServerSentEvent,\n\tgetTlsyncProtocolVersion,\n} from './protocol'\n\n/**\n * Function type for subscribing to events with a callback.\n * Returns an unsubscribe function to clean up the listener.\n *\n * @param cb - Callback function that receives the event value\n * @returns Function to call when you want to unsubscribe from the events\n *\n * @public\n */\nexport type SubscribingFn<T> = (cb: (val: T) => void) => () => void\n\n/** Network sync frame rate when in solo mode (no collaborators) @internal */\nconst SOLO_MODE_FPS = 1\n\n/** Network sync frame rate when in collaborative mode (with collaborators) @internal */\nconst COLLABORATIVE_MODE_FPS = 30\n\n/**\n * WebSocket close code used by the server to signal a non-recoverable sync error.\n * This close code indicates that the connection is being terminated due to an error\n * that cannot be automatically recovered from, such as authentication failures,\n * incompatible client versions, or invalid data.\n *\n * @example\n * ```ts\n * // Server-side: Close connection with specific error reason\n * socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.NOT_FOUND)\n *\n * // Client-side: Handle the error in your sync error handler\n * const syncClient = new TLSyncClient({\n * // ... other config\n * onSyncError: (reason) => {\n * console.error('Sync failed:', reason) // Will receive 'NOT_FOUND'\n * }\n * })\n * ```\n *\n * @public\n */\nexport const TLSyncErrorCloseEventCode = 4099 as const\n\n/**\n * Predefined reasons for server-initiated connection closures.\n * These constants represent different error conditions that can cause\n * the sync server to terminate a WebSocket connection.\n *\n * @example\n * ```ts\n * // Server usage\n * if (!user.hasPermission(roomId)) {\n * socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.FORBIDDEN)\n * }\n *\n * // Client error handling\n * syncClient.onSyncError((reason) => {\n * switch (reason) {\n * case TLSyncErrorCloseEventReason.NOT_FOUND:\n * showError('Room does not exist')\n * break\n * case TLSyncErrorCloseEventReason.FORBIDDEN:\n * showError('Access denied')\n * break\n * case TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:\n * showError('Please update your app')\n * break\n * }\n * })\n * ```\n *\n * @public\n */\nexport const TLSyncErrorCloseEventReason = {\n\t/** Room or resource not found */\n\tNOT_FOUND: 'NOT_FOUND',\n\t/** User lacks permission to access the room */\n\tFORBIDDEN: 'FORBIDDEN',\n\t/** User authentication required or invalid */\n\tNOT_AUTHENTICATED: 'NOT_AUTHENTICATED',\n\t/** Unexpected server error occurred */\n\tUNKNOWN_ERROR: 'UNKNOWN_ERROR',\n\t/** Client protocol version too old */\n\tCLIENT_TOO_OLD: 'CLIENT_TOO_OLD',\n\t/** Server protocol version too old */\n\tSERVER_TOO_OLD: 'SERVER_TOO_OLD',\n\t/** Client sent invalid or corrupted record data */\n\tINVALID_RECORD: 'INVALID_RECORD',\n\t/** Client exceeded rate limits */\n\tRATE_LIMITED: 'RATE_LIMITED',\n\t/** Room has reached maximum capacity */\n\tROOM_FULL: 'ROOM_FULL',\n} as const\n\n/**\n * @internal\n */\nexport class TLSyncError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic reason: TLSyncErrorCloseEventReason\n\t) {\n\t\tsuper(message)\n\t}\n}\n/**\n * Union type of all possible server connection close reasons.\n * Represents the string values that can be passed when a server closes\n * a sync connection due to an error condition.\n *\n * @public\n */\nexport type TLSyncErrorCloseEventReason =\n\t(typeof TLSyncErrorCloseEventReason)[keyof typeof TLSyncErrorCloseEventReason]\n\n/**\n * Handler function for custom application messages sent through the sync protocol.\n * These are user-defined messages that can be sent between clients via the sync server,\n * separate from the standard document synchronization messages.\n *\n * @param data - Custom message payload (application-defined structure)\n *\n * @example\n * ```ts\n * const customMessageHandler: TLCustomMessageHandler = (data) => {\n * if (data.type === 'user_joined') {\n * console.log(`${data.username} joined the session`)\n * showToast(`${data.username} is now collaborating`)\n * }\n * }\n *\n * const syncClient = new TLSyncClient({\n * // ... other config\n * onCustomMessageReceived: customMessageHandler\n * })\n * ```\n *\n * @public\n */\nexport type TLCustomMessageHandler = (this: null, data: any) => void\n\n/**\n * Event object describing changes in socket connection status.\n * Contains either a basic status change or an error with details.\n *\n * @public\n */\nexport type TLSocketStatusChangeEvent =\n\t| {\n\t\t\t/** Connection came online or went offline */\n\t\t\tstatus: 'online' | 'offline'\n\t }\n\t| {\n\t\t\t/** Connection encountered an error */\n\t\t\tstatus: 'error'\n\t\t\t/** Description of the error that occurred */\n\t\t\treason: string\n\t }\n/**\n * Callback function type for listening to socket status changes.\n *\n * @param params - Event object containing the new status and optional error details\n *\n * @internal\n */\nexport type TLSocketStatusListener = (params: TLSocketStatusChangeEvent) => void\n\n/**\n * Possible connection states for a persistent client socket.\n * Represents the current connectivity status between client and server.\n *\n * @internal\n */\nexport type TLPersistentClientSocketStatus = 'online' | 'offline' | 'error'\n\n/**\n * Mode for handling presence information in sync sessions.\n * Controls whether presence data (cursors, selections) is shared with other clients.\n *\n * @public\n */\nexport type TLPresenceMode =\n\t/** No presence sharing - client operates independently */\n\t| 'solo'\n\t/** Full presence sharing - cursors and selections visible to others */\n\t| 'full'\n/**\n * Interface for persistent WebSocket-like connections used by TLSyncClient.\n * Handles automatic reconnection and provides event-based communication with the sync server.\n * Implementations should maintain connection resilience and handle network interruptions gracefully.\n *\n * @example\n * ```ts\n * class MySocketAdapter implements TLPersistentClientSocket {\n * connectionStatus: 'offline' | 'online' | 'error' = 'offline'\n *\n * sendMessage(msg: TLSocketClientSentEvent) {\n * if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n * this.ws.send(JSON.stringify(msg))\n * }\n * }\n *\n * onReceiveMessage = (callback) => {\n * // Set up message listener and return cleanup function\n * }\n *\n * restart() {\n * this.disconnect()\n * this.connect()\n * }\n * }\n * ```\n *\n * @public\n */\nexport interface TLPersistentClientSocket<\n\tClientSentMessage extends object = object,\n\tServerSentMessage extends object = object,\n> {\n\t/** Current connection state - online means actively connected and ready */\n\tconnectionStatus: 'online' | 'offline' | 'error'\n\n\t/**\n\t * Send a protocol message to the sync server\n\t * @param msg - Message to send (connect, push, ping, etc.)\n\t */\n\tsendMessage(msg: ClientSentMessage): void\n\n\t/**\n\t * Subscribe to messages received from the server\n\t * @param callback - Function called for each received message\n\t * @returns Cleanup function to remove the listener\n\t */\n\tonReceiveMessage: SubscribingFn<ServerSentMessage>\n\n\t/**\n\t * Subscribe to connection status changes\n\t * @param callback - Function called when connection status changes\n\t * @returns Cleanup function to remove the listener\n\t */\n\tonStatusChange: SubscribingFn<TLSocketStatusChangeEvent>\n\n\t/**\n\t * Force a connection restart (disconnect then reconnect)\n\t * Used for error recovery or when connection health checks fail\n\t */\n\trestart(): void\n\n\t/**\n\t * Close the connection\n\t */\n\tclose(): void\n}\n\nconst PING_INTERVAL = 5000\nconst MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION = PING_INTERVAL * 2\n\n// Should connect support chunking the response to allow for large payloads?\n\nfunction getPresenceOp<R extends UnknownRecord>(\n\tlastPushedPresenceState: R | null,\n\tnextPresence: R | null\n): [typeof RecordOpType.Patch, ObjectDiff] | [typeof RecordOpType.Put, R] | undefined {\n\tif (!lastPushedPresenceState && nextPresence) {\n\t\treturn [RecordOpType.Put, nextPresence]\n\t}\n\tif (lastPushedPresenceState && nextPresence) {\n\t\tconst diff = diffRecord(lastPushedPresenceState, nextPresence)\n\t\tif (!diff) return undefined\n\t\treturn [RecordOpType.Patch, diff]\n\t}\n\treturn undefined\n}\n\n/**\n * Main client-side synchronization engine for collaborative tldraw applications.\n *\n * TLSyncClient manages bidirectional synchronization between a local tldraw Store\n * and a remote sync server. It uses an optimistic update model where local changes\n * are immediately applied for responsive UI, then sent to the server for validation\n * and distribution to other clients.\n *\n * The synchronization follows a git-like push/pull/rebase model:\n * - **Push**: Local changes are sent to server as diff operations\n * - **Pull**: Server changes are received and applied locally\n * - **Rebase**: Conflicting changes are resolved by undoing local changes,\n * applying server changes, then re-applying local changes on top\n *\n * @example\n * ```ts\n * import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core'\n * import { createTLStore } from '@tldraw/store'\n *\n * // Create store and socket\n * const store = createTLStore({ schema: mySchema })\n * const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync')\n *\n * // Create sync client\n * const syncClient = new TLSyncClient({\n * store,\n * socket,\n * presence: atom(null),\n * onLoad: () => console.log('Connected and loaded'),\n * onSyncError: (reason) => console.error('Sync failed:', reason)\n * })\n *\n * // Changes to store are now automatically synchronized\n * store.put([{ id: 'shape1', type: 'geo', x: 100, y: 100 }])\n * ```\n *\n * @example\n * ```ts\n * // Advanced usage with presence and custom messages\n * const syncClient = new TLSyncClient({\n * store,\n * socket,\n * presence: atom({ cursor: { x: 0, y: 0 }, userName: 'Alice' }),\n * presenceMode: atom('full'),\n * onCustomMessageReceived: (data) => {\n * if (data.type === 'chat') {\n * showChatMessage(data.message, data.from)\n * }\n * },\n * onAfterConnect: (client, { isReadonly }) => {\n * if (isReadonly) {\n * showNotification('Connected in read-only mode')\n * }\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>> {\n\t/** The last clock time from the most recent server update */\n\tprivate lastServerClock = -1\n\tprivate lastServerInteractionTimestamp = Date.now()\n\n\t/** The queue of in-flight push requests that have not yet been acknowledged by the server */\n\tprivate pendingPushRequests: TLPushRequest<R>[] = []\n\tprivate unsentChanges: {\n\t\tnextDiff?: RecordsDiff<R>\n\t\tnextPresence?: R | null\n\t} = { nextDiff: undefined, nextPresence: undefined }\n\n\t/**\n\t * The diff of 'unconfirmed', 'optimistic' changes that have been made locally by the user if we\n\t * take this diff, reverse it, and apply that to the store, our store will match exactly the most\n\t * recent state of the server that we know about\n\t */\n\tprivate speculativeChanges: RecordsDiff<R> = {\n\t\tadded: {} as any,\n\t\tupdated: {} as any,\n\t\tremoved: {} as any,\n\t}\n\n\tprivate disposables: Array<() => void> = []\n\n\t/** Separate scheduler instance for network sync operations */\n\tprivate readonly fpsScheduler: FpsScheduler\n\n\t/** Send any unsent push requests to the server */\n\tprivate readonly sendUnsentChanges: {\n\t\t(): void\n\t\tcancel?(): void\n\t}\n\n\t/** Schedule a rebase operation */\n\tprivate readonly scheduleRebase: {\n\t\t(): void\n\t\tcancel?(): void\n\t}\n\n\t/** @internal */\n\treadonly store: S\n\t/** @internal */\n\treadonly socket: TLPersistentClientSocket<TLSocketClientSentEvent<R>, TLSocketServerSentEvent<R>>\n\n\t/** @internal */\n\treadonly presenceState: Signal<R | null> | undefined\n\t/** @internal */\n\treadonly presenceMode: Signal<TLPresenceMode> | undefined\n\n\t// isOnline is true when we have an open socket connection and we have\n\t// established a connection with the server room (i.e. we have received a 'connect' message)\n\t/** @internal */\n\tisConnectedToRoom = false\n\n\t/**\n\t * The client clock is essentially a counter for push requests Each time a push request is created\n\t * the clock is incremented. This clock is sent with the push request to the server, and the\n\t * server returns it with the response so that we can match up the response with the request.\n\t *\n\t * The clock may also be used at one point in the future to allow the client to re-send push\n\t * requests idempotently (i.e. the server will keep track of each client's clock and not execute\n\t * requests it has already handled), but at the time of writing this is neither needed nor\n\t * implemented.\n\t */\n\tprivate clientClock = 0\n\n\t/**\n\t * Callback executed immediately after successful connection to sync room.\n\t * Use this to perform any post-connection setup required for your application,\n\t * such as initializing default content or updating UI state.\n\t *\n\t * @param self - The TLSyncClient instance that connected\n\t * @param details - Connection details\n\t * - isReadonly - Whether the connection is in read-only mode\n\t * - objectAccess - Write access for object-store lane record types (defaults to 'write'\n\t * when the server doesn't send it, e.g. older servers or rooms with no object lane)\n\t */\n\tprivate readonly onAfterConnect?: (\n\t\tself: this,\n\t\tdetails: { isReadonly: boolean; objectAccess: TLObjectStoreAccess }\n\t) => void\n\n\tprivate readonly onCustomMessageReceived?: TLCustomMessageHandler\n\n\tprivate isDebugging = false\n\tprivate debug(...args: any[]) {\n\t\tif (this.isDebugging) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.debug(...args)\n\t\t}\n\t}\n\n\tprivate readonly presenceType: R['typeName'] | null\n\n\tprivate didCancel?: () => boolean\n\n\t/**\n\t * Creates a new TLSyncClient instance to manage synchronization with a remote server.\n\t *\n\t * @param config - Configuration object for the sync client\n\t * - store - The local tldraw store to synchronize\n\t * - socket - WebSocket adapter for server communication\n\t * - presence - Reactive signal containing current user's presence data\n\t * - presenceMode - Optional signal controlling presence sharing (defaults to 'full')\n\t * - onLoad - Callback fired when initial sync completes successfully\n\t * - onSyncError - Callback fired when sync fails with error reason\n\t * - onCustomMessageReceived - Optional handler for custom messages\n\t * - onAfterConnect - Optional callback fired after successful connection\n\t * - self - The TLSyncClient instance\n\t * - details - Connection details including readonly status\n\t * - didCancel - Optional function to check if sync should be cancelled\n\t */\n\tconstructor(config: {\n\t\tstore: S\n\t\tsocket: TLPersistentClientSocket<any, any>\n\t\tpresence: Signal<R | null>\n\t\tpresenceMode?: Signal<TLPresenceMode>\n\t\tonLoad(self: TLSyncClient<R, S>): void\n\t\tonSyncError(reason: string): void\n\t\tonCustomMessageReceived?: TLCustomMessageHandler\n\t\tonAfterConnect?(\n\t\t\tself: TLSyncClient<R, S>,\n\t\t\tdetails: { isReadonly: boolean; objectAccess: TLObjectStoreAccess }\n\t\t): void\n\t\tdidCancel?(): boolean\n\t}) {\n\t\tthis.didCancel = config.didCancel\n\n\t\tthis.presenceType = config.store.scopedTypes.presence.values().next().value ?? null\n\n\t\t// Create a separate throttle instance for network sync operations\n\t\t// This ensures sync operations have their own queue separate from UI operations\n\t\tthis.fpsScheduler = new FpsScheduler(COLLABORATIVE_MODE_FPS)\n\n\t\t// Initialize throttled methods after throttle instance is created\n\t\tthis.sendUnsentChanges = this.fpsScheduler.fpsThrottle(() => {\n\t\t\tthis.debug('sending unsent changes', {\n\t\t\t\tisConnectedToRoom: this.isConnectedToRoom,\n\t\t\t\tunsentChanges: this.unsentChanges,\n\t\t\t})\n\t\t\tif (!this.isConnectedToRoom || this.store.isPossiblyCorrupted()) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.unsentChanges.nextDiff && !this.unsentChanges.nextPresence) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst diff = this.unsentChanges.nextDiff\n\t\t\t\t? (getNetworkDiff(this.unsentChanges.nextDiff) ?? undefined)\n\t\t\t\t: undefined\n\t\t\tconst presence = this.unsentChanges.nextPresence\n\t\t\t\t? getPresenceOp<R>(this.lastPushedPresenceState, this.unsentChanges.nextPresence)\n\t\t\t\t: undefined\n\n\t\t\tif (!diff && !presence) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst pushRequest: TLPushRequest<R> = {\n\t\t\t\ttype: 'push',\n\t\t\t\tclientClock: this.clientClock,\n\t\t\t\tdiff,\n\t\t\t\tpresence,\n\t\t\t}\n\n\t\t\tthis.debug('sending push request', pushRequest)\n\t\t\tthis.socket.sendMessage(pushRequest)\n\n\t\t\tif (this.unsentChanges.nextPresence) {\n\t\t\t\tthis.lastPushedPresenceState = this.unsentChanges.nextPresence\n\t\t\t}\n\t\t\tthis.clientClock++\n\t\t\tthis.pendingPushRequests.push(pushRequest)\n\t\t\tthis.unsentChanges.nextDiff = undefined\n\t\t\tthis.unsentChanges.nextPresence = undefined\n\t\t})\n\n\t\tthis.scheduleRebase = this.fpsScheduler.fpsThrottle(this.rebase)\n\n\t\tif (typeof window !== 'undefined') {\n\t\t\t;(window as any).tlsync = this\n\t\t}\n\t\tthis.store = config.store\n\t\tthis.socket = config.socket\n\t\tthis.onAfterConnect = config.onAfterConnect\n\t\tthis.onCustomMessageReceived = config.onCustomMessageReceived\n\n\t\tlet didLoad = false\n\n\t\tthis.presenceState = config.presence\n\t\tthis.presenceMode = config.presenceMode\n\n\t\tthis.disposables.push(\n\t\t\t// when local 'user' changes are made, send them to the server\n\t\t\t// or stash them locally in offline mode\n\t\t\tthis.store.listen(\n\t\t\t\t({ changes }) => {\n\t\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\t\tthis.debug('received store changes', { changes })\n\t\t\t\t\tthis.push(changes)\n\t\t\t\t},\n\t\t\t\t{ source: 'user', scope: 'document' }\n\t\t\t),\n\t\t\t// when the server sends us events, handle them\n\t\t\tthis.socket.onReceiveMessage((msg) => {\n\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\tthis.debug('received message from server', msg)\n\t\t\t\tthis.handleServerEvent(msg)\n\t\t\t\t// the first time we receive a message from the server, we should trigger\n\n\t\t\t\t// one of the load callbacks\n\t\t\t\tif (!didLoad) {\n\t\t\t\t\tdidLoad = true\n\t\t\t\t\tconfig.onLoad(this)\n\t\t\t\t}\n\t\t\t}),\n\t\t\t// handle switching between online and offline\n\t\t\tthis.socket.onStatusChange((ev) => {\n\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\tthis.debug('socket status changed', ev.status)\n\t\t\t\tif (ev.status === 'online') {\n\t\t\t\t\tthis.sendConnectMessage()\n\t\t\t\t} else {\n\t\t\t\t\tthis.resetConnection()\n\t\t\t\t\tif (ev.status === 'error') {\n\t\t\t\t\t\tdidLoad = true\n\t\t\t\t\t\tconfig.onSyncError(ev.reason)\n\t\t\t\t\t\tthis.close()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t\t// Send a ping every PING_INTERVAL ms while online\n\t\t\tinterval(() => {\n\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\tthis.debug('ping loop', { isConnectedToRoom: this.isConnectedToRoom })\n\t\t\t\tif (!this.isConnectedToRoom) return\n\t\t\t\ttry {\n\t\t\t\t\tthis.socket.sendMessage({ type: 'ping' })\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.warn('ping failed, resetting', error)\n\t\t\t\t\tthis.resetConnection()\n\t\t\t\t}\n\t\t\t}, PING_INTERVAL),\n\t\t\t// Check the server connection health, reset the connection if needed\n\t\t\tinterval(() => {\n\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\tthis.debug('health check loop', { isConnectedToRoom: this.isConnectedToRoom })\n\t\t\t\tif (!this.isConnectedToRoom) return\n\t\t\t\tconst timeSinceLastServerInteraction = Date.now() - this.lastServerInteractionTimestamp\n\n\t\t\t\tif (\n\t\t\t\t\ttimeSinceLastServerInteraction <\n\t\t\t\t\tMAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION\n\t\t\t\t) {\n\t\t\t\t\tthis.debug('health check passed', { timeSinceLastServerInteraction })\n\t\t\t\t\t// last ping was recent, so no need to take any action\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tconsole.warn(`Haven't heard from the server in a while, resetting connection...`)\n\t\t\t\tthis.resetConnection()\n\t\t\t}, PING_INTERVAL * 2)\n\t\t)\n\n\t\tif (this.presenceState) {\n\t\t\tthis.disposables.push(\n\t\t\t\treact('pushPresence', () => {\n\t\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\t\tconst mode = this.presenceMode?.get()\n\t\t\t\t\tthis.fpsScheduler.updateTargetFps(this.getSyncFps())\n\t\t\t\t\tif (mode !== 'full') return\n\t\t\t\t\tthis.pushPresence(this.presenceState!.get())\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\n\t\t// if the socket is already online before this client was instantiated\n\t\t// then we should send a connect message right away\n\t\tif (this.socket.connectionStatus === 'online') {\n\t\t\tthis.sendConnectMessage()\n\t\t}\n\t}\n\n\t/** @internal */\n\tlatestConnectRequestId: string | null = null\n\n\t/**\n\t * This is the first message that is sent over a newly established socket connection. And we need\n\t * to wait for the response before this client can be used.\n\t */\n\tprivate sendConnectMessage() {\n\t\tif (this.isConnectedToRoom) {\n\t\t\tconsole.error('sendConnectMessage called while already connected')\n\t\t\treturn\n\t\t}\n\t\tthis.debug('sending connect message')\n\t\tthis.latestConnectRequestId = uniqueId()\n\t\tthis.socket.sendMessage({\n\t\t\ttype: 'connect',\n\t\t\tconnectRequestId: this.latestConnectRequestId,\n\t\t\tschema: this.store.schema.serialize(),\n\t\t\tprotocolVersion: getTlsyncProtocolVersion(),\n\t\t\tlastServerClock: this.lastServerClock,\n\t\t})\n\t}\n\n\t/** Switch to offline mode */\n\tprivate resetConnection(hard = false) {\n\t\tthis.debug('resetting connection')\n\t\tif (hard) {\n\t\t\tthis.lastServerClock = 0\n\t\t}\n\t\t// kill all presence state\n\t\tconst keys = Object.keys(this.store.serialize('presence')) as any\n\t\tif (keys.length > 0) {\n\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\tthis.store.remove(keys)\n\t\t\t})\n\t\t}\n\t\tthis.lastPushedPresenceState = null\n\t\tthis.isConnectedToRoom = false\n\t\tthis.pendingPushRequests = []\n\t\tthis.incomingDiffBuffer = []\n\t\tthis.unsentChanges.nextDiff = undefined\n\t\tthis.unsentChanges.nextPresence = undefined\n\t\tif (this.socket.connectionStatus === 'online') {\n\t\t\tthis.socket.restart()\n\t\t}\n\t}\n\n\t/**\n\t * Invoked when the socket connection comes online, either for the first time or as the result of\n\t * a reconnect. The goal is to rebase on the server's state and fire off a new push request for\n\t * any local changes that were made while offline.\n\t */\n\tprivate didReconnect(event: Extract<TLSocketServerSentEvent<R>, { type: 'connect' }>) {\n\t\tthis.debug('did reconnect', event)\n\t\tif (event.connectRequestId !== this.latestConnectRequestId) {\n\t\t\t// ignore connect events for old connect requests\n\t\t\treturn\n\t\t}\n\t\tthis.latestConnectRequestId = null\n\n\t\tif (this.isConnectedToRoom) {\n\t\t\tconsole.error('didReconnect called while already connected')\n\t\t\tthis.resetConnection(true)\n\t\t\treturn\n\t\t}\n\t\tif (this.pendingPushRequests.length > 0) {\n\t\t\tconsole.error('pendingPushRequests should already be empty when we reconnect')\n\t\t\tthis.resetConnection(true)\n\t\t\treturn\n\t\t}\n\t\t// at the end of this process we want to have at most one pending push request\n\t\t// based on anything inside this.speculativeChanges\n\t\ttransact(() => {\n\t\t\t// Now our goal is to rebase on the server's state.\n\t\t\t// This means wiping away any peer presence data, which the server will replace in full on every connect.\n\t\t\t// If the server does not have enough history to give us a partial document state hydration we will\n\t\t\t// also need to wipe away all of our document state before hydrating with the server's state from scratch.\n\t\t\tconst stashedChanges = this.speculativeChanges\n\t\t\tthis.speculativeChanges = { added: {} as any, updated: {} as any, removed: {} as any }\n\n\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t// gather records to delete in a NetworkDiff\n\t\t\t\tconst wipeDiff: NetworkDiff<R> = {}\n\t\t\t\tconst wipeAll = event.hydrationType === 'wipe_all'\n\t\t\t\tif (!wipeAll) {\n\t\t\t\t\t// if we're only wiping presence data, undo the speculative changes first\n\t\t\t\t\tthis.store.applyDiff(reverseRecordsDiff(stashedChanges), { runCallbacks: false })\n\t\t\t\t}\n\n\t\t\t\t// now wipe all presence data and, if needed, all document data\n\t\t\t\tfor (const [id, record] of objectMapEntries(this.store.serialize('all'))) {\n\t\t\t\t\tif (\n\t\t\t\t\t\t(wipeAll && this.store.scopedTypes.document.has(record.typeName)) ||\n\t\t\t\t\t\trecord.typeName === this.presenceType\n\t\t\t\t\t) {\n\t\t\t\t\t\twipeDiff[id] = [RecordOpType.Remove]\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// then apply the upstream changes\n\t\t\t\tthis.applyNetworkDiff({ ...wipeDiff, ...event.diff }, true)\n\n\t\t\t\tthis.isConnectedToRoom = true\n\n\t\t\t\t// now re-apply the speculative changes creating a new push request with the\n\t\t\t\t// appropriate diff\n\t\t\t\tconst networkDiff = getNetworkDiff(stashedChanges)\n\t\t\t\tif (!networkDiff) return\n\t\t\t\tconst speculativeChanges = this.store.filterChangesByScope(\n\t\t\t\t\tthis.store.extractingChanges(() => {\n\t\t\t\t\t\tthis.applyNetworkDiff(networkDiff, true)\n\t\t\t\t\t}),\n\t\t\t\t\t'document'\n\t\t\t\t)\n\t\t\t\tif (speculativeChanges) this.push(speculativeChanges)\n\t\t\t})\n\n\t\t\t// this.isConnectedToRoom = true\n\t\t\t// this.store.applyDiff(stashedChanges, false)\n\n\t\t\tthis.onAfterConnect?.(this, {\n\t\t\t\tisReadonly: event.isReadonly,\n\t\t\t\tobjectAccess: event.objectAccess ?? 'write',\n\t\t\t})\n\t\t\tconst presence = this.presenceState?.get()\n\t\t\tif (presence) {\n\t\t\t\tthis.pushPresence(presence)\n\t\t\t}\n\t\t})\n\n\t\tthis.lastServerClock = event.serverClock\n\t}\n\n\tprivate incomingDiffBuffer: TLSocketServerSentDataEvent<R>[] = []\n\n\t/** Handle events received from the server */\n\tprivate handleServerEvent(event: TLSocketServerSentEvent<R>) {\n\t\tthis.debug('received server event', event)\n\t\tthis.lastServerInteractionTimestamp = Date.now()\n\t\t// always update the lastServerClock when it is present\n\t\tswitch (event.type) {\n\t\t\tcase 'connect':\n\t\t\t\tthis.didReconnect(event)\n\t\t\t\tbreak\n\t\t\t// legacy v4 events\n\t\t\tcase 'patch':\n\t\t\tcase 'push_result':\n\t\t\t\tif (!this.isConnectedToRoom) break\n\t\t\t\tthis.incomingDiffBuffer.push(event)\n\t\t\t\tthis.scheduleRebase()\n\t\t\t\tbreak\n\t\t\tcase 'data':\n\t\t\t\t// wait for a connect to succeed before processing more events\n\t\t\t\tif (!this.isConnectedToRoom) break\n\t\t\t\tthis.incomingDiffBuffer.push(...event.data)\n\t\t\t\tthis.scheduleRebase()\n\t\t\t\tbreak\n\t\t\tcase 'incompatibility_error':\n\t\t\t\t// legacy unrecoverable errors\n\t\t\t\tconsole.error('incompatibility error is legacy and should no longer be sent by the server')\n\t\t\t\tbreak\n\t\t\tcase 'pong':\n\t\t\t\t// noop, we only use ping/pong to set lastSeverInteractionTimestamp\n\t\t\t\tbreak\n\t\t\tcase 'custom':\n\t\t\t\tthis.onCustomMessageReceived?.call(null, event.data)\n\t\t\t\tbreak\n\n\t\t\tdefault:\n\t\t\t\texhaustiveSwitchError(event)\n\t\t}\n\t}\n\n\t/**\n\t * Closes the sync client and cleans up all resources.\n\t *\n\t * Call this method when you no longer need the sync client to prevent\n\t * memory leaks and close the WebSocket connection. After calling close(),\n\t * the client cannot be reused.\n\t *\n\t * @example\n\t * ```ts\n\t * // Clean shutdown\n\t * syncClient.close()\n\t * ```\n\t */\n\tclose() {\n\t\tthis.debug('closing')\n\t\tthis.disposables.forEach((dispose) => dispose())\n\t\tthis.sendUnsentChanges.cancel?.()\n\t\tthis.scheduleRebase.cancel?.()\n\t\tif (typeof window !== 'undefined' && (window as any).tlsync === this) {\n\t\t\tdelete (window as any).tlsync\n\t\t}\n\t}\n\n\tprivate lastPushedPresenceState: R | null = null\n\n\tprivate pushPresence(nextPresence: R | null) {\n\t\t// make sure we push any document changes first\n\t\tthis.store._flushHistory()\n\n\t\tif (!this.isConnectedToRoom) {\n\t\t\t// if we're offline, don't do anything\n\t\t\treturn\n\t\t}\n\n\t\tthis.unsentChanges.nextPresence = nextPresence\n\t\tthis.sendUnsentChanges()\n\t}\n\n\t/** Push a change to the server, or stash it locally if we're offline */\n\tprivate push(change: RecordsDiff<any>) {\n\t\tthis.debug('push', change)\n\t\tsquashRecordDiffsMutable(this.speculativeChanges, [change])\n\t\t// in offline mode, we only accumulate in speculativeChanges\n\t\tif (!this.isConnectedToRoom) return\n\t\tif (!this.unsentChanges.nextDiff) {\n\t\t\tthis.unsentChanges.nextDiff = { added: {} as any, updated: {} as any, removed: {} as any }\n\t\t}\n\t\t// records are immutable, so sharing their references with `change` is fine \u2014 the\n\t\t// squash gives nextDiff its own containers and tuples without deep-cloning records\n\t\tsquashRecordDiffsMutable(this.unsentChanges.nextDiff, [change])\n\t\tthis.sendUnsentChanges()\n\t}\n\n\t/** Get the target FPS for network operations based on presence mode */\n\tprivate getSyncFps(): number {\n\t\treturn this.presenceMode?.get() === 'solo' ? SOLO_MODE_FPS : COLLABORATIVE_MODE_FPS\n\t}\n\n\t/**\n\t * Applies a 'network' diff to the store this does value-based equality checking so that if the\n\t * data is the same (as opposed to merely identical with ===), then no change is made and no\n\t * changes will be propagated back to store listeners\n\t */\n\tprivate applyNetworkDiff(diff: NetworkDiff<R>, runCallbacks: boolean) {\n\t\tthis.debug('applyNetworkDiff', diff)\n\t\tconst changes: RecordsDiff<R> = { added: {} as any, updated: {} as any, removed: {} as any }\n\t\ttype k = keyof typeof changes.updated\n\t\tlet hasChanges = false\n\t\tfor (const [id, op] of objectMapEntries(diff)) {\n\t\t\tif (op[0] === RecordOpType.Put) {\n\t\t\t\tconst existing = this.store.get(id as RecordId<any>)\n\t\t\t\tif (existing && !isEqual(existing, op[1])) {\n\t\t\t\t\thasChanges = true\n\t\t\t\t\tchanges.updated[id as k] = [existing, op[1]]\n\t\t\t\t} else {\n\t\t\t\t\thasChanges = true\n\t\t\t\t\tchanges.added[id as k] = op[1]\n\t\t\t\t}\n\t\t\t} else if (op[0] === RecordOpType.Patch) {\n\t\t\t\tconst record = this.store.get(id as RecordId<any>)\n\t\t\t\tif (!record) {\n\t\t\t\t\t// the record was removed upstream\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst patched = applyObjectDiff(record, op[1])\n\t\t\t\thasChanges = true\n\t\t\t\tchanges.updated[id as k] = [record, patched]\n\t\t\t} else if (op[0] === RecordOpType.Remove) {\n\t\t\t\tif (this.store.has(id as RecordId<any>)) {\n\t\t\t\t\thasChanges = true\n\t\t\t\t\tchanges.removed[id as k] = this.store.get(id as RecordId<any>)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (hasChanges) {\n\t\t\tthis.store.applyDiff(changes, { runCallbacks })\n\t\t}\n\t}\n\n\t// eslint-disable-next-line tldraw/prefer-class-methods\n\tprivate rebase = () => {\n\t\t// need to make sure that our speculative changes are in sync with the actual store instance before\n\t\t// proceeding, to avoid inconsistency bugs.\n\t\tthis.store._flushHistory()\n\t\tif (this.incomingDiffBuffer.length === 0) return\n\n\t\tconst diffs = this.incomingDiffBuffer\n\t\tthis.incomingDiffBuffer = []\n\n\t\ttry {\n\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t// first undo speculative changes\n\t\t\t\tthis.store.applyDiff(reverseRecordsDiff(this.speculativeChanges), { runCallbacks: false })\n\n\t\t\t\t// then apply network diffs on top of known-to-be-synced data\n\t\t\t\tfor (const diff of diffs) {\n\t\t\t\t\tif (diff.type === 'patch') {\n\t\t\t\t\t\tthis.applyNetworkDiff(diff.diff, true)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// handling push_result\n\t\t\t\t\tif (this.pendingPushRequests.length === 0) {\n\t\t\t\t\t\tthrow new Error('Received push_result but there are no pending push requests')\n\t\t\t\t\t}\n\t\t\t\t\tif (this.pendingPushRequests[0].clientClock !== diff.clientClock) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'Received push_result for a push request that is not at the front of the queue'\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tif (diff.action === 'discard') {\n\t\t\t\t\t\tthis.pendingPushRequests.shift()\n\t\t\t\t\t} else if (diff.action === 'commit') {\n\t\t\t\t\t\tconst request = this.pendingPushRequests.shift()!\n\t\t\t\t\t\tif ('diff' in request && request.diff) {\n\t\t\t\t\t\t\tthis.applyNetworkDiff(request.diff, true)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.applyNetworkDiff(diff.action.rebaseWithDiff, true)\n\t\t\t\t\t\tthis.pendingPushRequests.shift()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// update the speculative diff while re-applying pending changes\n\t\t\t\ttry {\n\t\t\t\t\tthis.speculativeChanges = this.store.extractingChanges(() => {\n\t\t\t\t\t\tfor (const request of this.pendingPushRequests) {\n\t\t\t\t\t\t\tif (!('diff' in request) || !request.diff) continue\n\t\t\t\t\t\t\tthis.applyNetworkDiff(request.diff, true)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!this.unsentChanges.nextDiff) return\n\t\t\t\t\t\tconst diff = getNetworkDiff(this.unsentChanges.nextDiff)\n\t\t\t\t\t\tif (!diff) return\n\t\t\t\t\t\tthis.applyNetworkDiff(diff, true)\n\t\t\t\t\t})\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconsole.error(e)\n\t\t\t\t\t// throw away the speculative changes and start over\n\t\t\t\t\tthis.speculativeChanges = { added: {} as any, updated: {} as any, removed: {} as any }\n\t\t\t\t\tthis.resetConnection()\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.lastServerClock = diffs.at(-1)?.serverClock ?? this.lastServerClock\n\t\t} catch (e) {\n\t\t\tconsole.error(e)\n\t\t\tthis.store.ensureStoreIsUsable()\n\t\t\tthis.resetConnection()\n\t\t}\n\t}\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { Signal, react, transact } from '@tldraw/state'\nimport {\n\tRecordId,\n\tRecordsDiff,\n\tStore,\n\tUnknownRecord,\n\treverseRecordsDiff,\n\tsquashRecordDiffsMutable,\n} from '@tldraw/store'\nimport {\n\tFpsScheduler,\n\texhaustiveSwitchError,\n\tisEqual,\n\tobjectMapEntries,\n\tuniqueId,\n} from '@tldraw/utils'\nimport {\n\tNetworkDiff,\n\tObjectDiff,\n\tRecordOpType,\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n} from './diff'\nimport { interval } from './interval'\nimport {\n\tTLObjectStoreAccess,\n\tTLPushRequest,\n\tTLSocketClientSentEvent,\n\tTLSocketServerSentDataEvent,\n\tTLSocketServerSentEvent,\n\tgetTlsyncProtocolVersion,\n} from './protocol'\n\n/**\n * Function type for subscribing to events with a callback.\n * Returns an unsubscribe function to clean up the listener.\n *\n * @param cb - Callback function that receives the event value\n * @returns Function to call when you want to unsubscribe from the events\n *\n * @public\n */\nexport type SubscribingFn<T> = (cb: (val: T) => void) => () => void\n\n/** Network sync frame rate when in solo mode (no collaborators) @internal */\nconst SOLO_MODE_FPS = 1\n\n/** Network sync frame rate when in collaborative mode (with collaborators) @internal */\nconst COLLABORATIVE_MODE_FPS = 30\n\n/**\n * WebSocket close code used by the server to signal a non-recoverable sync error.\n * This close code indicates that the connection is being terminated due to an error\n * that cannot be automatically recovered from, such as authentication failures,\n * incompatible client versions, or invalid data.\n *\n * @example\n * ```ts\n * // Server-side: Close connection with specific error reason\n * socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.NOT_FOUND)\n *\n * // Client-side: Handle the error in your sync error handler\n * const syncClient = new TLSyncClient({\n * // ... other config\n * onSyncError: (reason) => {\n * console.error('Sync failed:', reason) // Will receive 'NOT_FOUND'\n * }\n * })\n * ```\n *\n * @public\n */\nexport const TLSyncErrorCloseEventCode = 4099 as const\n\n/**\n * Predefined reasons for server-initiated connection closures.\n * These constants represent different error conditions that can cause\n * the sync server to terminate a WebSocket connection.\n *\n * @example\n * ```ts\n * // Server usage\n * if (!user.hasPermission(roomId)) {\n * socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.FORBIDDEN)\n * }\n *\n * // Client error handling\n * syncClient.onSyncError((reason) => {\n * switch (reason) {\n * case TLSyncErrorCloseEventReason.NOT_FOUND:\n * showError('Room does not exist')\n * break\n * case TLSyncErrorCloseEventReason.FORBIDDEN:\n * showError('Access denied')\n * break\n * case TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:\n * showError('Please update your app')\n * break\n * }\n * })\n * ```\n *\n * @public\n */\nexport const TLSyncErrorCloseEventReason = {\n\t/** Room or resource not found */\n\tNOT_FOUND: 'NOT_FOUND',\n\t/** User lacks permission to access the room */\n\tFORBIDDEN: 'FORBIDDEN',\n\t/** User authentication required or invalid */\n\tNOT_AUTHENTICATED: 'NOT_AUTHENTICATED',\n\t/** Unexpected server error occurred */\n\tUNKNOWN_ERROR: 'UNKNOWN_ERROR',\n\t/** Client protocol version too old */\n\tCLIENT_TOO_OLD: 'CLIENT_TOO_OLD',\n\t/** Server protocol version too old */\n\tSERVER_TOO_OLD: 'SERVER_TOO_OLD',\n\t/** Client sent invalid or corrupted record data */\n\tINVALID_RECORD: 'INVALID_RECORD',\n\t/** Client exceeded rate limits */\n\tRATE_LIMITED: 'RATE_LIMITED',\n\t/** Room has reached maximum capacity */\n\tROOM_FULL: 'ROOM_FULL',\n} as const\n\n/**\n * @internal\n */\nexport class TLSyncError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic reason: TLSyncErrorCloseEventReason\n\t) {\n\t\tsuper(message)\n\t}\n}\n/**\n * Union type of all possible server connection close reasons.\n * Represents the string values that can be passed when a server closes\n * a sync connection due to an error condition.\n *\n * @public\n */\nexport type TLSyncErrorCloseEventReason =\n\t(typeof TLSyncErrorCloseEventReason)[keyof typeof TLSyncErrorCloseEventReason]\n\n/**\n * Handler function for custom application messages sent through the sync protocol.\n * These are user-defined messages that can be sent between clients via the sync server,\n * separate from the standard document synchronization messages.\n *\n * @param data - Custom message payload (application-defined structure)\n *\n * @example\n * ```ts\n * const customMessageHandler: TLCustomMessageHandler = (data) => {\n * if (data.type === 'user_joined') {\n * console.log(`${data.username} joined the session`)\n * showToast(`${data.username} is now collaborating`)\n * }\n * }\n *\n * const syncClient = new TLSyncClient({\n * // ... other config\n * onCustomMessageReceived: customMessageHandler\n * })\n * ```\n *\n * @public\n */\nexport type TLCustomMessageHandler = (this: null, data: any) => void\n\n/**\n * Event object describing changes in socket connection status.\n * Contains either a basic status change or an error with details.\n *\n * @public\n */\nexport type TLSocketStatusChangeEvent =\n\t| {\n\t\t\t/** Connection came online or went offline */\n\t\t\tstatus: 'online' | 'offline'\n\t }\n\t| {\n\t\t\t/** Connection encountered an error */\n\t\t\tstatus: 'error'\n\t\t\t/** Description of the error that occurred */\n\t\t\treason: string\n\t }\n/**\n * Callback function type for listening to socket status changes.\n *\n * @param params - Event object containing the new status and optional error details\n *\n * @internal\n */\nexport type TLSocketStatusListener = (params: TLSocketStatusChangeEvent) => void\n\n/**\n * Possible connection states for a persistent client socket.\n * Represents the current connectivity status between client and server.\n *\n * @internal\n */\nexport type TLPersistentClientSocketStatus = 'online' | 'offline' | 'error'\n\n/**\n * Mode for handling presence information in sync sessions.\n * Controls whether presence data (cursors, selections) is shared with other clients.\n *\n * @public\n */\nexport type TLPresenceMode =\n\t/** No presence sharing - client operates independently */\n\t| 'solo'\n\t/** Full presence sharing - cursors and selections visible to others */\n\t| 'full'\n/**\n * Interface for persistent WebSocket-like connections used by TLSyncClient.\n * Handles automatic reconnection and provides event-based communication with the sync server.\n * Implementations should maintain connection resilience and handle network interruptions gracefully.\n *\n * @example\n * ```ts\n * class MySocketAdapter implements TLPersistentClientSocket {\n * connectionStatus: 'offline' | 'online' | 'error' = 'offline'\n *\n * sendMessage(msg: TLSocketClientSentEvent) {\n * if (this.ws && this.ws.readyState === WebSocket.OPEN) {\n * this.ws.send(JSON.stringify(msg))\n * }\n * }\n *\n * onReceiveMessage = (callback) => {\n * // Set up message listener and return cleanup function\n * }\n *\n * restart() {\n * this.disconnect()\n * this.connect()\n * }\n * }\n * ```\n *\n * @public\n */\nexport interface TLPersistentClientSocket<\n\tClientSentMessage extends object = object,\n\tServerSentMessage extends object = object,\n> {\n\t/** Current connection state - online means actively connected and ready */\n\tconnectionStatus: 'online' | 'offline' | 'error'\n\n\t/**\n\t * Send a protocol message to the sync server\n\t * @param msg - Message to send (connect, push, ping, etc.)\n\t */\n\tsendMessage(msg: ClientSentMessage): void\n\n\t/**\n\t * Subscribe to messages received from the server\n\t * @param callback - Function called for each received message\n\t * @returns Cleanup function to remove the listener\n\t */\n\tonReceiveMessage: SubscribingFn<ServerSentMessage>\n\n\t/**\n\t * Subscribe to connection status changes\n\t * @param callback - Function called when connection status changes\n\t * @returns Cleanup function to remove the listener\n\t */\n\tonStatusChange: SubscribingFn<TLSocketStatusChangeEvent>\n\n\t/**\n\t * Force a connection restart (disconnect then reconnect)\n\t * Used for error recovery or when connection health checks fail\n\t */\n\trestart(): void\n\n\t/**\n\t * Close the connection\n\t */\n\tclose(): void\n}\n\nconst PING_INTERVAL = 5000\nconst MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION = PING_INTERVAL * 2\n/**\n * How long an unanswered ping may stay outstanding before the connection is considered dead.\n * Tuned together with MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION:\n * a reset needs both stale interaction and a ping overdue by this much.\n */\nconst PONG_TIMEOUT = PING_INTERVAL * 2\n\n// Should connect support chunking the response to allow for large payloads?\n\nfunction getPresenceOp<R extends UnknownRecord>(\n\tlastPushedPresenceState: R | null,\n\tnextPresence: R | null\n): [typeof RecordOpType.Patch, ObjectDiff] | [typeof RecordOpType.Put, R] | undefined {\n\tif (!lastPushedPresenceState && nextPresence) {\n\t\treturn [RecordOpType.Put, nextPresence]\n\t}\n\tif (lastPushedPresenceState && nextPresence) {\n\t\tconst diff = diffRecord(lastPushedPresenceState, nextPresence)\n\t\tif (!diff) return undefined\n\t\treturn [RecordOpType.Patch, diff]\n\t}\n\treturn undefined\n}\n\n/**\n * Main client-side synchronization engine for collaborative tldraw applications.\n *\n * TLSyncClient manages bidirectional synchronization between a local tldraw Store\n * and a remote sync server. It uses an optimistic update model where local changes\n * are immediately applied for responsive UI, then sent to the server for validation\n * and distribution to other clients.\n *\n * The synchronization follows a git-like push/pull/rebase model:\n * - **Push**: Local changes are sent to server as diff operations\n * - **Pull**: Server changes are received and applied locally\n * - **Rebase**: Conflicting changes are resolved by undoing local changes,\n * applying server changes, then re-applying local changes on top\n *\n * @example\n * ```ts\n * import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core'\n * import { createTLStore } from '@tldraw/store'\n *\n * // Create store and socket\n * const store = createTLStore({ schema: mySchema })\n * const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync')\n *\n * // Create sync client\n * const syncClient = new TLSyncClient({\n * store,\n * socket,\n * presence: atom(null),\n * onLoad: () => console.log('Connected and loaded'),\n * onSyncError: (reason) => console.error('Sync failed:', reason)\n * })\n *\n * // Changes to store are now automatically synchronized\n * store.put([{ id: 'shape1', type: 'geo', x: 100, y: 100 }])\n * ```\n *\n * @example\n * ```ts\n * // Advanced usage with presence and custom messages\n * const syncClient = new TLSyncClient({\n * store,\n * socket,\n * presence: atom({ cursor: { x: 0, y: 0 }, userName: 'Alice' }),\n * presenceMode: atom('full'),\n * onCustomMessageReceived: (data) => {\n * if (data.type === 'chat') {\n * showChatMessage(data.message, data.from)\n * }\n * },\n * onAfterConnect: (client, { isReadonly }) => {\n * if (isReadonly) {\n * showNotification('Connected in read-only mode')\n * }\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>> {\n\t/** The last clock time from the most recent server update */\n\tprivate lastServerClock = -1\n\tprivate lastServerInteractionTimestamp = Date.now()\n\t/**\n\t * Send time of the oldest outstanding ping; answered iff `lastServerInteractionTimestamp >= it`\n\t * (any server message counts). Null until the first ping after (re)connect.\n\t */\n\tprivate firstUnansweredPingAt: number | null = null\n\n\t/** The queue of in-flight push requests that have not yet been acknowledged by the server */\n\tprivate pendingPushRequests: TLPushRequest<R>[] = []\n\tprivate unsentChanges: {\n\t\tnextDiff?: RecordsDiff<R>\n\t\tnextPresence?: R | null\n\t} = { nextDiff: undefined, nextPresence: undefined }\n\n\t/**\n\t * The diff of 'unconfirmed', 'optimistic' changes that have been made locally by the user if we\n\t * take this diff, reverse it, and apply that to the store, our store will match exactly the most\n\t * recent state of the server that we know about\n\t */\n\tprivate speculativeChanges: RecordsDiff<R> = {\n\t\tadded: {} as any,\n\t\tupdated: {} as any,\n\t\tremoved: {} as any,\n\t}\n\n\tprivate disposables: Array<() => void> = []\n\n\t/** Separate scheduler instance for network sync operations */\n\tprivate readonly fpsScheduler: FpsScheduler\n\n\t/** Send any unsent push requests to the server */\n\tprivate readonly sendUnsentChanges: {\n\t\t(): void\n\t\tcancel?(): void\n\t}\n\n\t/** Schedule a rebase operation */\n\tprivate readonly scheduleRebase: {\n\t\t(): void\n\t\tcancel?(): void\n\t}\n\n\t/** @internal */\n\treadonly store: S\n\t/** @internal */\n\treadonly socket: TLPersistentClientSocket<TLSocketClientSentEvent<R>, TLSocketServerSentEvent<R>>\n\n\t/** @internal */\n\treadonly presenceState: Signal<R | null> | undefined\n\t/** @internal */\n\treadonly presenceMode: Signal<TLPresenceMode> | undefined\n\n\t// isOnline is true when we have an open socket connection and we have\n\t// established a connection with the server room (i.e. we have received a 'connect' message)\n\t/** @internal */\n\tisConnectedToRoom = false\n\n\t/**\n\t * The client clock is essentially a counter for push requests Each time a push request is created\n\t * the clock is incremented. This clock is sent with the push request to the server, and the\n\t * server returns it with the response so that we can match up the response with the request.\n\t *\n\t * The clock may also be used at one point in the future to allow the client to re-send push\n\t * requests idempotently (i.e. the server will keep track of each client's clock and not execute\n\t * requests it has already handled), but at the time of writing this is neither needed nor\n\t * implemented.\n\t */\n\tprivate clientClock = 0\n\n\t/**\n\t * Callback executed immediately after successful connection to sync room.\n\t * Use this to perform any post-connection setup required for your application,\n\t * such as initializing default content or updating UI state.\n\t *\n\t * @param self - The TLSyncClient instance that connected\n\t * @param details - Connection details\n\t * - isReadonly - Whether the connection is in read-only mode\n\t * - objectAccess - Write access for object-store lane record types (defaults to 'write'\n\t * when the server doesn't send it, e.g. older servers or rooms with no object lane)\n\t */\n\tprivate readonly onAfterConnect?: (\n\t\tself: this,\n\t\tdetails: { isReadonly: boolean; objectAccess: TLObjectStoreAccess }\n\t) => void\n\n\tprivate readonly onCustomMessageReceived?: TLCustomMessageHandler\n\n\tprivate isDebugging = false\n\tprivate debug(...args: any[]) {\n\t\tif (this.isDebugging) {\n\t\t\t// eslint-disable-next-line no-console\n\t\t\tconsole.debug(...args)\n\t\t}\n\t}\n\n\tprivate readonly presenceType: R['typeName'] | null\n\n\tprivate didCancel?: () => boolean\n\n\t/**\n\t * Creates a new TLSyncClient instance to manage synchronization with a remote server.\n\t *\n\t * @param config - Configuration object for the sync client\n\t * - store - The local tldraw store to synchronize\n\t * - socket - WebSocket adapter for server communication\n\t * - presence - Reactive signal containing current user's presence data\n\t * - presenceMode - Optional signal controlling presence sharing (defaults to 'full')\n\t * - onLoad - Callback fired when initial sync completes successfully\n\t * - onSyncError - Callback fired when sync fails with error reason\n\t * - onCustomMessageReceived - Optional handler for custom messages\n\t * - onAfterConnect - Optional callback fired after successful connection\n\t * - self - The TLSyncClient instance\n\t * - details - Connection details including readonly status\n\t * - didCancel - Optional function to check if sync should be cancelled\n\t */\n\tconstructor(config: {\n\t\tstore: S\n\t\tsocket: TLPersistentClientSocket<any, any>\n\t\tpresence: Signal<R | null>\n\t\tpresenceMode?: Signal<TLPresenceMode>\n\t\tonLoad(self: TLSyncClient<R, S>): void\n\t\tonSyncError(reason: string): void\n\t\tonCustomMessageReceived?: TLCustomMessageHandler\n\t\tonAfterConnect?(\n\t\t\tself: TLSyncClient<R, S>,\n\t\t\tdetails: { isReadonly: boolean; objectAccess: TLObjectStoreAccess }\n\t\t): void\n\t\tdidCancel?(): boolean\n\t}) {\n\t\tthis.didCancel = config.didCancel\n\n\t\tthis.presenceType = config.store.scopedTypes.presence.values().next().value ?? null\n\n\t\t// Create a separate throttle instance for network sync operations\n\t\t// This ensures sync operations have their own queue separate from UI operations\n\t\tthis.fpsScheduler = new FpsScheduler(COLLABORATIVE_MODE_FPS)\n\n\t\t// Initialize throttled methods after throttle instance is created\n\t\tthis.sendUnsentChanges = this.fpsScheduler.fpsThrottle(() => {\n\t\t\tthis.debug('sending unsent changes', {\n\t\t\t\tisConnectedToRoom: this.isConnectedToRoom,\n\t\t\t\tunsentChanges: this.unsentChanges,\n\t\t\t})\n\t\t\tif (!this.isConnectedToRoom || this.store.isPossiblyCorrupted()) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.unsentChanges.nextDiff && !this.unsentChanges.nextPresence) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst diff = this.unsentChanges.nextDiff\n\t\t\t\t? (getNetworkDiff(this.unsentChanges.nextDiff) ?? undefined)\n\t\t\t\t: undefined\n\t\t\tconst presence = this.unsentChanges.nextPresence\n\t\t\t\t? getPresenceOp<R>(this.lastPushedPresenceState, this.unsentChanges.nextPresence)\n\t\t\t\t: undefined\n\n\t\t\tif (!diff && !presence) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst pushRequest: TLPushRequest<R> = {\n\t\t\t\ttype: 'push',\n\t\t\t\tclientClock: this.clientClock,\n\t\t\t\tdiff,\n\t\t\t\tpresence,\n\t\t\t}\n\n\t\t\tthis.debug('sending push request', pushRequest)\n\t\t\tthis.socket.sendMessage(pushRequest)\n\n\t\t\tif (this.unsentChanges.nextPresence) {\n\t\t\t\tthis.lastPushedPresenceState = this.unsentChanges.nextPresence\n\t\t\t}\n\t\t\tthis.clientClock++\n\t\t\tthis.pendingPushRequests.push(pushRequest)\n\t\t\tthis.unsentChanges.nextDiff = undefined\n\t\t\tthis.unsentChanges.nextPresence = undefined\n\t\t})\n\n\t\tthis.scheduleRebase = this.fpsScheduler.fpsThrottle(this.rebase)\n\n\t\tif (typeof window !== 'undefined') {\n\t\t\t;(window as any).tlsync = this\n\t\t}\n\t\tthis.store = config.store\n\t\tthis.socket = config.socket\n\t\tthis.onAfterConnect = config.onAfterConnect\n\t\tthis.onCustomMessageReceived = config.onCustomMessageReceived\n\n\t\tlet didLoad = false\n\n\t\tthis.presenceState = config.presence\n\t\tthis.presenceMode = config.presenceMode\n\n\t\tthis.disposables.push(\n\t\t\t// when local 'user' changes are made, send them to the server\n\t\t\t// or stash them locally in offline mode\n\t\t\tthis.store.listen(\n\t\t\t\t({ changes }) => {\n\t\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\t\tthis.debug('received store changes', { changes })\n\t\t\t\t\tthis.push(changes)\n\t\t\t\t},\n\t\t\t\t{ source: 'user', scope: 'document' }\n\t\t\t),\n\t\t\t// when the server sends us events, handle them\n\t\t\tthis.socket.onReceiveMessage((msg) => {\n\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\tthis.debug('received message from server', msg)\n\t\t\t\tthis.handleServerEvent(msg)\n\t\t\t\t// the first time we receive a message from the server, we should trigger\n\n\t\t\t\t// one of the load callbacks\n\t\t\t\tif (!didLoad) {\n\t\t\t\t\tdidLoad = true\n\t\t\t\t\tconfig.onLoad(this)\n\t\t\t\t}\n\t\t\t}),\n\t\t\t// handle switching between online and offline\n\t\t\tthis.socket.onStatusChange((ev) => {\n\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\tthis.debug('socket status changed', ev.status)\n\t\t\t\tif (ev.status === 'online') {\n\t\t\t\t\tthis.sendConnectMessage()\n\t\t\t\t} else {\n\t\t\t\t\tthis.resetConnection()\n\t\t\t\t\tif (ev.status === 'error') {\n\t\t\t\t\t\tdidLoad = true\n\t\t\t\t\t\tconfig.onSyncError(ev.reason)\n\t\t\t\t\t\tthis.close()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}),\n\t\t\t// Send a ping every PING_INTERVAL ms while online\n\t\t\tinterval(() => {\n\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\tthis.debug('ping loop', { isConnectedToRoom: this.isConnectedToRoom })\n\t\t\t\tif (!this.isConnectedToRoom) return\n\t\t\t\ttry {\n\t\t\t\t\t// advance the marker only when the previous ping was answered: a half-open socket\n\t\t\t\t\t// that accepts sends but returns nothing must not keep refreshing its own alibi\n\t\t\t\t\tif (\n\t\t\t\t\t\tthis.firstUnansweredPingAt === null ||\n\t\t\t\t\t\tthis.lastServerInteractionTimestamp >= this.firstUnansweredPingAt\n\t\t\t\t\t) {\n\t\t\t\t\t\tthis.firstUnansweredPingAt = Date.now()\n\t\t\t\t\t}\n\t\t\t\t\tthis.socket.sendMessage({ type: 'ping' })\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.warn('ping failed, resetting', error)\n\t\t\t\t\tthis.resetConnection()\n\t\t\t\t}\n\t\t\t}, PING_INTERVAL),\n\t\t\t// Check the server connection health, reset the connection if needed\n\t\t\tinterval(() => {\n\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\tthis.debug('health check loop', { isConnectedToRoom: this.isConnectedToRoom })\n\t\t\t\tif (!this.isConnectedToRoom) return\n\t\t\t\tconst timeSinceLastServerInteraction = Date.now() - this.lastServerInteractionTimestamp\n\n\t\t\t\tif (\n\t\t\t\t\ttimeSinceLastServerInteraction <\n\t\t\t\t\tMAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION\n\t\t\t\t) {\n\t\t\t\t\tthis.debug('health check passed', { timeSinceLastServerInteraction })\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// Silence alone is not evidence of a dead socket: in a throttled hidden tab our own ping\n\t\t\t\t// loop stops, so the server had nothing to answer. Only reset on an unanswered ping.\n\t\t\t\tconst firstUnansweredPingAt = this.firstUnansweredPingAt\n\t\t\t\tconst pingOverdue =\n\t\t\t\t\tfirstUnansweredPingAt !== null &&\n\t\t\t\t\tthis.lastServerInteractionTimestamp < firstUnansweredPingAt &&\n\t\t\t\t\tDate.now() - firstUnansweredPingAt >= PONG_TIMEOUT\n\t\t\t\tif (!pingOverdue) {\n\t\t\t\t\tthis.debug('health check stale but no overdue ping', { timeSinceLastServerInteraction })\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t// A frozen tab can deliver a queued pong only after these catch-up ticks run, so one\n\t\t\t\t// spurious reset on unfreeze is still possible \u2014 rare, and the reconnect is clean.\n\t\t\t\tconsole.warn(`Haven't heard from the server in a while, resetting connection...`, {\n\t\t\t\t\ttimeSinceLastServerInteraction,\n\t\t\t\t\tpingOutstandingMs: Date.now() - firstUnansweredPingAt!,\n\t\t\t\t})\n\t\t\t\tthis.resetConnection()\n\t\t\t}, PING_INTERVAL * 2)\n\t\t)\n\n\t\tif (this.presenceState) {\n\t\t\tthis.disposables.push(\n\t\t\t\treact('pushPresence', () => {\n\t\t\t\t\tif (this.didCancel?.()) return this.close()\n\t\t\t\t\tconst mode = this.presenceMode?.get()\n\t\t\t\t\tthis.fpsScheduler.updateTargetFps(this.getSyncFps())\n\t\t\t\t\tif (mode !== 'full') return\n\t\t\t\t\tthis.pushPresence(this.presenceState!.get())\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\n\t\t// if the socket is already online before this client was instantiated\n\t\t// then we should send a connect message right away\n\t\tif (this.socket.connectionStatus === 'online') {\n\t\t\tthis.sendConnectMessage()\n\t\t}\n\t}\n\n\t/** @internal */\n\tlatestConnectRequestId: string | null = null\n\n\t/**\n\t * This is the first message that is sent over a newly established socket connection. And we need\n\t * to wait for the response before this client can be used.\n\t */\n\tprivate sendConnectMessage() {\n\t\tif (this.isConnectedToRoom) {\n\t\t\tconsole.error('sendConnectMessage called while already connected')\n\t\t\treturn\n\t\t}\n\t\tthis.debug('sending connect message')\n\t\tthis.latestConnectRequestId = uniqueId()\n\t\tthis.socket.sendMessage({\n\t\t\ttype: 'connect',\n\t\t\tconnectRequestId: this.latestConnectRequestId,\n\t\t\tschema: this.store.schema.serialize(),\n\t\t\tprotocolVersion: getTlsyncProtocolVersion(),\n\t\t\tlastServerClock: this.lastServerClock,\n\t\t})\n\t}\n\n\t/** Switch to offline mode */\n\tprivate resetConnection(hard = false) {\n\t\tthis.debug('resetting connection')\n\t\tif (hard) {\n\t\t\tthis.lastServerClock = 0\n\t\t}\n\t\t// kill all presence state\n\t\tconst keys = Object.keys(this.store.serialize('presence')) as any\n\t\tif (keys.length > 0) {\n\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\tthis.store.remove(keys)\n\t\t\t})\n\t\t}\n\t\tthis.lastPushedPresenceState = null\n\t\tthis.isConnectedToRoom = false\n\t\tthis.firstUnansweredPingAt = null\n\t\tthis.pendingPushRequests = []\n\t\tthis.incomingDiffBuffer = []\n\t\tthis.unsentChanges.nextDiff = undefined\n\t\tthis.unsentChanges.nextPresence = undefined\n\t\tif (this.socket.connectionStatus === 'online') {\n\t\t\tthis.socket.restart()\n\t\t}\n\t}\n\n\t/**\n\t * Invoked when the socket connection comes online, either for the first time or as the result of\n\t * a reconnect. The goal is to rebase on the server's state and fire off a new push request for\n\t * any local changes that were made while offline.\n\t */\n\tprivate didReconnect(event: Extract<TLSocketServerSentEvent<R>, { type: 'connect' }>) {\n\t\tthis.debug('did reconnect', event)\n\t\tif (event.connectRequestId !== this.latestConnectRequestId) {\n\t\t\t// ignore connect events for old connect requests\n\t\t\treturn\n\t\t}\n\t\tthis.latestConnectRequestId = null\n\n\t\tif (this.isConnectedToRoom) {\n\t\t\tconsole.error('didReconnect called while already connected')\n\t\t\tthis.resetConnection(true)\n\t\t\treturn\n\t\t}\n\t\tif (this.pendingPushRequests.length > 0) {\n\t\t\tconsole.error('pendingPushRequests should already be empty when we reconnect')\n\t\t\tthis.resetConnection(true)\n\t\t\treturn\n\t\t}\n\t\t// at the end of this process we want to have at most one pending push request\n\t\t// based on anything inside this.speculativeChanges\n\t\ttransact(() => {\n\t\t\t// Now our goal is to rebase on the server's state.\n\t\t\t// This means wiping away any peer presence data, which the server will replace in full on every connect.\n\t\t\t// If the server does not have enough history to give us a partial document state hydration we will\n\t\t\t// also need to wipe away all of our document state before hydrating with the server's state from scratch.\n\t\t\tconst stashedChanges = this.speculativeChanges\n\t\t\tthis.speculativeChanges = { added: {} as any, updated: {} as any, removed: {} as any }\n\n\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t// gather records to delete in a NetworkDiff\n\t\t\t\tconst wipeDiff: NetworkDiff<R> = {}\n\t\t\t\tconst wipeAll = event.hydrationType === 'wipe_all'\n\t\t\t\tif (!wipeAll) {\n\t\t\t\t\t// if we're only wiping presence data, undo the speculative changes first\n\t\t\t\t\tthis.store.applyDiff(reverseRecordsDiff(stashedChanges), { runCallbacks: false })\n\t\t\t\t}\n\n\t\t\t\t// now wipe all presence data and, if needed, all document data\n\t\t\t\tfor (const [id, record] of objectMapEntries(this.store.serialize('all'))) {\n\t\t\t\t\tif (\n\t\t\t\t\t\t(wipeAll && this.store.scopedTypes.document.has(record.typeName)) ||\n\t\t\t\t\t\trecord.typeName === this.presenceType\n\t\t\t\t\t) {\n\t\t\t\t\t\twipeDiff[id] = [RecordOpType.Remove]\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// then apply the upstream changes\n\t\t\t\tthis.applyNetworkDiff({ ...wipeDiff, ...event.diff }, true)\n\n\t\t\t\tthis.isConnectedToRoom = true\n\n\t\t\t\t// now re-apply the speculative changes creating a new push request with the\n\t\t\t\t// appropriate diff\n\t\t\t\tconst networkDiff = getNetworkDiff(stashedChanges)\n\t\t\t\tif (!networkDiff) return\n\t\t\t\tconst speculativeChanges = this.store.filterChangesByScope(\n\t\t\t\t\tthis.store.extractingChanges(() => {\n\t\t\t\t\t\tthis.applyNetworkDiff(networkDiff, true)\n\t\t\t\t\t}),\n\t\t\t\t\t'document'\n\t\t\t\t)\n\t\t\t\tif (speculativeChanges) this.push(speculativeChanges)\n\t\t\t})\n\n\t\t\t// this.isConnectedToRoom = true\n\t\t\t// this.store.applyDiff(stashedChanges, false)\n\n\t\t\tthis.onAfterConnect?.(this, {\n\t\t\t\tisReadonly: event.isReadonly,\n\t\t\t\tobjectAccess: event.objectAccess ?? 'write',\n\t\t\t})\n\t\t\tconst presence = this.presenceState?.get()\n\t\t\tif (presence) {\n\t\t\t\tthis.pushPresence(presence)\n\t\t\t}\n\t\t})\n\n\t\tthis.lastServerClock = event.serverClock\n\t}\n\n\tprivate incomingDiffBuffer: TLSocketServerSentDataEvent<R>[] = []\n\n\t/** Handle events received from the server */\n\tprivate handleServerEvent(event: TLSocketServerSentEvent<R>) {\n\t\tthis.debug('received server event', event)\n\t\tthis.lastServerInteractionTimestamp = Date.now()\n\t\t// always update the lastServerClock when it is present\n\t\tswitch (event.type) {\n\t\t\tcase 'connect':\n\t\t\t\tthis.didReconnect(event)\n\t\t\t\tbreak\n\t\t\t// legacy v4 events\n\t\t\tcase 'patch':\n\t\t\tcase 'push_result':\n\t\t\t\tif (!this.isConnectedToRoom) break\n\t\t\t\tthis.incomingDiffBuffer.push(event)\n\t\t\t\tthis.scheduleRebase()\n\t\t\t\tbreak\n\t\t\tcase 'data':\n\t\t\t\t// wait for a connect to succeed before processing more events\n\t\t\t\tif (!this.isConnectedToRoom) break\n\t\t\t\tthis.incomingDiffBuffer.push(...event.data)\n\t\t\t\tthis.scheduleRebase()\n\t\t\t\tbreak\n\t\t\tcase 'incompatibility_error':\n\t\t\t\t// legacy unrecoverable errors\n\t\t\t\tconsole.error('incompatibility error is legacy and should no longer be sent by the server')\n\t\t\t\tbreak\n\t\t\tcase 'pong':\n\t\t\t\t// noop, we only use ping/pong to set lastSeverInteractionTimestamp\n\t\t\t\tbreak\n\t\t\tcase 'custom':\n\t\t\t\tthis.onCustomMessageReceived?.call(null, event.data)\n\t\t\t\tbreak\n\n\t\t\tdefault:\n\t\t\t\texhaustiveSwitchError(event)\n\t\t}\n\t}\n\n\t/**\n\t * Closes the sync client and cleans up all resources.\n\t *\n\t * Call this method when you no longer need the sync client to prevent\n\t * memory leaks and close the WebSocket connection. After calling close(),\n\t * the client cannot be reused.\n\t *\n\t * @example\n\t * ```ts\n\t * // Clean shutdown\n\t * syncClient.close()\n\t * ```\n\t */\n\tclose() {\n\t\tthis.debug('closing')\n\t\tthis.disposables.forEach((dispose) => dispose())\n\t\tthis.sendUnsentChanges.cancel?.()\n\t\tthis.scheduleRebase.cancel?.()\n\t\tif (typeof window !== 'undefined' && (window as any).tlsync === this) {\n\t\t\tdelete (window as any).tlsync\n\t\t}\n\t}\n\n\tprivate lastPushedPresenceState: R | null = null\n\n\tprivate pushPresence(nextPresence: R | null) {\n\t\t// make sure we push any document changes first\n\t\tthis.store._flushHistory()\n\n\t\tif (!this.isConnectedToRoom) {\n\t\t\t// if we're offline, don't do anything\n\t\t\treturn\n\t\t}\n\n\t\tthis.unsentChanges.nextPresence = nextPresence\n\t\tthis.sendUnsentChanges()\n\t}\n\n\t/** Push a change to the server, or stash it locally if we're offline */\n\tprivate push(change: RecordsDiff<any>) {\n\t\tthis.debug('push', change)\n\t\tsquashRecordDiffsMutable(this.speculativeChanges, [change])\n\t\t// in offline mode, we only accumulate in speculativeChanges\n\t\tif (!this.isConnectedToRoom) return\n\t\tif (!this.unsentChanges.nextDiff) {\n\t\t\tthis.unsentChanges.nextDiff = { added: {} as any, updated: {} as any, removed: {} as any }\n\t\t}\n\t\t// records are immutable, so sharing their references with `change` is fine \u2014 the\n\t\t// squash gives nextDiff its own containers and tuples without deep-cloning records\n\t\tsquashRecordDiffsMutable(this.unsentChanges.nextDiff, [change])\n\t\tthis.sendUnsentChanges()\n\t}\n\n\t/** Get the target FPS for network operations based on presence mode */\n\tprivate getSyncFps(): number {\n\t\treturn this.presenceMode?.get() === 'solo' ? SOLO_MODE_FPS : COLLABORATIVE_MODE_FPS\n\t}\n\n\t/**\n\t * Applies a 'network' diff to the store this does value-based equality checking so that if the\n\t * data is the same (as opposed to merely identical with ===), then no change is made and no\n\t * changes will be propagated back to store listeners\n\t */\n\tprivate applyNetworkDiff(diff: NetworkDiff<R>, runCallbacks: boolean) {\n\t\tthis.debug('applyNetworkDiff', diff)\n\t\tconst changes: RecordsDiff<R> = { added: {} as any, updated: {} as any, removed: {} as any }\n\t\ttype k = keyof typeof changes.updated\n\t\tlet hasChanges = false\n\t\tfor (const [id, op] of objectMapEntries(diff)) {\n\t\t\tif (op[0] === RecordOpType.Put) {\n\t\t\t\tconst existing = this.store.get(id as RecordId<any>)\n\t\t\t\tif (existing && !isEqual(existing, op[1])) {\n\t\t\t\t\thasChanges = true\n\t\t\t\t\tchanges.updated[id as k] = [existing, op[1]]\n\t\t\t\t} else {\n\t\t\t\t\thasChanges = true\n\t\t\t\t\tchanges.added[id as k] = op[1]\n\t\t\t\t}\n\t\t\t} else if (op[0] === RecordOpType.Patch) {\n\t\t\t\tconst record = this.store.get(id as RecordId<any>)\n\t\t\t\tif (!record) {\n\t\t\t\t\t// the record was removed upstream\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst patched = applyObjectDiff(record, op[1])\n\t\t\t\thasChanges = true\n\t\t\t\tchanges.updated[id as k] = [record, patched]\n\t\t\t} else if (op[0] === RecordOpType.Remove) {\n\t\t\t\tif (this.store.has(id as RecordId<any>)) {\n\t\t\t\t\thasChanges = true\n\t\t\t\t\tchanges.removed[id as k] = this.store.get(id as RecordId<any>)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (hasChanges) {\n\t\t\tthis.store.applyDiff(changes, { runCallbacks })\n\t\t}\n\t}\n\n\t// eslint-disable-next-line tldraw/prefer-class-methods\n\tprivate rebase = () => {\n\t\t// need to make sure that our speculative changes are in sync with the actual store instance before\n\t\t// proceeding, to avoid inconsistency bugs.\n\t\tthis.store._flushHistory()\n\t\tif (this.incomingDiffBuffer.length === 0) return\n\n\t\tconst diffs = this.incomingDiffBuffer\n\t\tthis.incomingDiffBuffer = []\n\n\t\ttry {\n\t\t\tthis.store.mergeRemoteChanges(() => {\n\t\t\t\t// first undo speculative changes\n\t\t\t\tthis.store.applyDiff(reverseRecordsDiff(this.speculativeChanges), { runCallbacks: false })\n\n\t\t\t\t// then apply network diffs on top of known-to-be-synced data\n\t\t\t\tfor (const diff of diffs) {\n\t\t\t\t\tif (diff.type === 'patch') {\n\t\t\t\t\t\tthis.applyNetworkDiff(diff.diff, true)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// handling push_result\n\t\t\t\t\tif (this.pendingPushRequests.length === 0) {\n\t\t\t\t\t\tthrow new Error('Received push_result but there are no pending push requests')\n\t\t\t\t\t}\n\t\t\t\t\tif (this.pendingPushRequests[0].clientClock !== diff.clientClock) {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t'Received push_result for a push request that is not at the front of the queue'\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tif (diff.action === 'discard') {\n\t\t\t\t\t\tthis.pendingPushRequests.shift()\n\t\t\t\t\t} else if (diff.action === 'commit') {\n\t\t\t\t\t\tconst request = this.pendingPushRequests.shift()!\n\t\t\t\t\t\tif ('diff' in request && request.diff) {\n\t\t\t\t\t\t\tthis.applyNetworkDiff(request.diff, true)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.applyNetworkDiff(diff.action.rebaseWithDiff, true)\n\t\t\t\t\t\tthis.pendingPushRequests.shift()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// update the speculative diff while re-applying pending changes\n\t\t\t\ttry {\n\t\t\t\t\tthis.speculativeChanges = this.store.extractingChanges(() => {\n\t\t\t\t\t\tfor (const request of this.pendingPushRequests) {\n\t\t\t\t\t\t\tif (!('diff' in request) || !request.diff) continue\n\t\t\t\t\t\t\tthis.applyNetworkDiff(request.diff, true)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!this.unsentChanges.nextDiff) return\n\t\t\t\t\t\tconst diff = getNetworkDiff(this.unsentChanges.nextDiff)\n\t\t\t\t\t\tif (!diff) return\n\t\t\t\t\t\tthis.applyNetworkDiff(diff, true)\n\t\t\t\t\t})\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconsole.error(e)\n\t\t\t\t\t// throw away the speculative changes and start over\n\t\t\t\t\tthis.speculativeChanges = { added: {} as any, updated: {} as any, removed: {} as any }\n\t\t\t\t\tthis.resetConnection()\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.lastServerClock = diffs.at(-1)?.serverClock ?? this.lastServerClock\n\t\t} catch (e) {\n\t\t\tconsole.error(e)\n\t\t\tthis.store.ensureStoreIsUsable()\n\t\t\tthis.resetConnection()\n\t\t}\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -52,7 +52,7 @@ function loadSnapshotIntoStorage(txn, schema, snapshot) {
|
|
|
52
52
|
if ((0, import_utils.isEqual)(existing, doc.state)) continue;
|
|
53
53
|
txn.set(doc.state.id, doc.state);
|
|
54
54
|
}
|
|
55
|
-
for (const id of txn.keys()) {
|
|
55
|
+
for (const id of Array.from(txn.keys())) {
|
|
56
56
|
if (!docIds.has(id)) {
|
|
57
57
|
txn.delete(id);
|
|
58
58
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/TLSyncStorage.ts"],
|
|
4
|
-
"sourcesContent": ["import { StoreSchema, SynchronousStorage, UnknownRecord } from '@tldraw/store'\nimport {
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["import { StoreSchema, SynchronousStorage, UnknownRecord } from '@tldraw/store'\nimport { TLStoreSnapshot } from '@tldraw/tlschema'\nimport { assert, isEqual, objectMapEntriesIterable, objectMapValues } from '@tldraw/utils'\nimport { diffRecord, NetworkDiff, RecordOpType } from './diff'\nimport { RoomSnapshot } from './TLSyncRoom'\n\n/**\n * Transaction interface for storage operations. Provides methods to read and modify\n * documents, tombstones, and metadata within a transaction.\n *\n * @public\n */\nexport interface TLSyncStorageTransaction<R extends UnknownRecord> extends SynchronousStorage<R> {\n\t/**\n\t * Get the current clock value.\n\t * If the clock has incremented during the transaction,\n\t * the incremented value will be returned.\n\t *\n\t * @returns The current clock value\n\t */\n\tgetClock(): number\n\n\t/**\n\t * Get all changes (document updates and deletions) since a given clock time.\n\t * This is the main method for calculating diffs for client sync.\n\t *\n\t * @param sinceClock - The clock time to get changes since\n\t * @returns Changes since the specified clock time\n\t */\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined\n}\n\n/**\n * Options for a transaction.\n * @public\n */\nexport interface TLSyncStorageTransactionOptions {\n\t/**\n\t * Use this if you need to identify the transaction for logging or debugging purposes\n\t * or for ignoring certain changes in onChange callbacks\n\t */\n\tid?: string\n\t/**\n\t * Controls when the storage layer should emit the actual changes that occurred during the transaction.\n\t *\n\t * - `'always'` - Always emit the changes, regardless of whether they were applied verbatim\n\t * - `'when-different'` - Only emit changes if the storage layer modified/embellished the records\n\t * (e.g., added server timestamps, normalized data, etc.)\n\t *\n\t * When changes are emitted, they will be available in the `changes` field of the transaction result.\n\t * This is useful when the storage layer may transform records and the caller needs to know\n\t * what actually changed rather than what was requested.\n\t */\n\temitChanges?: 'always' | 'when-different'\n}\n\n/**\n * Callback type for a transaction.\n * The conditional return type ensures that the callback is synchronous.\n * @public\n */\nexport type TLSyncStorageTransactionCallback<R extends UnknownRecord, T> = (\n\ttxn: TLSyncStorageTransaction<R>\n) => T extends Promise<any>\n\t? {\n\t\t\t__error: 'Transaction callbacks cannot be async. Use synchronous operations only.'\n\t\t}\n\t: T\n\n/**\n * Pluggable synchronous transactional storage layer for TLSyncRoom.\n * Provides methods for managing documents, tombstones, and clocks within transactions.\n *\n * @public\n */\nexport interface TLSyncStorage<R extends UnknownRecord> {\n\ttransaction<T>(\n\t\tcallback: TLSyncStorageTransactionCallback<R, T>,\n\t\topts?: TLSyncStorageTransactionOptions\n\t): TLSyncStorageTransactionResult<T, R>\n\n\tgetClock(): number\n\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void\n\n\t/** A snapshot of the document lane only \u2014 never contains object-store lane records. */\n\tgetSnapshot?(): RoomSnapshot\n\n\t/**\n\t * A snapshot of the object-store lane (see `objectTypes` on the storage), for hosts that\n\t * persist object-lane records separately from the document. Same entry shape as\n\t * `RoomSnapshot['documents']` so a lane can be persisted and re-seeded via a merged snapshot.\n\t */\n\tgetObjectsSnapshot?(): RoomSnapshot['documents']\n\n\t/**\n\t * The same entries for the given ids, for callers that need a handful of records rather than the\n\t * whole (parsed) lane. Ids with no object-lane record are omitted \u2014 document-lane ids included \u2014\n\t * and duplicates yield one entry.\n\t */\n\tgetObjectsByIds?(ids: Iterable<string>): RoomSnapshot['documents']\n}\n\n/**\n * Properties passed to the onChange callback.\n * @public\n */\nexport interface TLSyncStorageOnChangeCallbackProps {\n\t/**\n\t * The ID of the transaction that caused the change.\n\t * This is useful for ignoring certain changes in onChange callbacks.\n\t */\n\tid?: string\n\tdocumentClock: number\n}\n\n/**\n * Result returned from a storage transaction.\n * @public\n */\nexport interface TLSyncStorageTransactionResult<T, R extends UnknownRecord = UnknownRecord> {\n\tdocumentClock: number\n\tdidChange: boolean\n\tresult: T\n\t/**\n\t * The actual changes that occurred during the transaction, if requested via `emitChanges` option.\n\t * This is a RecordsDiff where:\n\t * - `added` contains records that were put (we don't have \"from\" state for emitted changes)\n\t * - `removed` contains records that were deleted (with placeholder values since we only have IDs)\n\t * - `updated` is empty (emitted changes don't track before/after pairs)\n\t *\n\t * Only populated when:\n\t * - `emitChanges: 'always'` was specified, or\n\t * - `emitChanges: 'when-different'` was specified and the storage layer modified records\n\t */\n\tchanges?: TLSyncForwardDiff<R>\n}\n\n/**\n * Respresents a diff of puts and deletes.\n * @public\n */\nexport interface TLSyncForwardDiff<R extends UnknownRecord> {\n\tputs: Record<string, R | [before: R, after: R]>\n\tdeletes: string[]\n}\n\n/**\n * @internal\n */\nexport function toNetworkDiff<R extends UnknownRecord>(diff: TLSyncForwardDiff<R>): NetworkDiff<R> {\n\tconst networkDiff: NetworkDiff<R> = {}\n\tfor (const [id, put] of objectMapEntriesIterable(diff.puts)) {\n\t\tif (Array.isArray(put)) {\n\t\t\tconst patch = diffRecord(put[0], put[1])\n\t\t\tif (patch) {\n\t\t\t\tnetworkDiff[id] = [RecordOpType.Patch, patch]\n\t\t\t}\n\t\t} else {\n\t\t\tnetworkDiff[id] = [RecordOpType.Put, put]\n\t\t}\n\t}\n\tfor (const id of diff.deletes) {\n\t\tnetworkDiff[id] = [RecordOpType.Remove]\n\t}\n\treturn networkDiff\n}\n\n/**\n * Result returned from getChangesSince, containing all changes since a given clock time.\n * @public\n */\nexport interface TLSyncStorageGetChangesSinceResult<R extends UnknownRecord> {\n\t/**\n\t * The changes as a TLSyncForwardDiff.\n\t */\n\tdiff: TLSyncForwardDiff<R>\n\t/**\n\t * If true, the client should wipe all local data and replace with the server's state.\n\t * This happens when the client's clock is too old and we've lost tombstone history.\n\t */\n\twipeAll: boolean\n}\n\n/**\n * Loads a snapshot into storage during a transaction.\n * Migrates the snapshot to the current schema and loads it into storage.\n *\n * @public\n * @param txn - The transaction to load the snapshot into\n * @param schema - The current schema\n * @param snapshot - The snapshot to load\n */\nexport function loadSnapshotIntoStorage<R extends UnknownRecord>(\n\ttxn: TLSyncStorageTransaction<R>,\n\tschema: StoreSchema<R, any>,\n\tsnapshot: RoomSnapshot | TLStoreSnapshot\n) {\n\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\tassert(snapshot.schema, 'Schema is required')\n\tconst docIds = new Set<string>()\n\tfor (const doc of snapshot.documents) {\n\t\tdocIds.add(doc.state.id)\n\t\tconst existing = txn.get(doc.state.id)\n\t\tif (isEqual(existing, doc.state)) continue\n\t\ttxn.set(doc.state.id, doc.state as R)\n\t}\n\t// materialize the keys first: some SQLite drivers (better-sqlite3) refuse writes while a\n\t// statement iterator is open, so deleting during `keys()` would throw\n\tfor (const id of Array.from(txn.keys())) {\n\t\tif (!docIds.has(id)) {\n\t\t\ttxn.delete(id)\n\t\t}\n\t}\n\ttxn.setSchema(snapshot.schema)\n\tschema.migrateStorage(txn)\n}\n\nexport function convertStoreSnapshotToRoomSnapshot(\n\tsnapshot: RoomSnapshot | TLStoreSnapshot\n): RoomSnapshot {\n\tif ('documents' in snapshot) return snapshot\n\treturn {\n\t\tclock: 0,\n\t\tdocumentClock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAA2E;AAC3E,kBAAsD;AAmJ/C,SAAS,cAAuC,MAA4C;AAClG,QAAM,cAA8B,CAAC;AACrC,aAAW,CAAC,IAAI,GAAG,SAAK,uCAAyB,KAAK,IAAI,GAAG;AAC5D,QAAI,MAAM,QAAQ,GAAG,GAAG;AACvB,YAAM,YAAQ,wBAAW,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACvC,UAAI,OAAO;AACV,oBAAY,EAAE,IAAI,CAAC,yBAAa,OAAO,KAAK;AAAA,MAC7C;AAAA,IACD,OAAO;AACN,kBAAY,EAAE,IAAI,CAAC,yBAAa,KAAK,GAAG;AAAA,IACzC;AAAA,EACD;AACA,aAAW,MAAM,KAAK,SAAS;AAC9B,gBAAY,EAAE,IAAI,CAAC,yBAAa,MAAM;AAAA,EACvC;AACA,SAAO;AACR;AA2BO,SAAS,wBACf,KACA,QACA,UACC;AACD,aAAW,mCAAmC,QAAQ;AACtD,2BAAO,SAAS,QAAQ,oBAAoB;AAC5C,QAAM,SAAS,oBAAI,IAAY;AAC/B,aAAW,OAAO,SAAS,WAAW;AACrC,WAAO,IAAI,IAAI,MAAM,EAAE;AACvB,UAAM,WAAW,IAAI,IAAI,IAAI,MAAM,EAAE;AACrC,YAAI,sBAAQ,UAAU,IAAI,KAAK,EAAG;AAClC,QAAI,IAAI,IAAI,MAAM,IAAI,IAAI,KAAU;AAAA,EACrC;AAGA,aAAW,MAAM,MAAM,KAAK,IAAI,KAAK,CAAC,GAAG;AACxC,QAAI,CAAC,OAAO,IAAI,EAAE,GAAG;AACpB,UAAI,OAAO,EAAE;AAAA,IACd;AAAA,EACD;AACA,MAAI,UAAU,SAAS,MAAM;AAC7B,SAAO,eAAe,GAAG;AAC1B;AAEO,SAAS,mCACf,UACe;AACf,MAAI,eAAe,SAAU,QAAO;AACpC,SAAO;AAAA,IACN,OAAO;AAAA,IACP,eAAe;AAAA,IACf,eAAW,8BAAgB,SAAS,KAAK,EAAE,IAAI,CAAC,WAAW;AAAA,MAC1D;AAAA,MACA,kBAAkB;AAAA,IACnB,EAAE;AAAA,IACF,QAAQ,SAAS;AAAA,IACjB,YAAY,CAAC;AAAA,EACd;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.d.mts
CHANGED
|
@@ -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 |
|
|
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:
|
|
1621
|
+
readonly RATE_LIMITED: 'RATE_LIMITED';
|
|
1618
1622
|
/** Client protocol version too old */
|
|
1619
|
-
readonly CLIENT_TOO_OLD:
|
|
1623
|
+
readonly CLIENT_TOO_OLD: 'CLIENT_TOO_OLD';
|
|
1620
1624
|
/** Client sent invalid or corrupted record data */
|
|
1621
|
-
readonly INVALID_RECORD:
|
|
1625
|
+
readonly INVALID_RECORD: 'INVALID_RECORD';
|
|
1622
1626
|
/** Room has reached maximum capacity */
|
|
1623
|
-
readonly ROOM_FULL:
|
|
1627
|
+
readonly ROOM_FULL: 'ROOM_FULL';
|
|
1624
1628
|
/** Room or resource not found */
|
|
1625
|
-
readonly NOT_FOUND:
|
|
1629
|
+
readonly NOT_FOUND: 'NOT_FOUND';
|
|
1626
1630
|
/** Server protocol version too old */
|
|
1627
|
-
readonly SERVER_TOO_OLD:
|
|
1631
|
+
readonly SERVER_TOO_OLD: 'SERVER_TOO_OLD';
|
|
1628
1632
|
/** Unexpected server error occurred */
|
|
1629
|
-
readonly UNKNOWN_ERROR:
|
|
1633
|
+
readonly UNKNOWN_ERROR: 'UNKNOWN_ERROR';
|
|
1630
1634
|
/** User authentication required or invalid */
|
|
1631
|
-
readonly NOT_AUTHENTICATED:
|
|
1635
|
+
readonly NOT_AUTHENTICATED: 'NOT_AUTHENTICATED';
|
|
1632
1636
|
/** User lacks permission to access the room */
|
|
1633
|
-
readonly FORBIDDEN:
|
|
1637
|
+
readonly FORBIDDEN: 'FORBIDDEN';
|
|
1634
1638
|
};
|
|
1635
1639
|
|
|
1636
1640
|
/**
|
package/dist-esm/index.mjs
CHANGED
|
@@ -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
|
|
5
|
-
"mappings": "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,
|
|
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": "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
|
}
|