@tldraw/sync-core 4.2.0-canary.e45f694a6f24 → 4.2.0-canary.e7800b04c779

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist-cjs/index.d.ts +66 -2
  2. package/dist-cjs/index.js +2 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/RoomSession.js.map +1 -1
  5. package/dist-cjs/lib/TLSyncClient.js +2 -8
  6. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  7. package/dist-cjs/lib/TLSyncRoom.js +35 -9
  8. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  9. package/dist-cjs/lib/chunk.js +4 -4
  10. package/dist-cjs/lib/chunk.js.map +1 -1
  11. package/dist-cjs/lib/diff.js +29 -29
  12. package/dist-cjs/lib/diff.js.map +2 -2
  13. package/dist-cjs/lib/protocol.js +1 -1
  14. package/dist-cjs/lib/protocol.js.map +1 -1
  15. package/dist-esm/index.d.mts +66 -2
  16. package/dist-esm/index.mjs +3 -2
  17. package/dist-esm/index.mjs.map +2 -2
  18. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  19. package/dist-esm/lib/TLSyncClient.mjs +2 -8
  20. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  21. package/dist-esm/lib/TLSyncRoom.mjs +35 -9
  22. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  23. package/dist-esm/lib/chunk.mjs +4 -4
  24. package/dist-esm/lib/chunk.mjs.map +1 -1
  25. package/dist-esm/lib/diff.mjs +29 -29
  26. package/dist-esm/lib/diff.mjs.map +2 -2
  27. package/dist-esm/lib/protocol.mjs +1 -1
  28. package/dist-esm/lib/protocol.mjs.map +1 -1
  29. package/package.json +6 -6
  30. package/src/index.ts +2 -2
  31. package/src/lib/RoomSession.test.ts +3 -0
  32. package/src/lib/RoomSession.ts +28 -42
  33. package/src/lib/TLSyncClient.ts +2 -13
  34. package/src/lib/TLSyncRoom.ts +42 -7
  35. package/src/lib/chunk.ts +4 -4
  36. package/src/lib/diff.ts +55 -32
  37. package/src/lib/protocol.ts +1 -1
  38. package/src/test/TLSocketRoom.test.ts +2 -2
  39. package/src/test/TLSyncRoom.test.ts +22 -21
  40. package/src/test/diff.test.ts +200 -0
@@ -30,6 +30,70 @@ import { UnknownRecord } from '@tldraw/store';
30
30
 
31
31
  /* Excluded from this release type: getTlsyncProtocolVersion */
32
32
 
33
+ /**
34
+ * Assembles chunked JSON messages back into complete objects.
35
+ * Handles both regular JSON messages and chunked messages created by the chunk() function.
36
+ * Maintains internal state to track partially received chunked messages.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const assembler = new JsonChunkAssembler()
41
+ *
42
+ * // Handle regular JSON message
43
+ * const result1 = assembler.handleMessage('{"hello": "world"}')
44
+ * // Returns: { data: { hello: "world" }, stringified: '{"hello": "world"}' }
45
+ *
46
+ * // Handle chunked message
47
+ * assembler.handleMessage('1_hello') // Returns: null (partial)
48
+ * const result2 = assembler.handleMessage('0_ world')
49
+ * // Returns: { data: "hello world", stringified: "hello world" }
50
+ * ```
51
+ *
52
+ * @public
53
+ */
54
+ export declare class JsonChunkAssembler {
55
+ /**
56
+ * Current assembly state - either 'idle' or tracking chunks being received
57
+ */
58
+ state: 'idle' | {
59
+ chunksReceived: string[];
60
+ totalChunks: number;
61
+ };
62
+ /**
63
+ * Processes a single message, which can be either a complete JSON object or a chunk.
64
+ * For complete JSON objects (starting with '\{'), parses immediately.
65
+ * For chunks (prefixed with "\{number\}_"), accumulates until all chunks received.
66
+ *
67
+ * @param msg - The message to process, either JSON or chunk format
68
+ * @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks
69
+ * - `\{ data: object, stringified: string \}` - Successfully parsed complete message
70
+ * - `\{ error: Error \}` - Parse error or invalid chunk sequence
71
+ * - `null` - Chunk received but more chunks expected
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const assembler = new JsonChunkAssembler()
76
+ *
77
+ * // Complete JSON message
78
+ * const result = assembler.handleMessage('{"key": "value"}')
79
+ * if (result && 'data' in result) {
80
+ * console.log(result.data) // { key: "value" }
81
+ * }
82
+ *
83
+ * // Chunked message sequence
84
+ * assembler.handleMessage('2_hel') // null - more chunks expected
85
+ * assembler.handleMessage('1_lo ') // null - more chunks expected
86
+ * assembler.handleMessage('0_wor') // { data: "hello wor", stringified: "hello wor" }
87
+ * ```
88
+ */
89
+ handleMessage(msg: string): {
90
+ data: object;
91
+ stringified: string;
92
+ } | {
93
+ error: Error;
94
+ } | null;
95
+ }
96
+
33
97
  /* Excluded from this release type: NetworkDiff */
34
98
 
35
99
  /* Excluded from this release type: ObjectDiff */
@@ -65,6 +129,8 @@ export declare type OmitVoid<T, KS extends keyof T = keyof T> = {
65
129
 
66
130
  /* Excluded from this release type: RoomSession */
67
131
 
132
+ /* Excluded from this release type: RoomSessionBase */
133
+
68
134
  /* Excluded from this release type: RoomSessionState */
69
135
 
70
136
  /**
@@ -982,8 +1048,6 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
982
1048
  private pushPresence;
983
1049
  /** Push a change to the server, or stash it locally if we're offline */
984
1050
  private push;
985
- /** Get the target FPS for network operations based on presence mode */
986
- private getSyncFps;
987
1051
  /** Send any unsent push requests to the server */
988
1052
  private flushPendingPushRequests;
989
1053
  /**
package/dist-cjs/index.js CHANGED
@@ -20,6 +20,7 @@ var index_exports = {};
20
20
  __export(index_exports, {
21
21
  ClientWebSocketAdapter: () => import_ClientWebSocketAdapter.ClientWebSocketAdapter,
22
22
  DocumentState: () => import_TLSyncRoom.DocumentState,
23
+ JsonChunkAssembler: () => import_chunk.JsonChunkAssembler,
23
24
  ReconnectManager: () => import_ClientWebSocketAdapter.ReconnectManager,
24
25
  RecordOpType: () => import_diff.RecordOpType,
25
26
  RoomSessionState: () => import_RoomSession.RoomSessionState,
@@ -50,7 +51,7 @@ var import_TLSyncClient = require("./lib/TLSyncClient");
50
51
  var import_TLSyncRoom = require("./lib/TLSyncRoom");
51
52
  (0, import_utils.registerTldrawLibraryVersion)(
52
53
  "@tldraw/sync-core",
53
- "4.2.0-canary.e45f694a6f24",
54
+ "4.2.0-canary.e7800b04c779",
54
55
  "cjs"
55
56
  );
56
57
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport { TLSocketRoom, type OmitVoid, type TLSyncLog } from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tDocumentState,\n\tTLSyncRoom,\n\ttype RoomSnapshot,\n\ttype RoomStoreMethods,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,mBAAsB;AACtB,oCAAyD;AACzD,kBAcO;AACP,sBASO;AACP,yBAAmD;AAGnD,+BAAkC;AAClC,0BAA4D;AAC5D,0BAWO;AACP,wBAMO;AAAA,IAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport { TLSocketRoom, type OmitVoid, type TLSyncLog } from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tDocumentState,\n\tTLSyncRoom,\n\ttype RoomSnapshot,\n\ttype RoomStoreMethods,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,mBAA0C;AAC1C,oCAAyD;AACzD,kBAcO;AACP,sBASO;AACP,yBAAyE;AAGzE,+BAAkC;AAClC,0BAA4D;AAC5D,0BAWO;AACP,wBAMO;AAAA,IAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/RoomSession.ts"],
4
- "sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { TLRoomSocket } from './TLSyncRoom'\nimport { TLSocketServerSentDataEvent } from './protocol'\n\n/**\n * Enumeration of possible states for a room session during its lifecycle.\n *\n * Room sessions progress through these states as clients connect, authenticate,\n * and disconnect from collaborative rooms.\n *\n * @internal\n */\nexport const RoomSessionState = {\n\t/** Session is waiting for the initial connect message from the client */\n\tAwaitingConnectMessage: 'awaiting-connect-message',\n\t/** Session is disconnected but waiting for final cleanup before removal */\n\tAwaitingRemoval: 'awaiting-removal',\n\t/** Session is fully connected and actively synchronizing */\n\tConnected: 'connected',\n} as const\n\n/**\n * Type representing the possible states a room session can be in.\n *\n * @example\n * ```ts\n * const sessionState: RoomSessionState = RoomSessionState.Connected\n * if (sessionState === RoomSessionState.AwaitingConnectMessage) {\n * console.log('Session waiting for connect message')\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]\n\n/**\n * Maximum time in milliseconds to wait for a connect message after socket connection.\n *\n * If a client connects but doesn't send a connect message within this time,\n * the session will be terminated.\n *\n * @public\n */\nexport const SESSION_START_WAIT_TIME = 10000\n\n/**\n * Time in milliseconds to wait before completely removing a disconnected session.\n *\n * This grace period allows for quick reconnections without losing session state,\n * which is especially helpful for brief network interruptions.\n *\n * @public\n */\nexport const SESSION_REMOVAL_WAIT_TIME = 5000\n\n/**\n * Maximum time in milliseconds a connected session can remain idle before cleanup.\n *\n * Sessions that don't receive any messages or interactions for this duration\n * may be considered for cleanup to free server resources.\n *\n * @public\n */\nexport const SESSION_IDLE_TIMEOUT = 20000\n\n/**\n * Represents a client session within a collaborative room, tracking the connection\n * state, permissions, and synchronization details for a single user.\n *\n * Each session corresponds to one WebSocket connection and progresses through\n * different states during its lifecycle. The session type is a discriminated union\n * based on the current state, ensuring type safety when accessing state-specific properties.\n *\n * @example\n * ```ts\n * // Check session state and access appropriate properties\n * function handleSession(session: RoomSession<MyRecord, UserMeta>) {\n * switch (session.state) {\n * case RoomSessionState.AwaitingConnectMessage:\n * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)\n * break\n * case RoomSessionState.Connected:\n * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)\n * break\n * case RoomSessionState.AwaitingRemoval:\n * console.log(`Session will be removed at ${session.cancellationTime}`)\n * break\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSession<R extends UnknownRecord, Meta> =\n\t| {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingConnectMessage\n\t\t\t/** Unique identifier for this session */\n\t\t\tsessionId: string\n\t\t\t/** Presence identifier for live cursor/selection tracking, if available */\n\t\t\tpresenceId: string | null\n\t\t\t/** WebSocket connection wrapper for this session */\n\t\t\tsocket: TLRoomSocket<R>\n\t\t\t/** Timestamp when the session was created */\n\t\t\tsessionStartTime: number\n\t\t\t/** Custom metadata associated with this session */\n\t\t\tmeta: Meta\n\t\t\t/** Whether this session has read-only permissions */\n\t\t\tisReadonly: boolean\n\t\t\t/** Whether this session requires legacy protocol rejection handling */\n\t\t\trequiresLegacyRejection: boolean\n\t }\n\t| {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingRemoval\n\t\t\t/** Unique identifier for this session */\n\t\t\tsessionId: string\n\t\t\t/** Presence identifier for live cursor/selection tracking, if available */\n\t\t\tpresenceId: string | null\n\t\t\t/** WebSocket connection wrapper for this session */\n\t\t\tsocket: TLRoomSocket<R>\n\t\t\t/** Timestamp when the session was marked for removal */\n\t\t\tcancellationTime: number\n\t\t\t/** Custom metadata associated with this session */\n\t\t\tmeta: Meta\n\t\t\t/** Whether this session has read-only permissions */\n\t\t\tisReadonly: boolean\n\t\t\t/** Whether this session requires legacy protocol rejection handling */\n\t\t\trequiresLegacyRejection: boolean\n\t }\n\t| {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.Connected\n\t\t\t/** Unique identifier for this session */\n\t\t\tsessionId: string\n\t\t\t/** Presence identifier for live cursor/selection tracking, if available */\n\t\t\tpresenceId: string | null\n\t\t\t/** WebSocket connection wrapper for this session */\n\t\t\tsocket: TLRoomSocket<R>\n\t\t\t/** Serialized schema information for this connected session */\n\t\t\tserializedSchema: SerializedSchema\n\t\t\t/** Timestamp of the last interaction or message from this session */\n\t\t\tlastInteractionTime: number\n\t\t\t/** Timer for debouncing operations, if active */\n\t\t\tdebounceTimer: ReturnType<typeof setTimeout> | null\n\t\t\t/** Queue of data messages waiting to be sent to this session */\n\t\t\toutstandingDataMessages: TLSocketServerSentDataEvent<R>[]\n\t\t\t/** Custom metadata associated with this session */\n\t\t\tmeta: Meta\n\t\t\t/** Whether this session has read-only permissions */\n\t\t\tisReadonly: boolean\n\t\t\t/** Whether this session requires legacy protocol rejection handling */\n\t\t\trequiresLegacyRejection: boolean\n\t }\n"],
4
+ "sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { TLRoomSocket } from './TLSyncRoom'\nimport { TLSocketServerSentDataEvent } from './protocol'\n\n/**\n * Enumeration of possible states for a room session during its lifecycle.\n *\n * Room sessions progress through these states as clients connect, authenticate,\n * and disconnect from collaborative rooms.\n *\n * @internal\n */\nexport const RoomSessionState = {\n\t/** Session is waiting for the initial connect message from the client */\n\tAwaitingConnectMessage: 'awaiting-connect-message',\n\t/** Session is disconnected but waiting for final cleanup before removal */\n\tAwaitingRemoval: 'awaiting-removal',\n\t/** Session is fully connected and actively synchronizing */\n\tConnected: 'connected',\n} as const\n\n/**\n * Type representing the possible states a room session can be in.\n *\n * @example\n * ```ts\n * const sessionState: RoomSessionState = RoomSessionState.Connected\n * if (sessionState === RoomSessionState.AwaitingConnectMessage) {\n * console.log('Session waiting for connect message')\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]\n\n/**\n * Maximum time in milliseconds to wait for a connect message after socket connection.\n *\n * If a client connects but doesn't send a connect message within this time,\n * the session will be terminated.\n *\n * @public\n */\nexport const SESSION_START_WAIT_TIME = 10000\n\n/**\n * Time in milliseconds to wait before completely removing a disconnected session.\n *\n * This grace period allows for quick reconnections without losing session state,\n * which is especially helpful for brief network interruptions.\n *\n * @public\n */\nexport const SESSION_REMOVAL_WAIT_TIME = 5000\n\n/**\n * Maximum time in milliseconds a connected session can remain idle before cleanup.\n *\n * Sessions that don't receive any messages or interactions for this duration\n * may be considered for cleanup to free server resources.\n *\n * @public\n */\nexport const SESSION_IDLE_TIMEOUT = 20000\n\n/**\n * Base properties shared by all room session states.\n *\n * @internal\n */\nexport interface RoomSessionBase<R extends UnknownRecord, Meta> {\n\t/** Unique identifier for this session */\n\tsessionId: string\n\t/** Presence identifier for live cursor/selection tracking, if available */\n\tpresenceId: string | null\n\t/** WebSocket connection wrapper for this session */\n\tsocket: TLRoomSocket<R>\n\t/** Custom metadata associated with this session */\n\tmeta: Meta\n\t/** Whether this session has read-only permissions */\n\tisReadonly: boolean\n\t/** Whether this session requires legacy protocol rejection handling */\n\trequiresLegacyRejection: boolean\n\t/** Whether this session supports string append operations */\n\tsupportsStringAppend: boolean\n}\n\n/**\n * Represents a client session within a collaborative room, tracking the connection\n * state, permissions, and synchronization details for a single user.\n *\n * Each session corresponds to one WebSocket connection and progresses through\n * different states during its lifecycle. The session type is a discriminated union\n * based on the current state, ensuring type safety when accessing state-specific properties.\n *\n * @example\n * ```ts\n * // Check session state and access appropriate properties\n * function handleSession(session: RoomSession<MyRecord, UserMeta>) {\n * switch (session.state) {\n * case RoomSessionState.AwaitingConnectMessage:\n * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)\n * break\n * case RoomSessionState.Connected:\n * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)\n * break\n * case RoomSessionState.AwaitingRemoval:\n * console.log(`Session will be removed at ${session.cancellationTime}`)\n * break\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSession<R extends UnknownRecord, Meta> =\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingConnectMessage\n\t\t\t/** Timestamp when the session was created */\n\t\t\tsessionStartTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingRemoval\n\t\t\t/** Timestamp when the session was marked for removal */\n\t\t\tcancellationTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.Connected\n\t\t\t/** Serialized schema information for this connected session */\n\t\t\tserializedSchema: SerializedSchema\n\t\t\t/** Timestamp of the last interaction or message from this session */\n\t\t\tlastInteractionTime: number\n\t\t\t/** Timer for debouncing operations, if active */\n\t\t\tdebounceTimer: ReturnType<typeof setTimeout> | null\n\t\t\t/** Queue of data messages waiting to be sent to this session */\n\t\t\toutstandingDataMessages: TLSocketServerSentDataEvent<R>[]\n\t })\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYO,MAAM,mBAAmB;AAAA;AAAA,EAE/B,wBAAwB;AAAA;AAAA,EAExB,iBAAiB;AAAA;AAAA,EAEjB,WAAW;AACZ;AAyBO,MAAM,0BAA0B;AAUhC,MAAM,4BAA4B;AAUlC,MAAM,uBAAuB;",
6
6
  "names": []
7
7
  }
@@ -29,8 +29,6 @@ var import_utils = require("@tldraw/utils");
29
29
  var import_diff = require("./diff");
30
30
  var import_interval = require("./interval");
31
31
  var import_protocol = require("./protocol");
32
- const SOLO_MODE_FPS = 1;
33
- const COLLABORATIVE_MODE_FPS = 30;
34
32
  const TLSyncErrorCloseEventCode = 4099;
35
33
  const TLSyncErrorCloseEventReason = {
36
34
  /** Room or resource not found */
@@ -414,10 +412,6 @@ class TLSyncClient {
414
412
  this.pendingPushRequests.push({ request: pushRequest, sent: false });
415
413
  this.flushPendingPushRequests();
416
414
  }
417
- /** Get the target FPS for network operations based on presence mode */
418
- getSyncFps() {
419
- return this.presenceMode?.get() === "solo" ? SOLO_MODE_FPS : COLLABORATIVE_MODE_FPS;
420
- }
421
415
  /** Send any unsent push requests to the server */
422
416
  flushPendingPushRequests = (0, import_utils.fpsThrottle)(() => {
423
417
  this.debug("flushing pending push requests", {
@@ -436,7 +430,7 @@ class TLSyncClient {
436
430
  pendingPushRequest.sent = true;
437
431
  }
438
432
  }
439
- }, this.getSyncFps.bind(this));
433
+ });
440
434
  /**
441
435
  * Applies a 'network' diff to the store this does value-based equality checking so that if the
442
436
  * data is the same (as opposed to merely identical with ===), then no change is made and no
@@ -529,6 +523,6 @@ class TLSyncClient {
529
523
  this.resetConnection();
530
524
  }
531
525
  };
532
- scheduleRebase = (0, import_utils.fpsThrottle)(this.rebase, this.getSyncFps.bind(this));
526
+ scheduleRebase = (0, import_utils.fpsThrottle)(this.rebase);
533
527
  }
534
528
  //# sourceMappingURL=TLSyncClient.js.map
@@ -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\tsquashRecordDiffs,\n} from '@tldraw/store'\nimport {\n\texhaustiveSwitchError,\n\tfpsThrottle,\n\tisEqual,\n\tobjectMapEntries,\n\tuniqueId,\n} from '@tldraw/utils'\nimport { NetworkDiff, RecordOpType, applyObjectDiff, diffRecord, getNetworkDiff } from './diff'\nimport { interval } from './interval'\nimport {\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 * 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\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: { request: TLPushRequest<R>; sent: boolean }[] = []\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/** @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 */\n\tprivate readonly onAfterConnect?: (self: this, details: { isReadonly: boolean }) => 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?(self: TLSyncClient<R, S>, details: { isReadonly: boolean }): 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\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\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\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 speculativeChanges = this.store.filterChangesByScope(\n\t\t\t\t\tthis.store.extractingChanges(() => {\n\t\t\t\t\t\tthis.store.applyDiff(stashedChanges)\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, { isReadonly: event.isReadonly })\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.flushPendingPushRequests.cancel?.()\n\t\tthis.scheduleRebase.cancel?.()\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\tlet presence: TLPushRequest<any>['presence'] = undefined\n\t\tif (!this.lastPushedPresenceState && nextPresence) {\n\t\t\t// we don't have a last presence state, so we need to push the full state\n\t\t\tpresence = [RecordOpType.Put, nextPresence]\n\t\t} else if (this.lastPushedPresenceState && nextPresence) {\n\t\t\t// we have a last presence state, so we need to push a diff if there is one\n\t\t\tconst diff = diffRecord(this.lastPushedPresenceState, nextPresence)\n\t\t\tif (diff) {\n\t\t\t\tpresence = [RecordOpType.Patch, diff]\n\t\t\t}\n\t\t}\n\n\t\tif (!presence) return\n\t\tthis.lastPushedPresenceState = nextPresence\n\n\t\t// if there is a pending push that has not been sent and does not already include a presence update,\n\t\t// then add this presence update to it\n\t\tconst lastPush = this.pendingPushRequests.at(-1)\n\t\tif (lastPush && !lastPush.sent && !lastPush.request.presence) {\n\t\t\tlastPush.request.presence = presence\n\t\t\treturn\n\t\t}\n\n\t\t// otherwise, create a new push request\n\t\tconst req: TLPushRequest<R> = {\n\t\t\ttype: 'push',\n\t\t\tclientClock: this.clientClock++,\n\t\t\tpresence,\n\t\t}\n\n\t\tif (req) {\n\t\t\tthis.pendingPushRequests.push({ request: req, sent: false })\n\t\t\tthis.flushPendingPushRequests()\n\t\t}\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\t// the Store doesn't do deep equality checks when making changes\n\t\t// so it's possible that the diff passed in here is actually a no-op.\n\t\t// either way, we also don't want to send whole objects over the wire if\n\t\t// only small parts of them have changed, so we'll do a shallow-ish diff\n\t\t// which also uses deep equality checks to see if the change is actually\n\t\t// a no-op.\n\t\tconst diff = getNetworkDiff(change)\n\t\tif (!diff) return\n\n\t\t// the change is not a no-op so we'll send it to the server\n\t\t// but first let's merge the records diff into the speculative changes\n\t\tthis.speculativeChanges = squashRecordDiffs([this.speculativeChanges, change])\n\n\t\tif (!this.isConnectedToRoom) {\n\t\t\t// don't sent push requests or even store them up while offline\n\t\t\t// when we come back online we'll generate another push request from\n\t\t\t// scratch based on the speculativeChanges diff\n\t\t\treturn\n\t\t}\n\n\t\tconst pushRequest: TLPushRequest<R> = {\n\t\t\ttype: 'push',\n\t\t\tdiff,\n\t\t\tclientClock: this.clientClock++,\n\t\t}\n\n\t\tthis.pendingPushRequests.push({ request: pushRequest, sent: false })\n\n\t\t// immediately calling .send on the websocket here was causing some interaction\n\t\t// slugishness when e.g. drawing or translating shapes. Seems like it blocks\n\t\t// until the send completes. So instead we'll schedule a send to happen on some\n\t\t// tick in the near future.\n\t\tthis.flushPendingPushRequests()\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/** Send any unsent push requests to the server */\n\tprivate flushPendingPushRequests = fpsThrottle(() => {\n\t\tthis.debug('flushing pending push requests', {\n\t\t\tisConnectedToRoom: this.isConnectedToRoom,\n\t\t\tpendingPushRequests: this.pendingPushRequests,\n\t\t})\n\t\tif (!this.isConnectedToRoom || this.store.isPossiblyCorrupted()) {\n\t\t\treturn\n\t\t}\n\t\tfor (const pendingPushRequest of this.pendingPushRequests) {\n\t\t\tif (!pendingPushRequest.sent) {\n\t\t\t\tif (this.socket.connectionStatus !== 'online') {\n\t\t\t\t\t// we went offline, so don't send anything\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.socket.sendMessage(pendingPushRequest.request)\n\t\t\t\tpendingPushRequest.sent = true\n\t\t\t}\n\t\t}\n\t}, this.getSyncFps.bind(this))\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 local/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].request.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})\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\tprivate scheduleRebase = fpsThrottle(this.rebase, this.getSyncFps.bind(this))\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\tsquashRecordDiffs,\n} from '@tldraw/store'\nimport {\n\texhaustiveSwitchError,\n\tfpsThrottle,\n\tisEqual,\n\tobjectMapEntries,\n\tuniqueId,\n} from '@tldraw/utils'\nimport { NetworkDiff, RecordOpType, applyObjectDiff, diffRecord, getNetworkDiff } from './diff'\nimport { interval } from './interval'\nimport {\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/**\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 * 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\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: { request: TLPushRequest<R>; sent: boolean }[] = []\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/** @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 */\n\tprivate readonly onAfterConnect?: (self: this, details: { isReadonly: boolean }) => 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?(self: TLSyncClient<R, S>, details: { isReadonly: boolean }): 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\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\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\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 speculativeChanges = this.store.filterChangesByScope(\n\t\t\t\t\tthis.store.extractingChanges(() => {\n\t\t\t\t\t\tthis.store.applyDiff(stashedChanges)\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, { isReadonly: event.isReadonly })\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.flushPendingPushRequests.cancel?.()\n\t\tthis.scheduleRebase.cancel?.()\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\tlet presence: TLPushRequest<any>['presence'] = undefined\n\t\tif (!this.lastPushedPresenceState && nextPresence) {\n\t\t\t// we don't have a last presence state, so we need to push the full state\n\t\t\tpresence = [RecordOpType.Put, nextPresence]\n\t\t} else if (this.lastPushedPresenceState && nextPresence) {\n\t\t\t// we have a last presence state, so we need to push a diff if there is one\n\t\t\tconst diff = diffRecord(this.lastPushedPresenceState, nextPresence)\n\t\t\tif (diff) {\n\t\t\t\tpresence = [RecordOpType.Patch, diff]\n\t\t\t}\n\t\t}\n\n\t\tif (!presence) return\n\t\tthis.lastPushedPresenceState = nextPresence\n\n\t\t// if there is a pending push that has not been sent and does not already include a presence update,\n\t\t// then add this presence update to it\n\t\tconst lastPush = this.pendingPushRequests.at(-1)\n\t\tif (lastPush && !lastPush.sent && !lastPush.request.presence) {\n\t\t\tlastPush.request.presence = presence\n\t\t\treturn\n\t\t}\n\n\t\t// otherwise, create a new push request\n\t\tconst req: TLPushRequest<R> = {\n\t\t\ttype: 'push',\n\t\t\tclientClock: this.clientClock++,\n\t\t\tpresence,\n\t\t}\n\n\t\tif (req) {\n\t\t\tthis.pendingPushRequests.push({ request: req, sent: false })\n\t\t\tthis.flushPendingPushRequests()\n\t\t}\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\t// the Store doesn't do deep equality checks when making changes\n\t\t// so it's possible that the diff passed in here is actually a no-op.\n\t\t// either way, we also don't want to send whole objects over the wire if\n\t\t// only small parts of them have changed, so we'll do a shallow-ish diff\n\t\t// which also uses deep equality checks to see if the change is actually\n\t\t// a no-op.\n\t\tconst diff = getNetworkDiff(change)\n\t\tif (!diff) return\n\n\t\t// the change is not a no-op so we'll send it to the server\n\t\t// but first let's merge the records diff into the speculative changes\n\t\tthis.speculativeChanges = squashRecordDiffs([this.speculativeChanges, change])\n\n\t\tif (!this.isConnectedToRoom) {\n\t\t\t// don't sent push requests or even store them up while offline\n\t\t\t// when we come back online we'll generate another push request from\n\t\t\t// scratch based on the speculativeChanges diff\n\t\t\treturn\n\t\t}\n\n\t\tconst pushRequest: TLPushRequest<R> = {\n\t\t\ttype: 'push',\n\t\t\tdiff,\n\t\t\tclientClock: this.clientClock++,\n\t\t}\n\n\t\tthis.pendingPushRequests.push({ request: pushRequest, sent: false })\n\n\t\t// immediately calling .send on the websocket here was causing some interaction\n\t\t// slugishness when e.g. drawing or translating shapes. Seems like it blocks\n\t\t// until the send completes. So instead we'll schedule a send to happen on some\n\t\t// tick in the near future.\n\t\tthis.flushPendingPushRequests()\n\t}\n\n\t/** Send any unsent push requests to the server */\n\tprivate flushPendingPushRequests = fpsThrottle(() => {\n\t\tthis.debug('flushing pending push requests', {\n\t\t\tisConnectedToRoom: this.isConnectedToRoom,\n\t\t\tpendingPushRequests: this.pendingPushRequests,\n\t\t})\n\t\tif (!this.isConnectedToRoom || this.store.isPossiblyCorrupted()) {\n\t\t\treturn\n\t\t}\n\t\tfor (const pendingPushRequest of this.pendingPushRequests) {\n\t\t\tif (!pendingPushRequest.sent) {\n\t\t\t\tif (this.socket.connectionStatus !== 'online') {\n\t\t\t\t\t// we went offline, so don't send anything\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.socket.sendMessage(pendingPushRequest.request)\n\t\t\t\tpendingPushRequest.sent = true\n\t\t\t}\n\t\t}\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 local/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].request.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})\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\tprivate scheduleRebase = fpsThrottle(this.rebase)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -79,10 +79,11 @@ class DocumentState {
79
79
  *
80
80
  * @param state - The new record state
81
81
  * @param clock - The new clock value
82
+ * @param legacyAppendMode - If true, string append operations will be converted to Put operations
82
83
  * @returns Result containing the diff and new DocumentState, or null if no changes, or validation error
83
84
  */
84
- replaceState(state, clock) {
85
- const diff = (0, import_diff.diffRecord)(this.state, state);
85
+ replaceState(state, clock, legacyAppendMode = false) {
86
+ const diff = (0, import_diff.diffRecord)(this.state, state, legacyAppendMode);
86
87
  if (!diff) return import_utils.Result.ok(null);
87
88
  try {
88
89
  this.recordType.validate(state);
@@ -96,11 +97,12 @@ class DocumentState {
96
97
  *
97
98
  * @param diff - The object diff to apply
98
99
  * @param clock - The new clock value
100
+ * @param legacyAppendMode - If true, string append operations will be converted to Put operations
99
101
  * @returns Result containing the final diff and new DocumentState, or null if no changes, or validation error
100
102
  */
101
- mergeDiff(diff, clock) {
103
+ mergeDiff(diff, clock, legacyAppendMode = false) {
102
104
  const newState = (0, import_diff.applyObjectDiff)(this.state, diff);
103
- return this.replaceState(newState, clock);
105
+ return this.replaceState(newState, clock, legacyAppendMode);
104
106
  }
105
107
  }
106
108
  function getDocumentClock(snapshot) {
@@ -488,7 +490,8 @@ class TLSyncRoom {
488
490
  cancellationTime: Date.now(),
489
491
  meta: session.meta,
490
492
  isReadonly: session.isReadonly,
491
- requiresLegacyRejection: session.requiresLegacyRejection
493
+ requiresLegacyRejection: session.requiresLegacyRejection,
494
+ supportsStringAppend: session.supportsStringAppend
492
495
  });
493
496
  try {
494
497
  session.socket.close();
@@ -595,10 +598,27 @@ class TLSyncRoom {
595
598
  meta,
596
599
  isReadonly: isReadonly ?? false,
597
600
  // this gets set later during handleConnectMessage
598
- requiresLegacyRejection: false
601
+ requiresLegacyRejection: false,
602
+ supportsStringAppend: true
599
603
  });
600
604
  return this;
601
605
  }
606
+ /**
607
+ * Checks if all connected sessions support string append operations (protocol version 8+).
608
+ * If any client is on an older version, returns false to enable legacy append mode.
609
+ *
610
+ * @returns True if all connected sessions are on protocol version 8 or higher
611
+ */
612
+ getCanEmitStringAppend() {
613
+ for (const session of this.sessions.values()) {
614
+ if (session.state === import_RoomSession.RoomSessionState.Connected) {
615
+ if (!session.supportsStringAppend) {
616
+ return false;
617
+ }
618
+ }
619
+ }
620
+ return true;
621
+ }
602
622
  /**
603
623
  * When we send a diff to a client, if that client is on a lower version than us, we need to make
604
624
  * the diff compatible with their version. At the moment this means migrating each affected record
@@ -733,6 +753,10 @@ class TLSyncRoom {
733
753
  if (theirProtocolVersion === 6) {
734
754
  theirProtocolVersion++;
735
755
  }
756
+ if (theirProtocolVersion === 7) {
757
+ theirProtocolVersion++;
758
+ session.supportsStringAppend = false;
759
+ }
736
760
  if (theirProtocolVersion == null || theirProtocolVersion < (0, import_protocol.getTlsyncProtocolVersion)()) {
737
761
  this.rejectSession(session.sessionId, import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
738
762
  return;
@@ -760,6 +784,7 @@ class TLSyncRoom {
760
784
  lastInteractionTime: Date.now(),
761
785
  debounceTimer: null,
762
786
  outstandingDataMessages: [],
787
+ supportsStringAppend: session.supportsStringAppend,
763
788
  meta: session.meta,
764
789
  isReadonly: session.isReadonly,
765
790
  requiresLegacyRejection: session.requiresLegacyRejection
@@ -846,6 +871,7 @@ class TLSyncRoom {
846
871
  const initialDocumentClock = this.documentClock;
847
872
  let didPresenceChange = false;
848
873
  (0, import_state.transaction)((rollback) => {
874
+ const legacyAppendMode = !this.getCanEmitStringAppend();
849
875
  const docChanges = { diff: null };
850
876
  const presenceChanges = { diff: null };
851
877
  const propagateOp = (changes, id, op) => {
@@ -874,7 +900,7 @@ class TLSyncRoom {
874
900
  const { value: state } = res;
875
901
  const doc = this.getDocument(id);
876
902
  if (doc) {
877
- const diff = doc.replaceState(state, this.clock);
903
+ const diff = doc.replaceState(state, this.clock, legacyAppendMode);
878
904
  if (!diff.ok) {
879
905
  return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.INVALID_RECORD);
880
906
  }
@@ -899,7 +925,7 @@ class TLSyncRoom {
899
925
  return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
900
926
  }
901
927
  if (downgraded.value === doc.state) {
902
- const diff = doc.mergeDiff(patch, this.clock);
928
+ const diff = doc.mergeDiff(patch, this.clock, legacyAppendMode);
903
929
  if (!diff.ok) {
904
930
  return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.INVALID_RECORD);
905
931
  }
@@ -913,7 +939,7 @@ class TLSyncRoom {
913
939
  if (upgraded.type === "error") {
914
940
  return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
915
941
  }
916
- const diff = doc.replaceState(upgraded.value, this.clock);
942
+ const diff = doc.replaceState(upgraded.value, this.clock, legacyAppendMode);
917
943
  if (!diff.ok) {
918
944
  return fail(import_TLSyncClient.TLSyncErrorCloseEventReason.INVALID_RECORD);
919
945
  }