@tldraw/sync-core 4.5.2 → 4.6.0-canary.4ec045c286e1

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.
@@ -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 */
package/dist-cjs/index.js CHANGED
@@ -61,7 +61,7 @@ var import_TLSyncRoom = require("./lib/TLSyncRoom");
61
61
  var import_TLSyncStorage = require("./lib/TLSyncStorage");
62
62
  (0, import_utils.registerTldrawLibraryVersion)(
63
63
  "@tldraw/sync-core",
64
- "4.5.2",
64
+ "4.6.0-canary.4ec045c286e1",
65
65
  "cjs"
66
66
  );
67
67
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,mBAA0C;AAC1C,oCAAyD;AACzD,kBAcO;AACP,4CAA+C;AAC/C,iCAA8D;AAC9D,+BAA2D;AAC3D,sBASO;AACP,yBAAyE;AAGzE,+BAQO;AACP,+BAAkC;AAClC,0BAMO;AACP,0BAWO;AACP,wBAMO;AACP,2BAUO;AAAA,IAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport { 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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAA6C;AAC7C,mBAA0C;AAC1C,oCAAyD;AACzD,kBAcO;AACP,4CAA+C;AAC/C,iCAA8D;AAC9D,+BAA2D;AAC3D,sBASO;AACP,yBAAyE;AAGzE,+BAQO;AACP,+BAAkC;AAClC,0BAOO;AACP,0BAWO;AACP,wBAMO;AACP,2BAUO;AAAA,IAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -30,6 +30,15 @@ var import_TLSyncClient = require("./TLSyncClient");
30
30
  var import_TLSyncRoom = require("./TLSyncRoom");
31
31
  var import_TLSyncStorage = require("./TLSyncStorage");
32
32
  var import_chunk = require("./chunk");
33
+ function stripPresenceForSnapshot(record) {
34
+ if (record.typeName !== "instance_presence") return record;
35
+ const stripped = { ...record };
36
+ stripped.scribbles = [];
37
+ stripped.chatMessage = "";
38
+ stripped.selectedShapeIds = [];
39
+ stripped.brush = null;
40
+ return stripped;
41
+ }
33
42
  class TLSocketRoom {
34
43
  /**
35
44
  * Creates a new TLSocketRoom instance for managing collaborative document synchronization.
@@ -67,10 +76,12 @@ class TLSocketRoom {
67
76
  onPresenceChange: opts.onPresenceChange,
68
77
  schema: opts.schema ?? (0, import_tlschema.createTLSchema)(),
69
78
  log: opts.log,
70
- storage
79
+ storage,
80
+ clientTimeout: opts.clientTimeout
71
81
  });
72
82
  this.storage = storage;
73
83
  this.room.events.on("session_removed", (args) => {
84
+ this.clearSnapshotTimer(args.sessionId);
74
85
  this.sessions.delete(args.sessionId);
75
86
  if (this.opts.onSessionRemoved) {
76
87
  this.opts.onSessionRemoved(this, {
@@ -87,6 +98,7 @@ class TLSocketRoom {
87
98
  log;
88
99
  storage;
89
100
  disposables = /* @__PURE__ */ new Set();
101
+ snapshotTimers = /* @__PURE__ */ new Map();
90
102
  /**
91
103
  * Returns the number of active sessions.
92
104
  * Note that this is not the same as the number of connected sockets!
@@ -160,6 +172,25 @@ class TLSocketRoom {
160
172
  socket.addEventListener?.("close", handleSocketClose);
161
173
  socket.addEventListener?.("error", handleSocketError);
162
174
  }
175
+ clearSnapshotTimer(sessionId) {
176
+ const t = this.snapshotTimers.get(sessionId);
177
+ if (t) {
178
+ clearTimeout(t);
179
+ this.snapshotTimers.delete(sessionId);
180
+ }
181
+ }
182
+ scheduleDebouncedSnapshot(sessionId) {
183
+ if (!this.opts.onSessionSnapshot) return;
184
+ this.clearSnapshotTimer(sessionId);
185
+ this.snapshotTimers.set(
186
+ sessionId,
187
+ setTimeout(() => {
188
+ this.snapshotTimers.delete(sessionId);
189
+ const snapshot = this.getSessionSnapshot(sessionId);
190
+ if (snapshot) this.opts.onSessionSnapshot(sessionId, snapshot);
191
+ }, 5e3)
192
+ );
193
+ }
163
194
  /**
164
195
  * Processes a message received from a client WebSocket. Use this method in server
165
196
  * environments where WebSocket event listeners cannot be attached directly to socket
@@ -211,6 +242,8 @@ class TLSocketRoom {
211
242
  }
212
243
  }
213
244
  this.room.handleMessage(sessionId, res.data);
245
+ this.room.pruneSessions();
246
+ this.scheduleDebouncedSnapshot(sessionId);
214
247
  } else {
215
248
  this.log?.error?.("Error assembling message", res.error);
216
249
  this.handleSocketError(sessionId);
@@ -236,6 +269,7 @@ class TLSocketRoom {
236
269
  * ```
237
270
  */
238
271
  handleSocketError(sessionId) {
272
+ this.clearSnapshotTimer(sessionId);
239
273
  this.room.handleClose(sessionId);
240
274
  }
241
275
  /**
@@ -254,8 +288,107 @@ class TLSocketRoom {
254
288
  * ```
255
289
  */
256
290
  handleSocketClose(sessionId) {
291
+ this.clearSnapshotTimer(sessionId);
257
292
  this.room.handleClose(sessionId);
258
293
  }
294
+ /**
295
+ * Resumes a previously-connected session directly into `Connected` state, bypassing
296
+ * the connect handshake. Use this after server hibernation (e.g., Cloudflare Durable
297
+ * Object hibernation) when WebSocket connections survived but all in-memory state was lost.
298
+ *
299
+ * The session is restored using a {@link SessionStateSnapshot} previously obtained
300
+ * via {@link TLSocketRoom.getSessionSnapshot}. The client is unaware the server restarted and
301
+ * continues sending messages normally.
302
+ *
303
+ * Unlike {@link TLSocketRoom.handleSocketConnect}, this method does NOT attach WebSocket event
304
+ * listeners. In hibernation environments, events are delivered via class methods
305
+ * (e.g., `webSocketMessage`) rather than `addEventListener`.
306
+ *
307
+ * @param opts - Resume options
308
+ * - sessionId - Unique identifier for the client session
309
+ * - socket - WebSocket-like object for client communication
310
+ * - snapshot - Session state snapshot from {@link TLSocketRoom.getSessionSnapshot}
311
+ * - meta - Additional session metadata (required if SessionMeta is not void)
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * // After Cloudflare DO hibernation wake
316
+ * for (const ws of ctx.getWebSockets()) {
317
+ * const data = ws.deserializeAttachment()
318
+ * room.handleSocketResume({
319
+ * sessionId: data.sessionId,
320
+ * socket: ws,
321
+ * snapshot: data.snapshot,
322
+ * })
323
+ * }
324
+ * ```
325
+ */
326
+ handleSocketResume(opts) {
327
+ const { sessionId, socket, snapshot } = opts;
328
+ this.sessions.set(sessionId, {
329
+ assembler: new import_chunk.JsonChunkAssembler(),
330
+ socket,
331
+ unlisten: () => {
332
+ }
333
+ });
334
+ this.room.handleResumedSession({
335
+ sessionId,
336
+ isReadonly: snapshot.isReadonly,
337
+ serializedSchema: snapshot.serializedSchema,
338
+ presenceId: snapshot.presenceId,
339
+ presenceRecord: snapshot.presenceRecord,
340
+ requiresLegacyRejection: snapshot.requiresLegacyRejection,
341
+ supportsStringAppend: snapshot.supportsStringAppend,
342
+ socket: new import_ServerSocketAdapter.ServerSocketAdapter({
343
+ ws: socket,
344
+ onBeforeSendMessage: this.opts.onBeforeSendMessage ? (message, stringified) => this.opts.onBeforeSendMessage({
345
+ sessionId,
346
+ message,
347
+ stringified,
348
+ meta: this.room.sessions.get(sessionId)?.meta
349
+ }) : void 0
350
+ }),
351
+ meta: "meta" in opts ? opts.meta : void 0
352
+ });
353
+ }
354
+ /**
355
+ * Returns a snapshot of a connected session's state that can be persisted and later
356
+ * used with {@link TLSocketRoom.handleSocketResume} to restore the session after hibernation.
357
+ *
358
+ * Returns `null` if the session doesn't exist or isn't in the `Connected` state.
359
+ *
360
+ * @param sessionId - The session to snapshot
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * // Store snapshot in a Cloudflare WebSocket attachment
365
+ * const snapshot = room.getSessionSnapshot(sessionId)
366
+ * if (snapshot) {
367
+ * ws.serializeAttachment({ sessionId, snapshot })
368
+ * }
369
+ * ```
370
+ */
371
+ getSessionSnapshot(sessionId) {
372
+ const session = this.room.sessions.get(sessionId);
373
+ if (!session || session.state !== import_RoomSession.RoomSessionState.Connected) {
374
+ return null;
375
+ }
376
+ let presenceRecord = null;
377
+ if (session.presenceId) {
378
+ const record = this.room.presenceStore.get(session.presenceId);
379
+ if (record) {
380
+ presenceRecord = stripPresenceForSnapshot(record);
381
+ }
382
+ }
383
+ return {
384
+ serializedSchema: session.serializedSchema,
385
+ isReadonly: session.isReadonly,
386
+ presenceId: session.presenceId,
387
+ presenceRecord,
388
+ requiresLegacyRejection: session.requiresLegacyRejection,
389
+ supportsStringAppend: session.supportsStringAppend
390
+ };
391
+ }
259
392
  /**
260
393
  * Returns the current document clock value. The clock is a monotonically increasing
261
394
  * integer that increments with each document change, providing a consistent ordering
@@ -530,6 +663,9 @@ class TLSocketRoom {
530
663
  */
531
664
  close() {
532
665
  this.room.close();
666
+ for (const sessionId of this.snapshotTimers.keys()) {
667
+ this.clearSnapshotTimer(sessionId);
668
+ }
533
669
  this.disposables.forEach((d) => d());
534
670
  this.disposables.clear();
535
671
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/TLSocketRoom.ts"],
4
- "sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { createTLSchema, TLStoreSnapshot } from '@tldraw/tlschema'\nimport { getOwnProperty, hasOwnProperty, isEqual, structuredClone } from '@tldraw/utils'\nimport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './InMemorySyncStorage'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, TLSyncRoom } from './TLSyncRoom'\nimport {\n\tconvertStoreSnapshotToRoomSnapshot,\n\tloadSnapshotIntoStorage,\n\tTLSyncStorage,\n} from './TLSyncStorage'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n/**\n * Logging interface for TLSocketRoom operations. Provides optional methods\n * for warning and error logging during synchronization operations.\n *\n * @example\n * ```ts\n * const logger: TLSyncLog = {\n * warn: (...args) => console.warn('[SYNC]', ...args),\n * error: (...args) => console.error('[SYNC]', ...args)\n * }\n *\n * const room = new TLSocketRoom({ log: logger })\n * ```\n *\n * @public\n */\nexport interface TLSyncLog {\n\t/**\n\t * Optional warning logger for non-fatal sync issues\n\t * @param args - Arguments to log\n\t */\n\twarn?(...args: any[]): void\n\t/**\n\t * Optional error logger for sync errors and failures\n\t * @param args - Arguments to log\n\t */\n\terror?(...args: any[]): void\n}\n\n/**\n * Base options for TLSocketRoom.\n * @public\n */\nexport interface TLSocketRoomOptions<R extends UnknownRecord, SessionMeta> {\n\tstorage?: TLSyncStorage<R>\n\t/**\n\t * @deprecated use the storage option instead\n\t */\n\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t/**\n\t * @deprecated use the storage option with an onChange callback instead\n\t */\n\tonDataChange?(): void\n\tschema?: StoreSchema<R, any>\n\t// how long to wait for a client to communicate before disconnecting them\n\tclientTimeout?: number\n\tlog?: TLSyncLog\n\t// a callback that is called when a client is disconnected\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonSessionRemoved?: (\n\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t) => void\n\t// a callback that is called whenever a message is sent\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonBeforeSendMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonAfterReceiveMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t/** @internal */\n\tonPresenceChange?(): void\n}\n\n/**\n * A server-side room that manages WebSocket connections and synchronizes tldraw document state\n * between multiple clients in real-time. Each room represents a collaborative document space\n * where users can work together on drawings with automatic conflict resolution.\n *\n * TLSocketRoom handles:\n * - WebSocket connection lifecycle management\n * - Real-time synchronization of document changes\n * - Session management and presence tracking\n * - Message chunking for large payloads\n * - Automatic client timeout and cleanup\n *\n * @example\n * ```ts\n * // Basic room setup\n * const room = new TLSocketRoom({\n * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {\n * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)\n * if (numSessionsRemaining === 0) {\n * room.close()\n * }\n * },\n * onDataChange: () => {\n * console.log('Document data changed, consider persisting')\n * }\n * })\n *\n * // Handle new client connections\n * room.handleSocketConnect({\n * sessionId: 'user-session-123',\n * socket: webSocket,\n * isReadonly: false\n * })\n * ```\n *\n * @example\n * ```ts\n * // Room with initial snapshot and schema\n * const room = new TLSocketRoom({\n * initialSnapshot: existingSnapshot,\n * schema: myCustomSchema,\n * clientTimeout: 30000,\n * log: {\n * warn: (...args) => logger.warn('SYNC:', ...args),\n * error: (...args) => logger.error('SYNC:', ...args)\n * }\n * })\n *\n * // Update document programmatically\n * await room.updateStore(store => {\n * const shape = store.get('shape:abc123')\n * if (shape) {\n * shape.x = 100\n * store.put(shape)\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\n\tpublic storage: TLSyncStorage<R>\n\n\tprivate disposables = new Set<() => void>()\n\n\t/**\n\t * Creates a new TLSocketRoom instance for managing collaborative document synchronization.\n\t *\n\t * opts - Configuration options for the room\n\t * - initialSnapshot - Optional initial document state to load\n\t * - schema - Store schema defining record types and validation\n\t * - clientTimeout - Milliseconds to wait before disconnecting inactive clients\n\t * - log - Optional logger for warnings and errors\n\t * - onSessionRemoved - Called when a client session is removed\n\t * - onBeforeSendMessage - Called before sending messages to clients\n\t * - onAfterReceiveMessage - Called after receiving messages from clients\n\t * - onDataChange - Called when document data changes\n\t * - onPresenceChange - Called when presence data changes\n\t */\n\tconstructor(public readonly opts: TLSocketRoomOptions<R, SessionMeta>) {\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif (opts.storage && opts.initialSnapshot) {\n\t\t\tthrow new Error('Cannot provide both storage and initialSnapshot options')\n\t\t}\n\t\tconst storage = opts.storage\n\t\t\t? opts.storage\n\t\t\t: new InMemorySyncStorage<R>({\n\t\t\t\t\tsnapshot: convertStoreSnapshotToRoomSnapshot(\n\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\topts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT\n\t\t\t\t\t),\n\t\t\t\t})\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif ('onDataChange' in opts && opts.onDataChange) {\n\t\t\tthis.disposables.add(\n\t\t\t\tstorage.onChange(() => {\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\topts.onDataChange?.()\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tlog: opts.log,\n\t\t\tstorage,\n\t\t})\n\t\tthis.storage = storage\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Handles a new client WebSocket connection, creating a session within the room.\n\t * This should be called whenever a client establishes a WebSocket connection to join\n\t * the collaborative document.\n\t *\n\t * @param opts - Connection options\n\t * - sessionId - Unique identifier for the client session (typically from browser tab)\n\t * - socket - WebSocket-like object for client communication\n\t * - isReadonly - Whether the client can modify the document (defaults to false)\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // Handle new WebSocket connection\n\t * room.handleSocketConnect({\n\t * sessionId: 'user-session-abc123',\n\t * socket: webSocketConnection,\n\t * isReadonly: !userHasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // With session metadata\n\t * room.handleSocketConnect({\n\t * sessionId: 'session-xyz',\n\t * socket: ws,\n\t * meta: { userId: 'user-123', name: 'Alice' }\n\t * })\n\t * ```\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * Processes a message received from a client WebSocket. Use this method in server\n\t * environments where WebSocket event listeners cannot be attached directly to socket\n\t * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).\n\t *\n\t * The method handles message chunking/reassembly and forwards complete messages\n\t * to the underlying sync room for processing.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t * @param message - Raw message data from the client (string or binary)\n\t *\n\t * @example\n\t * ```ts\n\t * // In a Bun.serve handler\n\t * server.upgrade(req, {\n\t * data: { sessionId, room },\n\t * upgrade(res, req) {\n\t * // Connection established\n\t * },\n\t * message(ws, message) {\n\t * const { sessionId, room } = ws.data\n\t * room.handleSocketMessage(sessionId, message)\n\t * }\n\t * })\n\t * ```\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * Handles a WebSocket error for the specified session. Use this in server environments\n\t * where socket event listeners cannot be attached directly. This will initiate cleanup\n\t * and session removal for the affected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('error', () => {\n\t * room.handleSocketError(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Handles a WebSocket close event for the specified session. Use this in server\n\t * environments where socket event listeners cannot be attached directly. This will\n\t * initiate cleanup and session removal for the disconnected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('close', () => {\n\t * room.handleSocketClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current document clock value. The clock is a monotonically increasing\n\t * integer that increments with each document change, providing a consistent ordering\n\t * of changes across the distributed system.\n\t *\n\t * @returns The current document clock value\n\t *\n\t * @example\n\t * ```ts\n\t * const clock = room.getCurrentDocumentClock()\n\t * console.log(`Document is at version ${clock}`)\n\t * ```\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.storage.getClock()\n\t}\n\n\t/**\n\t * Retrieves a deeply cloned copy of a record from the document store.\n\t * Returns undefined if the record doesn't exist. The returned record is\n\t * safe to mutate without affecting the original store data.\n\t *\n\t * @param id - Unique identifier of the record to retrieve\n\t * @returns Deep clone of the record, or undefined if not found\n\t *\n\t * @example\n\t * ```ts\n\t * const shape = room.getRecord('shape:abc123')\n\t * if (shape) {\n\t * console.log('Shape position:', shape.x, shape.y)\n\t * // Safe to modify without affecting store\n\t * shape.x = 100\n\t * }\n\t * ```\n\t */\n\tgetRecord(id: string) {\n\t\treturn this.storage.transaction((txn) => {\n\t\t\treturn structuredClone(txn.get(id)) as any\n\t\t}).result as R\n\t}\n\n\t/**\n\t * Returns information about all active sessions in the room. Each session\n\t * represents a connected client with their current connection status and metadata.\n\t *\n\t * @returns Array of session information objects containing:\n\t * - sessionId - Unique session identifier\n\t * - isConnected - Whether the session has an active WebSocket connection\n\t * - isReadonly - Whether the session can modify the document\n\t * - meta - Custom session metadata\n\t *\n\t * @example\n\t * ```ts\n\t * const sessions = room.getSessions()\n\t * console.log(`Room has ${sessions.length} active sessions`)\n\t *\n\t * for (const session of sessions) {\n\t * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)\n\t * if (session.isReadonly) {\n\t * console.log(' (read-only access)')\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Creates a complete snapshot of the current document state, including all records\n\t * and synchronization metadata. This snapshot can be persisted to storage and used\n\t * to restore the room state later or revert to a previous version.\n\t *\n\t * @returns Complete room snapshot including documents, clock values, and tombstones\n\t * @deprecated if you need to do this use\n\t *\n\t * @example\n\t * ```ts\n\t * // Capture current state for persistence\n\t * const snapshot = room.getCurrentSnapshot()\n\t * await saveToDatabase(roomId, JSON.stringify(snapshot))\n\t *\n\t * // Later, restore from snapshot\n\t * const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))\n\t * const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })\n\t * ```\n\t */\n\tgetCurrentSnapshot() {\n\t\tif (this.storage.getSnapshot) {\n\t\t\treturn this.storage.getSnapshot()\n\t\t}\n\t\tthrow new Error('getCurrentSnapshot is not supported for this storage type')\n\t}\n\n\t/**\n\t * Retrieves all presence records from the document store. Presence records\n\t * contain ephemeral user state like cursor positions and selections.\n\t *\n\t * @returns Object mapping record IDs to presence record data\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const presence of this.room.presenceStore.values()) {\n\t\t\tresult[presence.id] = presence\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Loads a document snapshot, completely replacing the current room state.\n\t * This will disconnect all current clients and update the document to match\n\t * the provided snapshot. Use this for restoring from backups or implementing\n\t * document versioning.\n\t *\n\t * @param snapshot - Room or store snapshot to load\n\t *\n\t * @example\n\t * ```ts\n\t * // Restore from a saved snapshot\n\t * const backup = JSON.parse(await loadBackup(roomId))\n\t * room.loadSnapshot(backup)\n\t *\n\t * // All clients will be disconnected and need to reconnect\n\t * // to see the restored document state\n\t * ```\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tthis.storage.transaction((txn) => {\n\t\t\tloadSnapshotIntoStorage(txn, this.room.schema, snapshot)\n\t\t})\n\t}\n\n\t/**\n\t * Executes a transaction to modify the document store. Changes made within the\n\t * transaction are atomic and will be synchronized to all connected clients.\n\t * The transaction provides isolation from concurrent changes until it commits.\n\t *\n\t * @param updater - Function that receives store methods to make changes\n\t * - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)\n\t * - store.put(record) - Save a modified record\n\t * - store.getAll() - Get all records in the store\n\t * - store.delete(id) - Remove a record from the store\n\t * @returns Promise that resolves when the transaction completes\n\t *\n\t * @example\n\t * ```ts\n\t * // Update multiple shapes in a single transaction\n\t * await room.updateStore(store => {\n\t * const shape1 = store.get('shape:abc123')\n\t * const shape2 = store.get('shape:def456')\n\t *\n\t * if (shape1) {\n\t * shape1.x = 100\n\t * store.put(shape1)\n\t * }\n\t *\n\t * if (shape2) {\n\t * shape2.meta.approved = true\n\t * store.put(shape2)\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // Async transaction with external API call\n\t * await room.updateStore(async store => {\n\t * const doc = store.get('document:main')\n\t * if (doc) {\n\t * doc.lastModified = await getCurrentTimestamp()\n\t * store.put(doc)\n\t * }\n\t * })\n\t * ```\n\t * @deprecated use the storage.transaction method instead\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\tif (this.isClosed()) {\n\t\t\tthrow new Error('Cannot update store on a closed room')\n\t\t}\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tconst ctx = new StoreUpdateContext<R>(\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\tObject.fromEntries(this.getCurrentSnapshot().documents.map((d) => [d.state.id, d.state])),\n\t\t\tthis.room.schema\n\t\t)\n\t\ttry {\n\t\t\tawait updater(ctx)\n\t\t} finally {\n\t\t\tctx.close()\n\t\t}\n\t\tthis.storage.transaction((txn) => {\n\t\t\tfor (const [id, record] of Object.entries(ctx.updates.puts)) {\n\t\t\t\ttxn.set(id, record as R)\n\t\t\t}\n\t\t\tfor (const id of ctx.updates.deletes) {\n\t\t\t\ttxn.delete(id)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Sends a custom message to a specific client session. This allows sending\n\t * application-specific data that doesn't modify the document state, such as\n\t * notifications, chat messages, or custom commands.\n\t *\n\t * @param sessionId - Target session identifier\n\t * @param data - Custom payload to send (will be JSON serialized)\n\t *\n\t * @example\n\t * ```ts\n\t * // Send a notification to a specific user\n\t * room.sendCustomMessage('session-123', {\n\t * type: 'notification',\n\t * message: 'Your changes have been saved'\n\t * })\n\t *\n\t * // Send a chat message\n\t * room.sendCustomMessage('session-456', {\n\t * type: 'chat',\n\t * from: 'Alice',\n\t * text: 'Great work on this design!'\n\t * })\n\t * ```\n\t */\n\tsendCustomMessage(sessionId: string, data: any) {\n\t\tthis.room.sendCustomMessage(sessionId, data)\n\t}\n\n\t/**\n\t * Immediately removes a session from the room and closes its WebSocket connection.\n\t * The client will attempt to reconnect automatically unless a fatal reason is provided.\n\t *\n\t * @param sessionId - Session identifier to remove\n\t * @param fatalReason - Optional fatal error reason that prevents reconnection\n\t *\n\t * @example\n\t * ```ts\n\t * // Kick a user (they can reconnect)\n\t * room.closeSession('session-troublemaker')\n\t *\n\t * // Permanently ban a user\n\t * room.closeSession('session-banned', 'PERMISSION_DENIED')\n\t *\n\t * // Close session due to inactivity\n\t * room.closeSession('session-idle', 'TIMEOUT')\n\t * ```\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Closes the room and disconnects all connected clients. This should be called\n\t * when shutting down the room permanently, such as during server shutdown or\n\t * when the room is no longer needed. Once closed, the room cannot be reopened.\n\t *\n\t * @example\n\t * ```ts\n\t * // Clean shutdown when no users remain\n\t * if (room.getNumActiveSessions() === 0) {\n\t * await persistSnapshot(room.getCurrentSnapshot())\n\t * room.close()\n\t * }\n\t *\n\t * // Server shutdown\n\t * process.on('SIGTERM', () => {\n\t * for (const room of activeRooms.values()) {\n\t * room.close()\n\t * }\n\t * })\n\t * ```\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.disposables.clear()\n\t}\n\n\t/**\n\t * Checks whether the room has been permanently closed. Closed rooms cannot\n\t * accept new connections or process further changes.\n\t *\n\t * @returns True if the room is closed, false if still active\n\t *\n\t * @example\n\t * ```ts\n\t * if (room.isClosed()) {\n\t * console.log('Room has been shut down')\n\t * // Create a new room or redirect users\n\t * } else {\n\t * // Room is still accepting connections\n\t * room.handleSocketConnect({ sessionId, socket })\n\t * }\n\t * ```\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/**\n * Utility type that removes properties with void values from an object type.\n * This is used internally to conditionally require session metadata based on\n * whether SessionMeta extends void.\n *\n * @example\n * ```ts\n * type Example = { a: string, b: void, c: number }\n * type Result = OmitVoid<Example> // { a: string, c: number }\n * ```\n *\n * @public\n */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\n/**\n * Interface for making transactional changes to room store data. Used within\n * updateStore transactions to modify documents atomically.\n *\n * @example\n * ```ts\n * await room.updateStore((store) => {\n * const shape = store.get('shape:123')\n * if (shape) {\n * store.put({ ...shape, x: shape.x + 10 })\n * }\n * store.delete('shape:456')\n * })\n * ```\n *\n * @public\n * @deprecated use the storage.transaction method instead\n */\nexport interface RoomStoreMethods<R extends UnknownRecord = UnknownRecord> {\n\t/**\n\t * Add or update a record in the store.\n\t *\n\t * @param record - The record to store\n\t */\n\tput(record: R): void\n\t/**\n\t * Delete a record from the store.\n\t *\n\t * @param recordOrId - The record or record ID to delete\n\t */\n\tdelete(recordOrId: R | string): void\n\t/**\n\t * Get a record by its ID.\n\t *\n\t * @param id - The record ID\n\t * @returns The record or null if not found\n\t */\n\tget(id: string): R | null\n\t/**\n\t * Get all records in the store.\n\t *\n\t * @returns Array of all records\n\t */\n\tgetAll(): R[]\n}\n\n/**\n * @deprecated use the storage.transaction method instead\n */\n// eslint-disable-next-line @typescript-eslint/no-deprecated\nclass StoreUpdateContext<R extends UnknownRecord> implements RoomStoreMethods<R> {\n\tconstructor(\n\t\tprivate readonly snapshot: Record<string, UnknownRecord>,\n\t\tprivate readonly schema: StoreSchema<R, any>\n\t) {}\n\treadonly updates = {\n\t\tputs: {} as Record<string, UnknownRecord>,\n\t\tdeletes: new Set<string>(),\n\t}\n\tput(record: R): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst recordType = getOwnProperty(this.schema.types, record.typeName)\n\t\tif (!recordType) {\n\t\t\tthrow new Error(`Missing definition for record type ${record.typeName}`)\n\t\t}\n\t\tconst recordBefore = this.snapshot[record.id] ?? undefined\n\t\trecordType.validate(record, recordBefore as R)\n\n\t\tif (record.id in this.snapshot && isEqual(this.snapshot[record.id], record)) {\n\t\t\tdelete this.updates.puts[record.id]\n\t\t} else {\n\t\t\tthis.updates.puts[record.id] = structuredClone(record)\n\t\t}\n\t\tthis.updates.deletes.delete(record.id)\n\t}\n\tdelete(recordOrId: R | string): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst id = typeof recordOrId === 'string' ? recordOrId : recordOrId.id\n\t\tdelete this.updates.puts[id]\n\t\tif (this.snapshot[id]) {\n\t\t\tthis.updates.deletes.add(id)\n\t\t}\n\t}\n\tget(id: string): R | null {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tif (hasOwnProperty(this.updates.puts, id)) {\n\t\t\treturn structuredClone(this.updates.puts[id]) as R\n\t\t}\n\t\tif (this.updates.deletes.has(id)) {\n\t\t\treturn null\n\t\t}\n\t\treturn structuredClone(this.snapshot[id] ?? null) as R\n\t}\n\n\tgetAll(): R[] {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst result = Object.values(this.updates.puts)\n\t\tfor (const [id, record] of Object.entries(this.snapshot)) {\n\t\t\tif (!this.updates.deletes.has(id) && !hasOwnProperty(this.updates.puts, id)) {\n\t\t\t\tresult.push(record)\n\t\t\t}\n\t\t}\n\t\treturn structuredClone(result) as R[]\n\t}\n\n\tprivate _isClosed = false\n\tclose() {\n\t\tthis._isClosed = true\n\t}\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { SerializedSchema, StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { createTLSchema, TLInstancePresence, TLStoreSnapshot } from '@tldraw/tlschema'\nimport { getOwnProperty, hasOwnProperty, isEqual, structuredClone } from '@tldraw/utils'\nimport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './InMemorySyncStorage'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, TLSyncRoom } from './TLSyncRoom'\nimport {\n\tconvertStoreSnapshotToRoomSnapshot,\n\tloadSnapshotIntoStorage,\n\tTLSyncStorage,\n} from './TLSyncStorage'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n/**\n * Strip potentially large fields from a tldraw instance_presence record so the\n * snapshot stays small when stored in WebSocket attachments (e.g. for hibernation).\n * Keeps cursor, selection, page, and user identity; clears scribbles, chatMessage, brush.\n */\nfunction stripPresenceForSnapshot(record: UnknownRecord): UnknownRecord {\n\tif (record.typeName !== 'instance_presence') return record\n\tconst stripped = { ...record } as TLInstancePresence\n\tstripped.scribbles = []\n\tstripped.chatMessage = ''\n\tstripped.selectedShapeIds = []\n\tstripped.brush = null\n\n\treturn stripped as unknown as UnknownRecord\n}\n\n/**\n * Logging interface for TLSocketRoom operations. Provides optional methods\n * for warning and error logging during synchronization operations.\n *\n * @example\n * ```ts\n * const logger: TLSyncLog = {\n * warn: (...args) => console.warn('[SYNC]', ...args),\n * error: (...args) => console.error('[SYNC]', ...args)\n * }\n *\n * const room = new TLSocketRoom({ log: logger })\n * ```\n *\n * @public\n */\nexport interface TLSyncLog {\n\t/**\n\t * Optional warning logger for non-fatal sync issues\n\t * @param args - Arguments to log\n\t */\n\twarn?(...args: any[]): void\n\t/**\n\t * Optional error logger for sync errors and failures\n\t * @param args - Arguments to log\n\t */\n\terror?(...args: any[]): void\n}\n\n/**\n * A snapshot of per-session state that can be persisted and used to resume a session\n * after the server restarts (e.g., after Cloudflare Durable Object hibernation).\n *\n * Obtain via {@link TLSocketRoom.getSessionSnapshot} and restore via\n * {@link TLSocketRoom.handleSocketResume}.\n *\n * @public\n */\nexport interface SessionStateSnapshot {\n\tserializedSchema: SerializedSchema\n\tisReadonly: boolean\n\tpresenceId: string | null\n\tpresenceRecord: UnknownRecord | null\n\trequiresLegacyRejection: boolean\n\tsupportsStringAppend: boolean\n}\n\n/**\n * Base options for TLSocketRoom.\n * @public\n */\nexport interface TLSocketRoomOptions<R extends UnknownRecord, SessionMeta> {\n\tstorage?: TLSyncStorage<R>\n\t/**\n\t * @deprecated use the storage option instead\n\t */\n\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t/**\n\t * @deprecated use the storage option with an onChange callback instead\n\t */\n\tonDataChange?(): void\n\tschema?: StoreSchema<R, any>\n\t// how long to wait for a client to communicate before disconnecting them\n\tclientTimeout?: number\n\tlog?: TLSyncLog\n\t// a callback that is called when a client is disconnected\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonSessionRemoved?: (\n\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t) => void\n\t// a callback that is called whenever a message is sent\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonBeforeSendMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonAfterReceiveMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t/** @internal */\n\tonPresenceChange?(): void\n\t/**\n\t * When set, the room will call {@link TLSocketRoom.getSessionSnapshot} after\n\t * no message activity for a session for 5s and pass the result to this callback.\n\t * Use for persisting snapshots to WebSocket attachments (e.g. Cloudflare hibernation).\n\t * The room clears any pending snapshot when the session closes.\n\t */\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonSessionSnapshot?: (sessionId: string, snapshot: SessionStateSnapshot) => void\n}\n\n/**\n * A server-side room that manages WebSocket connections and synchronizes tldraw document state\n * between multiple clients in real-time. Each room represents a collaborative document space\n * where users can work together on drawings with automatic conflict resolution.\n *\n * TLSocketRoom handles:\n * - WebSocket connection lifecycle management\n * - Real-time synchronization of document changes\n * - Session management and presence tracking\n * - Message chunking for large payloads\n * - Automatic client timeout and cleanup\n *\n * @example\n * ```ts\n * // Basic room setup\n * const room = new TLSocketRoom({\n * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {\n * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)\n * if (numSessionsRemaining === 0) {\n * room.close()\n * }\n * },\n * onDataChange: () => {\n * console.log('Document data changed, consider persisting')\n * }\n * })\n *\n * // Handle new client connections\n * room.handleSocketConnect({\n * sessionId: 'user-session-123',\n * socket: webSocket,\n * isReadonly: false\n * })\n * ```\n *\n * @example\n * ```ts\n * // Room with initial snapshot and schema\n * const room = new TLSocketRoom({\n * initialSnapshot: existingSnapshot,\n * schema: myCustomSchema,\n * clientTimeout: 30000,\n * log: {\n * warn: (...args) => logger.warn('SYNC:', ...args),\n * error: (...args) => logger.error('SYNC:', ...args)\n * }\n * })\n *\n * // Update document programmatically\n * await room.updateStore(store => {\n * const shape = store.get('shape:abc123')\n * if (shape) {\n * shape.x = 100\n * store.put(shape)\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\n\tpublic storage: TLSyncStorage<R>\n\n\tprivate disposables = new Set<() => void>()\n\tprivate readonly snapshotTimers = new Map<string, ReturnType<typeof setTimeout>>()\n\n\t/**\n\t * Creates a new TLSocketRoom instance for managing collaborative document synchronization.\n\t *\n\t * opts - Configuration options for the room\n\t * - initialSnapshot - Optional initial document state to load\n\t * - schema - Store schema defining record types and validation\n\t * - clientTimeout - Milliseconds to wait before disconnecting inactive clients\n\t * - log - Optional logger for warnings and errors\n\t * - onSessionRemoved - Called when a client session is removed\n\t * - onBeforeSendMessage - Called before sending messages to clients\n\t * - onAfterReceiveMessage - Called after receiving messages from clients\n\t * - onDataChange - Called when document data changes\n\t * - onPresenceChange - Called when presence data changes\n\t */\n\tconstructor(public readonly opts: TLSocketRoomOptions<R, SessionMeta>) {\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif (opts.storage && opts.initialSnapshot) {\n\t\t\tthrow new Error('Cannot provide both storage and initialSnapshot options')\n\t\t}\n\t\tconst storage = opts.storage\n\t\t\t? opts.storage\n\t\t\t: new InMemorySyncStorage<R>({\n\t\t\t\t\tsnapshot: convertStoreSnapshotToRoomSnapshot(\n\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\topts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT\n\t\t\t\t\t),\n\t\t\t\t})\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif ('onDataChange' in opts && opts.onDataChange) {\n\t\t\tthis.disposables.add(\n\t\t\t\tstorage.onChange(() => {\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\topts.onDataChange?.()\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tlog: opts.log,\n\t\t\tstorage,\n\t\t\tclientTimeout: opts.clientTimeout,\n\t\t})\n\t\tthis.storage = storage\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.clearSnapshotTimer(args.sessionId)\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Handles a new client WebSocket connection, creating a session within the room.\n\t * This should be called whenever a client establishes a WebSocket connection to join\n\t * the collaborative document.\n\t *\n\t * @param opts - Connection options\n\t * - sessionId - Unique identifier for the client session (typically from browser tab)\n\t * - socket - WebSocket-like object for client communication\n\t * - isReadonly - Whether the client can modify the document (defaults to false)\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // Handle new WebSocket connection\n\t * room.handleSocketConnect({\n\t * sessionId: 'user-session-abc123',\n\t * socket: webSocketConnection,\n\t * isReadonly: !userHasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // With session metadata\n\t * room.handleSocketConnect({\n\t * sessionId: 'session-xyz',\n\t * socket: ws,\n\t * meta: { userId: 'user-123', name: 'Alice' }\n\t * })\n\t * ```\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\tprivate clearSnapshotTimer(sessionId: string) {\n\t\tconst t = this.snapshotTimers.get(sessionId)\n\t\tif (t) {\n\t\t\tclearTimeout(t)\n\t\t\tthis.snapshotTimers.delete(sessionId)\n\t\t}\n\t}\n\n\tprivate scheduleDebouncedSnapshot(sessionId: string) {\n\t\tif (!this.opts.onSessionSnapshot) return\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.snapshotTimers.set(\n\t\t\tsessionId,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.snapshotTimers.delete(sessionId)\n\t\t\t\tconst snapshot = this.getSessionSnapshot(sessionId)\n\t\t\t\tif (snapshot) this.opts.onSessionSnapshot!(sessionId, snapshot)\n\t\t\t}, 5000)\n\t\t)\n\t}\n\n\t/**\n\t * Processes a message received from a client WebSocket. Use this method in server\n\t * environments where WebSocket event listeners cannot be attached directly to socket\n\t * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).\n\t *\n\t * The method handles message chunking/reassembly and forwards complete messages\n\t * to the underlying sync room for processing.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t * @param message - Raw message data from the client (string or binary)\n\t *\n\t * @example\n\t * ```ts\n\t * // In a Bun.serve handler\n\t * server.upgrade(req, {\n\t * data: { sessionId, room },\n\t * upgrade(res, req) {\n\t * // Connection established\n\t * },\n\t * message(ws, message) {\n\t * const { sessionId, room } = ws.data\n\t * room.handleSocketMessage(sessionId, message)\n\t * }\n\t * })\n\t * ```\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t\tthis.room.pruneSessions()\n\t\t\t\tthis.scheduleDebouncedSnapshot(sessionId)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * Handles a WebSocket error for the specified session. Use this in server environments\n\t * where socket event listeners cannot be attached directly. This will initiate cleanup\n\t * and session removal for the affected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('error', () => {\n\t * room.handleSocketError(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Handles a WebSocket close event for the specified session. Use this in server\n\t * environments where socket event listeners cannot be attached directly. This will\n\t * initiate cleanup and session removal for the disconnected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('close', () => {\n\t * room.handleSocketClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Resumes a previously-connected session directly into `Connected` state, bypassing\n\t * the connect handshake. Use this after server hibernation (e.g., Cloudflare Durable\n\t * Object hibernation) when WebSocket connections survived but all in-memory state was lost.\n\t *\n\t * The session is restored using a {@link SessionStateSnapshot} previously obtained\n\t * via {@link TLSocketRoom.getSessionSnapshot}. The client is unaware the server restarted and\n\t * continues sending messages normally.\n\t *\n\t * Unlike {@link TLSocketRoom.handleSocketConnect}, this method does NOT attach WebSocket event\n\t * listeners. In hibernation environments, events are delivered via class methods\n\t * (e.g., `webSocketMessage`) rather than `addEventListener`.\n\t *\n\t * @param opts - Resume options\n\t * - sessionId - Unique identifier for the client session\n\t * - socket - WebSocket-like object for client communication\n\t * - snapshot - Session state snapshot from {@link TLSocketRoom.getSessionSnapshot}\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // After Cloudflare DO hibernation wake\n\t * for (const ws of ctx.getWebSockets()) {\n\t * const data = ws.deserializeAttachment()\n\t * room.handleSocketResume({\n\t * sessionId: data.sessionId,\n\t * socket: ws,\n\t * snapshot: data.snapshot,\n\t * })\n\t * }\n\t * ```\n\t */\n\thandleSocketResume(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tsnapshot: SessionStateSnapshot\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, snapshot } = opts\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\t// no-op: hibernation environments use class methods, not addEventListener\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleResumedSession({\n\t\t\tsessionId,\n\t\t\tisReadonly: snapshot.isReadonly,\n\t\t\tserializedSchema: snapshot.serializedSchema,\n\t\t\tpresenceId: snapshot.presenceId,\n\t\t\tpresenceRecord: snapshot.presenceRecord,\n\t\t\trequiresLegacyRejection: snapshot.requiresLegacyRejection,\n\t\t\tsupportsStringAppend: snapshot.supportsStringAppend,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\t}\n\n\t/**\n\t * Returns a snapshot of a connected session's state that can be persisted and later\n\t * used with {@link TLSocketRoom.handleSocketResume} to restore the session after hibernation.\n\t *\n\t * Returns `null` if the session doesn't exist or isn't in the `Connected` state.\n\t *\n\t * @param sessionId - The session to snapshot\n\t *\n\t * @example\n\t * ```ts\n\t * // Store snapshot in a Cloudflare WebSocket attachment\n\t * const snapshot = room.getSessionSnapshot(sessionId)\n\t * if (snapshot) {\n\t * ws.serializeAttachment({ sessionId, snapshot })\n\t * }\n\t * ```\n\t */\n\tgetSessionSnapshot(sessionId: string): SessionStateSnapshot | null {\n\t\tconst session = this.room.sessions.get(sessionId)\n\t\tif (!session || session.state !== RoomSessionState.Connected) {\n\t\t\treturn null\n\t\t}\n\n\t\tlet presenceRecord: UnknownRecord | null = null\n\t\tif (session.presenceId) {\n\t\t\tconst record = this.room.presenceStore.get(session.presenceId)\n\t\t\tif (record) {\n\t\t\t\tpresenceRecord = stripPresenceForSnapshot(record as UnknownRecord)\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tserializedSchema: session.serializedSchema,\n\t\t\tisReadonly: session.isReadonly,\n\t\t\tpresenceId: session.presenceId,\n\t\t\tpresenceRecord,\n\t\t\trequiresLegacyRejection: session.requiresLegacyRejection,\n\t\t\tsupportsStringAppend: session.supportsStringAppend,\n\t\t}\n\t}\n\n\t/**\n\t * Returns the current document clock value. The clock is a monotonically increasing\n\t * integer that increments with each document change, providing a consistent ordering\n\t * of changes across the distributed system.\n\t *\n\t * @returns The current document clock value\n\t *\n\t * @example\n\t * ```ts\n\t * const clock = room.getCurrentDocumentClock()\n\t * console.log(`Document is at version ${clock}`)\n\t * ```\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.storage.getClock()\n\t}\n\n\t/**\n\t * Retrieves a deeply cloned copy of a record from the document store.\n\t * Returns undefined if the record doesn't exist. The returned record is\n\t * safe to mutate without affecting the original store data.\n\t *\n\t * @param id - Unique identifier of the record to retrieve\n\t * @returns Deep clone of the record, or undefined if not found\n\t *\n\t * @example\n\t * ```ts\n\t * const shape = room.getRecord('shape:abc123')\n\t * if (shape) {\n\t * console.log('Shape position:', shape.x, shape.y)\n\t * // Safe to modify without affecting store\n\t * shape.x = 100\n\t * }\n\t * ```\n\t */\n\tgetRecord(id: string) {\n\t\treturn this.storage.transaction((txn) => {\n\t\t\treturn structuredClone(txn.get(id)) as any\n\t\t}).result as R\n\t}\n\n\t/**\n\t * Returns information about all active sessions in the room. Each session\n\t * represents a connected client with their current connection status and metadata.\n\t *\n\t * @returns Array of session information objects containing:\n\t * - sessionId - Unique session identifier\n\t * - isConnected - Whether the session has an active WebSocket connection\n\t * - isReadonly - Whether the session can modify the document\n\t * - meta - Custom session metadata\n\t *\n\t * @example\n\t * ```ts\n\t * const sessions = room.getSessions()\n\t * console.log(`Room has ${sessions.length} active sessions`)\n\t *\n\t * for (const session of sessions) {\n\t * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)\n\t * if (session.isReadonly) {\n\t * console.log(' (read-only access)')\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Creates a complete snapshot of the current document state, including all records\n\t * and synchronization metadata. This snapshot can be persisted to storage and used\n\t * to restore the room state later or revert to a previous version.\n\t *\n\t * @returns Complete room snapshot including documents, clock values, and tombstones\n\t * @deprecated if you need to do this use\n\t *\n\t * @example\n\t * ```ts\n\t * // Capture current state for persistence\n\t * const snapshot = room.getCurrentSnapshot()\n\t * await saveToDatabase(roomId, JSON.stringify(snapshot))\n\t *\n\t * // Later, restore from snapshot\n\t * const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))\n\t * const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })\n\t * ```\n\t */\n\tgetCurrentSnapshot() {\n\t\tif (this.storage.getSnapshot) {\n\t\t\treturn this.storage.getSnapshot()\n\t\t}\n\t\tthrow new Error('getCurrentSnapshot is not supported for this storage type')\n\t}\n\n\t/**\n\t * Retrieves all presence records from the document store. Presence records\n\t * contain ephemeral user state like cursor positions and selections.\n\t *\n\t * @returns Object mapping record IDs to presence record data\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const presence of this.room.presenceStore.values()) {\n\t\t\tresult[presence.id] = presence\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Loads a document snapshot, completely replacing the current room state.\n\t * This will disconnect all current clients and update the document to match\n\t * the provided snapshot. Use this for restoring from backups or implementing\n\t * document versioning.\n\t *\n\t * @param snapshot - Room or store snapshot to load\n\t *\n\t * @example\n\t * ```ts\n\t * // Restore from a saved snapshot\n\t * const backup = JSON.parse(await loadBackup(roomId))\n\t * room.loadSnapshot(backup)\n\t *\n\t * // All clients will be disconnected and need to reconnect\n\t * // to see the restored document state\n\t * ```\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tthis.storage.transaction((txn) => {\n\t\t\tloadSnapshotIntoStorage(txn, this.room.schema, snapshot)\n\t\t})\n\t}\n\n\t/**\n\t * Executes a transaction to modify the document store. Changes made within the\n\t * transaction are atomic and will be synchronized to all connected clients.\n\t * The transaction provides isolation from concurrent changes until it commits.\n\t *\n\t * @param updater - Function that receives store methods to make changes\n\t * - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)\n\t * - store.put(record) - Save a modified record\n\t * - store.getAll() - Get all records in the store\n\t * - store.delete(id) - Remove a record from the store\n\t * @returns Promise that resolves when the transaction completes\n\t *\n\t * @example\n\t * ```ts\n\t * // Update multiple shapes in a single transaction\n\t * await room.updateStore(store => {\n\t * const shape1 = store.get('shape:abc123')\n\t * const shape2 = store.get('shape:def456')\n\t *\n\t * if (shape1) {\n\t * shape1.x = 100\n\t * store.put(shape1)\n\t * }\n\t *\n\t * if (shape2) {\n\t * shape2.meta.approved = true\n\t * store.put(shape2)\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // Async transaction with external API call\n\t * await room.updateStore(async store => {\n\t * const doc = store.get('document:main')\n\t * if (doc) {\n\t * doc.lastModified = await getCurrentTimestamp()\n\t * store.put(doc)\n\t * }\n\t * })\n\t * ```\n\t * @deprecated use the storage.transaction method instead\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\tif (this.isClosed()) {\n\t\t\tthrow new Error('Cannot update store on a closed room')\n\t\t}\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tconst ctx = new StoreUpdateContext<R>(\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\tObject.fromEntries(this.getCurrentSnapshot().documents.map((d) => [d.state.id, d.state])),\n\t\t\tthis.room.schema\n\t\t)\n\t\ttry {\n\t\t\tawait updater(ctx)\n\t\t} finally {\n\t\t\tctx.close()\n\t\t}\n\t\tthis.storage.transaction((txn) => {\n\t\t\tfor (const [id, record] of Object.entries(ctx.updates.puts)) {\n\t\t\t\ttxn.set(id, record as R)\n\t\t\t}\n\t\t\tfor (const id of ctx.updates.deletes) {\n\t\t\t\ttxn.delete(id)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Sends a custom message to a specific client session. This allows sending\n\t * application-specific data that doesn't modify the document state, such as\n\t * notifications, chat messages, or custom commands.\n\t *\n\t * @param sessionId - Target session identifier\n\t * @param data - Custom payload to send (will be JSON serialized)\n\t *\n\t * @example\n\t * ```ts\n\t * // Send a notification to a specific user\n\t * room.sendCustomMessage('session-123', {\n\t * type: 'notification',\n\t * message: 'Your changes have been saved'\n\t * })\n\t *\n\t * // Send a chat message\n\t * room.sendCustomMessage('session-456', {\n\t * type: 'chat',\n\t * from: 'Alice',\n\t * text: 'Great work on this design!'\n\t * })\n\t * ```\n\t */\n\tsendCustomMessage(sessionId: string, data: any) {\n\t\tthis.room.sendCustomMessage(sessionId, data)\n\t}\n\n\t/**\n\t * Immediately removes a session from the room and closes its WebSocket connection.\n\t * The client will attempt to reconnect automatically unless a fatal reason is provided.\n\t *\n\t * @param sessionId - Session identifier to remove\n\t * @param fatalReason - Optional fatal error reason that prevents reconnection\n\t *\n\t * @example\n\t * ```ts\n\t * // Kick a user (they can reconnect)\n\t * room.closeSession('session-troublemaker')\n\t *\n\t * // Permanently ban a user\n\t * room.closeSession('session-banned', 'PERMISSION_DENIED')\n\t *\n\t * // Close session due to inactivity\n\t * room.closeSession('session-idle', 'TIMEOUT')\n\t * ```\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Closes the room and disconnects all connected clients. This should be called\n\t * when shutting down the room permanently, such as during server shutdown or\n\t * when the room is no longer needed. Once closed, the room cannot be reopened.\n\t *\n\t * @example\n\t * ```ts\n\t * // Clean shutdown when no users remain\n\t * if (room.getNumActiveSessions() === 0) {\n\t * await persistSnapshot(room.getCurrentSnapshot())\n\t * room.close()\n\t * }\n\t *\n\t * // Server shutdown\n\t * process.on('SIGTERM', () => {\n\t * for (const room of activeRooms.values()) {\n\t * room.close()\n\t * }\n\t * })\n\t * ```\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t\tfor (const sessionId of this.snapshotTimers.keys()) {\n\t\t\tthis.clearSnapshotTimer(sessionId)\n\t\t}\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.disposables.clear()\n\t}\n\n\t/**\n\t * Checks whether the room has been permanently closed. Closed rooms cannot\n\t * accept new connections or process further changes.\n\t *\n\t * @returns True if the room is closed, false if still active\n\t *\n\t * @example\n\t * ```ts\n\t * if (room.isClosed()) {\n\t * console.log('Room has been shut down')\n\t * // Create a new room or redirect users\n\t * } else {\n\t * // Room is still accepting connections\n\t * room.handleSocketConnect({ sessionId, socket })\n\t * }\n\t * ```\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/**\n * Utility type that removes properties with void values from an object type.\n * This is used internally to conditionally require session metadata based on\n * whether SessionMeta extends void.\n *\n * @example\n * ```ts\n * type Example = { a: string, b: void, c: number }\n * type Result = OmitVoid<Example> // { a: string, c: number }\n * ```\n *\n * @public\n */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\n/**\n * Interface for making transactional changes to room store data. Used within\n * updateStore transactions to modify documents atomically.\n *\n * @example\n * ```ts\n * await room.updateStore((store) => {\n * const shape = store.get('shape:123')\n * if (shape) {\n * store.put({ ...shape, x: shape.x + 10 })\n * }\n * store.delete('shape:456')\n * })\n * ```\n *\n * @public\n * @deprecated use the storage.transaction method instead\n */\nexport interface RoomStoreMethods<R extends UnknownRecord = UnknownRecord> {\n\t/**\n\t * Add or update a record in the store.\n\t *\n\t * @param record - The record to store\n\t */\n\tput(record: R): void\n\t/**\n\t * Delete a record from the store.\n\t *\n\t * @param recordOrId - The record or record ID to delete\n\t */\n\tdelete(recordOrId: R | string): void\n\t/**\n\t * Get a record by its ID.\n\t *\n\t * @param id - The record ID\n\t * @returns The record or null if not found\n\t */\n\tget(id: string): R | null\n\t/**\n\t * Get all records in the store.\n\t *\n\t * @returns Array of all records\n\t */\n\tgetAll(): R[]\n}\n\n/**\n * @deprecated use the storage.transaction method instead\n */\n// eslint-disable-next-line @typescript-eslint/no-deprecated\nclass StoreUpdateContext<R extends UnknownRecord> implements RoomStoreMethods<R> {\n\tconstructor(\n\t\tprivate readonly snapshot: Record<string, UnknownRecord>,\n\t\tprivate readonly schema: StoreSchema<R, any>\n\t) {}\n\treadonly updates = {\n\t\tputs: {} as Record<string, UnknownRecord>,\n\t\tdeletes: new Set<string>(),\n\t}\n\tput(record: R): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst recordType = getOwnProperty(this.schema.types, record.typeName)\n\t\tif (!recordType) {\n\t\t\tthrow new Error(`Missing definition for record type ${record.typeName}`)\n\t\t}\n\t\tconst recordBefore = this.snapshot[record.id] ?? undefined\n\t\trecordType.validate(record, recordBefore as R)\n\n\t\tif (record.id in this.snapshot && isEqual(this.snapshot[record.id], record)) {\n\t\t\tdelete this.updates.puts[record.id]\n\t\t} else {\n\t\t\tthis.updates.puts[record.id] = structuredClone(record)\n\t\t}\n\t\tthis.updates.deletes.delete(record.id)\n\t}\n\tdelete(recordOrId: R | string): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst id = typeof recordOrId === 'string' ? recordOrId : recordOrId.id\n\t\tdelete this.updates.puts[id]\n\t\tif (this.snapshot[id]) {\n\t\t\tthis.updates.deletes.add(id)\n\t\t}\n\t}\n\tget(id: string): R | null {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tif (hasOwnProperty(this.updates.puts, id)) {\n\t\t\treturn structuredClone(this.updates.puts[id]) as R\n\t\t}\n\t\tif (this.updates.deletes.has(id)) {\n\t\t\treturn null\n\t\t}\n\t\treturn structuredClone(this.snapshot[id] ?? null) as R\n\t}\n\n\tgetAll(): R[] {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst result = Object.values(this.updates.puts)\n\t\tfor (const [id, record] of Object.entries(this.snapshot)) {\n\t\t\tif (!this.updates.deletes.has(id) && !hasOwnProperty(this.updates.puts, id)) {\n\t\t\t\tresult.push(record)\n\t\t\t}\n\t\t}\n\t\treturn structuredClone(result) as R[]\n\t}\n\n\tprivate _isClosed = false\n\tclose() {\n\t\tthis._isClosed = true\n\t}\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }