@tldraw/sync-core 4.2.0-next.d76c345101d5 → 4.2.0-next.e824a30c434e

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.
@@ -148,7 +148,16 @@ export declare interface RoomStoreMethods<R extends UnknownRecord = UnknownRecor
148
148
  getAll(): R[];
149
149
  }
150
150
 
151
- /* Excluded from this release type: SubscribingFn */
151
+ /**
152
+ * Function type for subscribing to events with a callback.
153
+ * Returns an unsubscribe function to clean up the listener.
154
+ *
155
+ * @param cb - Callback function that receives the event value
156
+ * @returns Function to call when you want to unsubscribe from the events
157
+ *
158
+ * @public
159
+ */
160
+ export declare type SubscribingFn<T> = (cb: (val: T) => void) => () => void;
152
161
 
153
162
  /* Excluded from this release type: TLConnectRequest */
154
163
 
@@ -180,13 +189,81 @@ export declare type TLCustomMessageHandler = (this: null, data: any) => void;
180
189
 
181
190
  /* Excluded from this release type: TLIncompatibilityReason */
182
191
 
183
- /* Excluded from this release type: TLPersistentClientSocket */
192
+ /**
193
+ * Interface for persistent WebSocket-like connections used by TLSyncClient.
194
+ * Handles automatic reconnection and provides event-based communication with the sync server.
195
+ * Implementations should maintain connection resilience and handle network interruptions gracefully.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * class MySocketAdapter implements TLPersistentClientSocket {
200
+ * connectionStatus: 'offline' | 'online' | 'error' = 'offline'
201
+ *
202
+ * sendMessage(msg: TLSocketClientSentEvent) {
203
+ * if (this.ws && this.ws.readyState === WebSocket.OPEN) {
204
+ * this.ws.send(JSON.stringify(msg))
205
+ * }
206
+ * }
207
+ *
208
+ * onReceiveMessage = (callback) => {
209
+ * // Set up message listener and return cleanup function
210
+ * }
211
+ *
212
+ * restart() {
213
+ * this.disconnect()
214
+ * this.connect()
215
+ * }
216
+ * }
217
+ * ```
218
+ *
219
+ * @public
220
+ */
221
+ export declare interface TLPersistentClientSocket<ClientSentMessage extends object = object, ServerSentMessage extends object = object> {
222
+ /** Current connection state - online means actively connected and ready */
223
+ connectionStatus: 'error' | 'offline' | 'online';
224
+ /**
225
+ * Send a protocol message to the sync server
226
+ * @param msg - Message to send (connect, push, ping, etc.)
227
+ */
228
+ sendMessage(msg: ClientSentMessage): void;
229
+ /**
230
+ * Subscribe to messages received from the server
231
+ * @param callback - Function called for each received message
232
+ * @returns Cleanup function to remove the listener
233
+ */
234
+ onReceiveMessage: SubscribingFn<ServerSentMessage>;
235
+ /**
236
+ * Subscribe to connection status changes
237
+ * @param callback - Function called when connection status changes
238
+ * @returns Cleanup function to remove the listener
239
+ */
240
+ onStatusChange: SubscribingFn<TLSocketStatusChangeEvent>;
241
+ /**
242
+ * Force a connection restart (disconnect then reconnect)
243
+ * Used for error recovery or when connection health checks fail
244
+ */
245
+ restart(): void;
246
+ /**
247
+ * Close the connection
248
+ */
249
+ close(): void;
250
+ }
184
251
 
185
252
  /* Excluded from this release type: TLPersistentClientSocketStatus */
186
253
 
187
254
  /* Excluded from this release type: TLPingRequest */
188
255
 
189
- /* Excluded from this release type: TLPresenceMode */
256
+ /**
257
+ * Mode for handling presence information in sync sessions.
258
+ * Controls whether presence data (cursors, selections) is shared with other clients.
259
+ *
260
+ * @public
261
+ */
262
+ export declare type TLPresenceMode =
263
+ /** No presence sharing - client operates independently */
264
+ 'full'
265
+ | 'solo'
266
+ /** Full presence sharing - cursors and selections visible to others */;
190
267
 
191
268
  /* Excluded from this release type: TLPushRequest */
192
269
 
@@ -720,11 +797,202 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
720
797
 
721
798
  /* Excluded from this release type: TLSocketServerSentEvent */
722
799
 
723
- /* Excluded from this release type: TlSocketStatusChangeEvent */
800
+ /**
801
+ * Event object describing changes in socket connection status.
802
+ * Contains either a basic status change or an error with details.
803
+ *
804
+ * @public
805
+ */
806
+ export declare type TLSocketStatusChangeEvent = {
807
+ /** Connection came online or went offline */
808
+ status: 'offline' | 'online';
809
+ } | {
810
+ /** Connection encountered an error */
811
+ status: 'error';
812
+ /** Description of the error that occurred */
813
+ reason: string;
814
+ };
724
815
 
725
816
  /* Excluded from this release type: TLSocketStatusListener */
726
817
 
727
- /* Excluded from this release type: TLSyncClient */
818
+ /**
819
+ * Main client-side synchronization engine for collaborative tldraw applications.
820
+ *
821
+ * TLSyncClient manages bidirectional synchronization between a local tldraw Store
822
+ * and a remote sync server. It uses an optimistic update model where local changes
823
+ * are immediately applied for responsive UI, then sent to the server for validation
824
+ * and distribution to other clients.
825
+ *
826
+ * The synchronization follows a git-like push/pull/rebase model:
827
+ * - **Push**: Local changes are sent to server as diff operations
828
+ * - **Pull**: Server changes are received and applied locally
829
+ * - **Rebase**: Conflicting changes are resolved by undoing local changes,
830
+ * applying server changes, then re-applying local changes on top
831
+ *
832
+ * @example
833
+ * ```ts
834
+ * import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core'
835
+ * import { createTLStore } from '@tldraw/store'
836
+ *
837
+ * // Create store and socket
838
+ * const store = createTLStore({ schema: mySchema })
839
+ * const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync')
840
+ *
841
+ * // Create sync client
842
+ * const syncClient = new TLSyncClient({
843
+ * store,
844
+ * socket,
845
+ * presence: atom(null),
846
+ * onLoad: () => console.log('Connected and loaded'),
847
+ * onSyncError: (reason) => console.error('Sync failed:', reason)
848
+ * })
849
+ *
850
+ * // Changes to store are now automatically synchronized
851
+ * store.put([{ id: 'shape1', type: 'geo', x: 100, y: 100 }])
852
+ * ```
853
+ *
854
+ * @example
855
+ * ```ts
856
+ * // Advanced usage with presence and custom messages
857
+ * const syncClient = new TLSyncClient({
858
+ * store,
859
+ * socket,
860
+ * presence: atom({ cursor: { x: 0, y: 0 }, userName: 'Alice' }),
861
+ * presenceMode: atom('full'),
862
+ * onCustomMessageReceived: (data) => {
863
+ * if (data.type === 'chat') {
864
+ * showChatMessage(data.message, data.from)
865
+ * }
866
+ * },
867
+ * onAfterConnect: (client, { isReadonly }) => {
868
+ * if (isReadonly) {
869
+ * showNotification('Connected in read-only mode')
870
+ * }
871
+ * }
872
+ * })
873
+ * ```
874
+ *
875
+ * @public
876
+ */
877
+ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>> {
878
+ /** The last clock time from the most recent server update */
879
+ private lastServerClock;
880
+ private lastServerInteractionTimestamp;
881
+ /** The queue of in-flight push requests that have not yet been acknowledged by the server */
882
+ private pendingPushRequests;
883
+ /**
884
+ * The diff of 'unconfirmed', 'optimistic' changes that have been made locally by the user if we
885
+ * take this diff, reverse it, and apply that to the store, our store will match exactly the most
886
+ * recent state of the server that we know about
887
+ */
888
+ private speculativeChanges;
889
+ private disposables;
890
+ /* Excluded from this release type: store */
891
+ /* Excluded from this release type: socket */
892
+ /* Excluded from this release type: presenceState */
893
+ /* Excluded from this release type: presenceMode */
894
+ /* Excluded from this release type: isConnectedToRoom */
895
+ /**
896
+ * The client clock is essentially a counter for push requests Each time a push request is created
897
+ * the clock is incremented. This clock is sent with the push request to the server, and the
898
+ * server returns it with the response so that we can match up the response with the request.
899
+ *
900
+ * The clock may also be used at one point in the future to allow the client to re-send push
901
+ * requests idempotently (i.e. the server will keep track of each client's clock and not execute
902
+ * requests it has already handled), but at the time of writing this is neither needed nor
903
+ * implemented.
904
+ */
905
+ private clientClock;
906
+ /**
907
+ * Callback executed immediately after successful connection to sync room.
908
+ * Use this to perform any post-connection setup required for your application,
909
+ * such as initializing default content or updating UI state.
910
+ *
911
+ * @param self - The TLSyncClient instance that connected
912
+ * @param details - Connection details
913
+ * - isReadonly - Whether the connection is in read-only mode
914
+ */
915
+ private readonly onAfterConnect?;
916
+ private readonly onCustomMessageReceived?;
917
+ private isDebugging;
918
+ private debug;
919
+ private readonly presenceType;
920
+ private didCancel?;
921
+ /**
922
+ * Creates a new TLSyncClient instance to manage synchronization with a remote server.
923
+ *
924
+ * @param config - Configuration object for the sync client
925
+ * - store - The local tldraw store to synchronize
926
+ * - socket - WebSocket adapter for server communication
927
+ * - presence - Reactive signal containing current user's presence data
928
+ * - presenceMode - Optional signal controlling presence sharing (defaults to 'full')
929
+ * - onLoad - Callback fired when initial sync completes successfully
930
+ * - onSyncError - Callback fired when sync fails with error reason
931
+ * - onCustomMessageReceived - Optional handler for custom messages
932
+ * - onAfterConnect - Optional callback fired after successful connection
933
+ * - self - The TLSyncClient instance
934
+ * - details - Connection details including readonly status
935
+ * - didCancel - Optional function to check if sync should be cancelled
936
+ */
937
+ constructor(config: {
938
+ didCancel?(): boolean;
939
+ onAfterConnect?(self: TLSyncClient<R, S>, details: {
940
+ isReadonly: boolean;
941
+ }): void;
942
+ onCustomMessageReceived?: TLCustomMessageHandler;
943
+ onLoad(self: TLSyncClient<R, S>): void;
944
+ onSyncError(reason: string): void;
945
+ presence: Signal<null | R>;
946
+ presenceMode?: Signal<TLPresenceMode>;
947
+ socket: TLPersistentClientSocket<any, any>;
948
+ store: S;
949
+ });
950
+ /* Excluded from this release type: latestConnectRequestId */
951
+ /**
952
+ * This is the first message that is sent over a newly established socket connection. And we need
953
+ * to wait for the response before this client can be used.
954
+ */
955
+ private sendConnectMessage;
956
+ /** Switch to offline mode */
957
+ private resetConnection;
958
+ /**
959
+ * Invoked when the socket connection comes online, either for the first time or as the result of
960
+ * a reconnect. The goal is to rebase on the server's state and fire off a new push request for
961
+ * any local changes that were made while offline.
962
+ */
963
+ private didReconnect;
964
+ private incomingDiffBuffer;
965
+ /** Handle events received from the server */
966
+ private handleServerEvent;
967
+ /**
968
+ * Closes the sync client and cleans up all resources.
969
+ *
970
+ * Call this method when you no longer need the sync client to prevent
971
+ * memory leaks and close the WebSocket connection. After calling close(),
972
+ * the client cannot be reused.
973
+ *
974
+ * @example
975
+ * ```ts
976
+ * // Clean shutdown
977
+ * syncClient.close()
978
+ * ```
979
+ */
980
+ close(): void;
981
+ private lastPushedPresenceState;
982
+ private pushPresence;
983
+ /** Push a change to the server, or stash it locally if we're offline */
984
+ private push;
985
+ /** Send any unsent push requests to the server */
986
+ private flushPendingPushRequests;
987
+ /**
988
+ * Applies a 'network' diff to the store this does value-based equality checking so that if the
989
+ * data is the same (as opposed to merely identical with ===), then no change is made and no
990
+ * changes will be propagated back to store listeners
991
+ */
992
+ private applyNetworkDiff;
993
+ private rebase;
994
+ private scheduleRebase;
995
+ }
728
996
 
729
997
  /**
730
998
  * WebSocket close code used by the server to signal a non-recoverable sync error.
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.d76c345101d5",
53
+ "4.2.0-next.e824a30c434e",
54
54
  "cjs"
55
55
  );
56
56
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport { TLSocketRoom, type OmitVoid, type TLSyncLog } from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TlSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tDocumentState,\n\tTLSyncRoom,\n\ttype RoomSnapshot,\n\ttype RoomStoreMethods,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport { TLSocketRoom, type OmitVoid, type TLSyncLog } from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tDocumentState,\n\tTLSyncRoom,\n\ttype RoomSnapshot,\n\ttype RoomStoreMethods,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,mBAAsB;AACtB,oCAAyD;AACzD,kBAcO;AACP,sBASO;AACP,yBAAmD;AAGnD,+BAAkC;AAClC,0BAA4D;AAC5D,0BAWO;AACP,wBAMO;AAAA,IAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/ClientWebSocketAdapter.ts"],
4
- "sourcesContent": ["import { atom, Atom } from '@tldraw/state'\nimport { TLRecord } from '@tldraw/tlschema'\nimport { assert, warnOnce } from '@tldraw/utils'\nimport { chunk } from './chunk'\nimport { TLSocketClientSentEvent, TLSocketServerSentEvent } from './protocol'\nimport {\n\tTLPersistentClientSocket,\n\tTLPersistentClientSocketStatus,\n\tTLSocketStatusListener,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n} from './TLSyncClient'\n\nfunction listenTo<T extends EventTarget>(target: T, event: string, handler: () => void) {\n\ttarget.addEventListener(event, handler)\n\treturn () => {\n\t\ttarget.removeEventListener(event, handler)\n\t}\n}\n\nfunction debug(...args: any[]) {\n\t// @ts-ignore\n\tif (typeof window !== 'undefined' && window.__tldraw_socket_debug) {\n\t\tconst now = new Date()\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.log(\n\t\t\t`${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`,\n\t\t\t...args\n\t\t\t//, new Error().stack\n\t\t)\n\t}\n}\n\n// NOTE: ClientWebSocketAdapter requires its users to implement their own connection loss\n// detection, for example by regularly pinging the server and .restart()ing\n// the connection when a number of pings goes unanswered. Without this mechanism,\n// we might not be able to detect the websocket connection going down in a timely manner\n// (it will probably time out on outgoing data packets at some point).\n//\n// This is by design. While the Websocket protocol specifies protocol-level pings,\n// they don't seem to be surfaced in browser APIs and can't be relied on. Therefore,\n// pings need to be implemented one level up, on the application API side, which for our\n// codebase means whatever code that uses ClientWebSocketAdapter.\n/**\n * A WebSocket adapter that provides persistent connection management for tldraw synchronization.\n * This adapter handles connection establishment, reconnection logic, and message routing between\n * the sync client and server. It implements automatic reconnection with exponential backoff\n * and supports connection loss detection.\n *\n * Note: This adapter requires users to implement their own connection loss detection (e.g., pings)\n * as browser WebSocket APIs don't reliably surface protocol-level ping/pong frames.\n *\n * @internal\n * @example\n * ```ts\n * // Create a WebSocket adapter with connection URI\n * const adapter = new ClientWebSocketAdapter(() => 'ws://localhost:3000/sync')\n *\n * // Listen for connection status changes\n * adapter.onStatusChange((status) => {\n * console.log('Connection status:', status)\n * })\n *\n * // Listen for incoming messages\n * adapter.onReceiveMessage((message) => {\n * console.log('Received:', message)\n * })\n *\n * // Send a message when connected\n * if (adapter.connectionStatus === 'online') {\n * adapter.sendMessage({ type: 'ping' })\n * }\n * ```\n */\nexport class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord> {\n\t_ws: WebSocket | null = null\n\n\tisDisposed = false\n\n\t/** @internal */\n\treadonly _reconnectManager: ReconnectManager\n\n\t/**\n\t * Permanently closes the WebSocket adapter and disposes of all resources.\n\t * Once closed, the adapter cannot be reused and should be discarded.\n\t * This method is idempotent - calling it multiple times has no additional effect.\n\t */\n\t// TODO: .close should be a project-wide interface with a common contract (.close()d thing\n\t// can only be garbage collected, and can't be used anymore)\n\tclose() {\n\t\tthis.isDisposed = true\n\t\tthis._reconnectManager.close()\n\t\t// WebSocket.close() is idempotent\n\t\tthis._ws?.close()\n\t}\n\n\t/**\n\t * Creates a new ClientWebSocketAdapter instance.\n\t *\n\t * @param getUri - Function that returns the WebSocket URI to connect to.\n\t * Can return a string directly or a Promise that resolves to a string.\n\t * This function is called each time a connection attempt is made,\n\t * allowing for dynamic URI generation (e.g., for authentication tokens).\n\t */\n\tconstructor(getUri: () => Promise<string> | string) {\n\t\tthis._reconnectManager = new ReconnectManager(this, getUri)\n\t}\n\n\tprivate _handleConnect() {\n\t\tdebug('handleConnect')\n\n\t\tthis._connectionStatus.set('online')\n\t\tthis.statusListeners.forEach((cb) => cb({ status: 'online' }))\n\n\t\tthis._reconnectManager.connected()\n\t}\n\n\tprivate _handleDisconnect(\n\t\treason: 'closed' | 'manual',\n\t\tcloseCode?: number,\n\t\tdidOpen?: boolean,\n\t\tcloseReason?: string\n\t) {\n\t\tcloseReason = closeReason || TLSyncErrorCloseEventReason.UNKNOWN_ERROR\n\n\t\tdebug('handleDisconnect', {\n\t\t\tcurrentStatus: this.connectionStatus,\n\t\t\tcloseCode,\n\t\t\treason,\n\t\t})\n\n\t\tlet newStatus: 'offline' | 'error'\n\t\tswitch (reason) {\n\t\t\tcase 'closed':\n\t\t\t\tif (closeCode === TLSyncErrorCloseEventCode) {\n\t\t\t\t\tnewStatus = 'error'\n\t\t\t\t} else {\n\t\t\t\t\tnewStatus = 'offline'\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\tcase 'manual':\n\t\t\t\tnewStatus = 'offline'\n\t\t\t\tbreak\n\t\t}\n\n\t\tif (closeCode === 1006 && !didOpen) {\n\t\t\twarnOnce(\n\t\t\t\t\"Could not open WebSocket connection. This might be because you're trying to load a URL that doesn't support websockets. Check the URL you're trying to connect to.\"\n\t\t\t)\n\t\t}\n\n\t\tif (\n\t\t\t// it the status changed\n\t\t\tthis.connectionStatus !== newStatus &&\n\t\t\t// ignore errors if we're already in the offline state\n\t\t\t!(newStatus === 'error' && this.connectionStatus === 'offline')\n\t\t) {\n\t\t\tthis._connectionStatus.set(newStatus)\n\t\t\tthis.statusListeners.forEach((cb) =>\n\t\t\t\tcb(newStatus === 'error' ? { status: 'error', reason: closeReason } : { status: newStatus })\n\t\t\t)\n\t\t}\n\n\t\tthis._reconnectManager.disconnected()\n\t}\n\n\t_setNewSocket(ws: WebSocket) {\n\t\tassert(!this.isDisposed, 'Tried to set a new websocket on a disposed socket')\n\t\tassert(\n\t\t\tthis._ws === null ||\n\t\t\t\tthis._ws.readyState === WebSocket.CLOSED ||\n\t\t\t\tthis._ws.readyState === WebSocket.CLOSING,\n\t\t\t`Tried to set a new websocket in when the existing one was ${this._ws?.readyState}`\n\t\t)\n\n\t\tlet didOpen = false\n\n\t\t// NOTE: Sockets can stay for quite a while in the CLOSING state. This is because the transition\n\t\t// between CLOSING and CLOSED happens either after the closing handshake, or after a\n\t\t// timeout, but in either case those sockets don't need any special handling, the browser\n\t\t// will close them eventually. We just \"orphan\" such sockets and ignore their onclose/onerror.\n\t\tws.onopen = () => {\n\t\t\tdebug('ws.onopen')\n\t\t\tassert(\n\t\t\t\tthis._ws === ws,\n\t\t\t\t\"sockets must only be orphaned when they are CLOSING or CLOSED, so they can't open\"\n\t\t\t)\n\t\t\tdidOpen = true\n\t\t\tthis._handleConnect()\n\t\t}\n\t\tws.onclose = (event: CloseEvent) => {\n\t\t\tdebug('ws.onclose', event)\n\t\t\tif (this._ws === ws) {\n\t\t\t\tthis._handleDisconnect('closed', event.code, didOpen, event.reason)\n\t\t\t} else {\n\t\t\t\tdebug('ignoring onclose for an orphaned socket')\n\t\t\t}\n\t\t}\n\t\tws.onerror = (event) => {\n\t\t\tdebug('ws.onerror', event)\n\t\t\tif (this._ws === ws) {\n\t\t\t\tthis._handleDisconnect('closed')\n\t\t\t} else {\n\t\t\t\tdebug('ignoring onerror for an orphaned socket')\n\t\t\t}\n\t\t}\n\t\tws.onmessage = (ev) => {\n\t\t\tassert(\n\t\t\t\tthis._ws === ws,\n\t\t\t\t\"sockets must only be orphaned when they are CLOSING or CLOSED, so they can't receive messages\"\n\t\t\t)\n\t\t\tconst parsed = JSON.parse(ev.data.toString())\n\t\t\tthis.messageListeners.forEach((cb) => cb(parsed))\n\t\t}\n\n\t\tthis._ws = ws\n\t}\n\n\t_closeSocket() {\n\t\tif (this._ws === null) return\n\n\t\tthis._ws.close()\n\t\t// explicitly orphan the socket to ignore its onclose/onerror, because onclose can be delayed\n\t\tthis._ws = null\n\t\tthis._handleDisconnect('manual')\n\t}\n\n\t// TLPersistentClientSocket stuff\n\n\t_connectionStatus: Atom<TLPersistentClientSocketStatus | 'initial'> = atom(\n\t\t'websocket connection status',\n\t\t'initial'\n\t)\n\n\t/**\n\t * Gets the current connection status of the WebSocket.\n\t *\n\t * @returns The current connection status: 'online', 'offline', or 'error'\n\t */\n\t// eslint-disable-next-line no-restricted-syntax\n\tget connectionStatus(): TLPersistentClientSocketStatus {\n\t\tconst status = this._connectionStatus.get()\n\t\treturn status === 'initial' ? 'offline' : status\n\t}\n\n\t/**\n\t * Sends a message to the server through the WebSocket connection.\n\t * Messages are automatically chunked if they exceed size limits.\n\t *\n\t * @param msg - The message to send to the server\n\t *\n\t * @example\n\t * ```ts\n\t * adapter.sendMessage({\n\t * type: 'push',\n\t * diff: { 'shape:abc123': [2, { x: [1, 150] }] }\n\t * })\n\t * ```\n\t */\n\tsendMessage(msg: TLSocketClientSentEvent<TLRecord>) {\n\t\tassert(!this.isDisposed, 'Tried to send message on a disposed socket')\n\n\t\tif (!this._ws) return\n\t\tif (this.connectionStatus === 'online') {\n\t\t\tconst chunks = chunk(JSON.stringify(msg))\n\t\t\tfor (const part of chunks) {\n\t\t\t\tthis._ws.send(part)\n\t\t\t}\n\t\t} else {\n\t\t\tconsole.warn('Tried to send message while ' + this.connectionStatus)\n\t\t}\n\t}\n\n\tprivate messageListeners = new Set<(msg: TLSocketServerSentEvent<TLRecord>) => void>()\n\t/**\n\t * Registers a callback to handle incoming messages from the server.\n\t *\n\t * @param cb - Callback function that will be called with each received message\n\t * @returns A cleanup function to remove the message listener\n\t *\n\t * @example\n\t * ```ts\n\t * const unsubscribe = adapter.onReceiveMessage((message) => {\n\t * switch (message.type) {\n\t * case 'connect':\n\t * console.log('Connected to room')\n\t * break\n\t * case 'data':\n\t * console.log('Received data:', message.diff)\n\t * break\n\t * }\n\t * })\n\t *\n\t * // Later, remove the listener\n\t * unsubscribe()\n\t * ```\n\t */\n\tonReceiveMessage(cb: (val: TLSocketServerSentEvent<TLRecord>) => void) {\n\t\tassert(!this.isDisposed, 'Tried to add message listener on a disposed socket')\n\n\t\tthis.messageListeners.add(cb)\n\t\treturn () => {\n\t\t\tthis.messageListeners.delete(cb)\n\t\t}\n\t}\n\n\tprivate statusListeners = new Set<TLSocketStatusListener>()\n\t/**\n\t * Registers a callback to handle connection status changes.\n\t *\n\t * @param cb - Callback function that will be called when the connection status changes\n\t * @returns A cleanup function to remove the status listener\n\t *\n\t * @example\n\t * ```ts\n\t * const unsubscribe = adapter.onStatusChange((status) => {\n\t * if (status.status === 'error') {\n\t * console.error('Connection error:', status.reason)\n\t * } else {\n\t * console.log('Status changed to:', status.status)\n\t * }\n\t * })\n\t *\n\t * // Later, remove the listener\n\t * unsubscribe()\n\t * ```\n\t */\n\tonStatusChange(cb: TLSocketStatusListener) {\n\t\tassert(!this.isDisposed, 'Tried to add status listener on a disposed socket')\n\n\t\tthis.statusListeners.add(cb)\n\t\treturn () => {\n\t\t\tthis.statusListeners.delete(cb)\n\t\t}\n\t}\n\n\t/**\n\t * Manually restarts the WebSocket connection.\n\t * This closes the current connection (if any) and attempts to establish a new one.\n\t * Useful for implementing connection loss detection and recovery.\n\t *\n\t * @example\n\t * ```ts\n\t * // Restart connection after detecting it's stale\n\t * if (lastPongTime < Date.now() - 30000) {\n\t * adapter.restart()\n\t * }\n\t * ```\n\t */\n\trestart() {\n\t\tassert(!this.isDisposed, 'Tried to restart a disposed socket')\n\t\tdebug('restarting')\n\n\t\tthis._closeSocket()\n\t\tthis._reconnectManager.maybeReconnected()\n\t}\n}\n\n/**\n * Minimum reconnection delay in milliseconds when the browser tab is active and focused.\n *\n * @internal\n */\nexport const ACTIVE_MIN_DELAY = 500\n\n/**\n * Maximum reconnection delay in milliseconds when the browser tab is active and focused.\n *\n * @internal\n */\nexport const ACTIVE_MAX_DELAY = 2000\n\n/**\n * Minimum reconnection delay in milliseconds when the browser tab is inactive or hidden.\n * This longer delay helps reduce battery drain and server load when users aren't actively viewing the tab.\n *\n * @internal\n */\nexport const INACTIVE_MIN_DELAY = 1000\n\n/**\n * Maximum reconnection delay in milliseconds when the browser tab is inactive or hidden.\n * Set to 5 minutes to balance between maintaining sync and conserving resources.\n *\n * @internal\n */\nexport const INACTIVE_MAX_DELAY = 1000 * 60 * 5\n\n/**\n * Exponential backoff multiplier for calculating reconnection delays.\n * Each failed connection attempt increases the delay by this factor until max delay is reached.\n *\n * @internal\n */\nexport const DELAY_EXPONENT = 1.5\n\n/**\n * Maximum time in milliseconds to wait for a connection attempt before considering it failed.\n * This helps detect connections stuck in the CONNECTING state and retry with fresh attempts.\n *\n * @internal\n */\nexport const ATTEMPT_TIMEOUT = 1000\n\n/**\n * Manages automatic reconnection logic for WebSocket connections with intelligent backoff strategies.\n * This class handles connection attempts, tracks connection state, and implements exponential backoff\n * with different delays based on whether the browser tab is active or inactive.\n *\n * The ReconnectManager responds to various browser events like network status changes,\n * tab visibility changes, and connection events to optimize reconnection timing and\n * minimize unnecessary connection attempts.\n *\n * @internal\n *\n * @example\n * ```ts\n * const manager = new ReconnectManager(\n * socketAdapter,\n * () => 'ws://localhost:3000/sync'\n * )\n *\n * // Manager automatically handles:\n * // - Initial connection\n * // - Reconnection on disconnect\n * // - Exponential backoff on failures\n * // - Tab visibility-aware delays\n * // - Network status change responses\n * ```\n */\nexport class ReconnectManager {\n\tprivate isDisposed = false\n\tprivate disposables: (() => void)[] = [\n\t\t() => {\n\t\t\tif (this.reconnectTimeout) clearTimeout(this.reconnectTimeout)\n\t\t\tif (this.recheckConnectingTimeout) clearTimeout(this.recheckConnectingTimeout)\n\t\t},\n\t]\n\tprivate reconnectTimeout: ReturnType<typeof setTimeout> | null = null\n\tprivate recheckConnectingTimeout: ReturnType<typeof setTimeout> | null = null\n\n\tprivate lastAttemptStart: number | null = null\n\tintendedDelay: number = ACTIVE_MIN_DELAY\n\tprivate state: 'pendingAttempt' | 'pendingAttemptResult' | 'delay' | 'connected'\n\n\t/**\n\t * Creates a new ReconnectManager instance.\n\t *\n\t * socketAdapter - The ClientWebSocketAdapter instance to manage\n\t * getUri - Function that returns the WebSocket URI for connection attempts\n\t */\n\tconstructor(\n\t\tprivate socketAdapter: ClientWebSocketAdapter,\n\t\tprivate getUri: () => Promise<string> | string\n\t) {\n\t\tthis.subscribeToReconnectHints()\n\n\t\tthis.disposables.push(\n\t\t\tlistenTo(window, 'offline', () => {\n\t\t\t\tdebug('window went offline')\n\t\t\t\t// On the one hand, 'offline' event is not really reliable; on the other, the only\n\t\t\t\t// alternative is to wait for pings not being delivered, which takes more than 20 seconds,\n\t\t\t\t// which means we won't see the ClientWebSocketAdapter status change for more than\n\t\t\t\t// 20 seconds after the tab goes offline. Our application layer must be resistent to\n\t\t\t\t// connection restart anyway, so we can just try to reconnect and see if\n\t\t\t\t// we're truly offline.\n\t\t\t\tthis.socketAdapter._closeSocket()\n\t\t\t})\n\t\t)\n\n\t\tthis.state = 'pendingAttempt'\n\t\tthis.intendedDelay = ACTIVE_MIN_DELAY\n\t\tthis.scheduleAttempt()\n\t}\n\n\tprivate subscribeToReconnectHints() {\n\t\tthis.disposables.push(\n\t\t\tlistenTo(window, 'online', () => {\n\t\t\t\tdebug('window went online')\n\t\t\t\tthis.maybeReconnected()\n\t\t\t}),\n\t\t\tlistenTo(document, 'visibilitychange', () => {\n\t\t\t\tif (!document.hidden) {\n\t\t\t\t\tdebug('document became visible')\n\t\t\t\t\tthis.maybeReconnected()\n\t\t\t\t}\n\t\t\t})\n\t\t)\n\n\t\tif (Object.prototype.hasOwnProperty.call(navigator, 'connection')) {\n\t\t\tconst connection = (navigator as any)['connection'] as EventTarget\n\t\t\tthis.disposables.push(\n\t\t\t\tlistenTo(connection, 'change', () => {\n\t\t\t\t\tdebug('navigator.connection change')\n\t\t\t\t\tthis.maybeReconnected()\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t}\n\n\tprivate scheduleAttempt() {\n\t\tassert(this.state === 'pendingAttempt')\n\t\tdebug('scheduling a connection attempt')\n\t\tPromise.resolve(this.getUri()).then((uri) => {\n\t\t\t// this can happen if the promise gets resolved too late\n\t\t\tif (this.state !== 'pendingAttempt' || this.isDisposed) return\n\t\t\tassert(\n\t\t\t\tthis.socketAdapter._ws?.readyState !== WebSocket.OPEN,\n\t\t\t\t'There should be no connection attempts while already connected'\n\t\t\t)\n\n\t\t\tthis.lastAttemptStart = Date.now()\n\t\t\tthis.socketAdapter._setNewSocket(new WebSocket(httpToWs(uri)))\n\t\t\tthis.state = 'pendingAttemptResult'\n\t\t})\n\t}\n\n\tprivate getMaxDelay() {\n\t\treturn document.hidden ? INACTIVE_MAX_DELAY : ACTIVE_MAX_DELAY\n\t}\n\n\tprivate getMinDelay() {\n\t\treturn document.hidden ? INACTIVE_MIN_DELAY : ACTIVE_MIN_DELAY\n\t}\n\n\tprivate clearReconnectTimeout() {\n\t\tif (this.reconnectTimeout) {\n\t\t\tclearTimeout(this.reconnectTimeout)\n\t\t\tthis.reconnectTimeout = null\n\t\t}\n\t}\n\n\tprivate clearRecheckConnectingTimeout() {\n\t\tif (this.recheckConnectingTimeout) {\n\t\t\tclearTimeout(this.recheckConnectingTimeout)\n\t\t\tthis.recheckConnectingTimeout = null\n\t\t}\n\t}\n\n\t/**\n\t * Checks if reconnection should be attempted and initiates it if appropriate.\n\t * This method is called in response to network events, tab visibility changes,\n\t * and other hints that connectivity may have been restored.\n\t *\n\t * The method intelligently handles various connection states:\n\t * - Already connected: no action needed\n\t * - Currently connecting: waits or retries based on attempt age\n\t * - Disconnected: initiates immediate reconnection attempt\n\t *\n\t * @example\n\t * ```ts\n\t * // Called automatically on network/visibility events, but can be called manually\n\t * manager.maybeReconnected()\n\t * ```\n\t */\n\tmaybeReconnected() {\n\t\tdebug('ReconnectManager.maybeReconnected')\n\t\t// It doesn't make sense to have another check scheduled if we're already checking it now.\n\t\t// If we have a CONNECTING check scheduled and relevant, it'll be recreated below anyway\n\t\tthis.clearRecheckConnectingTimeout()\n\n\t\t// readyState can be CONNECTING, OPEN, CLOSING, CLOSED, or null (if getUri() is still pending)\n\t\tif (this.socketAdapter._ws?.readyState === WebSocket.OPEN) {\n\t\t\tdebug('ReconnectManager.maybeReconnected: already connected')\n\t\t\t// nothing to do, we're already OK\n\t\t\treturn\n\t\t}\n\n\t\tif (this.socketAdapter._ws?.readyState === WebSocket.CONNECTING) {\n\t\t\tdebug('ReconnectManager.maybeReconnected: connecting')\n\t\t\t// We might be waiting for a TCP connection that sent SYN out and will never get it back,\n\t\t\t// while a new connection appeared. On the other hand, we might have just started connecting\n\t\t\t// and will succeed in a bit. Thus, we're checking how old the attempt is and retry anew\n\t\t\t// if it's old enough. This by itself can delay the connection a bit, but shouldn't prevent\n\t\t\t// new connections as long as `maybeReconnected` is not looped itself\n\t\t\tassert(\n\t\t\t\tthis.lastAttemptStart,\n\t\t\t\t'ReadyState=CONNECTING without lastAttemptStart should be impossible'\n\t\t\t)\n\t\t\tconst sinceLastStart = Date.now() - this.lastAttemptStart\n\t\t\tif (sinceLastStart < ATTEMPT_TIMEOUT) {\n\t\t\t\tdebug('ReconnectManager.maybeReconnected: connecting, rechecking later')\n\t\t\t\tthis.recheckConnectingTimeout = setTimeout(\n\t\t\t\t\t() => this.maybeReconnected(),\n\t\t\t\t\tATTEMPT_TIMEOUT - sinceLastStart\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tdebug('ReconnectManager.maybeReconnected: connecting, but for too long, retry now')\n\t\t\t\t// Last connection attempt was started a while ago, it's possible that network conditions\n\t\t\t\t// changed, and it's worth retrying to connect. `disconnected` will handle reconnection\n\t\t\t\t//\n\t\t\t\t// NOTE: The danger here is looping in connection attemps if connections are slow.\n\t\t\t\t// Make sure that `maybeReconnected` is not called in the `disconnected` codepath!\n\t\t\t\tthis.clearRecheckConnectingTimeout()\n\t\t\t\tthis.socketAdapter._closeSocket()\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tdebug('ReconnectManager.maybeReconnected: closing/closed/null, retry now')\n\t\t// readyState is CLOSING or CLOSED, or the websocket is null\n\t\t// Restart the backoff and retry ASAP (honouring the min delay)\n\t\t// this.state doesn't really matter, because disconnected() will handle any state correctly\n\t\tthis.intendedDelay = ACTIVE_MIN_DELAY\n\t\tthis.disconnected()\n\t}\n\n\t/**\n\t * Handles disconnection events and schedules reconnection attempts with exponential backoff.\n\t * This method is called when the WebSocket connection is lost or fails to establish.\n\t *\n\t * It implements intelligent delay calculation based on:\n\t * - Previous attempt timing\n\t * - Current tab visibility (active vs inactive delays)\n\t * - Exponential backoff for repeated failures\n\t *\n\t * @example\n\t * ```ts\n\t * // Called automatically when connection is lost\n\t * // Schedules reconnection with appropriate delay\n\t * manager.disconnected()\n\t * ```\n\t */\n\tdisconnected() {\n\t\tdebug('ReconnectManager.disconnected')\n\t\t// This either means we're freshly disconnected, or the last connection attempt failed;\n\t\t// either way, time to try again.\n\n\t\t// Guard against delayed notifications and recheck synchronously\n\t\tif (\n\t\t\tthis.socketAdapter._ws?.readyState !== WebSocket.OPEN &&\n\t\t\tthis.socketAdapter._ws?.readyState !== WebSocket.CONNECTING\n\t\t) {\n\t\t\tdebug('ReconnectManager.disconnected: websocket is not OPEN or CONNECTING')\n\t\t\tthis.clearReconnectTimeout()\n\n\t\t\tlet delayLeft\n\t\t\tif (this.state === 'connected') {\n\t\t\t\t// it's the first sign that we got disconnected; the state will be updated below,\n\t\t\t\t// just set the appropriate delay for now\n\t\t\t\tthis.intendedDelay = this.getMinDelay()\n\t\t\t\tdelayLeft = this.intendedDelay\n\t\t\t} else {\n\t\t\t\tdelayLeft =\n\t\t\t\t\tthis.lastAttemptStart !== null\n\t\t\t\t\t\t? this.lastAttemptStart + this.intendedDelay - Date.now()\n\t\t\t\t\t\t: 0\n\t\t\t}\n\n\t\t\tif (delayLeft > 0) {\n\t\t\t\tdebug('ReconnectManager.disconnected: delaying, delayLeft', delayLeft)\n\t\t\t\t// try again later\n\t\t\t\tthis.state = 'delay'\n\n\t\t\t\tthis.reconnectTimeout = setTimeout(() => this.disconnected(), delayLeft)\n\t\t\t} else {\n\t\t\t\t// not connected and not delayed, time to retry\n\t\t\t\tthis.state = 'pendingAttempt'\n\n\t\t\t\tthis.intendedDelay = Math.min(\n\t\t\t\t\tthis.getMaxDelay(),\n\t\t\t\t\tMath.max(this.getMinDelay(), this.intendedDelay) * DELAY_EXPONENT\n\t\t\t\t)\n\t\t\t\tdebug(\n\t\t\t\t\t'ReconnectManager.disconnected: attempting a connection, next delay',\n\t\t\t\t\tthis.intendedDelay\n\t\t\t\t)\n\t\t\t\tthis.scheduleAttempt()\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Handles successful connection events and resets reconnection state.\n\t * This method is called when the WebSocket successfully connects to the server.\n\t *\n\t * It clears any pending reconnection attempts and resets the delay back to minimum\n\t * for future connection attempts.\n\t *\n\t * @example\n\t * ```ts\n\t * // Called automatically when WebSocket opens successfully\n\t * manager.connected()\n\t * ```\n\t */\n\tconnected() {\n\t\tdebug('ReconnectManager.connected')\n\t\t// this notification could've been delayed, recheck synchronously\n\t\tif (this.socketAdapter._ws?.readyState === WebSocket.OPEN) {\n\t\t\tdebug('ReconnectManager.connected: websocket is OPEN')\n\t\t\tthis.state = 'connected'\n\t\t\tthis.clearReconnectTimeout()\n\t\t\tthis.intendedDelay = ACTIVE_MIN_DELAY\n\t\t}\n\t}\n\n\t/**\n\t * Permanently closes the reconnection manager and cleans up all resources.\n\t * This stops all pending reconnection attempts and removes event listeners.\n\t * Once closed, the manager cannot be reused.\n\t */\n\tclose() {\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.isDisposed = true\n\t}\n}\n\nfunction httpToWs(url: string) {\n\treturn url.replace(/^http(s)?:/, 'ws$1:')\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { atom, Atom } from '@tldraw/state'\nimport { TLRecord } from '@tldraw/tlschema'\nimport { assert, warnOnce } from '@tldraw/utils'\nimport { chunk } from './chunk'\nimport { TLSocketClientSentEvent, TLSocketServerSentEvent } from './protocol'\nimport {\n\tTLPersistentClientSocket,\n\tTLPersistentClientSocketStatus,\n\tTLSocketStatusListener,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n} from './TLSyncClient'\n\nfunction listenTo<T extends EventTarget>(target: T, event: string, handler: () => void) {\n\ttarget.addEventListener(event, handler)\n\treturn () => {\n\t\ttarget.removeEventListener(event, handler)\n\t}\n}\n\nfunction debug(...args: any[]) {\n\t// @ts-ignore\n\tif (typeof window !== 'undefined' && window.__tldraw_socket_debug) {\n\t\tconst now = new Date()\n\t\t// eslint-disable-next-line no-console\n\t\tconsole.log(\n\t\t\t`${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}.${now.getMilliseconds()}`,\n\t\t\t...args\n\t\t\t//, new Error().stack\n\t\t)\n\t}\n}\n\n// NOTE: ClientWebSocketAdapter requires its users to implement their own connection loss\n// detection, for example by regularly pinging the server and .restart()ing\n// the connection when a number of pings goes unanswered. Without this mechanism,\n// we might not be able to detect the websocket connection going down in a timely manner\n// (it will probably time out on outgoing data packets at some point).\n//\n// This is by design. While the Websocket protocol specifies protocol-level pings,\n// they don't seem to be surfaced in browser APIs and can't be relied on. Therefore,\n// pings need to be implemented one level up, on the application API side, which for our\n// codebase means whatever code that uses ClientWebSocketAdapter.\n/**\n * A WebSocket adapter that provides persistent connection management for tldraw synchronization.\n * This adapter handles connection establishment, reconnection logic, and message routing between\n * the sync client and server. It implements automatic reconnection with exponential backoff\n * and supports connection loss detection.\n *\n * Note: This adapter requires users to implement their own connection loss detection (e.g., pings)\n * as browser WebSocket APIs don't reliably surface protocol-level ping/pong frames.\n *\n * @internal\n * @example\n * ```ts\n * // Create a WebSocket adapter with connection URI\n * const adapter = new ClientWebSocketAdapter(() => 'ws://localhost:3000/sync')\n *\n * // Listen for connection status changes\n * adapter.onStatusChange((status) => {\n * console.log('Connection status:', status)\n * })\n *\n * // Listen for incoming messages\n * adapter.onReceiveMessage((message) => {\n * console.log('Received:', message)\n * })\n *\n * // Send a message when connected\n * if (adapter.connectionStatus === 'online') {\n * adapter.sendMessage({ type: 'ping' })\n * }\n * ```\n */\nexport class ClientWebSocketAdapter\n\timplements\n\t\tTLPersistentClientSocket<TLSocketClientSentEvent<TLRecord>, TLSocketServerSentEvent<TLRecord>>\n{\n\t_ws: WebSocket | null = null\n\n\tisDisposed = false\n\n\t/** @internal */\n\treadonly _reconnectManager: ReconnectManager\n\n\t/**\n\t * Permanently closes the WebSocket adapter and disposes of all resources.\n\t * Once closed, the adapter cannot be reused and should be discarded.\n\t * This method is idempotent - calling it multiple times has no additional effect.\n\t */\n\t// TODO: .close should be a project-wide interface with a common contract (.close()d thing\n\t// can only be garbage collected, and can't be used anymore)\n\tclose() {\n\t\tthis.isDisposed = true\n\t\tthis._reconnectManager.close()\n\t\t// WebSocket.close() is idempotent\n\t\tthis._ws?.close()\n\t}\n\n\t/**\n\t * Creates a new ClientWebSocketAdapter instance.\n\t *\n\t * @param getUri - Function that returns the WebSocket URI to connect to.\n\t * Can return a string directly or a Promise that resolves to a string.\n\t * This function is called each time a connection attempt is made,\n\t * allowing for dynamic URI generation (e.g., for authentication tokens).\n\t */\n\tconstructor(getUri: () => Promise<string> | string) {\n\t\tthis._reconnectManager = new ReconnectManager(this, getUri)\n\t}\n\n\tprivate _handleConnect() {\n\t\tdebug('handleConnect')\n\n\t\tthis._connectionStatus.set('online')\n\t\tthis.statusListeners.forEach((cb) => cb({ status: 'online' }))\n\n\t\tthis._reconnectManager.connected()\n\t}\n\n\tprivate _handleDisconnect(\n\t\treason: 'closed' | 'manual',\n\t\tcloseCode?: number,\n\t\tdidOpen?: boolean,\n\t\tcloseReason?: string\n\t) {\n\t\tcloseReason = closeReason || TLSyncErrorCloseEventReason.UNKNOWN_ERROR\n\n\t\tdebug('handleDisconnect', {\n\t\t\tcurrentStatus: this.connectionStatus,\n\t\t\tcloseCode,\n\t\t\treason,\n\t\t})\n\n\t\tlet newStatus: 'offline' | 'error'\n\t\tswitch (reason) {\n\t\t\tcase 'closed':\n\t\t\t\tif (closeCode === TLSyncErrorCloseEventCode) {\n\t\t\t\t\tnewStatus = 'error'\n\t\t\t\t} else {\n\t\t\t\t\tnewStatus = 'offline'\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\tcase 'manual':\n\t\t\t\tnewStatus = 'offline'\n\t\t\t\tbreak\n\t\t}\n\n\t\tif (closeCode === 1006 && !didOpen) {\n\t\t\twarnOnce(\n\t\t\t\t\"Could not open WebSocket connection. This might be because you're trying to load a URL that doesn't support websockets. Check the URL you're trying to connect to.\"\n\t\t\t)\n\t\t}\n\n\t\tif (\n\t\t\t// it the status changed\n\t\t\tthis.connectionStatus !== newStatus &&\n\t\t\t// ignore errors if we're already in the offline state\n\t\t\t!(newStatus === 'error' && this.connectionStatus === 'offline')\n\t\t) {\n\t\t\tthis._connectionStatus.set(newStatus)\n\t\t\tthis.statusListeners.forEach((cb) =>\n\t\t\t\tcb(newStatus === 'error' ? { status: 'error', reason: closeReason } : { status: newStatus })\n\t\t\t)\n\t\t}\n\n\t\tthis._reconnectManager.disconnected()\n\t}\n\n\t_setNewSocket(ws: WebSocket) {\n\t\tassert(!this.isDisposed, 'Tried to set a new websocket on a disposed socket')\n\t\tassert(\n\t\t\tthis._ws === null ||\n\t\t\t\tthis._ws.readyState === WebSocket.CLOSED ||\n\t\t\t\tthis._ws.readyState === WebSocket.CLOSING,\n\t\t\t`Tried to set a new websocket in when the existing one was ${this._ws?.readyState}`\n\t\t)\n\n\t\tlet didOpen = false\n\n\t\t// NOTE: Sockets can stay for quite a while in the CLOSING state. This is because the transition\n\t\t// between CLOSING and CLOSED happens either after the closing handshake, or after a\n\t\t// timeout, but in either case those sockets don't need any special handling, the browser\n\t\t// will close them eventually. We just \"orphan\" such sockets and ignore their onclose/onerror.\n\t\tws.onopen = () => {\n\t\t\tdebug('ws.onopen')\n\t\t\tassert(\n\t\t\t\tthis._ws === ws,\n\t\t\t\t\"sockets must only be orphaned when they are CLOSING or CLOSED, so they can't open\"\n\t\t\t)\n\t\t\tdidOpen = true\n\t\t\tthis._handleConnect()\n\t\t}\n\t\tws.onclose = (event: CloseEvent) => {\n\t\t\tdebug('ws.onclose', event)\n\t\t\tif (this._ws === ws) {\n\t\t\t\tthis._handleDisconnect('closed', event.code, didOpen, event.reason)\n\t\t\t} else {\n\t\t\t\tdebug('ignoring onclose for an orphaned socket')\n\t\t\t}\n\t\t}\n\t\tws.onerror = (event) => {\n\t\t\tdebug('ws.onerror', event)\n\t\t\tif (this._ws === ws) {\n\t\t\t\tthis._handleDisconnect('closed')\n\t\t\t} else {\n\t\t\t\tdebug('ignoring onerror for an orphaned socket')\n\t\t\t}\n\t\t}\n\t\tws.onmessage = (ev) => {\n\t\t\tassert(\n\t\t\t\tthis._ws === ws,\n\t\t\t\t\"sockets must only be orphaned when they are CLOSING or CLOSED, so they can't receive messages\"\n\t\t\t)\n\t\t\tconst parsed = JSON.parse(ev.data.toString())\n\t\t\tthis.messageListeners.forEach((cb) => cb(parsed))\n\t\t}\n\n\t\tthis._ws = ws\n\t}\n\n\t_closeSocket() {\n\t\tif (this._ws === null) return\n\n\t\tthis._ws.close()\n\t\t// explicitly orphan the socket to ignore its onclose/onerror, because onclose can be delayed\n\t\tthis._ws = null\n\t\tthis._handleDisconnect('manual')\n\t}\n\n\t// TLPersistentClientSocket stuff\n\n\t_connectionStatus: Atom<TLPersistentClientSocketStatus | 'initial'> = atom(\n\t\t'websocket connection status',\n\t\t'initial'\n\t)\n\n\t/**\n\t * Gets the current connection status of the WebSocket.\n\t *\n\t * @returns The current connection status: 'online', 'offline', or 'error'\n\t */\n\t// eslint-disable-next-line no-restricted-syntax\n\tget connectionStatus(): TLPersistentClientSocketStatus {\n\t\tconst status = this._connectionStatus.get()\n\t\treturn status === 'initial' ? 'offline' : status\n\t}\n\n\t/**\n\t * Sends a message to the server through the WebSocket connection.\n\t * Messages are automatically chunked if they exceed size limits.\n\t *\n\t * @param msg - The message to send to the server\n\t *\n\t * @example\n\t * ```ts\n\t * adapter.sendMessage({\n\t * type: 'push',\n\t * diff: { 'shape:abc123': [2, { x: [1, 150] }] }\n\t * })\n\t * ```\n\t */\n\tsendMessage(msg: TLSocketClientSentEvent<TLRecord>) {\n\t\tassert(!this.isDisposed, 'Tried to send message on a disposed socket')\n\n\t\tif (!this._ws) return\n\t\tif (this.connectionStatus === 'online') {\n\t\t\tconst chunks = chunk(JSON.stringify(msg))\n\t\t\tfor (const part of chunks) {\n\t\t\t\tthis._ws.send(part)\n\t\t\t}\n\t\t} else {\n\t\t\tconsole.warn('Tried to send message while ' + this.connectionStatus)\n\t\t}\n\t}\n\n\tprivate messageListeners = new Set<(msg: TLSocketServerSentEvent<TLRecord>) => void>()\n\t/**\n\t * Registers a callback to handle incoming messages from the server.\n\t *\n\t * @param cb - Callback function that will be called with each received message\n\t * @returns A cleanup function to remove the message listener\n\t *\n\t * @example\n\t * ```ts\n\t * const unsubscribe = adapter.onReceiveMessage((message) => {\n\t * switch (message.type) {\n\t * case 'connect':\n\t * console.log('Connected to room')\n\t * break\n\t * case 'data':\n\t * console.log('Received data:', message.diff)\n\t * break\n\t * }\n\t * })\n\t *\n\t * // Later, remove the listener\n\t * unsubscribe()\n\t * ```\n\t */\n\tonReceiveMessage(cb: (val: TLSocketServerSentEvent<TLRecord>) => void) {\n\t\tassert(!this.isDisposed, 'Tried to add message listener on a disposed socket')\n\n\t\tthis.messageListeners.add(cb)\n\t\treturn () => {\n\t\t\tthis.messageListeners.delete(cb)\n\t\t}\n\t}\n\n\tprivate statusListeners = new Set<TLSocketStatusListener>()\n\t/**\n\t * Registers a callback to handle connection status changes.\n\t *\n\t * @param cb - Callback function that will be called when the connection status changes\n\t * @returns A cleanup function to remove the status listener\n\t *\n\t * @example\n\t * ```ts\n\t * const unsubscribe = adapter.onStatusChange((status) => {\n\t * if (status.status === 'error') {\n\t * console.error('Connection error:', status.reason)\n\t * } else {\n\t * console.log('Status changed to:', status.status)\n\t * }\n\t * })\n\t *\n\t * // Later, remove the listener\n\t * unsubscribe()\n\t * ```\n\t */\n\tonStatusChange(cb: TLSocketStatusListener) {\n\t\tassert(!this.isDisposed, 'Tried to add status listener on a disposed socket')\n\n\t\tthis.statusListeners.add(cb)\n\t\treturn () => {\n\t\t\tthis.statusListeners.delete(cb)\n\t\t}\n\t}\n\n\t/**\n\t * Manually restarts the WebSocket connection.\n\t * This closes the current connection (if any) and attempts to establish a new one.\n\t * Useful for implementing connection loss detection and recovery.\n\t *\n\t * @example\n\t * ```ts\n\t * // Restart connection after detecting it's stale\n\t * if (lastPongTime < Date.now() - 30000) {\n\t * adapter.restart()\n\t * }\n\t * ```\n\t */\n\trestart() {\n\t\tassert(!this.isDisposed, 'Tried to restart a disposed socket')\n\t\tdebug('restarting')\n\n\t\tthis._closeSocket()\n\t\tthis._reconnectManager.maybeReconnected()\n\t}\n}\n\n/**\n * Minimum reconnection delay in milliseconds when the browser tab is active and focused.\n *\n * @internal\n */\nexport const ACTIVE_MIN_DELAY = 500\n\n/**\n * Maximum reconnection delay in milliseconds when the browser tab is active and focused.\n *\n * @internal\n */\nexport const ACTIVE_MAX_DELAY = 2000\n\n/**\n * Minimum reconnection delay in milliseconds when the browser tab is inactive or hidden.\n * This longer delay helps reduce battery drain and server load when users aren't actively viewing the tab.\n *\n * @internal\n */\nexport const INACTIVE_MIN_DELAY = 1000\n\n/**\n * Maximum reconnection delay in milliseconds when the browser tab is inactive or hidden.\n * Set to 5 minutes to balance between maintaining sync and conserving resources.\n *\n * @internal\n */\nexport const INACTIVE_MAX_DELAY = 1000 * 60 * 5\n\n/**\n * Exponential backoff multiplier for calculating reconnection delays.\n * Each failed connection attempt increases the delay by this factor until max delay is reached.\n *\n * @internal\n */\nexport const DELAY_EXPONENT = 1.5\n\n/**\n * Maximum time in milliseconds to wait for a connection attempt before considering it failed.\n * This helps detect connections stuck in the CONNECTING state and retry with fresh attempts.\n *\n * @internal\n */\nexport const ATTEMPT_TIMEOUT = 1000\n\n/**\n * Manages automatic reconnection logic for WebSocket connections with intelligent backoff strategies.\n * This class handles connection attempts, tracks connection state, and implements exponential backoff\n * with different delays based on whether the browser tab is active or inactive.\n *\n * The ReconnectManager responds to various browser events like network status changes,\n * tab visibility changes, and connection events to optimize reconnection timing and\n * minimize unnecessary connection attempts.\n *\n * @internal\n *\n * @example\n * ```ts\n * const manager = new ReconnectManager(\n * socketAdapter,\n * () => 'ws://localhost:3000/sync'\n * )\n *\n * // Manager automatically handles:\n * // - Initial connection\n * // - Reconnection on disconnect\n * // - Exponential backoff on failures\n * // - Tab visibility-aware delays\n * // - Network status change responses\n * ```\n */\nexport class ReconnectManager {\n\tprivate isDisposed = false\n\tprivate disposables: (() => void)[] = [\n\t\t() => {\n\t\t\tif (this.reconnectTimeout) clearTimeout(this.reconnectTimeout)\n\t\t\tif (this.recheckConnectingTimeout) clearTimeout(this.recheckConnectingTimeout)\n\t\t},\n\t]\n\tprivate reconnectTimeout: ReturnType<typeof setTimeout> | null = null\n\tprivate recheckConnectingTimeout: ReturnType<typeof setTimeout> | null = null\n\n\tprivate lastAttemptStart: number | null = null\n\tintendedDelay: number = ACTIVE_MIN_DELAY\n\tprivate state: 'pendingAttempt' | 'pendingAttemptResult' | 'delay' | 'connected'\n\n\t/**\n\t * Creates a new ReconnectManager instance.\n\t *\n\t * socketAdapter - The ClientWebSocketAdapter instance to manage\n\t * getUri - Function that returns the WebSocket URI for connection attempts\n\t */\n\tconstructor(\n\t\tprivate socketAdapter: ClientWebSocketAdapter,\n\t\tprivate getUri: () => Promise<string> | string\n\t) {\n\t\tthis.subscribeToReconnectHints()\n\n\t\tthis.disposables.push(\n\t\t\tlistenTo(window, 'offline', () => {\n\t\t\t\tdebug('window went offline')\n\t\t\t\t// On the one hand, 'offline' event is not really reliable; on the other, the only\n\t\t\t\t// alternative is to wait for pings not being delivered, which takes more than 20 seconds,\n\t\t\t\t// which means we won't see the ClientWebSocketAdapter status change for more than\n\t\t\t\t// 20 seconds after the tab goes offline. Our application layer must be resistent to\n\t\t\t\t// connection restart anyway, so we can just try to reconnect and see if\n\t\t\t\t// we're truly offline.\n\t\t\t\tthis.socketAdapter._closeSocket()\n\t\t\t})\n\t\t)\n\n\t\tthis.state = 'pendingAttempt'\n\t\tthis.intendedDelay = ACTIVE_MIN_DELAY\n\t\tthis.scheduleAttempt()\n\t}\n\n\tprivate subscribeToReconnectHints() {\n\t\tthis.disposables.push(\n\t\t\tlistenTo(window, 'online', () => {\n\t\t\t\tdebug('window went online')\n\t\t\t\tthis.maybeReconnected()\n\t\t\t}),\n\t\t\tlistenTo(document, 'visibilitychange', () => {\n\t\t\t\tif (!document.hidden) {\n\t\t\t\t\tdebug('document became visible')\n\t\t\t\t\tthis.maybeReconnected()\n\t\t\t\t}\n\t\t\t})\n\t\t)\n\n\t\tif (Object.prototype.hasOwnProperty.call(navigator, 'connection')) {\n\t\t\tconst connection = (navigator as any)['connection'] as EventTarget\n\t\t\tthis.disposables.push(\n\t\t\t\tlistenTo(connection, 'change', () => {\n\t\t\t\t\tdebug('navigator.connection change')\n\t\t\t\t\tthis.maybeReconnected()\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t}\n\n\tprivate scheduleAttempt() {\n\t\tassert(this.state === 'pendingAttempt')\n\t\tdebug('scheduling a connection attempt')\n\t\tPromise.resolve(this.getUri()).then((uri) => {\n\t\t\t// this can happen if the promise gets resolved too late\n\t\t\tif (this.state !== 'pendingAttempt' || this.isDisposed) return\n\t\t\tassert(\n\t\t\t\tthis.socketAdapter._ws?.readyState !== WebSocket.OPEN,\n\t\t\t\t'There should be no connection attempts while already connected'\n\t\t\t)\n\n\t\t\tthis.lastAttemptStart = Date.now()\n\t\t\tthis.socketAdapter._setNewSocket(new WebSocket(httpToWs(uri)))\n\t\t\tthis.state = 'pendingAttemptResult'\n\t\t})\n\t}\n\n\tprivate getMaxDelay() {\n\t\treturn document.hidden ? INACTIVE_MAX_DELAY : ACTIVE_MAX_DELAY\n\t}\n\n\tprivate getMinDelay() {\n\t\treturn document.hidden ? INACTIVE_MIN_DELAY : ACTIVE_MIN_DELAY\n\t}\n\n\tprivate clearReconnectTimeout() {\n\t\tif (this.reconnectTimeout) {\n\t\t\tclearTimeout(this.reconnectTimeout)\n\t\t\tthis.reconnectTimeout = null\n\t\t}\n\t}\n\n\tprivate clearRecheckConnectingTimeout() {\n\t\tif (this.recheckConnectingTimeout) {\n\t\t\tclearTimeout(this.recheckConnectingTimeout)\n\t\t\tthis.recheckConnectingTimeout = null\n\t\t}\n\t}\n\n\t/**\n\t * Checks if reconnection should be attempted and initiates it if appropriate.\n\t * This method is called in response to network events, tab visibility changes,\n\t * and other hints that connectivity may have been restored.\n\t *\n\t * The method intelligently handles various connection states:\n\t * - Already connected: no action needed\n\t * - Currently connecting: waits or retries based on attempt age\n\t * - Disconnected: initiates immediate reconnection attempt\n\t *\n\t * @example\n\t * ```ts\n\t * // Called automatically on network/visibility events, but can be called manually\n\t * manager.maybeReconnected()\n\t * ```\n\t */\n\tmaybeReconnected() {\n\t\tdebug('ReconnectManager.maybeReconnected')\n\t\t// It doesn't make sense to have another check scheduled if we're already checking it now.\n\t\t// If we have a CONNECTING check scheduled and relevant, it'll be recreated below anyway\n\t\tthis.clearRecheckConnectingTimeout()\n\n\t\t// readyState can be CONNECTING, OPEN, CLOSING, CLOSED, or null (if getUri() is still pending)\n\t\tif (this.socketAdapter._ws?.readyState === WebSocket.OPEN) {\n\t\t\tdebug('ReconnectManager.maybeReconnected: already connected')\n\t\t\t// nothing to do, we're already OK\n\t\t\treturn\n\t\t}\n\n\t\tif (this.socketAdapter._ws?.readyState === WebSocket.CONNECTING) {\n\t\t\tdebug('ReconnectManager.maybeReconnected: connecting')\n\t\t\t// We might be waiting for a TCP connection that sent SYN out and will never get it back,\n\t\t\t// while a new connection appeared. On the other hand, we might have just started connecting\n\t\t\t// and will succeed in a bit. Thus, we're checking how old the attempt is and retry anew\n\t\t\t// if it's old enough. This by itself can delay the connection a bit, but shouldn't prevent\n\t\t\t// new connections as long as `maybeReconnected` is not looped itself\n\t\t\tassert(\n\t\t\t\tthis.lastAttemptStart,\n\t\t\t\t'ReadyState=CONNECTING without lastAttemptStart should be impossible'\n\t\t\t)\n\t\t\tconst sinceLastStart = Date.now() - this.lastAttemptStart\n\t\t\tif (sinceLastStart < ATTEMPT_TIMEOUT) {\n\t\t\t\tdebug('ReconnectManager.maybeReconnected: connecting, rechecking later')\n\t\t\t\tthis.recheckConnectingTimeout = setTimeout(\n\t\t\t\t\t() => this.maybeReconnected(),\n\t\t\t\t\tATTEMPT_TIMEOUT - sinceLastStart\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tdebug('ReconnectManager.maybeReconnected: connecting, but for too long, retry now')\n\t\t\t\t// Last connection attempt was started a while ago, it's possible that network conditions\n\t\t\t\t// changed, and it's worth retrying to connect. `disconnected` will handle reconnection\n\t\t\t\t//\n\t\t\t\t// NOTE: The danger here is looping in connection attemps if connections are slow.\n\t\t\t\t// Make sure that `maybeReconnected` is not called in the `disconnected` codepath!\n\t\t\t\tthis.clearRecheckConnectingTimeout()\n\t\t\t\tthis.socketAdapter._closeSocket()\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tdebug('ReconnectManager.maybeReconnected: closing/closed/null, retry now')\n\t\t// readyState is CLOSING or CLOSED, or the websocket is null\n\t\t// Restart the backoff and retry ASAP (honouring the min delay)\n\t\t// this.state doesn't really matter, because disconnected() will handle any state correctly\n\t\tthis.intendedDelay = ACTIVE_MIN_DELAY\n\t\tthis.disconnected()\n\t}\n\n\t/**\n\t * Handles disconnection events and schedules reconnection attempts with exponential backoff.\n\t * This method is called when the WebSocket connection is lost or fails to establish.\n\t *\n\t * It implements intelligent delay calculation based on:\n\t * - Previous attempt timing\n\t * - Current tab visibility (active vs inactive delays)\n\t * - Exponential backoff for repeated failures\n\t *\n\t * @example\n\t * ```ts\n\t * // Called automatically when connection is lost\n\t * // Schedules reconnection with appropriate delay\n\t * manager.disconnected()\n\t * ```\n\t */\n\tdisconnected() {\n\t\tdebug('ReconnectManager.disconnected')\n\t\t// This either means we're freshly disconnected, or the last connection attempt failed;\n\t\t// either way, time to try again.\n\n\t\t// Guard against delayed notifications and recheck synchronously\n\t\tif (\n\t\t\tthis.socketAdapter._ws?.readyState !== WebSocket.OPEN &&\n\t\t\tthis.socketAdapter._ws?.readyState !== WebSocket.CONNECTING\n\t\t) {\n\t\t\tdebug('ReconnectManager.disconnected: websocket is not OPEN or CONNECTING')\n\t\t\tthis.clearReconnectTimeout()\n\n\t\t\tlet delayLeft\n\t\t\tif (this.state === 'connected') {\n\t\t\t\t// it's the first sign that we got disconnected; the state will be updated below,\n\t\t\t\t// just set the appropriate delay for now\n\t\t\t\tthis.intendedDelay = this.getMinDelay()\n\t\t\t\tdelayLeft = this.intendedDelay\n\t\t\t} else {\n\t\t\t\tdelayLeft =\n\t\t\t\t\tthis.lastAttemptStart !== null\n\t\t\t\t\t\t? this.lastAttemptStart + this.intendedDelay - Date.now()\n\t\t\t\t\t\t: 0\n\t\t\t}\n\n\t\t\tif (delayLeft > 0) {\n\t\t\t\tdebug('ReconnectManager.disconnected: delaying, delayLeft', delayLeft)\n\t\t\t\t// try again later\n\t\t\t\tthis.state = 'delay'\n\n\t\t\t\tthis.reconnectTimeout = setTimeout(() => this.disconnected(), delayLeft)\n\t\t\t} else {\n\t\t\t\t// not connected and not delayed, time to retry\n\t\t\t\tthis.state = 'pendingAttempt'\n\n\t\t\t\tthis.intendedDelay = Math.min(\n\t\t\t\t\tthis.getMaxDelay(),\n\t\t\t\t\tMath.max(this.getMinDelay(), this.intendedDelay) * DELAY_EXPONENT\n\t\t\t\t)\n\t\t\t\tdebug(\n\t\t\t\t\t'ReconnectManager.disconnected: attempting a connection, next delay',\n\t\t\t\t\tthis.intendedDelay\n\t\t\t\t)\n\t\t\t\tthis.scheduleAttempt()\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Handles successful connection events and resets reconnection state.\n\t * This method is called when the WebSocket successfully connects to the server.\n\t *\n\t * It clears any pending reconnection attempts and resets the delay back to minimum\n\t * for future connection attempts.\n\t *\n\t * @example\n\t * ```ts\n\t * // Called automatically when WebSocket opens successfully\n\t * manager.connected()\n\t * ```\n\t */\n\tconnected() {\n\t\tdebug('ReconnectManager.connected')\n\t\t// this notification could've been delayed, recheck synchronously\n\t\tif (this.socketAdapter._ws?.readyState === WebSocket.OPEN) {\n\t\t\tdebug('ReconnectManager.connected: websocket is OPEN')\n\t\t\tthis.state = 'connected'\n\t\t\tthis.clearReconnectTimeout()\n\t\t\tthis.intendedDelay = ACTIVE_MIN_DELAY\n\t\t}\n\t}\n\n\t/**\n\t * Permanently closes the reconnection manager and cleans up all resources.\n\t * This stops all pending reconnection attempts and removes event listeners.\n\t * Once closed, the manager cannot be reused.\n\t */\n\tclose() {\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.isDisposed = true\n\t}\n}\n\nfunction httpToWs(url: string) {\n\treturn url.replace(/^http(s)?:/, 'ws$1:')\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -69,12 +69,17 @@ class TLSyncClient {
69
69
  removed: {}
70
70
  };
71
71
  disposables = [];
72
+ /** @internal */
72
73
  store;
74
+ /** @internal */
73
75
  socket;
76
+ /** @internal */
74
77
  presenceState;
78
+ /** @internal */
75
79
  presenceMode;
76
80
  // isOnline is true when we have an open socket connection and we have
77
81
  // established a connection with the server room (i.e. we have received a 'connect' message)
82
+ /** @internal */
78
83
  isConnectedToRoom = false;
79
84
  /**
80
85
  * The client clock is essentially a counter for push requests Each time a push request is created
@@ -212,6 +217,7 @@ class TLSyncClient {
212
217
  this.sendConnectMessage();
213
218
  }
214
219
  }
220
+ /** @internal */
215
221
  latestConnectRequestId = null;
216
222
  /**
217
223
  * This is the first message that is sent over a newly established socket connection. And we need