@tldraw/sync-core 4.5.3 → 4.6.0-canary.0bcbb3ed5bcb

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.
@@ -39,12 +39,16 @@ class TLSyncRoom {
39
39
  // A table of connected clients
40
40
  sessions = /* @__PURE__ */ new Map();
41
41
  lastDocumentClock = 0;
42
- // eslint-disable-next-line local/prefer-class-methods
43
- pruneSessions = () => {
42
+ pruneTimer = null;
43
+ pruneSessions = (0, import_utils.throttle)(() => {
44
+ if (this.pruneTimer) {
45
+ clearTimeout(this.pruneTimer);
46
+ this.pruneTimer = null;
47
+ }
44
48
  for (const client of this.sessions.values()) {
45
49
  switch (client.state) {
46
50
  case import_RoomSession.RoomSessionState.Connected: {
47
- const hasTimedOut = timeSince(client.lastInteractionTime) > import_RoomSession.SESSION_IDLE_TIMEOUT;
51
+ const hasTimedOut = timeSince(client.lastInteractionTime) > this.sessionIdleTimeout;
48
52
  if (hasTimedOut || !client.socket.isOpen) {
49
53
  this.cancelSession(client.sessionId);
50
54
  }
@@ -54,6 +58,8 @@ class TLSyncRoom {
54
58
  const hasTimedOut = timeSince(client.sessionStartTime) > import_RoomSession.SESSION_START_WAIT_TIME;
55
59
  if (hasTimedOut || !client.socket.isOpen) {
56
60
  this.removeSession(client.sessionId);
61
+ } else {
62
+ this.scheduleFollowUpPrune();
57
63
  }
58
64
  break;
59
65
  }
@@ -61,6 +67,8 @@ class TLSyncRoom {
61
67
  const hasTimedOut = timeSince(client.cancellationTime) > import_RoomSession.SESSION_REMOVAL_WAIT_TIME;
62
68
  if (hasTimedOut) {
63
69
  this.removeSession(client.sessionId);
70
+ } else {
71
+ this.scheduleFollowUpPrune();
64
72
  }
65
73
  break;
66
74
  }
@@ -69,9 +77,13 @@ class TLSyncRoom {
69
77
  }
70
78
  }
71
79
  }
72
- };
80
+ }, 1e3);
81
+ scheduleFollowUpPrune() {
82
+ if (this.pruneTimer) return;
83
+ this.pruneTimer = setTimeout(this.pruneSessions, import_RoomSession.SESSION_REMOVAL_WAIT_TIME + 100);
84
+ }
73
85
  presenceStore = new PresenceStore();
74
- disposables = [(0, import_interval.interval)(this.pruneSessions, 2e3)];
86
+ disposables = [];
75
87
  _isClosed = false;
76
88
  /**
77
89
  * Close the room and clean up all resources. Disconnects all sessions
@@ -100,11 +112,13 @@ class TLSyncRoom {
100
112
  presenceType;
101
113
  log;
102
114
  schema;
115
+ sessionIdleTimeout;
103
116
  constructor(opts) {
104
117
  this.schema = opts.schema;
105
118
  this.log = opts.log;
106
119
  this.onPresenceChange = opts.onPresenceChange;
107
120
  this.storage = opts.storage;
121
+ this.sessionIdleTimeout = opts.clientTimeout ?? import_RoomSession.SESSION_IDLE_TIMEOUT;
108
122
  (0, import_utils.assert)(
109
123
  import_utils.isNativeStructuredClone,
110
124
  "TLSyncRoom is supposed to run either on Cloudflare Workersor on a 18+ version of Node.js, which both support the native structuredClone API"
@@ -133,6 +147,17 @@ class TLSyncRoom {
133
147
  }
134
148
  })
135
149
  );
150
+ this.disposables.push(() => {
151
+ this.pruneSessions.cancel();
152
+ if (this.pruneTimer) {
153
+ clearTimeout(this.pruneTimer);
154
+ this.pruneTimer = null;
155
+ }
156
+ });
157
+ if (Number.isFinite(this.sessionIdleTimeout) && this.sessionIdleTimeout > 0) {
158
+ const pruneIntervalMs = Math.min(2e3, Math.floor(this.sessionIdleTimeout / 4));
159
+ this.disposables.push((0, import_interval.interval)(() => this.pruneSessions(), pruneIntervalMs));
160
+ }
136
161
  }
137
162
  broadcastExternalStorageChanges() {
138
163
  this.storage.transaction((txn) => {
@@ -243,6 +268,7 @@ class TLSyncRoom {
243
268
  session.socket.close();
244
269
  } catch {
245
270
  }
271
+ this.scheduleFollowUpPrune();
246
272
  }
247
273
  internalTxnId = "TLSyncRoom.txn";
248
274
  /**
@@ -342,6 +368,46 @@ class TLSyncRoom {
342
368
  });
343
369
  return this;
344
370
  }
371
+ /**
372
+ * Resume a previously-connected session directly into `Connected` state, bypassing the
373
+ * connect handshake. Used after server hibernation when the WebSocket is still alive but
374
+ * all in-memory state has been lost.
375
+ *
376
+ * @internal
377
+ */
378
+ handleResumedSession(opts) {
379
+ const {
380
+ sessionId,
381
+ socket,
382
+ meta,
383
+ isReadonly,
384
+ serializedSchema,
385
+ presenceId,
386
+ presenceRecord,
387
+ requiresLegacyRejection,
388
+ supportsStringAppend
389
+ } = opts;
390
+ const migrations = this.schema.getMigrationsSince(serializedSchema);
391
+ const requiresDownMigrations = migrations.ok ? migrations.value.length > 0 : false;
392
+ this.sessions.set(sessionId, {
393
+ state: import_RoomSession.RoomSessionState.Connected,
394
+ sessionId,
395
+ socket,
396
+ presenceId: presenceId ?? this.presenceType?.createId() ?? null,
397
+ serializedSchema,
398
+ requiresDownMigrations,
399
+ lastInteractionTime: Date.now(),
400
+ debounceTimer: null,
401
+ outstandingDataMessages: [],
402
+ meta,
403
+ isReadonly,
404
+ requiresLegacyRejection,
405
+ supportsStringAppend
406
+ });
407
+ if (presenceRecord && presenceId) {
408
+ this.presenceStore.set(presenceId, presenceRecord);
409
+ }
410
+ }
345
411
  /**
346
412
  * Checks if all connected sessions support string append operations (protocol version 8+).
347
413
  * If any client is on an older version, returns false to enable legacy append mode.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/TLSyncRoom.ts"],
4
- "sourcesContent": ["import {\n\tAtomMap,\n\tMigrationFailureReason,\n\tRecordType,\n\tSerializedSchema,\n\tStoreSchema,\n\tUnknownRecord,\n} from '@tldraw/store'\nimport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tgetOwnProperty,\n\tisEqual,\n\tisNativeStructuredClone,\n\tobjectMapEntriesIterable,\n\tResult,\n} from '@tldraw/utils'\nimport { createNanoEvents } from 'nanoevents'\nimport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tNetworkDiff,\n\tObjectDiff,\n\tRecordOp,\n\tRecordOpType,\n\tValueOpType,\n} from './diff'\nimport { interval } from './interval'\nimport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\tTLSocketClientSentEvent,\n\tTLSocketServerSentDataEvent,\n\tTLSocketServerSentEvent,\n} from './protocol'\nimport { applyAndDiffRecord, diffAndValidateRecord, validateRecord } from './recordDiff'\nimport {\n\tRoomSession,\n\tRoomSessionState,\n\tSESSION_IDLE_TIMEOUT,\n\tSESSION_REMOVAL_WAIT_TIME,\n\tSESSION_START_WAIT_TIME,\n} from './RoomSession'\nimport { TLSyncLog } from './TLSocketRoom'\nimport { TLSyncError, TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport {\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n\tTLSyncStorageTransaction,\n\ttoNetworkDiff,\n} from './TLSyncStorage'\n\n/**\n * WebSocket interface for server-side room connections. This defines the contract\n * that socket implementations must follow to work with TLSyncRoom.\n *\n * @internal\n */\nexport interface TLRoomSocket<R extends UnknownRecord> {\n\t/**\n\t * Whether the socket connection is currently open and ready to send messages.\n\t */\n\tisOpen: boolean\n\t/**\n\t * Send a message to the connected client through this socket.\n\t *\n\t * @param msg - The server-sent event message to transmit\n\t */\n\tsendMessage(msg: TLSocketServerSentEvent<R>): void\n\t/**\n\t * Close the socket connection with optional status code and reason.\n\t *\n\t * @param code - WebSocket close code (optional)\n\t * @param reason - Human-readable close reason (optional)\n\t */\n\tclose(code?: number, reason?: string): void\n}\n\n/**\n * The minimum time interval (in milliseconds) between sending batched data messages\n * to clients. This debouncing prevents overwhelming clients with rapid updates.\n * @public\n */\nexport const DATA_MESSAGE_DEBOUNCE_INTERVAL = 1000 / 60\n\nconst timeSince = (time: number) => Date.now() - time\n\n/**\n * Snapshot of a room's complete state that can be persisted and restored.\n * Contains all documents, tombstones, and metadata needed to reconstruct the room.\n *\n * @public\n */\nexport interface RoomSnapshot {\n\t/**\n\t * The current logical clock value for the room\n\t */\n\tclock?: number\n\t/**\n\t * Clock value when document data was last changed (optional for backwards compatibility)\n\t */\n\tdocumentClock?: number\n\t/**\n\t * Array of all document records with their last modification clocks\n\t */\n\tdocuments: Array<{ state: UnknownRecord; lastChangedClock: number }>\n\t/**\n\t * Map of deleted record IDs to their deletion clock values (optional)\n\t */\n\ttombstones?: Record<string, number>\n\t/**\n\t * Clock value where tombstone history begins - older deletions are not tracked (optional)\n\t */\n\ttombstoneHistoryStartsAtClock?: number\n\t/**\n\t * Serialized schema used when creating this snapshot (optional)\n\t */\n\tschema?: SerializedSchema\n}\n\n/**\n * A collaborative workspace that manages multiple client sessions and synchronizes\n * document changes between them. The room serves as the authoritative source for\n * all document state and handles conflict resolution, schema migrations, and\n * real-time data distribution.\n *\n * @example\n * ```ts\n * const room = new TLSyncRoom({\n * schema: mySchema,\n * onDataChange: () => saveToDatabase(room.getSnapshot()),\n * onPresenceChange: () => updateLiveCursors()\n * })\n *\n * // Handle new client connections\n * room.handleNewSession({\n * sessionId: 'user-123',\n * socket: webSocketAdapter,\n * meta: { userId: '123', name: 'Alice' },\n * isReadonly: false\n * })\n * ```\n *\n * @internal\n */\nexport class TLSyncRoom<R extends UnknownRecord, SessionMeta> {\n\t// A table of connected clients\n\treadonly sessions = new Map<string, RoomSession<R, SessionMeta>>()\n\n\tprivate lastDocumentClock = 0\n\n\t// eslint-disable-next-line local/prefer-class-methods\n\tpruneSessions = () => {\n\t\tfor (const client of this.sessions.values()) {\n\t\t\tswitch (client.state) {\n\t\t\t\tcase RoomSessionState.Connected: {\n\t\t\t\t\tconst hasTimedOut = timeSince(client.lastInteractionTime) > SESSION_IDLE_TIMEOUT\n\t\t\t\t\tif (hasTimedOut || !client.socket.isOpen) {\n\t\t\t\t\t\tthis.cancelSession(client.sessionId)\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase RoomSessionState.AwaitingConnectMessage: {\n\t\t\t\t\tconst hasTimedOut = timeSince(client.sessionStartTime) > SESSION_START_WAIT_TIME\n\t\t\t\t\tif (hasTimedOut || !client.socket.isOpen) {\n\t\t\t\t\t\t// remove immediately\n\t\t\t\t\t\tthis.removeSession(client.sessionId)\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase RoomSessionState.AwaitingRemoval: {\n\t\t\t\t\tconst hasTimedOut = timeSince(client.cancellationTime) > SESSION_REMOVAL_WAIT_TIME\n\t\t\t\t\tif (hasTimedOut) {\n\t\t\t\t\t\tthis.removeSession(client.sessionId)\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\texhaustiveSwitchError(client)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treadonly presenceStore = new PresenceStore<R>()\n\n\tprivate disposables: Array<() => void> = [interval(this.pruneSessions, 2000)]\n\n\tprivate _isClosed = false\n\n\t/**\n\t * Close the room and clean up all resources. Disconnects all sessions\n\t * and stops background processes.\n\t */\n\tclose() {\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.sessions.forEach((session) => {\n\t\t\tsession.socket.close()\n\t\t})\n\t\tthis._isClosed = true\n\t}\n\n\t/**\n\t * Check if the room has been closed and is no longer accepting connections.\n\t *\n\t * @returns True if the room is closed\n\t */\n\tisClosed() {\n\t\treturn this._isClosed\n\t}\n\n\treadonly events = createNanoEvents<{\n\t\troom_became_empty(): void\n\t\tsession_removed(args: { sessionId: string; meta: SessionMeta }): void\n\t}>()\n\n\t// Storage layer for documents, tombstones, and clocks\n\tprivate readonly storage: TLSyncStorage<R>\n\n\treadonly serializedSchema: SerializedSchema\n\n\treadonly documentTypes: Set<string>\n\treadonly presenceType: RecordType<R, any> | null\n\tprivate log?: TLSyncLog\n\tpublic readonly schema: StoreSchema<R, any>\n\tprivate onPresenceChange?(): void\n\n\tconstructor(opts: {\n\t\tlog?: TLSyncLog\n\t\tschema: StoreSchema<R, any>\n\t\tonPresenceChange?(): void\n\t\tstorage: TLSyncStorage<R>\n\t}) {\n\t\tthis.schema = opts.schema\n\t\tthis.log = opts.log\n\t\tthis.onPresenceChange = opts.onPresenceChange\n\t\tthis.storage = opts.storage\n\n\t\tassert(\n\t\t\tisNativeStructuredClone,\n\t\t\t'TLSyncRoom is supposed to run either on Cloudflare Workers' +\n\t\t\t\t'or on a 18+ version of Node.js, which both support the native structuredClone API'\n\t\t)\n\n\t\t// do a json serialization cycle to make sure the schema has no 'undefined' values\n\t\tthis.serializedSchema = JSON.parse(JSON.stringify(this.schema.serialize()))\n\n\t\tthis.documentTypes = new Set(\n\t\t\tObject.values<RecordType<R, any>>(this.schema.types)\n\t\t\t\t.filter((t) => t.scope === 'document')\n\t\t\t\t.map((t) => t.typeName)\n\t\t)\n\n\t\tconst presenceTypes = new Set(\n\t\t\tObject.values<RecordType<R, any>>(this.schema.types).filter((t) => t.scope === 'presence')\n\t\t)\n\n\t\tif (presenceTypes.size > 1) {\n\t\t\tthrow new Error(\n\t\t\t\t`TLSyncRoom: exactly zero or one presence type is expected, but found ${presenceTypes.size}`\n\t\t\t)\n\t\t}\n\n\t\tthis.presenceType = presenceTypes.values().next()?.value ?? null\n\n\t\tconst { documentClock } = this.storage.transaction((txn) => {\n\t\t\tthis.schema.migrateStorage(txn)\n\t\t})\n\n\t\tthis.lastDocumentClock = documentClock\n\n\t\tthis.disposables.push(\n\t\t\tthis.storage.onChange(({ id }) => {\n\t\t\t\tif (id !== this.internalTxnId) {\n\t\t\t\t\tthis.broadcastExternalStorageChanges()\n\t\t\t\t}\n\t\t\t})\n\t\t)\n\t}\n\tprivate broadcastExternalStorageChanges() {\n\t\tthis.storage.transaction((txn) => {\n\t\t\tthis.broadcastChanges(txn)\n\t\t\tthis.lastDocumentClock = txn.getClock()\n\t\t}) // no id needed because this only reads, no writes.\n\t}\n\n\t/**\n\t * Send a message to a particular client. Debounces data events\n\t *\n\t * @param sessionId - The id of the session to send the message to.\n\t * @param message - The message to send. UNSAFE Any diffs must have been downgraded already if necessary\n\t */\n\tprivate _unsafe_sendMessage(\n\t\tsessionId: string,\n\t\tmessage: TLSocketServerSentEvent<R> | TLSocketServerSentDataEvent<R>\n\t) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) {\n\t\t\tthis.log?.warn?.('Tried to send message to unknown session', message.type)\n\t\t\treturn\n\t\t}\n\t\tif (session.state !== RoomSessionState.Connected) {\n\t\t\tthis.log?.warn?.('Tried to send message to disconnected client', message.type)\n\t\t\treturn\n\t\t}\n\t\tif (session.socket.isOpen) {\n\t\t\tif (message.type !== 'patch' && message.type !== 'push_result') {\n\t\t\t\t// this is not a data message\n\t\t\t\tif (message.type !== 'pong') {\n\t\t\t\t\t// non-data messages like \"connect\" might still need to be ordered correctly with\n\t\t\t\t\t// respect to data messages, so it's better to flush just in case\n\t\t\t\t\tthis._flushDataMessages(sessionId)\n\t\t\t\t}\n\t\t\t\tsession.socket.sendMessage(message)\n\t\t\t} else {\n\t\t\t\tif (session.debounceTimer === null) {\n\t\t\t\t\t// this is the first message since the last flush, don't delay it\n\t\t\t\t\tsession.socket.sendMessage({ type: 'data', data: [message] })\n\n\t\t\t\t\tsession.debounceTimer = setTimeout(\n\t\t\t\t\t\t() => this._flushDataMessages(sessionId),\n\t\t\t\t\t\tDATA_MESSAGE_DEBOUNCE_INTERVAL\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tsession.outstandingDataMessages.push(message)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.cancelSession(session.sessionId)\n\t\t}\n\t}\n\n\t// needs to accept sessionId and not a session because the session might be dead by the time\n\t// the timer fires\n\t_flushDataMessages(sessionId: string) {\n\t\tconst session = this.sessions.get(sessionId)\n\n\t\tif (!session || session.state !== RoomSessionState.Connected) {\n\t\t\treturn\n\t\t}\n\n\t\tsession.debounceTimer = null\n\n\t\tif (session.outstandingDataMessages.length > 0) {\n\t\t\tsession.socket.sendMessage({ type: 'data', data: session.outstandingDataMessages })\n\t\t\tsession.outstandingDataMessages.length = 0\n\t\t}\n\t}\n\n\t/** @internal */\n\tprivate removeSession(sessionId: string, fatalReason?: string) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) {\n\t\t\tthis.log?.warn?.('Tried to remove unknown session')\n\t\t\treturn\n\t\t}\n\n\t\tthis.sessions.delete(sessionId)\n\n\t\ttry {\n\t\t\tif (fatalReason) {\n\t\t\t\tsession.socket.close(TLSyncErrorCloseEventCode, fatalReason)\n\t\t\t} else {\n\t\t\t\tsession.socket.close()\n\t\t\t}\n\t\t} catch {\n\t\t\t// noop, calling .close() multiple times is fine\n\t\t}\n\n\t\tconst presence = this.presenceStore.get(session.presenceId ?? '')\n\t\tif (presence) {\n\t\t\tthis.presenceStore.delete(session.presenceId!)\n\t\t\t// Broadcast presence removal - use RecordsDiff with the removed record\n\t\t\tthis.broadcastPatch({\n\t\t\t\tputs: {},\n\t\t\t\tdeletes: [session.presenceId!],\n\t\t\t})\n\t\t}\n\n\t\tthis.events.emit('session_removed', { sessionId, meta: session.meta })\n\t\tif (this.sessions.size === 0) {\n\t\t\tthis.events.emit('room_became_empty')\n\t\t}\n\t}\n\n\tprivate cancelSession(sessionId: string) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) {\n\t\t\treturn\n\t\t}\n\n\t\tif (session.state === RoomSessionState.AwaitingRemoval) {\n\t\t\tthis.log?.warn?.('Tried to cancel session that is already awaiting removal')\n\t\t\treturn\n\t\t}\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tstate: RoomSessionState.AwaitingRemoval,\n\t\t\tsessionId,\n\t\t\tpresenceId: session.presenceId,\n\t\t\tsocket: session.socket,\n\t\t\tcancellationTime: Date.now(),\n\t\t\tmeta: session.meta,\n\t\t\tisReadonly: session.isReadonly,\n\t\t\trequiresLegacyRejection: session.requiresLegacyRejection,\n\t\t\tsupportsStringAppend: session.supportsStringAppend,\n\t\t})\n\n\t\ttry {\n\t\t\tsession.socket.close()\n\t\t} catch {\n\t\t\t// noop, calling .close() multiple times is fine\n\t\t}\n\t}\n\n\treadonly internalTxnId = 'TLSyncRoom.txn'\n\n\t/**\n\t * Broadcast a patch to all connected clients except the one with the sessionId provided.\n\t *\n\t * @param diff - The TLSyncForwardDiff with full records (used for migration)\n\t * @param networkDiff - Optional pre-computed NetworkDiff for sessions not needing migration.\n\t * If not provided, will be computed from recordsDiff.\n\t * @param sourceSessionId - Optional session ID to exclude from the broadcast\n\t */\n\tprivate broadcastPatch(\n\t\tdiff: TLSyncForwardDiff<R>,\n\t\tnetworkDiff?: NetworkDiff<R> | null,\n\t\tsourceSessionId?: string\n\t) {\n\t\t// Pre-compute network diff if not provided\n\t\tconst unmigrated = networkDiff ?? toNetworkDiff(diff)\n\t\tif (!unmigrated) return this\n\n\t\tthis.sessions.forEach((session) => {\n\t\t\tif (session.state !== RoomSessionState.Connected) return\n\t\t\tif (sourceSessionId === session.sessionId) return\n\t\t\tif (!session.socket.isOpen) {\n\t\t\t\tthis.cancelSession(session.sessionId)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst diffResult = this.migrateDiffOrRejectSession(\n\t\t\t\tsession.sessionId,\n\t\t\t\tsession.serializedSchema,\n\t\t\t\tsession.requiresDownMigrations,\n\t\t\t\tdiff\n\t\t\t)\n\t\t\tif (!diffResult.ok) return\n\n\t\t\tthis._unsafe_sendMessage(session.sessionId, {\n\t\t\t\ttype: 'patch',\n\t\t\t\tdiff: diffResult.value,\n\t\t\t\tserverClock: this.lastDocumentClock,\n\t\t\t})\n\t\t})\n\t\treturn this\n\t}\n\n\t/**\n\t * Send a custom message to a connected client. Useful for application-specific\n\t * communication that doesn't involve document synchronization.\n\t *\n\t * @param sessionId - The ID of the session to send the message to\n\t * @param data - The custom payload to send (will be JSON serialized)\n\t * @example\n\t * ```ts\n\t * // Send a custom notification\n\t * room.sendCustomMessage('user-123', {\n\t * type: 'notification',\n\t * message: 'Document saved successfully'\n\t * })\n\t *\n\t * // Send user-specific data\n\t * room.sendCustomMessage('user-456', {\n\t * type: 'user_permissions',\n\t * canEdit: true,\n\t * canDelete: false\n\t * })\n\t * ```\n\t */\n\tsendCustomMessage(sessionId: string, data: any): void {\n\t\tthis._unsafe_sendMessage(sessionId, { type: 'custom', data })\n\t}\n\n\t/**\n\t * Register a new client session with the room. The session will be in an awaiting\n\t * state until it sends a connect message with protocol handshake.\n\t *\n\t * @param opts - Session configuration\n\t * - sessionId - Unique identifier for this session\n\t * - socket - WebSocket adapter for communication\n\t * - meta - Application-specific metadata for this session\n\t * - isReadonly - Whether this session can modify documents\n\t * @returns This room instance for method chaining\n\t * @example\n\t * ```ts\n\t * room.handleNewSession({\n\t * sessionId: crypto.randomUUID(),\n\t * socket: new WebSocketAdapter(ws),\n\t * meta: { userId: '123', name: 'Alice', avatar: 'url' },\n\t * isReadonly: !hasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @internal\n\t */\n\thandleNewSession(opts: {\n\t\tsessionId: string\n\t\tsocket: TLRoomSocket<R>\n\t\tmeta: SessionMeta\n\t\tisReadonly: boolean\n\t}) {\n\t\tconst { sessionId, socket, meta, isReadonly } = opts\n\t\tconst existing = this.sessions.get(sessionId)\n\t\tthis.sessions.set(sessionId, {\n\t\t\tstate: RoomSessionState.AwaitingConnectMessage,\n\t\t\tsessionId,\n\t\t\tsocket,\n\t\t\tpresenceId: existing?.presenceId ?? this.presenceType?.createId() ?? null,\n\t\t\tsessionStartTime: Date.now(),\n\t\t\tmeta,\n\t\t\tisReadonly: isReadonly ?? false,\n\t\t\t// this gets set later during handleConnectMessage\n\t\t\trequiresLegacyRejection: false,\n\t\t\tsupportsStringAppend: true,\n\t\t})\n\t\treturn this\n\t}\n\n\t/**\n\t * Checks if all connected sessions support string append operations (protocol version 8+).\n\t * If any client is on an older version, returns false to enable legacy append mode.\n\t *\n\t * @returns True if all connected sessions are on protocol version 8 or higher\n\t */\n\tgetCanEmitStringAppend(): boolean {\n\t\tfor (const session of this.sessions.values()) {\n\t\t\tif (session.state === RoomSessionState.Connected) {\n\t\t\t\tif (!session.supportsStringAppend) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t/**\n\t * When we send a diff to a client, if that client is on a lower version than us, we need to make\n\t * the diff compatible with their version. This method takes a TLSyncForwardDiff (which has full\n\t * records) and migrates all records down to the client's schema version, returning a NetworkDiff.\n\t *\n\t * For updates (entries with [before, after] tuples), both records are migrated and a patch is\n\t * computed from the migrated versions, preserving efficient patch semantics even across versions.\n\t *\n\t * If a migration fails, the session will be rejected.\n\t *\n\t * @param sessionId - The session ID (for rejection on migration failure)\n\t * @param serializedSchema - The client's schema to migrate to\n\t * @param requiresDownMigrations - Whether the client needs down migrations\n\t * @param diff - The TLSyncForwardDiff containing full records to migrate\n\t * @param unmigrated - Optional pre-computed NetworkDiff for when no migration is needed\n\t * @returns A NetworkDiff with migrated records, or a migration failure\n\t */\n\tprivate migrateDiffOrRejectSession(\n\t\tsessionId: string,\n\t\tserializedSchema: SerializedSchema,\n\t\trequiresDownMigrations: boolean,\n\t\tdiff: TLSyncForwardDiff<R>,\n\t\tunmigrated?: NetworkDiff<R>\n\t): Result<NetworkDiff<R>, MigrationFailureReason> {\n\t\tif (!requiresDownMigrations) {\n\t\t\treturn Result.ok(unmigrated ?? toNetworkDiff(diff) ?? {})\n\t\t}\n\n\t\tconst result: NetworkDiff<R> = {}\n\n\t\t// Migrate puts (either adds or updates)\n\t\tfor (const [id, put] of objectMapEntriesIterable(diff.puts)) {\n\t\t\tif (Array.isArray(put)) {\n\t\t\t\t// Update: [before, after] tuple - migrate both and compute patch\n\t\t\t\tconst [from, to] = put\n\t\t\t\tconst fromResult = this.schema.migratePersistedRecord(from, serializedSchema, 'down')\n\t\t\t\tif (fromResult.type === 'error') {\n\t\t\t\t\tthis.rejectSession(sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t\t\treturn Result.err(fromResult.reason)\n\t\t\t\t}\n\t\t\t\tconst toResult = this.schema.migratePersistedRecord(to, serializedSchema, 'down')\n\t\t\t\tif (toResult.type === 'error') {\n\t\t\t\t\tthis.rejectSession(sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t\t\treturn Result.err(toResult.reason)\n\t\t\t\t}\n\t\t\t\tconst patch = diffRecord(fromResult.value, toResult.value)\n\t\t\t\tif (patch) {\n\t\t\t\t\tresult[id] = [RecordOpType.Patch, patch]\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Add: single record - migrate and put\n\t\t\t\tconst migrationResult = this.schema.migratePersistedRecord(put, serializedSchema, 'down')\n\t\t\t\tif (migrationResult.type === 'error') {\n\t\t\t\t\tthis.rejectSession(sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t\t\treturn Result.err(migrationResult.reason)\n\t\t\t\t}\n\t\t\t\tresult[id] = [RecordOpType.Put, migrationResult.value]\n\t\t\t}\n\t\t}\n\n\t\t// Deletes don't need migration\n\t\tfor (const id of diff.deletes) {\n\t\t\tresult[id] = [RecordOpType.Remove]\n\t\t}\n\n\t\treturn Result.ok(result)\n\t}\n\n\t/**\n\t * Process an incoming message from a client session. Handles connection requests,\n\t * data synchronization pushes, and ping/pong for connection health.\n\t *\n\t * @param sessionId - The ID of the session that sent the message\n\t * @param message - The client message to process\n\t * @example\n\t * ```ts\n\t * // Typically called by WebSocket message handlers\n\t * websocket.onMessage((data) => {\n\t * const message = JSON.parse(data)\n\t * room.handleMessage(sessionId, message)\n\t * })\n\t * ```\n\t */\n\tasync handleMessage(sessionId: string, message: TLSocketClientSentEvent<R>) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) {\n\t\t\tthis.log?.warn?.('Received message from unknown session')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tswitch (message.type) {\n\t\t\t\tcase 'connect': {\n\t\t\t\t\treturn this.handleConnectRequest(session, message)\n\t\t\t\t}\n\t\t\t\tcase 'push': {\n\t\t\t\t\treturn this.handlePushRequest(session, message)\n\t\t\t\t}\n\t\t\t\tcase 'ping': {\n\t\t\t\t\tif (session.state === RoomSessionState.Connected) {\n\t\t\t\t\t\tsession.lastInteractionTime = Date.now()\n\t\t\t\t\t}\n\t\t\t\t\treturn this._unsafe_sendMessage(session.sessionId, { type: 'pong' })\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\texhaustiveSwitchError(message)\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tif (e instanceof TLSyncError) {\n\t\t\t\tthis.rejectSession(session.sessionId, e.reason)\n\t\t\t} else {\n\t\t\t\t// log error and reboot the room?\n\t\t\t\tthrow e\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Reject and disconnect a session due to incompatibility or other fatal errors.\n\t * Sends appropriate error messages before closing the connection.\n\t *\n\t * @param sessionId - The session to reject\n\t * @param fatalReason - The reason for rejection (optional)\n\t * @example\n\t * ```ts\n\t * // Reject due to version mismatch\n\t * room.rejectSession('user-123', TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t *\n\t * // Reject due to permission issue\n\t * room.rejectSession('user-456', 'Insufficient permissions')\n\t * ```\n\t */\n\trejectSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) return\n\t\tif (!fatalReason) {\n\t\t\tthis.removeSession(sessionId)\n\t\t\treturn\n\t\t}\n\t\tif (session.requiresLegacyRejection) {\n\t\t\ttry {\n\t\t\t\tif (session.socket.isOpen) {\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\tlet legacyReason: TLIncompatibilityReason\n\t\t\t\t\tswitch (fatalReason) {\n\t\t\t\t\t\tcase TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\t\tlegacyReason = TLIncompatibilityReason.ClientTooOld\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tcase TLSyncErrorCloseEventReason.SERVER_TOO_OLD:\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\t\tlegacyReason = TLIncompatibilityReason.ServerTooOld\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tcase TLSyncErrorCloseEventReason.INVALID_RECORD:\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\t\tlegacyReason = TLIncompatibilityReason.InvalidRecord\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\t\tlegacyReason = TLIncompatibilityReason.InvalidOperation\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tsession.socket.sendMessage({\n\t\t\t\t\t\ttype: 'incompatibility_error',\n\t\t\t\t\t\treason: legacyReason,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// noop\n\t\t\t} finally {\n\t\t\t\tthis.removeSession(sessionId)\n\t\t\t}\n\t\t} else {\n\t\t\tthis.removeSession(sessionId, fatalReason)\n\t\t}\n\t}\n\n\tprivate forceAllReconnect() {\n\t\tfor (const session of this.sessions.values()) {\n\t\t\tthis.removeSession(session.sessionId)\n\t\t}\n\t}\n\n\tprivate broadcastChanges(txn: TLSyncStorageTransaction<R>) {\n\t\tconst changes = txn.getChangesSince(this.lastDocumentClock)\n\t\tif (!changes) return\n\t\tconst { wipeAll, diff } = changes\n\t\tthis.lastDocumentClock = txn.getClock()\n\t\tif (wipeAll) {\n\t\t\t// If this happens it means we'd need to broadcast a wipe_all message to all clients,\n\t\t\t// which is not part of the protocol yet, so we need to force all clients to reconnect instead.\n\t\t\tthis.forceAllReconnect()\n\t\t\treturn\n\t\t}\n\t\tthis.broadcastPatch(diff)\n\t}\n\n\tprivate handleConnectRequest(\n\t\tsession: RoomSession<R, SessionMeta>,\n\t\tmessage: Extract<TLSocketClientSentEvent<R>, { type: 'connect' }>\n\t) {\n\t\t// if the protocol versions don't match, disconnect the client\n\t\t// we will eventually want to try to make our protocol backwards compatible to some degree\n\t\t// and have a MIN_PROTOCOL_VERSION constant that the TLSyncRoom implements support for\n\t\tlet theirProtocolVersion = message.protocolVersion\n\t\t// 5 is the same as 6\n\t\tif (theirProtocolVersion === 5) {\n\t\t\ttheirProtocolVersion = 6\n\t\t}\n\t\t// 6 is almost the same as 7\n\t\tsession.requiresLegacyRejection = theirProtocolVersion === 6\n\t\tif (theirProtocolVersion === 6) {\n\t\t\ttheirProtocolVersion++\n\t\t}\n\t\tif (theirProtocolVersion === 7) {\n\t\t\ttheirProtocolVersion++\n\t\t\tsession.supportsStringAppend = false\n\t\t}\n\n\t\tif (theirProtocolVersion == null || theirProtocolVersion < getTlsyncProtocolVersion()) {\n\t\t\tthis.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\treturn\n\t\t} else if (theirProtocolVersion > getTlsyncProtocolVersion()) {\n\t\t\tthis.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.SERVER_TOO_OLD)\n\t\t\treturn\n\t\t}\n\t\t// If the client's store is at a different version to ours, it could cause corruption.\n\t\t// We should disconnect the client and ask them to refresh.\n\t\tif (message.schema == null) {\n\t\t\tthis.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\treturn\n\t\t}\n\t\tconst migrations = this.schema.getMigrationsSince(message.schema)\n\t\t// if the client's store is at a different version to ours, we can't support them\n\t\tif (!migrations.ok || migrations.value.some((m) => m.scope !== 'record' || !m.down)) {\n\t\t\tthis.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\treturn\n\t\t}\n\n\t\tconst sessionSchema = isEqual(message.schema, this.serializedSchema)\n\t\t\t? this.serializedSchema\n\t\t\t: message.schema\n\n\t\tconst requiresDownMigrations = migrations.value.length > 0\n\n\t\tconst connect = async (msg: Extract<TLSocketServerSentEvent<R>, { type: 'connect' }>) => {\n\t\t\tthis.sessions.set(session.sessionId, {\n\t\t\t\tstate: RoomSessionState.Connected,\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tpresenceId: session.presenceId,\n\t\t\t\tsocket: session.socket,\n\t\t\t\tserializedSchema: sessionSchema,\n\t\t\t\trequiresDownMigrations,\n\t\t\t\tlastInteractionTime: Date.now(),\n\t\t\t\tdebounceTimer: null,\n\t\t\t\toutstandingDataMessages: [],\n\t\t\t\tsupportsStringAppend: session.supportsStringAppend,\n\t\t\t\tmeta: session.meta,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\trequiresLegacyRejection: session.requiresLegacyRejection,\n\t\t\t})\n\t\t\tthis._unsafe_sendMessage(session.sessionId, msg)\n\t\t}\n\n\t\tconst { documentClock, result } = this.storage.transaction((txn) => {\n\t\t\tthis.broadcastChanges(txn)\n\t\t\tconst docChanges = txn.getChangesSince(message.lastServerClock)\n\t\t\tconst presenceDiff = this.migrateDiffOrRejectSession(\n\t\t\t\tsession.sessionId,\n\t\t\t\tsessionSchema,\n\t\t\t\trequiresDownMigrations,\n\t\t\t\t{\n\t\t\t\t\tputs: Object.fromEntries([...this.presenceStore.values()].map((p) => [p.id, p])),\n\t\t\t\t\tdeletes: [],\n\t\t\t\t}\n\t\t\t)\n\t\t\tif (!presenceDiff.ok) return null\n\n\t\t\t// Migrate the diff if needed, or use the pre-computed network diff\n\t\t\tlet docDiff: NetworkDiff<R> | null = null\n\t\t\tif (docChanges && sessionSchema !== this.serializedSchema) {\n\t\t\t\tconst migrated = this.migrateDiffOrRejectSession(\n\t\t\t\t\tsession.sessionId,\n\t\t\t\t\tsessionSchema,\n\t\t\t\t\trequiresDownMigrations,\n\t\t\t\t\tdocChanges.diff\n\t\t\t\t)\n\t\t\t\tif (!migrated.ok) return null\n\t\t\t\tdocDiff = migrated.value\n\t\t\t} else if (docChanges) {\n\t\t\t\tdocDiff = toNetworkDiff(docChanges.diff)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: 'connect',\n\t\t\t\tconnectRequestId: message.connectRequestId,\n\t\t\t\thydrationType: docChanges?.wipeAll ? 'wipe_all' : 'wipe_presence',\n\t\t\t\tprotocolVersion: getTlsyncProtocolVersion(),\n\t\t\t\tschema: this.schema.serialize(),\n\t\t\t\tserverClock: txn.getClock(),\n\t\t\t\tdiff: { ...presenceDiff.value, ...docDiff },\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t} satisfies Extract<TLSocketServerSentEvent<R>, { type: 'connect' }>\n\t\t}) // no id needed because this only reads, no writes.\n\n\t\tthis.lastDocumentClock = documentClock\n\n\t\tif (result) {\n\t\t\tconnect(result)\n\t\t}\n\t}\n\n\tprivate handlePushRequest(\n\t\tsession: RoomSession<R, SessionMeta> | null,\n\t\tmessage: Extract<TLSocketClientSentEvent<R>, { type: 'push' }>\n\t) {\n\t\t// We must be connected to handle push requests\n\t\tif (session && session.state !== RoomSessionState.Connected) {\n\t\t\treturn\n\t\t}\n\t\t// update the last interaction time\n\t\tif (session) {\n\t\t\tsession.lastInteractionTime = Date.now()\n\t\t}\n\n\t\tconst legacyAppendMode = !this.getCanEmitStringAppend()\n\n\t\tinterface ActualChanges {\n\t\t\tdiffs: {\n\t\t\t\tnetworkDiff: NetworkDiff<R>\n\t\t\t\tdiff: TLSyncForwardDiff<R>\n\t\t\t} | null\n\t\t}\n\n\t\tconst propagateOp = (\n\t\t\tchanges: ActualChanges,\n\t\t\tid: string,\n\t\t\top: RecordOp<R>,\n\t\t\tbefore: R | undefined,\n\t\t\tafter: R | undefined\n\t\t) => {\n\t\t\tif (!changes.diffs) changes.diffs = { networkDiff: {}, diff: { puts: {}, deletes: [] } }\n\t\t\tchanges.diffs.networkDiff[id] = op\n\t\t\tswitch (op[0]) {\n\t\t\t\tcase RecordOpType.Put:\n\t\t\t\t\tchanges.diffs.diff.puts[id] = op[1]\n\t\t\t\t\tbreak\n\t\t\t\tcase RecordOpType.Patch:\n\t\t\t\t\tassert(before && after, 'before and after are required for patches')\n\t\t\t\t\tchanges.diffs.diff.puts[id] = [before, after]\n\t\t\t\t\tbreak\n\t\t\t\tcase RecordOpType.Remove:\n\t\t\t\t\tchanges.diffs.diff.deletes.push(id)\n\t\t\t\t\tbreak\n\t\t\t\tdefault:\n\t\t\t\t\texhaustiveSwitchError(op[0])\n\t\t\t}\n\t\t}\n\n\t\tconst addDocument = (\n\t\t\tstorage: MinimalDocStore<R>,\n\t\t\tchanges: ActualChanges,\n\t\t\tid: string,\n\t\t\t_state: R\n\t\t): Result<void, void> => {\n\t\t\tconst res = session\n\t\t\t\t? this.schema.migratePersistedRecord(_state, session.serializedSchema, 'up')\n\t\t\t\t: { type: 'success' as const, value: _state }\n\t\t\tif (res.type === 'error') {\n\t\t\t\tthrow new TLSyncError(res.reason, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t}\n\t\t\tconst { value: state } = res\n\n\t\t\t// Get the existing document, if any\n\t\t\tconst doc = storage.get(id) as R | undefined\n\n\t\t\tif (doc) {\n\t\t\t\t// If there's an existing document, replace it with the new state\n\t\t\t\t// but propagate a diff rather than the entire value\n\t\t\t\tconst recordType = assertExists(getOwnProperty(this.schema.types, doc.typeName))\n\t\t\t\tconst diff = diffAndValidateRecord(doc, state, recordType)\n\t\t\t\tif (diff) {\n\t\t\t\t\tstorage.set(id, state)\n\t\t\t\t\tpropagateOp(changes, id, [RecordOpType.Patch, diff], doc, state)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Otherwise, if we don't already have a document with this id\n\t\t\t\t// create the document and propagate the put op\n\t\t\t\t// set automatically clears tombstones if they exist\n\t\t\t\tconst recordType = assertExists(getOwnProperty(this.schema.types, state.typeName))\n\t\t\t\tvalidateRecord(state, recordType)\n\t\t\t\tstorage.set(id, state)\n\t\t\t\tpropagateOp(changes, id, [RecordOpType.Put, state], undefined, undefined)\n\t\t\t}\n\n\t\t\treturn Result.ok(undefined)\n\t\t}\n\n\t\tconst patchDocument = (\n\t\t\tstorage: MinimalDocStore<R>,\n\t\t\tchanges: ActualChanges,\n\t\t\tid: string,\n\t\t\tpatch: ObjectDiff\n\t\t) => {\n\t\t\t// if it was already deleted, there's no need to apply the patch\n\t\t\tconst doc = storage.get(id) as R | undefined\n\t\t\tif (!doc) return\n\n\t\t\tconst recordType = assertExists(getOwnProperty(this.schema.types, doc.typeName))\n\t\t\t// If the client's version of the record is older than ours,\n\t\t\t// we apply the patch to the downgraded version of the record\n\t\t\tconst downgraded = session\n\t\t\t\t? this.schema.migratePersistedRecord(doc, session.serializedSchema, 'down')\n\t\t\t\t: { type: 'success' as const, value: doc }\n\t\t\tif (downgraded.type === 'error') {\n\t\t\t\tthrow new TLSyncError(downgraded.reason, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t}\n\n\t\t\tif (downgraded.value === doc) {\n\t\t\t\t// If the versions are compatible, apply the patch and propagate the patch op\n\t\t\t\tconst diff = applyAndDiffRecord(doc, patch, recordType, legacyAppendMode)\n\t\t\t\tif (diff) {\n\t\t\t\t\tstorage.set(id, diff[1])\n\t\t\t\t\tpropagateOp(changes, id, [RecordOpType.Patch, diff[0]], doc, diff[1])\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// need to apply the patch to the downgraded version and then upgrade it\n\n\t\t\t\t// apply the patch to the downgraded version\n\t\t\t\tconst patched = applyObjectDiff(downgraded.value, patch)\n\t\t\t\t// then upgrade the patched version and use that as the new state\n\t\t\t\tconst upgraded = session\n\t\t\t\t\t? this.schema.migratePersistedRecord(patched, session.serializedSchema, 'up')\n\t\t\t\t\t: { type: 'success' as const, value: patched }\n\t\t\t\t// If the client's version is too old, we'll hit an error\n\t\t\t\tif (upgraded.type === 'error') {\n\t\t\t\t\tthrow new TLSyncError(upgraded.reason, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t\t}\n\t\t\t\t// replace the state with the upgraded version and propagate the patch op\n\t\t\t\tconst diff = diffAndValidateRecord(doc, upgraded.value, recordType, legacyAppendMode)\n\t\t\t\tif (diff) {\n\t\t\t\t\tstorage.set(id, upgraded.value)\n\t\t\t\t\tpropagateOp(changes, id, [RecordOpType.Patch, diff], doc, upgraded.value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst { result, documentClock, changes } = this.storage.transaction(\n\t\t\t(txn) => {\n\t\t\t\tthis.broadcastChanges(txn)\n\t\t\t\t// collect actual ops that resulted from the push\n\t\t\t\t// these will be broadcast to other users\n\n\t\t\t\tconst docChanges: ActualChanges = { diffs: null }\n\t\t\t\tconst presenceChanges: ActualChanges = { diffs: null }\n\n\t\t\t\tif (this.presenceType && session?.presenceId && 'presence' in message && message.presence) {\n\t\t\t\t\tif (!session) throw new Error('session is required for presence pushes')\n\t\t\t\t\t// The push request was for the presence scope.\n\t\t\t\t\tconst id = session.presenceId\n\t\t\t\t\tconst [type, val] = message.presence\n\t\t\t\t\tconst { typeName } = this.presenceType\n\t\t\t\t\tswitch (type) {\n\t\t\t\t\t\tcase RecordOpType.Put: {\n\t\t\t\t\t\t\t// Try to put the document. If it fails, stop here.\n\t\t\t\t\t\t\taddDocument(this.presenceStore, presenceChanges, id, {\n\t\t\t\t\t\t\t\t...val,\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\ttypeName,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcase RecordOpType.Patch: {\n\t\t\t\t\t\t\t// Try to patch the document. If it fails, stop here.\n\t\t\t\t\t\t\tpatchDocument(this.presenceStore, presenceChanges, id, {\n\t\t\t\t\t\t\t\t...val,\n\t\t\t\t\t\t\t\tid: [ValueOpType.Put, id],\n\t\t\t\t\t\t\t\ttypeName: [ValueOpType.Put, typeName],\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (message.diff && !session?.isReadonly) {\n\t\t\t\t\t// The push request was for the document scope.\n\t\t\t\t\tfor (const [id, op] of objectMapEntriesIterable(message.diff!)) {\n\t\t\t\t\t\tswitch (op[0]) {\n\t\t\t\t\t\t\tcase RecordOpType.Put: {\n\t\t\t\t\t\t\t\t// Try to add the document.\n\t\t\t\t\t\t\t\t// If we're putting a record with a type that we don't recognize, fail\n\t\t\t\t\t\t\t\tif (!this.documentTypes.has(op[1].typeName)) {\n\t\t\t\t\t\t\t\t\tthrow new TLSyncError(\n\t\t\t\t\t\t\t\t\t\t'invalid record',\n\t\t\t\t\t\t\t\t\t\tTLSyncErrorCloseEventReason.INVALID_RECORD\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\taddDocument(txn, docChanges, id, op[1])\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcase RecordOpType.Patch: {\n\t\t\t\t\t\t\t\t// Try to patch the document. If it fails, stop here.\n\t\t\t\t\t\t\t\tpatchDocument(txn, docChanges, id, op[1])\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcase RecordOpType.Remove: {\n\t\t\t\t\t\t\t\tconst doc = txn.get(id)\n\t\t\t\t\t\t\t\tif (!doc) {\n\t\t\t\t\t\t\t\t\t// If the doc was already deleted, don't do anything, no need to propagate a delete op\n\t\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Delete the document and propagate the delete op\n\t\t\t\t\t\t\t\t// delete automatically creates tombstones\n\t\t\t\t\t\t\t\ttxn.delete(id)\n\t\t\t\t\t\t\t\tpropagateOp(docChanges, id, op, doc, undefined)\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn { docChanges, presenceChanges }\n\t\t\t},\n\t\t\t{ id: this.internalTxnId, emitChanges: 'when-different' }\n\t\t)\n\n\t\tthis.lastDocumentClock = documentClock\n\n\t\tlet pushResult: TLSocketServerSentEvent<R> | undefined\n\t\tif (changes && session) {\n\t\t\t// txn did not apply verbatim so we should broadcast the actual changes\n\t\t\tresult.docChanges.diffs = { networkDiff: toNetworkDiff(changes) ?? {}, diff: changes }\n\t\t}\n\n\t\tif (isEqual(result.docChanges.diffs?.networkDiff, message.diff)) {\n\t\t\tpushResult = {\n\t\t\t\ttype: 'push_result',\n\t\t\t\tclientClock: message.clientClock,\n\t\t\t\tserverClock: documentClock,\n\t\t\t\taction: 'commit',\n\t\t\t}\n\t\t} else if (!result.docChanges.diffs?.networkDiff) {\n\t\t\tpushResult = {\n\t\t\t\ttype: 'push_result',\n\t\t\t\tclientClock: message.clientClock,\n\t\t\t\tserverClock: documentClock,\n\t\t\t\taction: 'discard',\n\t\t\t}\n\t\t} else if (session) {\n\t\t\t// if recordsDiff is null but diff is not, then there are no clients that need down migrations\n\t\t\t// so we can just use the diff directly\n\t\t\tconst diff = this.migrateDiffOrRejectSession(\n\t\t\t\tsession.sessionId,\n\t\t\t\tsession.serializedSchema,\n\t\t\t\tsession.requiresDownMigrations,\n\t\t\t\tresult.docChanges.diffs.diff,\n\t\t\t\tresult.docChanges.diffs.networkDiff\n\t\t\t)\n\t\t\tif (diff.ok) {\n\t\t\t\tpushResult = {\n\t\t\t\t\ttype: 'push_result',\n\t\t\t\t\tclientClock: message.clientClock,\n\t\t\t\t\tserverClock: documentClock,\n\t\t\t\t\taction: { rebaseWithDiff: diff.value },\n\t\t\t\t}\n\t\t\t}\n\t\t\t// if the difff was not ok then the session was rejected and it's ok to continue without a push result\n\t\t}\n\n\t\tif (session && pushResult) {\n\t\t\tthis._unsafe_sendMessage(session.sessionId, pushResult)\n\t\t}\n\t\tif (result.docChanges.diffs || result.presenceChanges.diffs) {\n\t\t\tthis.broadcastPatch(\n\t\t\t\t{\n\t\t\t\t\tputs: {\n\t\t\t\t\t\t...result.docChanges.diffs?.diff.puts,\n\t\t\t\t\t\t...result.presenceChanges.diffs?.diff.puts,\n\t\t\t\t\t},\n\t\t\t\t\tdeletes: [\n\t\t\t\t\t\t...(result.docChanges.diffs?.diff.deletes ?? []),\n\t\t\t\t\t\t...(result.presenceChanges.diffs?.diff.deletes ?? []),\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t...result.docChanges.diffs?.networkDiff,\n\t\t\t\t\t...result.presenceChanges.diffs?.networkDiff,\n\t\t\t\t},\n\t\t\t\tsession?.sessionId\n\t\t\t)\n\t\t}\n\n\t\tif (result.presenceChanges.diffs) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tthis.onPresenceChange?.()\n\t\t\t})\n\t\t}\n\t}\n\n\t/**\n\t * Handle the event when a client disconnects. Cleans up the session and\n\t * removes any presence information.\n\t *\n\t * @param sessionId - The session that disconnected\n\t * @example\n\t * ```ts\n\t * websocket.onClose(() => {\n\t * room.handleClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleClose(sessionId: string) {\n\t\tthis.cancelSession(sessionId)\n\t}\n}\n\n/** @internal */\nexport interface MinimalDocStore<R extends UnknownRecord> {\n\tget(id: string): UnknownRecord | undefined\n\tset(id: string, record: R): void\n\tdelete(id: string): void\n}\n\n/** @internal */\nexport class PresenceStore<R extends UnknownRecord> implements MinimalDocStore<R> {\n\tprivate readonly presences = new AtomMap<string, R>('presences')\n\n\tget(id: string): UnknownRecord | undefined {\n\t\treturn this.presences.get(id)\n\t}\n\n\tset(id: string, state: R): void {\n\t\tthis.presences.set(id, state)\n\t}\n\n\tdelete(id: string): void {\n\t\tthis.presences.delete(id)\n\t}\n\n\tvalues() {\n\t\treturn this.presences.values()\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import {\n\tAtomMap,\n\tMigrationFailureReason,\n\tRecordType,\n\tSerializedSchema,\n\tStoreSchema,\n\tUnknownRecord,\n} from '@tldraw/store'\nimport {\n\tassert,\n\tassertExists,\n\texhaustiveSwitchError,\n\tgetOwnProperty,\n\tisEqual,\n\tisNativeStructuredClone,\n\tobjectMapEntriesIterable,\n\tResult,\n\tthrottle,\n} from '@tldraw/utils'\nimport { createNanoEvents } from 'nanoevents'\nimport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tNetworkDiff,\n\tObjectDiff,\n\tRecordOp,\n\tRecordOpType,\n\tValueOpType,\n} from './diff'\nimport { interval } from './interval'\nimport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\tTLSocketClientSentEvent,\n\tTLSocketServerSentDataEvent,\n\tTLSocketServerSentEvent,\n} from './protocol'\nimport { applyAndDiffRecord, diffAndValidateRecord, validateRecord } from './recordDiff'\nimport {\n\tRoomSession,\n\tRoomSessionState,\n\tSESSION_IDLE_TIMEOUT,\n\tSESSION_REMOVAL_WAIT_TIME,\n\tSESSION_START_WAIT_TIME,\n} from './RoomSession'\nimport { TLSyncLog } from './TLSocketRoom'\nimport { TLSyncError, TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport {\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n\tTLSyncStorageTransaction,\n\ttoNetworkDiff,\n} from './TLSyncStorage'\n\n/**\n * WebSocket interface for server-side room connections. This defines the contract\n * that socket implementations must follow to work with TLSyncRoom.\n *\n * @internal\n */\nexport interface TLRoomSocket<R extends UnknownRecord> {\n\t/**\n\t * Whether the socket connection is currently open and ready to send messages.\n\t */\n\tisOpen: boolean\n\t/**\n\t * Send a message to the connected client through this socket.\n\t *\n\t * @param msg - The server-sent event message to transmit\n\t */\n\tsendMessage(msg: TLSocketServerSentEvent<R>): void\n\t/**\n\t * Close the socket connection with optional status code and reason.\n\t *\n\t * @param code - WebSocket close code (optional)\n\t * @param reason - Human-readable close reason (optional)\n\t */\n\tclose(code?: number, reason?: string): void\n}\n\n/**\n * The minimum time interval (in milliseconds) between sending batched data messages\n * to clients. This debouncing prevents overwhelming clients with rapid updates.\n * @public\n */\nexport const DATA_MESSAGE_DEBOUNCE_INTERVAL = 1000 / 60\n\nconst timeSince = (time: number) => Date.now() - time\n\n/**\n * Snapshot of a room's complete state that can be persisted and restored.\n * Contains all documents, tombstones, and metadata needed to reconstruct the room.\n *\n * @public\n */\nexport interface RoomSnapshot {\n\t/**\n\t * The current logical clock value for the room\n\t */\n\tclock?: number\n\t/**\n\t * Clock value when document data was last changed (optional for backwards compatibility)\n\t */\n\tdocumentClock?: number\n\t/**\n\t * Array of all document records with their last modification clocks\n\t */\n\tdocuments: Array<{ state: UnknownRecord; lastChangedClock: number }>\n\t/**\n\t * Map of deleted record IDs to their deletion clock values (optional)\n\t */\n\ttombstones?: Record<string, number>\n\t/**\n\t * Clock value where tombstone history begins - older deletions are not tracked (optional)\n\t */\n\ttombstoneHistoryStartsAtClock?: number\n\t/**\n\t * Serialized schema used when creating this snapshot (optional)\n\t */\n\tschema?: SerializedSchema\n}\n\n/**\n * A collaborative workspace that manages multiple client sessions and synchronizes\n * document changes between them. The room serves as the authoritative source for\n * all document state and handles conflict resolution, schema migrations, and\n * real-time data distribution.\n *\n * @example\n * ```ts\n * const room = new TLSyncRoom({\n * schema: mySchema,\n * onDataChange: () => saveToDatabase(room.getSnapshot()),\n * onPresenceChange: () => updateLiveCursors()\n * })\n *\n * // Handle new client connections\n * room.handleNewSession({\n * sessionId: 'user-123',\n * socket: webSocketAdapter,\n * meta: { userId: '123', name: 'Alice' },\n * isReadonly: false\n * })\n * ```\n *\n * @internal\n */\nexport class TLSyncRoom<R extends UnknownRecord, SessionMeta> {\n\t// A table of connected clients\n\treadonly sessions = new Map<string, RoomSession<R, SessionMeta>>()\n\n\tprivate lastDocumentClock = 0\n\n\tprivate pruneTimer: ReturnType<typeof setTimeout> | null = null\n\n\tpruneSessions = throttle(() => {\n\t\tif (this.pruneTimer) {\n\t\t\tclearTimeout(this.pruneTimer)\n\t\t\tthis.pruneTimer = null\n\t\t}\n\t\tfor (const client of this.sessions.values()) {\n\t\t\tswitch (client.state) {\n\t\t\t\tcase RoomSessionState.Connected: {\n\t\t\t\t\tconst hasTimedOut = timeSince(client.lastInteractionTime) > this.sessionIdleTimeout\n\t\t\t\t\tif (hasTimedOut || !client.socket.isOpen) {\n\t\t\t\t\t\tthis.cancelSession(client.sessionId)\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase RoomSessionState.AwaitingConnectMessage: {\n\t\t\t\t\tconst hasTimedOut = timeSince(client.sessionStartTime) > SESSION_START_WAIT_TIME\n\t\t\t\t\tif (hasTimedOut || !client.socket.isOpen) {\n\t\t\t\t\t\t// remove immediately\n\t\t\t\t\t\tthis.removeSession(client.sessionId)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.scheduleFollowUpPrune()\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tcase RoomSessionState.AwaitingRemoval: {\n\t\t\t\t\tconst hasTimedOut = timeSince(client.cancellationTime) > SESSION_REMOVAL_WAIT_TIME\n\t\t\t\t\tif (hasTimedOut) {\n\t\t\t\t\t\tthis.removeSession(client.sessionId)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.scheduleFollowUpPrune()\n\t\t\t\t\t}\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\texhaustiveSwitchError(client)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, 1000)\n\n\tprivate scheduleFollowUpPrune() {\n\t\tif (this.pruneTimer) return\n\t\tthis.pruneTimer = setTimeout(this.pruneSessions, SESSION_REMOVAL_WAIT_TIME + 100)\n\t}\n\n\treadonly presenceStore = new PresenceStore<R>()\n\n\tprivate disposables: Array<() => void> = []\n\n\tprivate _isClosed = false\n\n\t/**\n\t * Close the room and clean up all resources. Disconnects all sessions\n\t * and stops background processes.\n\t */\n\tclose() {\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.sessions.forEach((session) => {\n\t\t\tsession.socket.close()\n\t\t})\n\t\tthis._isClosed = true\n\t}\n\n\t/**\n\t * Check if the room has been closed and is no longer accepting connections.\n\t *\n\t * @returns True if the room is closed\n\t */\n\tisClosed() {\n\t\treturn this._isClosed\n\t}\n\n\treadonly events = createNanoEvents<{\n\t\troom_became_empty(): void\n\t\tsession_removed(args: { sessionId: string; meta: SessionMeta }): void\n\t}>()\n\n\t// Storage layer for documents, tombstones, and clocks\n\tprivate readonly storage: TLSyncStorage<R>\n\n\treadonly serializedSchema: SerializedSchema\n\n\treadonly documentTypes: Set<string>\n\treadonly presenceType: RecordType<R, any> | null\n\tprivate log?: TLSyncLog\n\tpublic readonly schema: StoreSchema<R, any>\n\tprivate onPresenceChange?(): void\n\tprivate readonly sessionIdleTimeout: number\n\n\tconstructor(opts: {\n\t\tlog?: TLSyncLog\n\t\tschema: StoreSchema<R, any>\n\t\tonPresenceChange?(): void\n\t\tstorage: TLSyncStorage<R>\n\t\tclientTimeout?: number\n\t}) {\n\t\tthis.schema = opts.schema\n\t\tthis.log = opts.log\n\t\tthis.onPresenceChange = opts.onPresenceChange\n\t\tthis.storage = opts.storage\n\t\tthis.sessionIdleTimeout = opts.clientTimeout ?? SESSION_IDLE_TIMEOUT\n\n\t\tassert(\n\t\t\tisNativeStructuredClone,\n\t\t\t'TLSyncRoom is supposed to run either on Cloudflare Workers' +\n\t\t\t\t'or on a 18+ version of Node.js, which both support the native structuredClone API'\n\t\t)\n\n\t\t// do a json serialization cycle to make sure the schema has no 'undefined' values\n\t\tthis.serializedSchema = JSON.parse(JSON.stringify(this.schema.serialize()))\n\n\t\tthis.documentTypes = new Set(\n\t\t\tObject.values<RecordType<R, any>>(this.schema.types)\n\t\t\t\t.filter((t) => t.scope === 'document')\n\t\t\t\t.map((t) => t.typeName)\n\t\t)\n\n\t\tconst presenceTypes = new Set(\n\t\t\tObject.values<RecordType<R, any>>(this.schema.types).filter((t) => t.scope === 'presence')\n\t\t)\n\n\t\tif (presenceTypes.size > 1) {\n\t\t\tthrow new Error(\n\t\t\t\t`TLSyncRoom: exactly zero or one presence type is expected, but found ${presenceTypes.size}`\n\t\t\t)\n\t\t}\n\n\t\tthis.presenceType = presenceTypes.values().next()?.value ?? null\n\n\t\tconst { documentClock } = this.storage.transaction((txn) => {\n\t\t\tthis.schema.migrateStorage(txn)\n\t\t})\n\n\t\tthis.lastDocumentClock = documentClock\n\n\t\tthis.disposables.push(\n\t\t\tthis.storage.onChange(({ id }) => {\n\t\t\t\tif (id !== this.internalTxnId) {\n\t\t\t\t\tthis.broadcastExternalStorageChanges()\n\t\t\t\t}\n\t\t\t})\n\t\t)\n\n\t\tthis.disposables.push(() => {\n\t\t\tthis.pruneSessions.cancel()\n\t\t\tif (this.pruneTimer) {\n\t\t\t\tclearTimeout(this.pruneTimer)\n\t\t\t\tthis.pruneTimer = null\n\t\t\t}\n\t\t})\n\n\t\t// When clientTimeout is finite, run periodic pruning so idle sessions are\n\t\t// cleaned up even with no traffic. When Infinity or 0 we skip the interval\n\t\t// (e.g. for hibernation); without it, pruning only runs on message or when\n\t\t// socket close/error triggers cancelSession, so pruning idle sessions\n\t\t// reliably depends on the runtime delivering those events.\n\t\tif (Number.isFinite(this.sessionIdleTimeout) && this.sessionIdleTimeout > 0) {\n\t\t\tconst pruneIntervalMs = Math.min(2000, Math.floor(this.sessionIdleTimeout / 4))\n\t\t\tthis.disposables.push(interval(() => this.pruneSessions(), pruneIntervalMs))\n\t\t}\n\t}\n\tprivate broadcastExternalStorageChanges() {\n\t\tthis.storage.transaction((txn) => {\n\t\t\tthis.broadcastChanges(txn)\n\t\t\tthis.lastDocumentClock = txn.getClock()\n\t\t}) // no id needed because this only reads, no writes.\n\t}\n\n\t/**\n\t * Send a message to a particular client. Debounces data events\n\t *\n\t * @param sessionId - The id of the session to send the message to.\n\t * @param message - The message to send. UNSAFE Any diffs must have been downgraded already if necessary\n\t */\n\tprivate _unsafe_sendMessage(\n\t\tsessionId: string,\n\t\tmessage: TLSocketServerSentEvent<R> | TLSocketServerSentDataEvent<R>\n\t) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) {\n\t\t\tthis.log?.warn?.('Tried to send message to unknown session', message.type)\n\t\t\treturn\n\t\t}\n\t\tif (session.state !== RoomSessionState.Connected) {\n\t\t\tthis.log?.warn?.('Tried to send message to disconnected client', message.type)\n\t\t\treturn\n\t\t}\n\t\tif (session.socket.isOpen) {\n\t\t\tif (message.type !== 'patch' && message.type !== 'push_result') {\n\t\t\t\t// this is not a data message\n\t\t\t\tif (message.type !== 'pong') {\n\t\t\t\t\t// non-data messages like \"connect\" might still need to be ordered correctly with\n\t\t\t\t\t// respect to data messages, so it's better to flush just in case\n\t\t\t\t\tthis._flushDataMessages(sessionId)\n\t\t\t\t}\n\t\t\t\tsession.socket.sendMessage(message)\n\t\t\t} else {\n\t\t\t\tif (session.debounceTimer === null) {\n\t\t\t\t\t// this is the first message since the last flush, don't delay it\n\t\t\t\t\tsession.socket.sendMessage({ type: 'data', data: [message] })\n\n\t\t\t\t\tsession.debounceTimer = setTimeout(\n\t\t\t\t\t\t() => this._flushDataMessages(sessionId),\n\t\t\t\t\t\tDATA_MESSAGE_DEBOUNCE_INTERVAL\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tsession.outstandingDataMessages.push(message)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.cancelSession(session.sessionId)\n\t\t}\n\t}\n\n\t// needs to accept sessionId and not a session because the session might be dead by the time\n\t// the timer fires\n\t_flushDataMessages(sessionId: string) {\n\t\tconst session = this.sessions.get(sessionId)\n\n\t\tif (!session || session.state !== RoomSessionState.Connected) {\n\t\t\treturn\n\t\t}\n\n\t\tsession.debounceTimer = null\n\n\t\tif (session.outstandingDataMessages.length > 0) {\n\t\t\tsession.socket.sendMessage({ type: 'data', data: session.outstandingDataMessages })\n\t\t\tsession.outstandingDataMessages.length = 0\n\t\t}\n\t}\n\n\t/** @internal */\n\tprivate removeSession(sessionId: string, fatalReason?: string) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) {\n\t\t\tthis.log?.warn?.('Tried to remove unknown session')\n\t\t\treturn\n\t\t}\n\n\t\tthis.sessions.delete(sessionId)\n\n\t\ttry {\n\t\t\tif (fatalReason) {\n\t\t\t\tsession.socket.close(TLSyncErrorCloseEventCode, fatalReason)\n\t\t\t} else {\n\t\t\t\tsession.socket.close()\n\t\t\t}\n\t\t} catch {\n\t\t\t// noop, calling .close() multiple times is fine\n\t\t}\n\n\t\tconst presence = this.presenceStore.get(session.presenceId ?? '')\n\t\tif (presence) {\n\t\t\tthis.presenceStore.delete(session.presenceId!)\n\t\t\t// Broadcast presence removal - use RecordsDiff with the removed record\n\t\t\tthis.broadcastPatch({\n\t\t\t\tputs: {},\n\t\t\t\tdeletes: [session.presenceId!],\n\t\t\t})\n\t\t}\n\n\t\tthis.events.emit('session_removed', { sessionId, meta: session.meta })\n\t\tif (this.sessions.size === 0) {\n\t\t\tthis.events.emit('room_became_empty')\n\t\t}\n\t}\n\n\tprivate cancelSession(sessionId: string) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) {\n\t\t\treturn\n\t\t}\n\n\t\tif (session.state === RoomSessionState.AwaitingRemoval) {\n\t\t\tthis.log?.warn?.('Tried to cancel session that is already awaiting removal')\n\t\t\treturn\n\t\t}\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tstate: RoomSessionState.AwaitingRemoval,\n\t\t\tsessionId,\n\t\t\tpresenceId: session.presenceId,\n\t\t\tsocket: session.socket,\n\t\t\tcancellationTime: Date.now(),\n\t\t\tmeta: session.meta,\n\t\t\tisReadonly: session.isReadonly,\n\t\t\trequiresLegacyRejection: session.requiresLegacyRejection,\n\t\t\tsupportsStringAppend: session.supportsStringAppend,\n\t\t})\n\n\t\ttry {\n\t\t\tsession.socket.close()\n\t\t} catch {\n\t\t\t// noop, calling .close() multiple times is fine\n\t\t}\n\n\t\tthis.scheduleFollowUpPrune()\n\t}\n\n\treadonly internalTxnId = 'TLSyncRoom.txn'\n\n\t/**\n\t * Broadcast a patch to all connected clients except the one with the sessionId provided.\n\t *\n\t * @param diff - The TLSyncForwardDiff with full records (used for migration)\n\t * @param networkDiff - Optional pre-computed NetworkDiff for sessions not needing migration.\n\t * If not provided, will be computed from recordsDiff.\n\t * @param sourceSessionId - Optional session ID to exclude from the broadcast\n\t */\n\tprivate broadcastPatch(\n\t\tdiff: TLSyncForwardDiff<R>,\n\t\tnetworkDiff?: NetworkDiff<R> | null,\n\t\tsourceSessionId?: string\n\t) {\n\t\t// Pre-compute network diff if not provided\n\t\tconst unmigrated = networkDiff ?? toNetworkDiff(diff)\n\t\tif (!unmigrated) return this\n\n\t\tthis.sessions.forEach((session) => {\n\t\t\tif (session.state !== RoomSessionState.Connected) return\n\t\t\tif (sourceSessionId === session.sessionId) return\n\t\t\tif (!session.socket.isOpen) {\n\t\t\t\tthis.cancelSession(session.sessionId)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst diffResult = this.migrateDiffOrRejectSession(\n\t\t\t\tsession.sessionId,\n\t\t\t\tsession.serializedSchema,\n\t\t\t\tsession.requiresDownMigrations,\n\t\t\t\tdiff\n\t\t\t)\n\t\t\tif (!diffResult.ok) return\n\n\t\t\tthis._unsafe_sendMessage(session.sessionId, {\n\t\t\t\ttype: 'patch',\n\t\t\t\tdiff: diffResult.value,\n\t\t\t\tserverClock: this.lastDocumentClock,\n\t\t\t})\n\t\t})\n\t\treturn this\n\t}\n\n\t/**\n\t * Send a custom message to a connected client. Useful for application-specific\n\t * communication that doesn't involve document synchronization.\n\t *\n\t * @param sessionId - The ID of the session to send the message to\n\t * @param data - The custom payload to send (will be JSON serialized)\n\t * @example\n\t * ```ts\n\t * // Send a custom notification\n\t * room.sendCustomMessage('user-123', {\n\t * type: 'notification',\n\t * message: 'Document saved successfully'\n\t * })\n\t *\n\t * // Send user-specific data\n\t * room.sendCustomMessage('user-456', {\n\t * type: 'user_permissions',\n\t * canEdit: true,\n\t * canDelete: false\n\t * })\n\t * ```\n\t */\n\tsendCustomMessage(sessionId: string, data: any): void {\n\t\tthis._unsafe_sendMessage(sessionId, { type: 'custom', data })\n\t}\n\n\t/**\n\t * Register a new client session with the room. The session will be in an awaiting\n\t * state until it sends a connect message with protocol handshake.\n\t *\n\t * @param opts - Session configuration\n\t * - sessionId - Unique identifier for this session\n\t * - socket - WebSocket adapter for communication\n\t * - meta - Application-specific metadata for this session\n\t * - isReadonly - Whether this session can modify documents\n\t * @returns This room instance for method chaining\n\t * @example\n\t * ```ts\n\t * room.handleNewSession({\n\t * sessionId: crypto.randomUUID(),\n\t * socket: new WebSocketAdapter(ws),\n\t * meta: { userId: '123', name: 'Alice', avatar: 'url' },\n\t * isReadonly: !hasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @internal\n\t */\n\thandleNewSession(opts: {\n\t\tsessionId: string\n\t\tsocket: TLRoomSocket<R>\n\t\tmeta: SessionMeta\n\t\tisReadonly: boolean\n\t}) {\n\t\tconst { sessionId, socket, meta, isReadonly } = opts\n\t\tconst existing = this.sessions.get(sessionId)\n\t\tthis.sessions.set(sessionId, {\n\t\t\tstate: RoomSessionState.AwaitingConnectMessage,\n\t\t\tsessionId,\n\t\t\tsocket,\n\t\t\tpresenceId: existing?.presenceId ?? this.presenceType?.createId() ?? null,\n\t\t\tsessionStartTime: Date.now(),\n\t\t\tmeta,\n\t\t\tisReadonly: isReadonly ?? false,\n\t\t\t// this gets set later during handleConnectMessage\n\t\t\trequiresLegacyRejection: false,\n\t\t\tsupportsStringAppend: true,\n\t\t})\n\t\treturn this\n\t}\n\n\t/**\n\t * Resume a previously-connected session directly into `Connected` state, bypassing the\n\t * connect handshake. Used after server hibernation when the WebSocket is still alive but\n\t * all in-memory state has been lost.\n\t *\n\t * @internal\n\t */\n\thandleResumedSession(opts: {\n\t\tsessionId: string\n\t\tsocket: TLRoomSocket<R>\n\t\tmeta: SessionMeta\n\t\tisReadonly: boolean\n\t\tserializedSchema: SerializedSchema\n\t\tpresenceId: string | null\n\t\tpresenceRecord: UnknownRecord | null\n\t\trequiresLegacyRejection: boolean\n\t\tsupportsStringAppend: boolean\n\t}) {\n\t\tconst {\n\t\t\tsessionId,\n\t\t\tsocket,\n\t\t\tmeta,\n\t\t\tisReadonly,\n\t\t\tserializedSchema,\n\t\t\tpresenceId,\n\t\t\tpresenceRecord,\n\t\t\trequiresLegacyRejection,\n\t\t\tsupportsStringAppend,\n\t\t} = opts\n\n\t\tconst migrations = this.schema.getMigrationsSince(serializedSchema)\n\t\tconst requiresDownMigrations = migrations.ok ? migrations.value.length > 0 : false\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tstate: RoomSessionState.Connected,\n\t\t\tsessionId,\n\t\t\tsocket,\n\t\t\tpresenceId: presenceId ?? this.presenceType?.createId() ?? null,\n\t\t\tserializedSchema,\n\t\t\trequiresDownMigrations,\n\t\t\tlastInteractionTime: Date.now(),\n\t\t\tdebounceTimer: null,\n\t\t\toutstandingDataMessages: [],\n\t\t\tmeta,\n\t\t\tisReadonly,\n\t\t\trequiresLegacyRejection,\n\t\t\tsupportsStringAppend,\n\t\t})\n\n\t\tif (presenceRecord && presenceId) {\n\t\t\tthis.presenceStore.set(presenceId, presenceRecord as R)\n\t\t}\n\t}\n\n\t/**\n\t * Checks if all connected sessions support string append operations (protocol version 8+).\n\t * If any client is on an older version, returns false to enable legacy append mode.\n\t *\n\t * @returns True if all connected sessions are on protocol version 8 or higher\n\t */\n\tgetCanEmitStringAppend(): boolean {\n\t\tfor (const session of this.sessions.values()) {\n\t\t\tif (session.state === RoomSessionState.Connected) {\n\t\t\t\tif (!session.supportsStringAppend) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t/**\n\t * When we send a diff to a client, if that client is on a lower version than us, we need to make\n\t * the diff compatible with their version. This method takes a TLSyncForwardDiff (which has full\n\t * records) and migrates all records down to the client's schema version, returning a NetworkDiff.\n\t *\n\t * For updates (entries with [before, after] tuples), both records are migrated and a patch is\n\t * computed from the migrated versions, preserving efficient patch semantics even across versions.\n\t *\n\t * If a migration fails, the session will be rejected.\n\t *\n\t * @param sessionId - The session ID (for rejection on migration failure)\n\t * @param serializedSchema - The client's schema to migrate to\n\t * @param requiresDownMigrations - Whether the client needs down migrations\n\t * @param diff - The TLSyncForwardDiff containing full records to migrate\n\t * @param unmigrated - Optional pre-computed NetworkDiff for when no migration is needed\n\t * @returns A NetworkDiff with migrated records, or a migration failure\n\t */\n\tprivate migrateDiffOrRejectSession(\n\t\tsessionId: string,\n\t\tserializedSchema: SerializedSchema,\n\t\trequiresDownMigrations: boolean,\n\t\tdiff: TLSyncForwardDiff<R>,\n\t\tunmigrated?: NetworkDiff<R>\n\t): Result<NetworkDiff<R>, MigrationFailureReason> {\n\t\tif (!requiresDownMigrations) {\n\t\t\treturn Result.ok(unmigrated ?? toNetworkDiff(diff) ?? {})\n\t\t}\n\n\t\tconst result: NetworkDiff<R> = {}\n\n\t\t// Migrate puts (either adds or updates)\n\t\tfor (const [id, put] of objectMapEntriesIterable(diff.puts)) {\n\t\t\tif (Array.isArray(put)) {\n\t\t\t\t// Update: [before, after] tuple - migrate both and compute patch\n\t\t\t\tconst [from, to] = put\n\t\t\t\tconst fromResult = this.schema.migratePersistedRecord(from, serializedSchema, 'down')\n\t\t\t\tif (fromResult.type === 'error') {\n\t\t\t\t\tthis.rejectSession(sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t\t\treturn Result.err(fromResult.reason)\n\t\t\t\t}\n\t\t\t\tconst toResult = this.schema.migratePersistedRecord(to, serializedSchema, 'down')\n\t\t\t\tif (toResult.type === 'error') {\n\t\t\t\t\tthis.rejectSession(sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t\t\treturn Result.err(toResult.reason)\n\t\t\t\t}\n\t\t\t\tconst patch = diffRecord(fromResult.value, toResult.value)\n\t\t\t\tif (patch) {\n\t\t\t\t\tresult[id] = [RecordOpType.Patch, patch]\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Add: single record - migrate and put\n\t\t\t\tconst migrationResult = this.schema.migratePersistedRecord(put, serializedSchema, 'down')\n\t\t\t\tif (migrationResult.type === 'error') {\n\t\t\t\t\tthis.rejectSession(sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t\t\treturn Result.err(migrationResult.reason)\n\t\t\t\t}\n\t\t\t\tresult[id] = [RecordOpType.Put, migrationResult.value]\n\t\t\t}\n\t\t}\n\n\t\t// Deletes don't need migration\n\t\tfor (const id of diff.deletes) {\n\t\t\tresult[id] = [RecordOpType.Remove]\n\t\t}\n\n\t\treturn Result.ok(result)\n\t}\n\n\t/**\n\t * Process an incoming message from a client session. Handles connection requests,\n\t * data synchronization pushes, and ping/pong for connection health.\n\t *\n\t * @param sessionId - The ID of the session that sent the message\n\t * @param message - The client message to process\n\t * @example\n\t * ```ts\n\t * // Typically called by WebSocket message handlers\n\t * websocket.onMessage((data) => {\n\t * const message = JSON.parse(data)\n\t * room.handleMessage(sessionId, message)\n\t * })\n\t * ```\n\t */\n\tasync handleMessage(sessionId: string, message: TLSocketClientSentEvent<R>) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) {\n\t\t\tthis.log?.warn?.('Received message from unknown session')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tswitch (message.type) {\n\t\t\t\tcase 'connect': {\n\t\t\t\t\treturn this.handleConnectRequest(session, message)\n\t\t\t\t}\n\t\t\t\tcase 'push': {\n\t\t\t\t\treturn this.handlePushRequest(session, message)\n\t\t\t\t}\n\t\t\t\tcase 'ping': {\n\t\t\t\t\tif (session.state === RoomSessionState.Connected) {\n\t\t\t\t\t\tsession.lastInteractionTime = Date.now()\n\t\t\t\t\t}\n\t\t\t\t\treturn this._unsafe_sendMessage(session.sessionId, { type: 'pong' })\n\t\t\t\t}\n\t\t\t\tdefault: {\n\t\t\t\t\texhaustiveSwitchError(message)\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tif (e instanceof TLSyncError) {\n\t\t\t\tthis.rejectSession(session.sessionId, e.reason)\n\t\t\t} else {\n\t\t\t\t// log error and reboot the room?\n\t\t\t\tthrow e\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Reject and disconnect a session due to incompatibility or other fatal errors.\n\t * Sends appropriate error messages before closing the connection.\n\t *\n\t * @param sessionId - The session to reject\n\t * @param fatalReason - The reason for rejection (optional)\n\t * @example\n\t * ```ts\n\t * // Reject due to version mismatch\n\t * room.rejectSession('user-123', TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t *\n\t * // Reject due to permission issue\n\t * room.rejectSession('user-456', 'Insufficient permissions')\n\t * ```\n\t */\n\trejectSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tconst session = this.sessions.get(sessionId)\n\t\tif (!session) return\n\t\tif (!fatalReason) {\n\t\t\tthis.removeSession(sessionId)\n\t\t\treturn\n\t\t}\n\t\tif (session.requiresLegacyRejection) {\n\t\t\ttry {\n\t\t\t\tif (session.socket.isOpen) {\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\tlet legacyReason: TLIncompatibilityReason\n\t\t\t\t\tswitch (fatalReason) {\n\t\t\t\t\t\tcase TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\t\tlegacyReason = TLIncompatibilityReason.ClientTooOld\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tcase TLSyncErrorCloseEventReason.SERVER_TOO_OLD:\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\t\tlegacyReason = TLIncompatibilityReason.ServerTooOld\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tcase TLSyncErrorCloseEventReason.INVALID_RECORD:\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\t\tlegacyReason = TLIncompatibilityReason.InvalidRecord\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\t\tlegacyReason = TLIncompatibilityReason.InvalidOperation\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t\tsession.socket.sendMessage({\n\t\t\t\t\t\ttype: 'incompatibility_error',\n\t\t\t\t\t\treason: legacyReason,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// noop\n\t\t\t} finally {\n\t\t\t\tthis.removeSession(sessionId)\n\t\t\t}\n\t\t} else {\n\t\t\tthis.removeSession(sessionId, fatalReason)\n\t\t}\n\t}\n\n\tprivate forceAllReconnect() {\n\t\tfor (const session of this.sessions.values()) {\n\t\t\tthis.removeSession(session.sessionId)\n\t\t}\n\t}\n\n\tprivate broadcastChanges(txn: TLSyncStorageTransaction<R>) {\n\t\tconst changes = txn.getChangesSince(this.lastDocumentClock)\n\t\tif (!changes) return\n\t\tconst { wipeAll, diff } = changes\n\t\tthis.lastDocumentClock = txn.getClock()\n\t\tif (wipeAll) {\n\t\t\t// If this happens it means we'd need to broadcast a wipe_all message to all clients,\n\t\t\t// which is not part of the protocol yet, so we need to force all clients to reconnect instead.\n\t\t\tthis.forceAllReconnect()\n\t\t\treturn\n\t\t}\n\t\tthis.broadcastPatch(diff)\n\t}\n\n\tprivate handleConnectRequest(\n\t\tsession: RoomSession<R, SessionMeta>,\n\t\tmessage: Extract<TLSocketClientSentEvent<R>, { type: 'connect' }>\n\t) {\n\t\t// if the protocol versions don't match, disconnect the client\n\t\t// we will eventually want to try to make our protocol backwards compatible to some degree\n\t\t// and have a MIN_PROTOCOL_VERSION constant that the TLSyncRoom implements support for\n\t\tlet theirProtocolVersion = message.protocolVersion\n\t\t// 5 is the same as 6\n\t\tif (theirProtocolVersion === 5) {\n\t\t\ttheirProtocolVersion = 6\n\t\t}\n\t\t// 6 is almost the same as 7\n\t\tsession.requiresLegacyRejection = theirProtocolVersion === 6\n\t\tif (theirProtocolVersion === 6) {\n\t\t\ttheirProtocolVersion++\n\t\t}\n\t\tif (theirProtocolVersion === 7) {\n\t\t\ttheirProtocolVersion++\n\t\t\tsession.supportsStringAppend = false\n\t\t}\n\n\t\tif (theirProtocolVersion == null || theirProtocolVersion < getTlsyncProtocolVersion()) {\n\t\t\tthis.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\treturn\n\t\t} else if (theirProtocolVersion > getTlsyncProtocolVersion()) {\n\t\t\tthis.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.SERVER_TOO_OLD)\n\t\t\treturn\n\t\t}\n\t\t// If the client's store is at a different version to ours, it could cause corruption.\n\t\t// We should disconnect the client and ask them to refresh.\n\t\tif (message.schema == null) {\n\t\t\tthis.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\treturn\n\t\t}\n\t\tconst migrations = this.schema.getMigrationsSince(message.schema)\n\t\t// if the client's store is at a different version to ours, we can't support them\n\t\tif (!migrations.ok || migrations.value.some((m) => m.scope !== 'record' || !m.down)) {\n\t\t\tthis.rejectSession(session.sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\treturn\n\t\t}\n\n\t\tconst sessionSchema = isEqual(message.schema, this.serializedSchema)\n\t\t\t? this.serializedSchema\n\t\t\t: message.schema\n\n\t\tconst requiresDownMigrations = migrations.value.length > 0\n\n\t\tconst connect = async (msg: Extract<TLSocketServerSentEvent<R>, { type: 'connect' }>) => {\n\t\t\tthis.sessions.set(session.sessionId, {\n\t\t\t\tstate: RoomSessionState.Connected,\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tpresenceId: session.presenceId,\n\t\t\t\tsocket: session.socket,\n\t\t\t\tserializedSchema: sessionSchema,\n\t\t\t\trequiresDownMigrations,\n\t\t\t\tlastInteractionTime: Date.now(),\n\t\t\t\tdebounceTimer: null,\n\t\t\t\toutstandingDataMessages: [],\n\t\t\t\tsupportsStringAppend: session.supportsStringAppend,\n\t\t\t\tmeta: session.meta,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\trequiresLegacyRejection: session.requiresLegacyRejection,\n\t\t\t})\n\t\t\tthis._unsafe_sendMessage(session.sessionId, msg)\n\t\t}\n\n\t\tconst { documentClock, result } = this.storage.transaction((txn) => {\n\t\t\tthis.broadcastChanges(txn)\n\t\t\tconst docChanges = txn.getChangesSince(message.lastServerClock)\n\t\t\tconst presenceDiff = this.migrateDiffOrRejectSession(\n\t\t\t\tsession.sessionId,\n\t\t\t\tsessionSchema,\n\t\t\t\trequiresDownMigrations,\n\t\t\t\t{\n\t\t\t\t\tputs: Object.fromEntries([...this.presenceStore.values()].map((p) => [p.id, p])),\n\t\t\t\t\tdeletes: [],\n\t\t\t\t}\n\t\t\t)\n\t\t\tif (!presenceDiff.ok) return null\n\n\t\t\t// Migrate the diff if needed, or use the pre-computed network diff\n\t\t\tlet docDiff: NetworkDiff<R> | null = null\n\t\t\tif (docChanges && sessionSchema !== this.serializedSchema) {\n\t\t\t\tconst migrated = this.migrateDiffOrRejectSession(\n\t\t\t\t\tsession.sessionId,\n\t\t\t\t\tsessionSchema,\n\t\t\t\t\trequiresDownMigrations,\n\t\t\t\t\tdocChanges.diff\n\t\t\t\t)\n\t\t\t\tif (!migrated.ok) return null\n\t\t\t\tdocDiff = migrated.value\n\t\t\t} else if (docChanges) {\n\t\t\t\tdocDiff = toNetworkDiff(docChanges.diff)\n\t\t\t}\n\t\t\treturn {\n\t\t\t\ttype: 'connect',\n\t\t\t\tconnectRequestId: message.connectRequestId,\n\t\t\t\thydrationType: docChanges?.wipeAll ? 'wipe_all' : 'wipe_presence',\n\t\t\t\tprotocolVersion: getTlsyncProtocolVersion(),\n\t\t\t\tschema: this.schema.serialize(),\n\t\t\t\tserverClock: txn.getClock(),\n\t\t\t\tdiff: { ...presenceDiff.value, ...docDiff },\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t} satisfies Extract<TLSocketServerSentEvent<R>, { type: 'connect' }>\n\t\t}) // no id needed because this only reads, no writes.\n\n\t\tthis.lastDocumentClock = documentClock\n\n\t\tif (result) {\n\t\t\tconnect(result)\n\t\t}\n\t}\n\n\tprivate handlePushRequest(\n\t\tsession: RoomSession<R, SessionMeta> | null,\n\t\tmessage: Extract<TLSocketClientSentEvent<R>, { type: 'push' }>\n\t) {\n\t\t// We must be connected to handle push requests\n\t\tif (session && session.state !== RoomSessionState.Connected) {\n\t\t\treturn\n\t\t}\n\t\t// update the last interaction time\n\t\tif (session) {\n\t\t\tsession.lastInteractionTime = Date.now()\n\t\t}\n\n\t\tconst legacyAppendMode = !this.getCanEmitStringAppend()\n\n\t\tinterface ActualChanges {\n\t\t\tdiffs: {\n\t\t\t\tnetworkDiff: NetworkDiff<R>\n\t\t\t\tdiff: TLSyncForwardDiff<R>\n\t\t\t} | null\n\t\t}\n\n\t\tconst propagateOp = (\n\t\t\tchanges: ActualChanges,\n\t\t\tid: string,\n\t\t\top: RecordOp<R>,\n\t\t\tbefore: R | undefined,\n\t\t\tafter: R | undefined\n\t\t) => {\n\t\t\tif (!changes.diffs) changes.diffs = { networkDiff: {}, diff: { puts: {}, deletes: [] } }\n\t\t\tchanges.diffs.networkDiff[id] = op\n\t\t\tswitch (op[0]) {\n\t\t\t\tcase RecordOpType.Put:\n\t\t\t\t\tchanges.diffs.diff.puts[id] = op[1]\n\t\t\t\t\tbreak\n\t\t\t\tcase RecordOpType.Patch:\n\t\t\t\t\tassert(before && after, 'before and after are required for patches')\n\t\t\t\t\tchanges.diffs.diff.puts[id] = [before, after]\n\t\t\t\t\tbreak\n\t\t\t\tcase RecordOpType.Remove:\n\t\t\t\t\tchanges.diffs.diff.deletes.push(id)\n\t\t\t\t\tbreak\n\t\t\t\tdefault:\n\t\t\t\t\texhaustiveSwitchError(op[0])\n\t\t\t}\n\t\t}\n\n\t\tconst addDocument = (\n\t\t\tstorage: MinimalDocStore<R>,\n\t\t\tchanges: ActualChanges,\n\t\t\tid: string,\n\t\t\t_state: R\n\t\t): Result<void, void> => {\n\t\t\tconst res = session\n\t\t\t\t? this.schema.migratePersistedRecord(_state, session.serializedSchema, 'up')\n\t\t\t\t: { type: 'success' as const, value: _state }\n\t\t\tif (res.type === 'error') {\n\t\t\t\tthrow new TLSyncError(res.reason, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t}\n\t\t\tconst { value: state } = res\n\n\t\t\t// Get the existing document, if any\n\t\t\tconst doc = storage.get(id) as R | undefined\n\n\t\t\tif (doc) {\n\t\t\t\t// If there's an existing document, replace it with the new state\n\t\t\t\t// but propagate a diff rather than the entire value\n\t\t\t\tconst recordType = assertExists(getOwnProperty(this.schema.types, doc.typeName))\n\t\t\t\tconst diff = diffAndValidateRecord(doc, state, recordType)\n\t\t\t\tif (diff) {\n\t\t\t\t\tstorage.set(id, state)\n\t\t\t\t\tpropagateOp(changes, id, [RecordOpType.Patch, diff], doc, state)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Otherwise, if we don't already have a document with this id\n\t\t\t\t// create the document and propagate the put op\n\t\t\t\t// set automatically clears tombstones if they exist\n\t\t\t\tconst recordType = assertExists(getOwnProperty(this.schema.types, state.typeName))\n\t\t\t\tvalidateRecord(state, recordType)\n\t\t\t\tstorage.set(id, state)\n\t\t\t\tpropagateOp(changes, id, [RecordOpType.Put, state], undefined, undefined)\n\t\t\t}\n\n\t\t\treturn Result.ok(undefined)\n\t\t}\n\n\t\tconst patchDocument = (\n\t\t\tstorage: MinimalDocStore<R>,\n\t\t\tchanges: ActualChanges,\n\t\t\tid: string,\n\t\t\tpatch: ObjectDiff\n\t\t) => {\n\t\t\t// if it was already deleted, there's no need to apply the patch\n\t\t\tconst doc = storage.get(id) as R | undefined\n\t\t\tif (!doc) return\n\n\t\t\tconst recordType = assertExists(getOwnProperty(this.schema.types, doc.typeName))\n\t\t\t// If the client's version of the record is older than ours,\n\t\t\t// we apply the patch to the downgraded version of the record\n\t\t\tconst downgraded = session\n\t\t\t\t? this.schema.migratePersistedRecord(doc, session.serializedSchema, 'down')\n\t\t\t\t: { type: 'success' as const, value: doc }\n\t\t\tif (downgraded.type === 'error') {\n\t\t\t\tthrow new TLSyncError(downgraded.reason, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t}\n\n\t\t\tif (downgraded.value === doc) {\n\t\t\t\t// If the versions are compatible, apply the patch and propagate the patch op\n\t\t\t\tconst diff = applyAndDiffRecord(doc, patch, recordType, legacyAppendMode)\n\t\t\t\tif (diff) {\n\t\t\t\t\tstorage.set(id, diff[1])\n\t\t\t\t\tpropagateOp(changes, id, [RecordOpType.Patch, diff[0]], doc, diff[1])\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// need to apply the patch to the downgraded version and then upgrade it\n\n\t\t\t\t// apply the patch to the downgraded version\n\t\t\t\tconst patched = applyObjectDiff(downgraded.value, patch)\n\t\t\t\t// then upgrade the patched version and use that as the new state\n\t\t\t\tconst upgraded = session\n\t\t\t\t\t? this.schema.migratePersistedRecord(patched, session.serializedSchema, 'up')\n\t\t\t\t\t: { type: 'success' as const, value: patched }\n\t\t\t\t// If the client's version is too old, we'll hit an error\n\t\t\t\tif (upgraded.type === 'error') {\n\t\t\t\t\tthrow new TLSyncError(upgraded.reason, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\t\t}\n\t\t\t\t// replace the state with the upgraded version and propagate the patch op\n\t\t\t\tconst diff = diffAndValidateRecord(doc, upgraded.value, recordType, legacyAppendMode)\n\t\t\t\tif (diff) {\n\t\t\t\t\tstorage.set(id, upgraded.value)\n\t\t\t\t\tpropagateOp(changes, id, [RecordOpType.Patch, diff], doc, upgraded.value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst { result, documentClock, changes } = this.storage.transaction(\n\t\t\t(txn) => {\n\t\t\t\tthis.broadcastChanges(txn)\n\t\t\t\t// collect actual ops that resulted from the push\n\t\t\t\t// these will be broadcast to other users\n\n\t\t\t\tconst docChanges: ActualChanges = { diffs: null }\n\t\t\t\tconst presenceChanges: ActualChanges = { diffs: null }\n\n\t\t\t\tif (this.presenceType && session?.presenceId && 'presence' in message && message.presence) {\n\t\t\t\t\tif (!session) throw new Error('session is required for presence pushes')\n\t\t\t\t\t// The push request was for the presence scope.\n\t\t\t\t\tconst id = session.presenceId\n\t\t\t\t\tconst [type, val] = message.presence\n\t\t\t\t\tconst { typeName } = this.presenceType\n\t\t\t\t\tswitch (type) {\n\t\t\t\t\t\tcase RecordOpType.Put: {\n\t\t\t\t\t\t\t// Try to put the document. If it fails, stop here.\n\t\t\t\t\t\t\taddDocument(this.presenceStore, presenceChanges, id, {\n\t\t\t\t\t\t\t\t...val,\n\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\ttypeName,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcase RecordOpType.Patch: {\n\t\t\t\t\t\t\t// Try to patch the document. If it fails, stop here.\n\t\t\t\t\t\t\tpatchDocument(this.presenceStore, presenceChanges, id, {\n\t\t\t\t\t\t\t\t...val,\n\t\t\t\t\t\t\t\tid: [ValueOpType.Put, id],\n\t\t\t\t\t\t\t\ttypeName: [ValueOpType.Put, typeName],\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (message.diff && !session?.isReadonly) {\n\t\t\t\t\t// The push request was for the document scope.\n\t\t\t\t\tfor (const [id, op] of objectMapEntriesIterable(message.diff!)) {\n\t\t\t\t\t\tswitch (op[0]) {\n\t\t\t\t\t\t\tcase RecordOpType.Put: {\n\t\t\t\t\t\t\t\t// Try to add the document.\n\t\t\t\t\t\t\t\t// If we're putting a record with a type that we don't recognize, fail\n\t\t\t\t\t\t\t\tif (!this.documentTypes.has(op[1].typeName)) {\n\t\t\t\t\t\t\t\t\tthrow new TLSyncError(\n\t\t\t\t\t\t\t\t\t\t'invalid record',\n\t\t\t\t\t\t\t\t\t\tTLSyncErrorCloseEventReason.INVALID_RECORD\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\taddDocument(txn, docChanges, id, op[1])\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcase RecordOpType.Patch: {\n\t\t\t\t\t\t\t\t// Try to patch the document. If it fails, stop here.\n\t\t\t\t\t\t\t\tpatchDocument(txn, docChanges, id, op[1])\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcase RecordOpType.Remove: {\n\t\t\t\t\t\t\t\tconst doc = txn.get(id)\n\t\t\t\t\t\t\t\tif (!doc) {\n\t\t\t\t\t\t\t\t\t// If the doc was already deleted, don't do anything, no need to propagate a delete op\n\t\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// Delete the document and propagate the delete op\n\t\t\t\t\t\t\t\t// delete automatically creates tombstones\n\t\t\t\t\t\t\t\ttxn.delete(id)\n\t\t\t\t\t\t\t\tpropagateOp(docChanges, id, op, doc, undefined)\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn { docChanges, presenceChanges }\n\t\t\t},\n\t\t\t{ id: this.internalTxnId, emitChanges: 'when-different' }\n\t\t)\n\n\t\tthis.lastDocumentClock = documentClock\n\n\t\tlet pushResult: TLSocketServerSentEvent<R> | undefined\n\t\tif (changes && session) {\n\t\t\t// txn did not apply verbatim so we should broadcast the actual changes\n\t\t\tresult.docChanges.diffs = { networkDiff: toNetworkDiff(changes) ?? {}, diff: changes }\n\t\t}\n\n\t\tif (isEqual(result.docChanges.diffs?.networkDiff, message.diff)) {\n\t\t\tpushResult = {\n\t\t\t\ttype: 'push_result',\n\t\t\t\tclientClock: message.clientClock,\n\t\t\t\tserverClock: documentClock,\n\t\t\t\taction: 'commit',\n\t\t\t}\n\t\t} else if (!result.docChanges.diffs?.networkDiff) {\n\t\t\tpushResult = {\n\t\t\t\ttype: 'push_result',\n\t\t\t\tclientClock: message.clientClock,\n\t\t\t\tserverClock: documentClock,\n\t\t\t\taction: 'discard',\n\t\t\t}\n\t\t} else if (session) {\n\t\t\t// if recordsDiff is null but diff is not, then there are no clients that need down migrations\n\t\t\t// so we can just use the diff directly\n\t\t\tconst diff = this.migrateDiffOrRejectSession(\n\t\t\t\tsession.sessionId,\n\t\t\t\tsession.serializedSchema,\n\t\t\t\tsession.requiresDownMigrations,\n\t\t\t\tresult.docChanges.diffs.diff,\n\t\t\t\tresult.docChanges.diffs.networkDiff\n\t\t\t)\n\t\t\tif (diff.ok) {\n\t\t\t\tpushResult = {\n\t\t\t\t\ttype: 'push_result',\n\t\t\t\t\tclientClock: message.clientClock,\n\t\t\t\t\tserverClock: documentClock,\n\t\t\t\t\taction: { rebaseWithDiff: diff.value },\n\t\t\t\t}\n\t\t\t}\n\t\t\t// if the difff was not ok then the session was rejected and it's ok to continue without a push result\n\t\t}\n\n\t\tif (session && pushResult) {\n\t\t\tthis._unsafe_sendMessage(session.sessionId, pushResult)\n\t\t}\n\t\tif (result.docChanges.diffs || result.presenceChanges.diffs) {\n\t\t\tthis.broadcastPatch(\n\t\t\t\t{\n\t\t\t\t\tputs: {\n\t\t\t\t\t\t...result.docChanges.diffs?.diff.puts,\n\t\t\t\t\t\t...result.presenceChanges.diffs?.diff.puts,\n\t\t\t\t\t},\n\t\t\t\t\tdeletes: [\n\t\t\t\t\t\t...(result.docChanges.diffs?.diff.deletes ?? []),\n\t\t\t\t\t\t...(result.presenceChanges.diffs?.diff.deletes ?? []),\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t...result.docChanges.diffs?.networkDiff,\n\t\t\t\t\t...result.presenceChanges.diffs?.networkDiff,\n\t\t\t\t},\n\t\t\t\tsession?.sessionId\n\t\t\t)\n\t\t}\n\n\t\tif (result.presenceChanges.diffs) {\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tthis.onPresenceChange?.()\n\t\t\t})\n\t\t}\n\t}\n\n\t/**\n\t * Handle the event when a client disconnects. Cleans up the session and\n\t * removes any presence information.\n\t *\n\t * @param sessionId - The session that disconnected\n\t * @example\n\t * ```ts\n\t * websocket.onClose(() => {\n\t * room.handleClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleClose(sessionId: string) {\n\t\tthis.cancelSession(sessionId)\n\t}\n}\n\n/** @internal */\nexport interface MinimalDocStore<R extends UnknownRecord> {\n\tget(id: string): UnknownRecord | undefined\n\tset(id: string, record: R): void\n\tdelete(id: string): void\n}\n\n/** @internal */\nexport class PresenceStore<R extends UnknownRecord> implements MinimalDocStore<R> {\n\tprivate readonly presences = new AtomMap<string, R>('presences')\n\n\tget(id: string): UnknownRecord | undefined {\n\t\treturn this.presences.get(id)\n\t}\n\n\tset(id: string, state: R): void {\n\t\tthis.presences.set(id, state)\n\t}\n\n\tdelete(id: string): void {\n\t\tthis.presences.delete(id)\n\t}\n\n\tvalues() {\n\t\treturn this.presences.values()\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["changes"]
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { Atom } from '@tldraw/state';
2
2
  import { AtomMap } from '@tldraw/store';
3
3
  import { DebouncedFunc } from 'lodash';
4
+ import { DebouncedFuncLeading } from 'lodash';
4
5
  import { Emitter } from 'nanoevents';
5
6
  import { RecordsDiff } from '@tldraw/store';
6
7
  import { RecordType } from '@tldraw/store';
@@ -370,6 +371,24 @@ export declare interface RoomStoreMethods<R extends UnknownRecord = UnknownRecor
370
371
  getAll(): R[];
371
372
  }
372
373
 
374
+ /**
375
+ * A snapshot of per-session state that can be persisted and used to resume a session
376
+ * after the server restarts (e.g., after Cloudflare Durable Object hibernation).
377
+ *
378
+ * Obtain via {@link TLSocketRoom.getSessionSnapshot} and restore via
379
+ * {@link TLSocketRoom.handleSocketResume}.
380
+ *
381
+ * @public
382
+ */
383
+ export declare interface SessionStateSnapshot {
384
+ serializedSchema: SerializedSchema;
385
+ isReadonly: boolean;
386
+ presenceId: null | string;
387
+ presenceRecord: null | UnknownRecord;
388
+ requiresLegacyRejection: boolean;
389
+ supportsStringAppend: boolean;
390
+ }
391
+
373
392
  /**
374
393
  * SQLite-based implementation of TLSyncStorage.
375
394
  * Stores documents, tombstones, metadata, and clock values in SQLite tables.
@@ -705,6 +724,7 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
705
724
  readonly log?: TLSyncLog;
706
725
  storage: TLSyncStorage<R>;
707
726
  private disposables;
727
+ private readonly snapshotTimers;
708
728
  /**
709
729
  * Creates a new TLSocketRoom instance for managing collaborative document synchronization.
710
730
  *
@@ -766,6 +786,8 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
766
786
  } & (SessionMeta extends void ? object : {
767
787
  meta: SessionMeta;
768
788
  })): void;
789
+ private clearSnapshotTimer;
790
+ private scheduleDebouncedSnapshot;
769
791
  /**
770
792
  * Processes a message received from a client WebSocket. Use this method in server
771
793
  * environments where WebSocket event listeners cannot be attached directly to socket
@@ -825,6 +847,63 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
825
847
  * ```
826
848
  */
827
849
  handleSocketClose(sessionId: string): void;
850
+ /**
851
+ * Resumes a previously-connected session directly into `Connected` state, bypassing
852
+ * the connect handshake. Use this after server hibernation (e.g., Cloudflare Durable
853
+ * Object hibernation) when WebSocket connections survived but all in-memory state was lost.
854
+ *
855
+ * The session is restored using a {@link SessionStateSnapshot} previously obtained
856
+ * via {@link TLSocketRoom.getSessionSnapshot}. The client is unaware the server restarted and
857
+ * continues sending messages normally.
858
+ *
859
+ * Unlike {@link TLSocketRoom.handleSocketConnect}, this method does NOT attach WebSocket event
860
+ * listeners. In hibernation environments, events are delivered via class methods
861
+ * (e.g., `webSocketMessage`) rather than `addEventListener`.
862
+ *
863
+ * @param opts - Resume options
864
+ * - sessionId - Unique identifier for the client session
865
+ * - socket - WebSocket-like object for client communication
866
+ * - snapshot - Session state snapshot from {@link TLSocketRoom.getSessionSnapshot}
867
+ * - meta - Additional session metadata (required if SessionMeta is not void)
868
+ *
869
+ * @example
870
+ * ```ts
871
+ * // After Cloudflare DO hibernation wake
872
+ * for (const ws of ctx.getWebSockets()) {
873
+ * const data = ws.deserializeAttachment()
874
+ * room.handleSocketResume({
875
+ * sessionId: data.sessionId,
876
+ * socket: ws,
877
+ * snapshot: data.snapshot,
878
+ * })
879
+ * }
880
+ * ```
881
+ */
882
+ handleSocketResume(opts: {
883
+ sessionId: string;
884
+ snapshot: SessionStateSnapshot;
885
+ socket: WebSocketMinimal;
886
+ } & (SessionMeta extends void ? object : {
887
+ meta: SessionMeta;
888
+ })): void;
889
+ /**
890
+ * Returns a snapshot of a connected session's state that can be persisted and later
891
+ * used with {@link TLSocketRoom.handleSocketResume} to restore the session after hibernation.
892
+ *
893
+ * Returns `null` if the session doesn't exist or isn't in the `Connected` state.
894
+ *
895
+ * @param sessionId - The session to snapshot
896
+ *
897
+ * @example
898
+ * ```ts
899
+ * // Store snapshot in a Cloudflare WebSocket attachment
900
+ * const snapshot = room.getSessionSnapshot(sessionId)
901
+ * if (snapshot) {
902
+ * ws.serializeAttachment({ sessionId, snapshot })
903
+ * }
904
+ * ```
905
+ */
906
+ getSessionSnapshot(sessionId: string): null | SessionStateSnapshot;
828
907
  /**
829
908
  * Returns the current document clock value. The clock is a monotonically increasing
830
909
  * integer that increments with each document change, providing a consistent ordering
@@ -1094,6 +1173,13 @@ export declare interface TLSocketRoomOptions<R extends UnknownRecord, SessionMet
1094
1173
  stringified: string;
1095
1174
  }) => void;
1096
1175
  /* Excluded from this release type: onPresenceChange */
1176
+ /**
1177
+ * When set, the room will call {@link TLSocketRoom.getSessionSnapshot} after
1178
+ * no message activity for a session for 5s and pass the result to this callback.
1179
+ * Use for persisting snapshots to WebSocket attachments (e.g. Cloudflare hibernation).
1180
+ * The room clears any pending snapshot when the session closes.
1181
+ */
1182
+ onSessionSnapshot?: (sessionId: string, snapshot: SessionStateSnapshot) => void;
1097
1183
  }
1098
1184
 
1099
1185
  /* Excluded from this release type: TLSocketServerSentDataEvent */
@@ -36,7 +36,7 @@ import {
36
36
  } from "./lib/TLSyncStorage.mjs";
37
37
  registerTldrawLibraryVersion(
38
38
  "@tldraw/sync-core",
39
- "4.5.3",
39
+ "4.6.0-canary.0bcbb3ed5bcb",
40
40
  "esm"
41
41
  );
42
42
  export {
@@ -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, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport { DurableObjectSqliteSyncWrapper } from './lib/DurableObjectSqliteSyncWrapper'\nexport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './lib/InMemorySyncStorage'\nexport { NodeSqliteWrapper, type SyncSqliteDatabase } from './lib/NodeSqliteWrapper'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport {\n\tSQLiteSyncStorage,\n\ttype TLSqliteInputValue,\n\ttype TLSqliteOutputValue,\n\ttype TLSqliteRow,\n\ttype TLSyncSqliteStatement,\n\ttype TLSyncSqliteWrapper,\n\ttype TLSyncSqliteWrapperConfig,\n} from './lib/SQLiteSyncStorage'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport {\n\tTLSocketRoom,\n\ttype OmitVoid,\n\ttype RoomStoreMethods,\n\ttype TLSocketRoomOptions,\n\ttype TLSyncLog,\n} 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\tTLSyncRoom,\n\ttype MinimalDocStore,\n\ttype PresenceStore,\n\ttype RoomSnapshot,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\nexport {\n\tloadSnapshotIntoStorage,\n\ttype TLSyncForwardDiff,\n\ttype TLSyncStorage,\n\ttype TLSyncStorageGetChangesSinceResult,\n\ttype TLSyncStorageOnChangeCallbackProps,\n\ttype TLSyncStorageTransaction,\n\ttype TLSyncStorageTransactionCallback,\n\ttype TLSyncStorageTransactionOptions,\n\ttype TLSyncStorageTransactionResult,\n} from './lib/TLSyncStorage'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": "AAAA,SAAS,oCAAoC;AAC7C,SAAS,OAAO,0BAA0B;AAC1C,SAAS,wBAAwB,wBAAwB;AACzD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASM;AACP,SAAS,sCAAsC;AAC/C,SAAS,0BAA0B,2BAA2B;AAC9D,SAAS,yBAAkD;AAC3D;AAAA,EACC;AAAA,EACA;AAAA,OAOM;AACP,SAAS,wBAAgE;AAGzE;AAAA,EACC;AAAA,OAOM;AACP,SAAS,yBAAyB;AAClC;AAAA,EACC;AAAA,OAKM;AACP;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,OAQM;AACP;AAAA,EACC;AAAA,OAKM;AACP;AAAA,EACC;AAAA,OASM;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport { DurableObjectSqliteSyncWrapper } from './lib/DurableObjectSqliteSyncWrapper'\nexport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './lib/InMemorySyncStorage'\nexport { NodeSqliteWrapper, type SyncSqliteDatabase } from './lib/NodeSqliteWrapper'\nexport {\n\tgetTlsyncProtocolVersion,\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport {\n\tSQLiteSyncStorage,\n\ttype TLSqliteInputValue,\n\ttype TLSqliteOutputValue,\n\ttype TLSqliteRow,\n\ttype TLSyncSqliteStatement,\n\ttype TLSyncSqliteWrapper,\n\ttype TLSyncSqliteWrapperConfig,\n} from './lib/SQLiteSyncStorage'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport {\n\tTLSocketRoom,\n\ttype OmitVoid,\n\ttype RoomStoreMethods,\n\ttype SessionStateSnapshot,\n\ttype TLSocketRoomOptions,\n\ttype TLSyncLog,\n} 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\tTLSyncRoom,\n\ttype MinimalDocStore,\n\ttype PresenceStore,\n\ttype RoomSnapshot,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\nexport {\n\tloadSnapshotIntoStorage,\n\ttype TLSyncForwardDiff,\n\ttype TLSyncStorage,\n\ttype TLSyncStorageGetChangesSinceResult,\n\ttype TLSyncStorageOnChangeCallbackProps,\n\ttype TLSyncStorageTransaction,\n\ttype TLSyncStorageTransactionCallback,\n\ttype TLSyncStorageTransactionOptions,\n\ttype TLSyncStorageTransactionResult,\n} from './lib/TLSyncStorage'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": "AAAA,SAAS,oCAAoC;AAC7C,SAAS,OAAO,0BAA0B;AAC1C,SAAS,wBAAwB,wBAAwB;AACzD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASM;AACP,SAAS,sCAAsC;AAC/C,SAAS,0BAA0B,2BAA2B;AAC9D,SAAS,yBAAkD;AAC3D;AAAA,EACC;AAAA,EACA;AAAA,OAOM;AACP,SAAS,wBAAgE;AAGzE;AAAA,EACC;AAAA,OAOM;AACP,SAAS,yBAAyB;AAClC;AAAA,EACC;AAAA,OAMM;AACP;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,OAQM;AACP;AAAA,EACC;AAAA,OAKM;AACP;AAAA,EACC;AAAA,OASM;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }