@tldraw/sync-core 4.2.0-next.86048a4c313d → 4.2.0-next.873779de89c7

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.
@@ -982,8 +982,6 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
982
982
  private pushPresence;
983
983
  /** Push a change to the server, or stash it locally if we're offline */
984
984
  private push;
985
- /** Get the target FPS for network operations based on presence mode */
986
- private getSyncFps;
987
985
  /** Send any unsent push requests to the server */
988
986
  private flushPendingPushRequests;
989
987
  /**
package/dist-cjs/index.js CHANGED
@@ -50,7 +50,7 @@ var import_TLSyncClient = require("./lib/TLSyncClient");
50
50
  var import_TLSyncRoom = require("./lib/TLSyncRoom");
51
51
  (0, import_utils.registerTldrawLibraryVersion)(
52
52
  "@tldraw/sync-core",
53
- "4.2.0-next.86048a4c313d",
53
+ "4.2.0-next.873779de89c7",
54
54
  "cjs"
55
55
  );
56
56
  //# sourceMappingURL=index.js.map
@@ -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
  }
@@ -982,8 +982,6 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
982
982
  private pushPresence;
983
983
  /** Push a change to the server, or stash it locally if we're offline */
984
984
  private push;
985
- /** Get the target FPS for network operations based on presence mode */
986
- private getSyncFps;
987
985
  /** Send any unsent push requests to the server */
988
986
  private flushPendingPushRequests;
989
987
  /**
@@ -26,7 +26,7 @@ import {
26
26
  } from "./lib/TLSyncRoom.mjs";
27
27
  registerTldrawLibraryVersion(
28
28
  "@tldraw/sync-core",
29
- "4.2.0-next.86048a4c313d",
29
+ "4.2.0-next.873779de89c7",
30
30
  "esm"
31
31
  );
32
32
  export {
@@ -15,8 +15,6 @@ import { interval } from "./interval.mjs";
15
15
  import {
16
16
  getTlsyncProtocolVersion
17
17
  } from "./protocol.mjs";
18
- const SOLO_MODE_FPS = 1;
19
- const COLLABORATIVE_MODE_FPS = 30;
20
18
  const TLSyncErrorCloseEventCode = 4099;
21
19
  const TLSyncErrorCloseEventReason = {
22
20
  /** Room or resource not found */
@@ -400,10 +398,6 @@ class TLSyncClient {
400
398
  this.pendingPushRequests.push({ request: pushRequest, sent: false });
401
399
  this.flushPendingPushRequests();
402
400
  }
403
- /** Get the target FPS for network operations based on presence mode */
404
- getSyncFps() {
405
- return this.presenceMode?.get() === "solo" ? SOLO_MODE_FPS : COLLABORATIVE_MODE_FPS;
406
- }
407
401
  /** Send any unsent push requests to the server */
408
402
  flushPendingPushRequests = fpsThrottle(() => {
409
403
  this.debug("flushing pending push requests", {
@@ -422,7 +416,7 @@ class TLSyncClient {
422
416
  pendingPushRequest.sent = true;
423
417
  }
424
418
  }
425
- }, this.getSyncFps.bind(this));
419
+ });
426
420
  /**
427
421
  * Applies a 'network' diff to the store this does value-based equality checking so that if the
428
422
  * data is the same (as opposed to merely identical with ===), then no change is made and no
@@ -515,7 +509,7 @@ class TLSyncClient {
515
509
  this.resetConnection();
516
510
  }
517
511
  };
518
- scheduleRebase = fpsThrottle(this.rebase, this.getSyncFps.bind(this));
512
+ scheduleRebase = fpsThrottle(this.rebase);
519
513
  }
520
514
  export {
521
515
  TLSyncClient,
@@ -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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync-core",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync).",
4
- "version": "4.2.0-next.86048a4c313d",
4
+ "version": "4.2.0-next.873779de89c7",
5
5
  "author": {
6
6
  "name": "tldraw GB Ltd.",
7
7
  "email": "hello@tldraw.com"
@@ -48,17 +48,17 @@
48
48
  "@types/uuid-readable": "^0.0.3",
49
49
  "react": "^18.3.1",
50
50
  "react-dom": "^18.3.1",
51
- "tldraw": "4.2.0-next.86048a4c313d",
51
+ "tldraw": "4.2.0-next.873779de89c7",
52
52
  "typescript": "^5.8.3",
53
53
  "uuid-by-string": "^4.0.0",
54
54
  "uuid-readable": "^0.0.2",
55
55
  "vitest": "^3.2.4"
56
56
  },
57
57
  "dependencies": {
58
- "@tldraw/state": "4.2.0-next.86048a4c313d",
59
- "@tldraw/store": "4.2.0-next.86048a4c313d",
60
- "@tldraw/tlschema": "4.2.0-next.86048a4c313d",
61
- "@tldraw/utils": "4.2.0-next.86048a4c313d",
58
+ "@tldraw/state": "4.2.0-next.873779de89c7",
59
+ "@tldraw/store": "4.2.0-next.873779de89c7",
60
+ "@tldraw/tlschema": "4.2.0-next.873779de89c7",
61
+ "@tldraw/utils": "4.2.0-next.873779de89c7",
62
62
  "nanoevents": "^7.0.1",
63
63
  "ws": "^8.18.0"
64
64
  },
@@ -35,12 +35,6 @@ import {
35
35
  */
36
36
  export type SubscribingFn<T> = (cb: (val: T) => void) => () => void
37
37
 
38
- /** Network sync frame rate when in solo mode (no collaborators) @internal */
39
- const SOLO_MODE_FPS = 1
40
-
41
- /** Network sync frame rate when in collaborative mode (with collaborators) @internal */
42
- const COLLABORATIVE_MODE_FPS = 30
43
-
44
38
  /**
45
39
  * WebSocket close code used by the server to signal a non-recoverable sync error.
46
40
  * This close code indicates that the connection is being terminated due to an error
@@ -806,11 +800,6 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
806
800
  this.flushPendingPushRequests()
807
801
  }
808
802
 
809
- /** Get the target FPS for network operations based on presence mode */
810
- private getSyncFps(): number {
811
- return this.presenceMode?.get() === 'solo' ? SOLO_MODE_FPS : COLLABORATIVE_MODE_FPS
812
- }
813
-
814
803
  /** Send any unsent push requests to the server */
815
804
  private flushPendingPushRequests = fpsThrottle(() => {
816
805
  this.debug('flushing pending push requests', {
@@ -830,7 +819,7 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
830
819
  pendingPushRequest.sent = true
831
820
  }
832
821
  }
833
- }, this.getSyncFps.bind(this))
822
+ })
834
823
 
835
824
  /**
836
825
  * Applies a 'network' diff to the store this does value-based equality checking so that if the
@@ -938,5 +927,5 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
938
927
  }
939
928
  }
940
929
 
941
- private scheduleRebase = fpsThrottle(this.rebase, this.getSyncFps.bind(this))
930
+ private scheduleRebase = fpsThrottle(this.rebase)
942
931
  }