@tldraw/sync-core 5.3.0-internal.d205573d66cb → 5.3.0-next.299378752aaf

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist-cjs/index.d.ts +3 -76
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/InMemorySyncStorage.js +20 -55
  5. package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
  6. package/dist-cjs/lib/RoomSession.js.map +1 -1
  7. package/dist-cjs/lib/SQLiteSyncStorage.js +52 -115
  8. package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
  9. package/dist-cjs/lib/TLSocketRoom.js +2 -26
  10. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  11. package/dist-cjs/lib/TLSyncClient.js +1 -6
  12. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  13. package/dist-cjs/lib/TLSyncRoom.js +5 -43
  14. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  15. package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
  16. package/dist-cjs/lib/protocol.js.map +2 -2
  17. package/dist-esm/index.d.mts +3 -76
  18. package/dist-esm/index.mjs +1 -1
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/InMemorySyncStorage.mjs +20 -55
  21. package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
  22. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  23. package/dist-esm/lib/SQLiteSyncStorage.mjs +52 -115
  24. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
  25. package/dist-esm/lib/TLSocketRoom.mjs +2 -26
  26. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  27. package/dist-esm/lib/TLSyncClient.mjs +1 -6
  28. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  29. package/dist-esm/lib/TLSyncRoom.mjs +5 -43
  30. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  31. package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
  32. package/dist-esm/lib/protocol.mjs.map +2 -2
  33. package/package.json +6 -6
  34. package/src/index.ts +0 -1
  35. package/src/lib/InMemorySyncStorage.ts +21 -74
  36. package/src/lib/RoomSession.ts +2 -7
  37. package/src/lib/SQLiteSyncStorage.test.ts +2 -29
  38. package/src/lib/SQLiteSyncStorage.ts +59 -158
  39. package/src/lib/TLSocketRoom.ts +2 -53
  40. package/src/lib/TLSyncClient.test.ts +2 -9
  41. package/src/lib/TLSyncClient.ts +3 -15
  42. package/src/lib/TLSyncRoom.ts +4 -76
  43. package/src/lib/TLSyncStorage.ts +0 -8
  44. package/src/lib/protocol.ts +0 -17
  45. package/src/test/TLSocketRoom.test.ts +2 -37
  46. package/src/test/TLSyncRoom.test.ts +0 -25
  47. package/src/test/TestServer.ts +4 -14
  48. package/src/test/storageContractSuite.ts +0 -79
  49. package/src/test/upgradeDowngrade.test.ts +0 -47
  50. package/src/test/objectStore.test.ts +0 -387
@@ -109,12 +109,6 @@ class TLSyncRoom {
109
109
  storage;
110
110
  serializedSchema;
111
111
  documentTypes;
112
- /**
113
- * Record types served by the object-store lane. Object records ride the same wire messages
114
- * as document records but are gated by the session's `objectAccess` instead of `isReadonly`,
115
- * and are excluded from `documentTypes` so hosts can persist them in a separate lane.
116
- */
117
- objectTypes;
118
112
  presenceType;
119
113
  log;
120
114
  schema;
@@ -123,7 +117,6 @@ class TLSyncRoom {
123
117
  this.schema = opts.schema;
124
118
  this.log = opts.log;
125
119
  this.onPresenceChange = opts.onPresenceChange;
126
- this.onCommittedChanges = opts.onCommittedChanges;
127
120
  this.storage = opts.storage;
128
121
  this.sessionIdleTimeout = opts.clientTimeout ?? import_RoomSession.SESSION_IDLE_TIMEOUT;
129
122
  (0, import_utils.assert)(
@@ -131,17 +124,8 @@ class TLSyncRoom {
131
124
  "TLSyncRoom is supposed to run either on Cloudflare Workersor on a 18+ version of Node.js, which both support the native structuredClone API"
132
125
  );
133
126
  this.serializedSchema = JSON.parse(JSON.stringify(this.schema.serialize()));
134
- this.objectTypes = new Set(opts.objectTypes ?? []);
135
- for (const typeName of this.objectTypes) {
136
- const type = (0, import_utils.getOwnProperty)(this.schema.types, typeName);
137
- (0, import_utils.assert)(type, `TLSyncRoom: object type '${typeName}' is not registered in the schema`);
138
- (0, import_utils.assert)(
139
- type.scope === "document",
140
- `TLSyncRoom: object type '${typeName}' must have scope 'document', got '${type.scope}'`
141
- );
142
- }
143
127
  this.documentTypes = new Set(
144
- Object.values(this.schema.types).filter((t) => t.scope === "document" && !this.objectTypes.has(t.typeName)).map((t) => t.typeName)
128
+ Object.values(this.schema.types).filter((t) => t.scope === "document").map((t) => t.typeName)
145
129
  );
146
130
  const presenceTypes = new Set(
147
131
  Object.values(this.schema.types).filter((t) => t.scope === "presence")
@@ -278,7 +262,6 @@ class TLSyncRoom {
278
262
  cancellationTime: Date.now(),
279
263
  meta: session.meta,
280
264
  isReadonly: session.isReadonly,
281
- objectAccess: session.objectAccess,
282
265
  requiresLegacyRejection: session.requiresLegacyRejection,
283
266
  supportsStringAppend: session.supportsStringAppend
284
267
  });
@@ -370,7 +353,7 @@ class TLSyncRoom {
370
353
  * @internal
371
354
  */
372
355
  handleNewSession(opts) {
373
- const { sessionId, socket, meta, isReadonly, objectAccess } = opts;
356
+ const { sessionId, socket, meta, isReadonly } = opts;
374
357
  const existing = this.sessions.get(sessionId);
375
358
  this.sessions.set(sessionId, {
376
359
  state: import_RoomSession.RoomSessionState.AwaitingConnectMessage,
@@ -380,7 +363,6 @@ class TLSyncRoom {
380
363
  sessionStartTime: Date.now(),
381
364
  meta,
382
365
  isReadonly: isReadonly ?? false,
383
- objectAccess: objectAccess ?? "write",
384
366
  // this gets set later during handleConnectMessage
385
367
  requiresLegacyRejection: false,
386
368
  supportsStringAppend: true
@@ -400,7 +382,6 @@ class TLSyncRoom {
400
382
  socket,
401
383
  meta,
402
384
  isReadonly,
403
- objectAccess,
404
385
  serializedSchema,
405
386
  presenceId,
406
387
  presenceRecord,
@@ -421,7 +402,6 @@ class TLSyncRoom {
421
402
  outstandingDataMessages: [],
422
403
  meta,
423
404
  isReadonly,
424
- objectAccess: objectAccess ?? "write",
425
405
  requiresLegacyRejection,
426
406
  supportsStringAppend
427
407
  });
@@ -682,7 +662,6 @@ class TLSyncRoom {
682
662
  supportsStringAppend: session.supportsStringAppend,
683
663
  meta: session.meta,
684
664
  isReadonly: session.isReadonly,
685
- objectAccess: session.objectAccess,
686
665
  requiresLegacyRejection: session.requiresLegacyRejection
687
666
  });
688
667
  this._unsafe_sendMessage(session.sessionId, msg);
@@ -727,8 +706,7 @@ class TLSyncRoom {
727
706
  schema: this.schema.serialize(),
728
707
  serverClock: txn.getClock(),
729
708
  diff: { ...presenceDiff.value, ...docDiff },
730
- isReadonly: session.isReadonly,
731
- objectAccess: session.objectAccess
709
+ isReadonly: session.isReadonly
732
710
  };
733
711
  });
734
712
  this.lastDocumentClock = documentClock;
@@ -840,13 +818,11 @@ class TLSyncRoom {
840
818
  }
841
819
  }
842
820
  }
843
- const canWrite = (typeName) => !session || (this.objectTypes.has(typeName) ? session.objectAccess !== "read" : !session.isReadonly);
844
- if (message.diff) {
821
+ if (message.diff && !session?.isReadonly) {
845
822
  for (const [id, op] of (0, import_utils.objectMapEntriesIterable)(message.diff)) {
846
823
  switch (op[0]) {
847
824
  case import_diff.RecordOpType.Put: {
848
- if (!canWrite(op[1].typeName)) continue;
849
- if (!this.documentTypes.has(op[1].typeName) && !this.objectTypes.has(op[1].typeName)) {
825
+ if (!this.documentTypes.has(op[1].typeName)) {
850
826
  throw new import_TLSyncClient.TLSyncError(
851
827
  "invalid record",
852
828
  import_TLSyncClient.TLSyncErrorCloseEventReason.INVALID_RECORD
@@ -856,9 +832,6 @@ class TLSyncRoom {
856
832
  break;
857
833
  }
858
834
  case import_diff.RecordOpType.Patch: {
859
- const doc = txn.get(id);
860
- if (!doc) continue;
861
- if (!canWrite(doc.typeName)) continue;
862
835
  patchDocument(txn, docChanges, id, op[1]);
863
836
  break;
864
837
  }
@@ -867,7 +840,6 @@ class TLSyncRoom {
867
840
  if (!doc) {
868
841
  continue;
869
842
  }
870
- if (!canWrite(doc.typeName)) continue;
871
843
  txn.delete(id);
872
844
  propagateOp(docChanges, id, op, doc, void 0);
873
845
  break;
@@ -942,16 +914,6 @@ class TLSyncRoom {
942
914
  this.onPresenceChange?.();
943
915
  });
944
916
  }
945
- if (result.docChanges.diffs && this.onCommittedChanges) {
946
- const diff = result.docChanges.diffs.diff;
947
- queueMicrotask(() => {
948
- try {
949
- this.onCommittedChanges?.({ diff, documentClock });
950
- } catch (e) {
951
- this.log?.error?.("onCommittedChanges threw", e);
952
- }
953
- });
954
- }
955
917
  }
956
918
  /**
957
919
  * Handle the event when a client disconnects. Cleans up the session and
@@ -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\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\tTLObjectStoreAccess,\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\t/**\n\t * Record types served by the object-store lane. Object records ride the same wire messages\n\t * as document records but are gated by the session's `objectAccess` instead of `isReadonly`,\n\t * and are excluded from `documentTypes` so hosts can persist them in a separate lane.\n\t */\n\treadonly objectTypes: 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 onCommittedChanges?(args: { diff: TLSyncForwardDiff<R>; documentClock: number }): 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\t/**\n\t\t * Called once after a client push commits, with the committed document diff. Fires for\n\t\t * local and remote pushes. Use this to react to document changes (e.g. persist certain\n\t\t * record types to a separate lane, or project them to an external store) as soon as they\n\t\t * commit. Best-effort \u2014 do not throw; do not block.\n\t\t */\n\t\tonCommittedChanges?(args: { diff: TLSyncForwardDiff<R>; documentClock: number }): void\n\t\t/**\n\t\t * Record type names to serve through the object-store lane instead of the document lane.\n\t\t * Each must be a document-scoped type registered in the schema. Object-lane writes are\n\t\t * gated per session by `objectAccess` rather than `isReadonly`.\n\t\t */\n\t\tobjectTypes?: readonly string[]\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.onCommittedChanges = opts.onCommittedChanges\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.objectTypes = new Set(opts.objectTypes ?? [])\n\t\tfor (const typeName of this.objectTypes) {\n\t\t\tconst type = getOwnProperty(this.schema.types, typeName)\n\t\t\tassert(type, `TLSyncRoom: object type '${typeName}' is not registered in the schema`)\n\t\t\tassert(\n\t\t\t\ttype.scope === 'document',\n\t\t\t\t`TLSyncRoom: object type '${typeName}' must have scope 'document', got '${type.scope}'`\n\t\t\t)\n\t\t}\n\n\t\t// object-lane types are partitioned out of the document lane\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' && !this.objectTypes.has(t.typeName))\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\t// hand the buffer over and start a fresh one, rather than truncating in\n\t\t\t// place, so sockets that defer serialization don't see an emptied array\n\t\t\tconst data = session.outstandingDataMessages\n\t\t\tsession.outstandingDataMessages = []\n\t\t\tsession.socket.sendMessage({ type: 'data', data })\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\tobjectAccess: session.objectAccess,\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\tobjectAccess?: TLObjectStoreAccess\n\t}) {\n\t\tconst { sessionId, socket, meta, isReadonly, objectAccess } = 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\tobjectAccess: objectAccess ?? 'write',\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\tobjectAccess?: TLObjectStoreAccess\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\tobjectAccess,\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\tobjectAccess: objectAccess ?? 'write',\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\t/**\n\t * Work out whether a client we can't reconcile schemas with is running a newer or older SDK\n\t * than us.\n\t */\n\tprivate getVersionMismatchReason(theirSchema: SerializedSchema) {\n\t\tconst ourSchema = this.serializedSchema\n\n\t\tif (theirSchema.schemaVersion > ourSchema.schemaVersion) {\n\t\t\treturn TLSyncErrorCloseEventReason.SERVER_TOO_OLD\n\t\t}\n\n\t\tif (theirSchema.schemaVersion === 2 && ourSchema.schemaVersion === 2) {\n\t\t\tfor (const [sequenceId, theirVersion] of Object.entries(theirSchema.sequences)) {\n\t\t\t\tconst ourVersion = ourSchema.sequences[sequenceId]\n\t\t\t\tif (ourVersion === undefined || theirVersion > ourVersion) {\n\t\t\t\t\treturn TLSyncErrorCloseEventReason.SERVER_TOO_OLD\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn TLSyncErrorCloseEventReason.CLIENT_TOO_OLD\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\tif (!migrations.ok) {\n\t\t\tthis.rejectSession(session.sessionId, this.getVersionMismatchReason(message.schema))\n\t\t\treturn\n\t\t}\n\t\t// The client's schema is older than ours, but we can't migrate our data down to their\n\t\t// version (a migration isn't record-scoped or has no down migration), so they're too old.\n\t\tif (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\tobjectAccess: session.objectAccess,\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\t// Exclude the connecting session's own presence \u2014 it will push fresh\n\t\t\t\t\t// data immediately after connecting. Sending the stale record back\n\t\t\t\t\t// would leave an orphaned presence in the client's local store (the\n\t\t\t\t\t// server never echoes a session's own updates back to it).\n\t\t\t\t\tputs: Object.fromEntries(\n\t\t\t\t\t\t[...this.presenceStore.values()]\n\t\t\t\t\t\t\t.filter((p) => p.id !== session.presenceId)\n\t\t\t\t\t\t\t.map((p) => [p.id, p])\n\t\t\t\t\t),\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\tobjectAccess: session.objectAccess,\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\t// Per-op write gate: document-lane records are gated by `isReadonly`, object-lane\n\t\t\t\t// records by `objectAccess`. Denied ops are skipped (the client is corrected by the\n\t\t\t\t// resulting discard/rebase push_result, exactly as whole-diff readonly skips were).\n\t\t\t\t// Server-initiated pushes (no session) are always allowed.\n\t\t\t\tconst canWrite = (typeName: string) =>\n\t\t\t\t\t!session ||\n\t\t\t\t\t(this.objectTypes.has(typeName) ? session.objectAccess !== 'read' : !session.isReadonly)\n\n\t\t\t\tif (message.diff) {\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// The write gate is checked before type validation so that a denied\n\t\t\t\t\t\t\t\t// session's ops are skipped without rejection, matching the previous\n\t\t\t\t\t\t\t\t// whole-diff readonly behavior.\n\t\t\t\t\t\t\t\tif (!canWrite(op[1].typeName)) continue\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 (\n\t\t\t\t\t\t\t\t\t!this.documentTypes.has(op[1].typeName) &&\n\t\t\t\t\t\t\t\t\t!this.objectTypes.has(op[1].typeName)\n\t\t\t\t\t\t\t\t) {\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\tconst doc = txn.get(id) as R | undefined\n\t\t\t\t\t\t\t\t// if it was already deleted, there's no need to apply the patch\n\t\t\t\t\t\t\t\tif (!doc) continue\n\t\t\t\t\t\t\t\tif (!canWrite(doc.typeName)) continue\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\t\t\t\t\t\t\t\tif (!canWrite(doc.typeName)) continue\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\n\t\tif (result.docChanges.diffs && this.onCommittedChanges) {\n\t\t\tconst diff = result.docChanges.diffs.diff\n\t\t\tqueueMicrotask(() => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.onCommittedChanges?.({ diff, documentClock })\n\t\t\t\t} catch (e) {\n\t\t\t\t\tthis.log?.error?.('onCommittedChanges threw', e)\n\t\t\t\t}\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\t// hand the buffer over and start a fresh one, rather than truncating in\n\t\t\t// place, so sockets that defer serialization don't see an emptied array\n\t\t\tconst data = session.outstandingDataMessages\n\t\t\tsession.outstandingDataMessages = []\n\t\t\tsession.socket.sendMessage({ type: 'data', data })\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\t/**\n\t * Work out whether a client we can't reconcile schemas with is running a newer or older SDK\n\t * than us.\n\t */\n\tprivate getVersionMismatchReason(theirSchema: SerializedSchema) {\n\t\tconst ourSchema = this.serializedSchema\n\n\t\tif (theirSchema.schemaVersion > ourSchema.schemaVersion) {\n\t\t\treturn TLSyncErrorCloseEventReason.SERVER_TOO_OLD\n\t\t}\n\n\t\tif (theirSchema.schemaVersion === 2 && ourSchema.schemaVersion === 2) {\n\t\t\tfor (const [sequenceId, theirVersion] of Object.entries(theirSchema.sequences)) {\n\t\t\t\tconst ourVersion = ourSchema.sequences[sequenceId]\n\t\t\t\tif (ourVersion === undefined || theirVersion > ourVersion) {\n\t\t\t\t\treturn TLSyncErrorCloseEventReason.SERVER_TOO_OLD\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn TLSyncErrorCloseEventReason.CLIENT_TOO_OLD\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\tif (!migrations.ok) {\n\t\t\tthis.rejectSession(session.sessionId, this.getVersionMismatchReason(message.schema))\n\t\t\treturn\n\t\t}\n\t\t// The client's schema is older than ours, but we can't migrate our data down to their\n\t\t// version (a migration isn't record-scoped or has no down migration), so they're too old.\n\t\tif (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\t// Exclude the connecting session's own presence \u2014 it will push fresh\n\t\t\t\t\t// data immediately after connecting. Sending the stale record back\n\t\t\t\t\t// would leave an orphaned presence in the client's local store (the\n\t\t\t\t\t// server never echoes a session's own updates back to it).\n\t\t\t\t\tputs: Object.fromEntries(\n\t\t\t\t\t\t[...this.presenceStore.values()]\n\t\t\t\t\t\t\t.filter((p) => p.id !== session.presenceId)\n\t\t\t\t\t\t\t.map((p) => [p.id, p])\n\t\t\t\t\t),\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,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/TLSyncStorage.ts"],
4
- "sourcesContent": ["import { StoreSchema, SynchronousStorage, UnknownRecord } from '@tldraw/store'\nimport { assert, isEqual, objectMapEntriesIterable, objectMapValues } from '@tldraw/utils'\nimport { TLStoreSnapshot } from 'tldraw'\nimport { diffRecord, NetworkDiff, RecordOpType } from './diff'\nimport { RoomSnapshot } from './TLSyncRoom'\n\n/**\n * Transaction interface for storage operations. Provides methods to read and modify\n * documents, tombstones, and metadata within a transaction.\n *\n * @public\n */\nexport interface TLSyncStorageTransaction<R extends UnknownRecord> extends SynchronousStorage<R> {\n\t/**\n\t * Get the current clock value.\n\t * If the clock has incremented during the transaction,\n\t * the incremented value will be returned.\n\t *\n\t * @returns The current clock value\n\t */\n\tgetClock(): number\n\n\t/**\n\t * Get all changes (document updates and deletions) since a given clock time.\n\t * This is the main method for calculating diffs for client sync.\n\t *\n\t * @param sinceClock - The clock time to get changes since\n\t * @returns Changes since the specified clock time\n\t */\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined\n}\n\n/**\n * Options for a transaction.\n * @public\n */\nexport interface TLSyncStorageTransactionOptions {\n\t/**\n\t * Use this if you need to identify the transaction for logging or debugging purposes\n\t * or for ignoring certain changes in onChange callbacks\n\t */\n\tid?: string\n\t/**\n\t * Controls when the storage layer should emit the actual changes that occurred during the transaction.\n\t *\n\t * - `'always'` - Always emit the changes, regardless of whether they were applied verbatim\n\t * - `'when-different'` - Only emit changes if the storage layer modified/embellished the records\n\t * (e.g., added server timestamps, normalized data, etc.)\n\t *\n\t * When changes are emitted, they will be available in the `changes` field of the transaction result.\n\t * This is useful when the storage layer may transform records and the caller needs to know\n\t * what actually changed rather than what was requested.\n\t */\n\temitChanges?: 'always' | 'when-different'\n}\n\n/**\n * Callback type for a transaction.\n * The conditional return type ensures that the callback is synchronous.\n * @public\n */\nexport type TLSyncStorageTransactionCallback<R extends UnknownRecord, T> = (\n\ttxn: TLSyncStorageTransaction<R>\n) => T extends Promise<any>\n\t? {\n\t\t\t__error: 'Transaction callbacks cannot be async. Use synchronous operations only.'\n\t\t}\n\t: T\n\n/**\n * Pluggable synchronous transactional storage layer for TLSyncRoom.\n * Provides methods for managing documents, tombstones, and clocks within transactions.\n *\n * @public\n */\nexport interface TLSyncStorage<R extends UnknownRecord> {\n\ttransaction<T>(\n\t\tcallback: TLSyncStorageTransactionCallback<R, T>,\n\t\topts?: TLSyncStorageTransactionOptions\n\t): TLSyncStorageTransactionResult<T, R>\n\n\tgetClock(): number\n\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void\n\n\t/** A snapshot of the document lane only \u2014 never contains object-store lane records. */\n\tgetSnapshot?(): RoomSnapshot\n\n\t/**\n\t * A snapshot of the object-store lane (see `objectTypes` on the storage), for hosts that\n\t * persist object-lane records separately from the document. Same entry shape as\n\t * `RoomSnapshot['documents']` so a lane can be persisted and re-seeded via a merged snapshot.\n\t */\n\tgetObjectsSnapshot?(): RoomSnapshot['documents']\n}\n\n/**\n * Properties passed to the onChange callback.\n * @public\n */\nexport interface TLSyncStorageOnChangeCallbackProps {\n\t/**\n\t * The ID of the transaction that caused the change.\n\t * This is useful for ignoring certain changes in onChange callbacks.\n\t */\n\tid?: string\n\tdocumentClock: number\n}\n\n/**\n * Result returned from a storage transaction.\n * @public\n */\nexport interface TLSyncStorageTransactionResult<T, R extends UnknownRecord = UnknownRecord> {\n\tdocumentClock: number\n\tdidChange: boolean\n\tresult: T\n\t/**\n\t * The actual changes that occurred during the transaction, if requested via `emitChanges` option.\n\t * This is a RecordsDiff where:\n\t * - `added` contains records that were put (we don't have \"from\" state for emitted changes)\n\t * - `removed` contains records that were deleted (with placeholder values since we only have IDs)\n\t * - `updated` is empty (emitted changes don't track before/after pairs)\n\t *\n\t * Only populated when:\n\t * - `emitChanges: 'always'` was specified, or\n\t * - `emitChanges: 'when-different'` was specified and the storage layer modified records\n\t */\n\tchanges?: TLSyncForwardDiff<R>\n}\n\n/**\n * Respresents a diff of puts and deletes.\n * @public\n */\nexport interface TLSyncForwardDiff<R extends UnknownRecord> {\n\tputs: Record<string, R | [before: R, after: R]>\n\tdeletes: string[]\n}\n\n/**\n * @internal\n */\nexport function toNetworkDiff<R extends UnknownRecord>(diff: TLSyncForwardDiff<R>): NetworkDiff<R> {\n\tconst networkDiff: NetworkDiff<R> = {}\n\tfor (const [id, put] of objectMapEntriesIterable(diff.puts)) {\n\t\tif (Array.isArray(put)) {\n\t\t\tconst patch = diffRecord(put[0], put[1])\n\t\t\tif (patch) {\n\t\t\t\tnetworkDiff[id] = [RecordOpType.Patch, patch]\n\t\t\t}\n\t\t} else {\n\t\t\tnetworkDiff[id] = [RecordOpType.Put, put]\n\t\t}\n\t}\n\tfor (const id of diff.deletes) {\n\t\tnetworkDiff[id] = [RecordOpType.Remove]\n\t}\n\treturn networkDiff\n}\n\n/**\n * Result returned from getChangesSince, containing all changes since a given clock time.\n * @public\n */\nexport interface TLSyncStorageGetChangesSinceResult<R extends UnknownRecord> {\n\t/**\n\t * The changes as a TLSyncForwardDiff.\n\t */\n\tdiff: TLSyncForwardDiff<R>\n\t/**\n\t * If true, the client should wipe all local data and replace with the server's state.\n\t * This happens when the client's clock is too old and we've lost tombstone history.\n\t */\n\twipeAll: boolean\n}\n\n/**\n * Loads a snapshot into storage during a transaction.\n * Migrates the snapshot to the current schema and loads it into storage.\n *\n * @public\n * @param txn - The transaction to load the snapshot into\n * @param schema - The current schema\n * @param snapshot - The snapshot to load\n */\nexport function loadSnapshotIntoStorage<R extends UnknownRecord>(\n\ttxn: TLSyncStorageTransaction<R>,\n\tschema: StoreSchema<R, any>,\n\tsnapshot: RoomSnapshot | TLStoreSnapshot\n) {\n\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\tassert(snapshot.schema, 'Schema is required')\n\tconst docIds = new Set<string>()\n\tfor (const doc of snapshot.documents) {\n\t\tdocIds.add(doc.state.id)\n\t\tconst existing = txn.get(doc.state.id)\n\t\tif (isEqual(existing, doc.state)) continue\n\t\ttxn.set(doc.state.id, doc.state as R)\n\t}\n\tfor (const id of txn.keys()) {\n\t\tif (!docIds.has(id)) {\n\t\t\ttxn.delete(id)\n\t\t}\n\t}\n\ttxn.setSchema(snapshot.schema)\n\tschema.migrateStorage(txn)\n}\n\nexport function convertStoreSnapshotToRoomSnapshot(\n\tsnapshot: RoomSnapshot | TLStoreSnapshot\n): RoomSnapshot {\n\tif ('documents' in snapshot) return snapshot\n\treturn {\n\t\tclock: 0,\n\t\tdocumentClock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,mBAA2E;AAE3E,kBAAsD;AA4I/C,SAAS,cAAuC,MAA4C;AAClG,QAAM,cAA8B,CAAC;AACrC,aAAW,CAAC,IAAI,GAAG,SAAK,uCAAyB,KAAK,IAAI,GAAG;AAC5D,QAAI,MAAM,QAAQ,GAAG,GAAG;AACvB,YAAM,YAAQ,wBAAW,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACvC,UAAI,OAAO;AACV,oBAAY,EAAE,IAAI,CAAC,yBAAa,OAAO,KAAK;AAAA,MAC7C;AAAA,IACD,OAAO;AACN,kBAAY,EAAE,IAAI,CAAC,yBAAa,KAAK,GAAG;AAAA,IACzC;AAAA,EACD;AACA,aAAW,MAAM,KAAK,SAAS;AAC9B,gBAAY,EAAE,IAAI,CAAC,yBAAa,MAAM;AAAA,EACvC;AACA,SAAO;AACR;AA2BO,SAAS,wBACf,KACA,QACA,UACC;AACD,aAAW,mCAAmC,QAAQ;AACtD,2BAAO,SAAS,QAAQ,oBAAoB;AAC5C,QAAM,SAAS,oBAAI,IAAY;AAC/B,aAAW,OAAO,SAAS,WAAW;AACrC,WAAO,IAAI,IAAI,MAAM,EAAE;AACvB,UAAM,WAAW,IAAI,IAAI,IAAI,MAAM,EAAE;AACrC,YAAI,sBAAQ,UAAU,IAAI,KAAK,EAAG;AAClC,QAAI,IAAI,IAAI,MAAM,IAAI,IAAI,KAAU;AAAA,EACrC;AACA,aAAW,MAAM,IAAI,KAAK,GAAG;AAC5B,QAAI,CAAC,OAAO,IAAI,EAAE,GAAG;AACpB,UAAI,OAAO,EAAE;AAAA,IACd;AAAA,EACD;AACA,MAAI,UAAU,SAAS,MAAM;AAC7B,SAAO,eAAe,GAAG;AAC1B;AAEO,SAAS,mCACf,UACe;AACf,MAAI,eAAe,SAAU,QAAO;AACpC,SAAO;AAAA,IACN,OAAO;AAAA,IACP,eAAe;AAAA,IACf,eAAW,8BAAgB,SAAS,KAAK,EAAE,IAAI,CAAC,WAAW;AAAA,MAC1D;AAAA,MACA,kBAAkB;AAAA,IACnB,EAAE;AAAA,IACF,QAAQ,SAAS;AAAA,IACjB,YAAY,CAAC;AAAA,EACd;AACD;",
4
+ "sourcesContent": ["import { StoreSchema, SynchronousStorage, UnknownRecord } from '@tldraw/store'\nimport { assert, isEqual, objectMapEntriesIterable, objectMapValues } from '@tldraw/utils'\nimport { TLStoreSnapshot } from 'tldraw'\nimport { diffRecord, NetworkDiff, RecordOpType } from './diff'\nimport { RoomSnapshot } from './TLSyncRoom'\n\n/**\n * Transaction interface for storage operations. Provides methods to read and modify\n * documents, tombstones, and metadata within a transaction.\n *\n * @public\n */\nexport interface TLSyncStorageTransaction<R extends UnknownRecord> extends SynchronousStorage<R> {\n\t/**\n\t * Get the current clock value.\n\t * If the clock has incremented during the transaction,\n\t * the incremented value will be returned.\n\t *\n\t * @returns The current clock value\n\t */\n\tgetClock(): number\n\n\t/**\n\t * Get all changes (document updates and deletions) since a given clock time.\n\t * This is the main method for calculating diffs for client sync.\n\t *\n\t * @param sinceClock - The clock time to get changes since\n\t * @returns Changes since the specified clock time\n\t */\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined\n}\n\n/**\n * Options for a transaction.\n * @public\n */\nexport interface TLSyncStorageTransactionOptions {\n\t/**\n\t * Use this if you need to identify the transaction for logging or debugging purposes\n\t * or for ignoring certain changes in onChange callbacks\n\t */\n\tid?: string\n\t/**\n\t * Controls when the storage layer should emit the actual changes that occurred during the transaction.\n\t *\n\t * - `'always'` - Always emit the changes, regardless of whether they were applied verbatim\n\t * - `'when-different'` - Only emit changes if the storage layer modified/embellished the records\n\t * (e.g., added server timestamps, normalized data, etc.)\n\t *\n\t * When changes are emitted, they will be available in the `changes` field of the transaction result.\n\t * This is useful when the storage layer may transform records and the caller needs to know\n\t * what actually changed rather than what was requested.\n\t */\n\temitChanges?: 'always' | 'when-different'\n}\n\n/**\n * Callback type for a transaction.\n * The conditional return type ensures that the callback is synchronous.\n * @public\n */\nexport type TLSyncStorageTransactionCallback<R extends UnknownRecord, T> = (\n\ttxn: TLSyncStorageTransaction<R>\n) => T extends Promise<any>\n\t? {\n\t\t\t__error: 'Transaction callbacks cannot be async. Use synchronous operations only.'\n\t\t}\n\t: T\n\n/**\n * Pluggable synchronous transactional storage layer for TLSyncRoom.\n * Provides methods for managing documents, tombstones, and clocks within transactions.\n *\n * @public\n */\nexport interface TLSyncStorage<R extends UnknownRecord> {\n\ttransaction<T>(\n\t\tcallback: TLSyncStorageTransactionCallback<R, T>,\n\t\topts?: TLSyncStorageTransactionOptions\n\t): TLSyncStorageTransactionResult<T, R>\n\n\tgetClock(): number\n\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void\n\n\tgetSnapshot?(): RoomSnapshot\n}\n\n/**\n * Properties passed to the onChange callback.\n * @public\n */\nexport interface TLSyncStorageOnChangeCallbackProps {\n\t/**\n\t * The ID of the transaction that caused the change.\n\t * This is useful for ignoring certain changes in onChange callbacks.\n\t */\n\tid?: string\n\tdocumentClock: number\n}\n\n/**\n * Result returned from a storage transaction.\n * @public\n */\nexport interface TLSyncStorageTransactionResult<T, R extends UnknownRecord = UnknownRecord> {\n\tdocumentClock: number\n\tdidChange: boolean\n\tresult: T\n\t/**\n\t * The actual changes that occurred during the transaction, if requested via `emitChanges` option.\n\t * This is a RecordsDiff where:\n\t * - `added` contains records that were put (we don't have \"from\" state for emitted changes)\n\t * - `removed` contains records that were deleted (with placeholder values since we only have IDs)\n\t * - `updated` is empty (emitted changes don't track before/after pairs)\n\t *\n\t * Only populated when:\n\t * - `emitChanges: 'always'` was specified, or\n\t * - `emitChanges: 'when-different'` was specified and the storage layer modified records\n\t */\n\tchanges?: TLSyncForwardDiff<R>\n}\n\n/**\n * Respresents a diff of puts and deletes.\n * @public\n */\nexport interface TLSyncForwardDiff<R extends UnknownRecord> {\n\tputs: Record<string, R | [before: R, after: R]>\n\tdeletes: string[]\n}\n\n/**\n * @internal\n */\nexport function toNetworkDiff<R extends UnknownRecord>(diff: TLSyncForwardDiff<R>): NetworkDiff<R> {\n\tconst networkDiff: NetworkDiff<R> = {}\n\tfor (const [id, put] of objectMapEntriesIterable(diff.puts)) {\n\t\tif (Array.isArray(put)) {\n\t\t\tconst patch = diffRecord(put[0], put[1])\n\t\t\tif (patch) {\n\t\t\t\tnetworkDiff[id] = [RecordOpType.Patch, patch]\n\t\t\t}\n\t\t} else {\n\t\t\tnetworkDiff[id] = [RecordOpType.Put, put]\n\t\t}\n\t}\n\tfor (const id of diff.deletes) {\n\t\tnetworkDiff[id] = [RecordOpType.Remove]\n\t}\n\treturn networkDiff\n}\n\n/**\n * Result returned from getChangesSince, containing all changes since a given clock time.\n * @public\n */\nexport interface TLSyncStorageGetChangesSinceResult<R extends UnknownRecord> {\n\t/**\n\t * The changes as a TLSyncForwardDiff.\n\t */\n\tdiff: TLSyncForwardDiff<R>\n\t/**\n\t * If true, the client should wipe all local data and replace with the server's state.\n\t * This happens when the client's clock is too old and we've lost tombstone history.\n\t */\n\twipeAll: boolean\n}\n\n/**\n * Loads a snapshot into storage during a transaction.\n * Migrates the snapshot to the current schema and loads it into storage.\n *\n * @public\n * @param txn - The transaction to load the snapshot into\n * @param schema - The current schema\n * @param snapshot - The snapshot to load\n */\nexport function loadSnapshotIntoStorage<R extends UnknownRecord>(\n\ttxn: TLSyncStorageTransaction<R>,\n\tschema: StoreSchema<R, any>,\n\tsnapshot: RoomSnapshot | TLStoreSnapshot\n) {\n\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\tassert(snapshot.schema, 'Schema is required')\n\tconst docIds = new Set<string>()\n\tfor (const doc of snapshot.documents) {\n\t\tdocIds.add(doc.state.id)\n\t\tconst existing = txn.get(doc.state.id)\n\t\tif (isEqual(existing, doc.state)) continue\n\t\ttxn.set(doc.state.id, doc.state as R)\n\t}\n\tfor (const id of txn.keys()) {\n\t\tif (!docIds.has(id)) {\n\t\t\ttxn.delete(id)\n\t\t}\n\t}\n\ttxn.setSchema(snapshot.schema)\n\tschema.migrateStorage(txn)\n}\n\nexport function convertStoreSnapshotToRoomSnapshot(\n\tsnapshot: RoomSnapshot | TLStoreSnapshot\n): RoomSnapshot {\n\tif ('documents' in snapshot) return snapshot\n\treturn {\n\t\tclock: 0,\n\t\tdocumentClock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,mBAA2E;AAE3E,kBAAsD;AAoI/C,SAAS,cAAuC,MAA4C;AAClG,QAAM,cAA8B,CAAC;AACrC,aAAW,CAAC,IAAI,GAAG,SAAK,uCAAyB,KAAK,IAAI,GAAG;AAC5D,QAAI,MAAM,QAAQ,GAAG,GAAG;AACvB,YAAM,YAAQ,wBAAW,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACvC,UAAI,OAAO;AACV,oBAAY,EAAE,IAAI,CAAC,yBAAa,OAAO,KAAK;AAAA,MAC7C;AAAA,IACD,OAAO;AACN,kBAAY,EAAE,IAAI,CAAC,yBAAa,KAAK,GAAG;AAAA,IACzC;AAAA,EACD;AACA,aAAW,MAAM,KAAK,SAAS;AAC9B,gBAAY,EAAE,IAAI,CAAC,yBAAa,MAAM;AAAA,EACvC;AACA,SAAO;AACR;AA2BO,SAAS,wBACf,KACA,QACA,UACC;AACD,aAAW,mCAAmC,QAAQ;AACtD,2BAAO,SAAS,QAAQ,oBAAoB;AAC5C,QAAM,SAAS,oBAAI,IAAY;AAC/B,aAAW,OAAO,SAAS,WAAW;AACrC,WAAO,IAAI,IAAI,MAAM,EAAE;AACvB,UAAM,WAAW,IAAI,IAAI,IAAI,MAAM,EAAE;AACrC,YAAI,sBAAQ,UAAU,IAAI,KAAK,EAAG;AAClC,QAAI,IAAI,IAAI,MAAM,IAAI,IAAI,KAAU;AAAA,EACrC;AACA,aAAW,MAAM,IAAI,KAAK,GAAG;AAC5B,QAAI,CAAC,OAAO,IAAI,EAAE,GAAG;AACpB,UAAI,OAAO,EAAE;AAAA,IACd;AAAA,EACD;AACA,MAAI,UAAU,SAAS,MAAM;AAC7B,SAAO,eAAe,GAAG;AAC1B;AAEO,SAAS,mCACf,UACe;AACf,MAAI,eAAe,SAAU,QAAO;AACpC,SAAO;AAAA,IACN,OAAO;AAAA,IACP,eAAe;AAAA,IACf,eAAW,8BAAgB,SAAS,KAAK,EAAE,IAAI,CAAC,WAAW;AAAA,MAC1D;AAAA,MACA,kBAAkB;AAAA,IACnB,EAAE;AAAA,IACF,QAAQ,SAAS;AAAA,IACjB,YAAY,CAAC;AAAA,EACd;AACD;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/protocol.ts"],
4
- "sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { NetworkDiff, ObjectDiff, RecordOpType } from './diff'\n\nconst TLSYNC_PROTOCOL_VERSION = 8\n\n/**\n * Gets the current tldraw sync protocol version number.\n *\n * This version number is used during WebSocket connection handshake to ensure\n * client and server compatibility. When versions don't match, the connection\n * will be rejected with an incompatibility error.\n *\n * @returns The current protocol version number\n *\n * @example\n * ```ts\n * const version = getTlsyncProtocolVersion()\n * console.log(`Using protocol version: ${version}`)\n * ```\n *\n * @internal\n */\nexport function getTlsyncProtocolVersion() {\n\treturn TLSYNC_PROTOCOL_VERSION\n}\n\n/**\n * Access level for object-store lane record types (see `objectTypes` on the room).\n *\n * Object-lane records (e.g. comments) are gated by this per-session value instead of the\n * document-lane `isReadonly` flag, so a session can be allowed to write objects without\n * being allowed to edit the document (\"can comment but not edit\"), or vice versa.\n *\n * @public\n */\nexport type TLObjectStoreAccess = 'read' | 'write'\n\n/**\n * Constants defining the different types of protocol incompatibility reasons.\n *\n * These values indicate why a client-server connection was rejected due to\n * version or compatibility issues. Each reason helps diagnose specific problems\n * during the connection handshake.\n *\n * @example\n * ```ts\n * if (error.reason === TLIncompatibilityReason.ClientTooOld) {\n * showUpgradeMessage('Please update your client')\n * }\n * ```\n *\n * @internal\n * @deprecated Replaced by websocket .close status/reason\n */\nexport const TLIncompatibilityReason = {\n\tClientTooOld: 'clientTooOld',\n\tServerTooOld: 'serverTooOld',\n\tInvalidRecord: 'invalidRecord',\n\tInvalidOperation: 'invalidOperation',\n} as const\n\n/**\n * Union type representing all possible incompatibility reason values.\n *\n * This type represents the different reasons why a client-server connection\n * might fail due to protocol or version mismatches.\n *\n * @example\n * ```ts\n * function handleIncompatibility(reason: TLIncompatibilityReason) {\n * switch (reason) {\n * case 'clientTooOld':\n * return 'Client needs to be updated'\n * case 'serverTooOld':\n * return 'Server needs to be updated'\n * }\n * }\n * ```\n *\n * @internal\n * @deprecated replaced by websocket .close status/reason\n */\n/* eslint-disable @typescript-eslint/no-deprecated */\nexport type TLIncompatibilityReason =\n\t(typeof TLIncompatibilityReason)[keyof typeof TLIncompatibilityReason]\n/* eslint-enable @typescript-eslint/no-deprecated */\n\n/**\n * Union type representing all possible message types that can be sent from server to client.\n *\n * This encompasses the complete set of server-originated WebSocket messages in the tldraw\n * sync protocol, including connection establishment, data synchronization, and error handling.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * syncClient.onReceiveMessage((message: TLSocketServerSentEvent<MyRecord>) => {\n * switch (message.type) {\n * case 'connect':\n * console.log('Connected to room with clock:', message.serverClock)\n * break\n * case 'data':\n * console.log('Received data updates:', message.data)\n * break\n * }\n * })\n * ```\n *\n * @internal\n */\nexport type TLSocketServerSentEvent<R extends UnknownRecord> =\n\t| {\n\t\t\ttype: 'connect'\n\t\t\thydrationType: 'wipe_all' | 'wipe_presence'\n\t\t\tconnectRequestId: string\n\t\t\tprotocolVersion: number\n\t\t\tschema: SerializedSchema\n\t\t\tdiff: NetworkDiff<R>\n\t\t\tserverClock: number\n\t\t\tisReadonly: boolean\n\t\t\t/**\n\t\t\t * Write access for object-store lane record types, when the room has an object lane.\n\t\t\t * Optional and additive: servers without an object lane (or older servers) omit it, and\n\t\t\t * older clients ignore it \u2014 no protocol version bump needed.\n\t\t\t */\n\t\t\tobjectAccess?: TLObjectStoreAccess\n\t }\n\t| {\n\t\t\ttype: 'incompatibility_error'\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\treason: TLIncompatibilityReason\n\t }\n\t| {\n\t\t\ttype: 'pong'\n\t }\n\t| { type: 'data'; data: TLSocketServerSentDataEvent<R>[] }\n\t| { type: 'custom'; data: any }\n\t| TLSocketServerSentDataEvent<R>\n\n/**\n * Union type representing data-related messages sent from server to client.\n *\n * These messages handle the core synchronization operations: applying patches from\n * other clients and confirming the results of client push operations.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * function handleDataEvent(event: TLSocketServerSentDataEvent<MyRecord>) {\n * if (event.type === 'patch') {\n * // Apply changes from other clients\n * applyNetworkDiff(event.diff)\n * } else if (event.type === 'push_result') {\n * // Handle result of our push request\n * if (event.action === 'commit') {\n * console.log('Changes accepted by server')\n * }\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type TLSocketServerSentDataEvent<R extends UnknownRecord> =\n\t| {\n\t\t\ttype: 'patch'\n\t\t\tdiff: NetworkDiff<R>\n\t\t\tserverClock: number\n\t }\n\t| {\n\t\t\ttype: 'push_result'\n\t\t\tclientClock: number\n\t\t\tserverClock: number\n\t\t\taction: 'discard' | 'commit' | { rebaseWithDiff: NetworkDiff<R> }\n\t }\n\n/**\n * Interface defining a client-to-server push request message.\n *\n * Push requests are sent when the client wants to synchronize local changes\n * with the server. They contain document changes and optionally presence updates\n * (like cursor position or user selection).\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * const pushRequest: TLPushRequest<MyRecord> = {\n * type: 'push',\n * clientClock: 15,\n * diff: {\n * 'shape:abc123': [RecordOpType.Patch, { x: [ValueOpType.Put, 100] }]\n * },\n * presence: [RecordOpType.Put, { cursor: { x: 150, y: 200 } }]\n * }\n * socket.sendMessage(pushRequest)\n * ```\n *\n * @internal\n */\nexport interface TLPushRequest<R extends UnknownRecord> {\n\ttype: 'push'\n\tclientClock: number\n\tdiff?: NetworkDiff<R>\n\tpresence?: [typeof RecordOpType.Patch, ObjectDiff] | [typeof RecordOpType.Put, R]\n}\n\n/**\n * Interface defining a client-to-server connection request message.\n *\n * This message initiates a WebSocket connection to a sync room. It includes\n * the client's schema, protocol version, and last known server clock for\n * proper synchronization state management.\n *\n * @example\n * ```ts\n * const connectRequest: TLConnectRequest = {\n * type: 'connect',\n * connectRequestId: 'conn-123',\n * lastServerClock: 42,\n * protocolVersion: getTlsyncProtocolVersion(),\n * schema: mySchema.serialize()\n * }\n * socket.sendMessage(connectRequest)\n * ```\n *\n * @internal\n */\nexport interface TLConnectRequest {\n\ttype: 'connect'\n\tconnectRequestId: string\n\tlastServerClock: number\n\tprotocolVersion: number\n\tschema: SerializedSchema\n}\n\n/**\n * Interface defining a client-to-server ping request message.\n *\n * Ping requests are used to measure network latency and ensure the connection\n * is still active. The server responds with a 'pong' message.\n *\n * @example\n * ```ts\n * const pingRequest: TLPingRequest = { type: 'ping' }\n * socket.sendMessage(pingRequest)\n *\n * // Server will respond with { type: 'pong' }\n * ```\n *\n * @internal\n */\nexport interface TLPingRequest {\n\ttype: 'ping'\n}\n\n/**\n * Union type representing all possible message types that can be sent from client to server.\n *\n * This encompasses the complete set of client-originated WebSocket messages in the tldraw\n * sync protocol, covering connection establishment, data synchronization, and connectivity checks.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * function sendMessage(message: TLSocketClientSentEvent<MyRecord>) {\n * switch (message.type) {\n * case 'connect':\n * console.log('Establishing connection...')\n * break\n * case 'push':\n * console.log('Pushing changes:', message.diff)\n * break\n * case 'ping':\n * console.log('Checking connection latency')\n * break\n * }\n * socket.send(JSON.stringify(message))\n * }\n * ```\n *\n * @internal\n */\nexport type TLSocketClientSentEvent<R extends UnknownRecord> =\n\t| TLPushRequest<R>\n\t| TLConnectRequest\n\t| TLPingRequest\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,MAAM,0BAA0B;AAmBzB,SAAS,2BAA2B;AAC1C,SAAO;AACR;AA8BO,MAAM,0BAA0B;AAAA,EACtC,cAAc;AAAA,EACd,cAAc;AAAA,EACd,eAAe;AAAA,EACf,kBAAkB;AACnB;",
4
+ "sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { NetworkDiff, ObjectDiff, RecordOpType } from './diff'\n\nconst TLSYNC_PROTOCOL_VERSION = 8\n\n/**\n * Gets the current tldraw sync protocol version number.\n *\n * This version number is used during WebSocket connection handshake to ensure\n * client and server compatibility. When versions don't match, the connection\n * will be rejected with an incompatibility error.\n *\n * @returns The current protocol version number\n *\n * @example\n * ```ts\n * const version = getTlsyncProtocolVersion()\n * console.log(`Using protocol version: ${version}`)\n * ```\n *\n * @internal\n */\nexport function getTlsyncProtocolVersion() {\n\treturn TLSYNC_PROTOCOL_VERSION\n}\n\n/**\n * Constants defining the different types of protocol incompatibility reasons.\n *\n * These values indicate why a client-server connection was rejected due to\n * version or compatibility issues. Each reason helps diagnose specific problems\n * during the connection handshake.\n *\n * @example\n * ```ts\n * if (error.reason === TLIncompatibilityReason.ClientTooOld) {\n * showUpgradeMessage('Please update your client')\n * }\n * ```\n *\n * @internal\n * @deprecated Replaced by websocket .close status/reason\n */\nexport const TLIncompatibilityReason = {\n\tClientTooOld: 'clientTooOld',\n\tServerTooOld: 'serverTooOld',\n\tInvalidRecord: 'invalidRecord',\n\tInvalidOperation: 'invalidOperation',\n} as const\n\n/**\n * Union type representing all possible incompatibility reason values.\n *\n * This type represents the different reasons why a client-server connection\n * might fail due to protocol or version mismatches.\n *\n * @example\n * ```ts\n * function handleIncompatibility(reason: TLIncompatibilityReason) {\n * switch (reason) {\n * case 'clientTooOld':\n * return 'Client needs to be updated'\n * case 'serverTooOld':\n * return 'Server needs to be updated'\n * }\n * }\n * ```\n *\n * @internal\n * @deprecated replaced by websocket .close status/reason\n */\n/* eslint-disable @typescript-eslint/no-deprecated */\nexport type TLIncompatibilityReason =\n\t(typeof TLIncompatibilityReason)[keyof typeof TLIncompatibilityReason]\n/* eslint-enable @typescript-eslint/no-deprecated */\n\n/**\n * Union type representing all possible message types that can be sent from server to client.\n *\n * This encompasses the complete set of server-originated WebSocket messages in the tldraw\n * sync protocol, including connection establishment, data synchronization, and error handling.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * syncClient.onReceiveMessage((message: TLSocketServerSentEvent<MyRecord>) => {\n * switch (message.type) {\n * case 'connect':\n * console.log('Connected to room with clock:', message.serverClock)\n * break\n * case 'data':\n * console.log('Received data updates:', message.data)\n * break\n * }\n * })\n * ```\n *\n * @internal\n */\nexport type TLSocketServerSentEvent<R extends UnknownRecord> =\n\t| {\n\t\t\ttype: 'connect'\n\t\t\thydrationType: 'wipe_all' | 'wipe_presence'\n\t\t\tconnectRequestId: string\n\t\t\tprotocolVersion: number\n\t\t\tschema: SerializedSchema\n\t\t\tdiff: NetworkDiff<R>\n\t\t\tserverClock: number\n\t\t\tisReadonly: boolean\n\t }\n\t| {\n\t\t\ttype: 'incompatibility_error'\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\treason: TLIncompatibilityReason\n\t }\n\t| {\n\t\t\ttype: 'pong'\n\t }\n\t| { type: 'data'; data: TLSocketServerSentDataEvent<R>[] }\n\t| { type: 'custom'; data: any }\n\t| TLSocketServerSentDataEvent<R>\n\n/**\n * Union type representing data-related messages sent from server to client.\n *\n * These messages handle the core synchronization operations: applying patches from\n * other clients and confirming the results of client push operations.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * function handleDataEvent(event: TLSocketServerSentDataEvent<MyRecord>) {\n * if (event.type === 'patch') {\n * // Apply changes from other clients\n * applyNetworkDiff(event.diff)\n * } else if (event.type === 'push_result') {\n * // Handle result of our push request\n * if (event.action === 'commit') {\n * console.log('Changes accepted by server')\n * }\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type TLSocketServerSentDataEvent<R extends UnknownRecord> =\n\t| {\n\t\t\ttype: 'patch'\n\t\t\tdiff: NetworkDiff<R>\n\t\t\tserverClock: number\n\t }\n\t| {\n\t\t\ttype: 'push_result'\n\t\t\tclientClock: number\n\t\t\tserverClock: number\n\t\t\taction: 'discard' | 'commit' | { rebaseWithDiff: NetworkDiff<R> }\n\t }\n\n/**\n * Interface defining a client-to-server push request message.\n *\n * Push requests are sent when the client wants to synchronize local changes\n * with the server. They contain document changes and optionally presence updates\n * (like cursor position or user selection).\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * const pushRequest: TLPushRequest<MyRecord> = {\n * type: 'push',\n * clientClock: 15,\n * diff: {\n * 'shape:abc123': [RecordOpType.Patch, { x: [ValueOpType.Put, 100] }]\n * },\n * presence: [RecordOpType.Put, { cursor: { x: 150, y: 200 } }]\n * }\n * socket.sendMessage(pushRequest)\n * ```\n *\n * @internal\n */\nexport interface TLPushRequest<R extends UnknownRecord> {\n\ttype: 'push'\n\tclientClock: number\n\tdiff?: NetworkDiff<R>\n\tpresence?: [typeof RecordOpType.Patch, ObjectDiff] | [typeof RecordOpType.Put, R]\n}\n\n/**\n * Interface defining a client-to-server connection request message.\n *\n * This message initiates a WebSocket connection to a sync room. It includes\n * the client's schema, protocol version, and last known server clock for\n * proper synchronization state management.\n *\n * @example\n * ```ts\n * const connectRequest: TLConnectRequest = {\n * type: 'connect',\n * connectRequestId: 'conn-123',\n * lastServerClock: 42,\n * protocolVersion: getTlsyncProtocolVersion(),\n * schema: mySchema.serialize()\n * }\n * socket.sendMessage(connectRequest)\n * ```\n *\n * @internal\n */\nexport interface TLConnectRequest {\n\ttype: 'connect'\n\tconnectRequestId: string\n\tlastServerClock: number\n\tprotocolVersion: number\n\tschema: SerializedSchema\n}\n\n/**\n * Interface defining a client-to-server ping request message.\n *\n * Ping requests are used to measure network latency and ensure the connection\n * is still active. The server responds with a 'pong' message.\n *\n * @example\n * ```ts\n * const pingRequest: TLPingRequest = { type: 'ping' }\n * socket.sendMessage(pingRequest)\n *\n * // Server will respond with { type: 'pong' }\n * ```\n *\n * @internal\n */\nexport interface TLPingRequest {\n\ttype: 'ping'\n}\n\n/**\n * Union type representing all possible message types that can be sent from client to server.\n *\n * This encompasses the complete set of client-originated WebSocket messages in the tldraw\n * sync protocol, covering connection establishment, data synchronization, and connectivity checks.\n *\n * @param R - The record type being synchronized (extends UnknownRecord)\n *\n * @example\n * ```ts\n * function sendMessage(message: TLSocketClientSentEvent<MyRecord>) {\n * switch (message.type) {\n * case 'connect':\n * console.log('Establishing connection...')\n * break\n * case 'push':\n * console.log('Pushing changes:', message.diff)\n * break\n * case 'ping':\n * console.log('Checking connection latency')\n * break\n * }\n * socket.send(JSON.stringify(message))\n * }\n * ```\n *\n * @internal\n */\nexport type TLSocketClientSentEvent<R extends UnknownRecord> =\n\t| TLPushRequest<R>\n\t| TLConnectRequest\n\t| TLPingRequest\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,MAAM,0BAA0B;AAmBzB,SAAS,2BAA2B;AAC1C,SAAO;AACR;AAmBO,MAAM,0BAA0B;AAAA,EACtC,cAAc;AAAA,EACd,cAAc;AAAA,EACd,eAAe;AAAA,EACf,kBAAkB;AACnB;",
6
6
  "names": []
7
7
  }