@tldraw/sync-core 5.5.2 → 5.6.0-canary.0091c91994d6

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 (41) hide show
  1. package/dist-cjs/index.d.ts +23 -8
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
  4. package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
  5. package/dist-cjs/lib/TLSocketRoom.js +40 -11
  6. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  7. package/dist-cjs/lib/TLSyncClient.js +5 -0
  8. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  9. package/dist-cjs/lib/TLSyncRoom.js +30 -44
  10. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  11. package/dist-cjs/lib/TLSyncStorage.js +2 -2
  12. package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
  13. package/dist-cjs/lib/diff.js +15 -7
  14. package/dist-cjs/lib/diff.js.map +2 -2
  15. package/dist-esm/index.d.mts +23 -8
  16. package/dist-esm/index.mjs +1 -1
  17. package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
  18. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
  19. package/dist-esm/lib/TLSocketRoom.mjs +40 -11
  20. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  21. package/dist-esm/lib/TLSyncClient.mjs +5 -0
  22. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  23. package/dist-esm/lib/TLSyncRoom.mjs +30 -44
  24. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  25. package/dist-esm/lib/TLSyncStorage.mjs +2 -2
  26. package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
  27. package/dist-esm/lib/diff.mjs +16 -8
  28. package/dist-esm/lib/diff.mjs.map +2 -2
  29. package/package.json +18 -18
  30. package/src/lib/InMemorySyncStorage.ts +2 -2
  31. package/src/lib/SQLiteSyncStorage.ts +2 -2
  32. package/src/lib/TLSocketRoom.ts +50 -11
  33. package/src/lib/TLSyncClient.test.ts +61 -0
  34. package/src/lib/TLSyncClient.ts +9 -0
  35. package/src/lib/TLSyncRoom.ts +51 -48
  36. package/src/lib/TLSyncStorage.ts +7 -2
  37. package/src/lib/diff.test.ts +91 -1
  38. package/src/lib/diff.ts +30 -9
  39. package/src/test/TLSocketRoom.test.ts +132 -1
  40. package/src/test/TLSyncRoom.test.ts +124 -0
  41. package/src/test/upgradeDowngrade.test.ts +21 -0
@@ -359,8 +359,8 @@ class TLSyncRoom {
359
359
  * @param sourceSessionId - Optional session ID to exclude from the broadcast
360
360
  */
361
361
  broadcastPatch(diff, networkDiff, sourceSessionId) {
362
- const unmigrated = networkDiff ?? (0, import_TLSyncStorage.toNetworkDiff)(diff);
363
- if (!unmigrated) return this;
362
+ const legacyAppendMode = !this.getCanEmitStringAppend();
363
+ const unmigrated = networkDiff ?? (0, import_TLSyncStorage.toNetworkDiff)(diff, legacyAppendMode);
364
364
  this.sessions.forEach((session) => {
365
365
  if (session.state !== import_RoomSession.RoomSessionState.Connected) return;
366
366
  if (sourceSessionId === session.sessionId) return;
@@ -372,7 +372,9 @@ class TLSyncRoom {
372
372
  session.sessionId,
373
373
  session.serializedSchema,
374
374
  session.requiresDownMigrations,
375
- diff
375
+ diff,
376
+ unmigrated,
377
+ legacyAppendMode
376
378
  );
377
379
  if (!diffResult.ok) return;
378
380
  this._unsafe_sendMessage(session.sessionId, {
@@ -498,6 +500,14 @@ class TLSyncRoom {
498
500
  if (presenceRecord && presenceId) {
499
501
  this.presenceStore.set(presenceId, presenceRecord);
500
502
  }
503
+ if (!migrations.ok) {
504
+ this.rejectSession(sessionId, this.getVersionMismatchReason(serializedSchema));
505
+ return;
506
+ }
507
+ if (migrations.value.some((m) => m.scope !== "record" || !m.down)) {
508
+ this.rejectSession(sessionId, import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
509
+ return;
510
+ }
501
511
  }
502
512
  /**
503
513
  * Checks if all connected sessions support string append operations (protocol version 8+).
@@ -530,11 +540,12 @@ class TLSyncRoom {
530
540
  * @param requiresDownMigrations - Whether the client needs down migrations
531
541
  * @param diff - The TLSyncForwardDiff containing full records to migrate
532
542
  * @param unmigrated - Optional pre-computed NetworkDiff for when no migration is needed
543
+ * @param legacyAppendMode - Emit string appends as puts (SES5); defaults to the room-wide state
533
544
  * @returns A NetworkDiff with migrated records, or a migration failure
534
545
  */
535
- migrateDiffOrRejectSession(sessionId, serializedSchema, requiresDownMigrations, diff, unmigrated) {
546
+ migrateDiffOrRejectSession(sessionId, serializedSchema, requiresDownMigrations, diff, unmigrated, legacyAppendMode = !this.getCanEmitStringAppend()) {
536
547
  if (!requiresDownMigrations) {
537
- return import_utils.Result.ok(unmigrated ?? (0, import_TLSyncStorage.toNetworkDiff)(diff) ?? {});
548
+ return import_utils.Result.ok(unmigrated ?? (0, import_TLSyncStorage.toNetworkDiff)(diff, legacyAppendMode));
538
549
  }
539
550
  const result = {};
540
551
  for (const [id, put] of (0, import_utils.objectMapEntriesIterable)(diff.puts)) {
@@ -550,7 +561,7 @@ class TLSyncRoom {
550
561
  this.rejectSession(sessionId, import_TLSyncClient.TLSyncErrorCloseEventReason.CLIENT_TOO_OLD);
551
562
  return import_utils.Result.err(toResult.reason);
552
563
  }
553
- const patch = (0, import_diff.diffRecord)(fromResult.value, toResult.value);
564
+ const patch = (0, import_diff.diffRecord)(fromResult.value, toResult.value, legacyAppendMode);
554
565
  if (patch) {
555
566
  result[id] = [import_diff.RecordOpType.Patch, patch];
556
567
  }
@@ -695,7 +706,8 @@ class TLSyncRoom {
695
706
  if (theirSchema.schemaVersion > ourSchema.schemaVersion) {
696
707
  return import_TLSyncClient.TLSyncErrorCloseEventReason.SERVER_TOO_OLD;
697
708
  }
698
- if (theirSchema.schemaVersion === 2 && ourSchema.schemaVersion === 2) {
709
+ if (theirSchema.schemaVersion === 2 && ourSchema.schemaVersion === 2 && // a malformed client schema must reject the session, not throw
710
+ typeof theirSchema.sequences === "object" && theirSchema.sequences !== null) {
699
711
  for (const [sequenceId, theirVersion] of Object.entries(theirSchema.sequences)) {
700
712
  const ourVersion = ourSchema.sequences[sequenceId];
701
713
  if (ourVersion === void 0 || theirVersion > ourVersion) {
@@ -848,7 +860,7 @@ class TLSyncRoom {
848
860
  }
849
861
  if (doc) {
850
862
  const recordType = (0, import_utils.assertExists)((0, import_utils.getOwnProperty)(this.schema.types, doc.typeName));
851
- const diff = (0, import_recordDiff.diffAndValidateRecord)(doc, state, recordType);
863
+ const diff = (0, import_recordDiff.diffAndValidateRecord)(doc, state, recordType, legacyAppendMode);
852
864
  if (diff) {
853
865
  storage.set(id, state);
854
866
  propagateOp(changes2, id, [import_diff.RecordOpType.Patch, diff], doc, state);
@@ -920,6 +932,7 @@ class TLSyncRoom {
920
932
  }
921
933
  }
922
934
  const canWrite = (typeName) => !session || (this.objectTypes.has(typeName) ? session.objectAccess !== "read" : !session.isReadonly);
935
+ const authSession = session ? { sessionId: session.sessionId, isReadonly: session.isReadonly, meta: session.meta } : null;
923
936
  if (message.diff) {
924
937
  for (const [id, op] of (0, import_utils.objectMapEntriesIterable)(message.diff)) {
925
938
  switch (op[0]) {
@@ -949,25 +962,7 @@ class TLSyncRoom {
949
962
  if (authorizePut) {
950
963
  authorize = (prevRec, next) => {
951
964
  const result2 = authorizePut(
952
- prevRec ? {
953
- session: {
954
- sessionId: session.sessionId,
955
- isReadonly: session.isReadonly,
956
- meta: session.meta
957
- },
958
- type: "update",
959
- prev: prevRec,
960
- next
961
- } : {
962
- session: {
963
- sessionId: session.sessionId,
964
- isReadonly: session.isReadonly,
965
- meta: session.meta
966
- },
967
- type: "create",
968
- prev: null,
969
- next
970
- }
965
+ prevRec ? { session: authSession, type: "update", prev: prevRec, next } : { session: authSession, type: "create", prev: null, next }
971
966
  );
972
967
  if (!result2) {
973
968
  this.log?.warn?.(
@@ -993,11 +988,7 @@ class TLSyncRoom {
993
988
  const authorizePatch = session && this.authorizerFor(doc.typeName);
994
989
  const authorize = authorizePatch ? (prev, next) => {
995
990
  const result2 = authorizePatch({
996
- session: {
997
- sessionId: session.sessionId,
998
- isReadonly: session.isReadonly,
999
- meta: session.meta
1000
- },
991
+ session: authSession,
1001
992
  type: "update",
1002
993
  prev,
1003
994
  next
@@ -1023,16 +1014,7 @@ class TLSyncRoom {
1023
1014
  }
1024
1015
  if (!canWrite(doc.typeName)) continue;
1025
1016
  const authorizeRemove = session && this.authorizerFor(doc.typeName);
1026
- if (authorizeRemove && !authorizeRemove({
1027
- session: {
1028
- sessionId: session.sessionId,
1029
- isReadonly: session.isReadonly,
1030
- meta: session.meta
1031
- },
1032
- type: "delete",
1033
- prev: doc,
1034
- next: null
1035
- })) {
1017
+ if (authorizeRemove && !authorizeRemove({ session: authSession, type: "delete", prev: doc, next: null })) {
1036
1018
  this.log?.warn?.(
1037
1019
  "authorizer vetoed delete",
1038
1020
  doc.typeName,
@@ -1056,7 +1038,10 @@ class TLSyncRoom {
1056
1038
  this.lastDocumentClock = documentClock;
1057
1039
  let pushResult;
1058
1040
  if (changes && session) {
1059
- result.docChanges.diffs = { networkDiff: (0, import_TLSyncStorage.toNetworkDiff)(changes) ?? {}, diff: changes };
1041
+ result.docChanges.diffs = {
1042
+ networkDiff: (0, import_TLSyncStorage.toNetworkDiff)(changes, legacyAppendMode),
1043
+ diff: changes
1044
+ };
1060
1045
  }
1061
1046
  if ((0, import_utils.isEqual)(result.docChanges.diffs?.networkDiff, message.diff)) {
1062
1047
  pushResult = {
@@ -1078,7 +1063,8 @@ class TLSyncRoom {
1078
1063
  session.serializedSchema,
1079
1064
  session.requiresDownMigrations,
1080
1065
  result.docChanges.diffs.diff,
1081
- result.docChanges.diffs.networkDiff
1066
+ result.docChanges.diffs.networkDiff,
1067
+ legacyAppendMode
1082
1068
  );
1083
1069
  if (diff.ok) {
1084
1070
  pushResult = {
@@ -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// WebSocket close frames cap the reason at 123 UTF-8 bytes; a longer one makes `close()` throw\nconst MAX_CLOSE_REASON_BYTES = 123\nconst closeReasonEncoder = new TextEncoder()\n\nconst truncatedSuffix = (droppedBytes: number) => `... (+${droppedBytes} bytes)`\n\nfunction truncateCloseReason(reason: string) {\n\tconst totalBytes = closeReasonEncoder.encode(reason).length\n\tif (totalBytes <= MAX_CLOSE_REASON_BYTES) return reason\n\t// the dropped count can't have more digits than the total, so reserving room for it always fits\n\tconst budget = MAX_CLOSE_REASON_BYTES - truncatedSuffix(totalBytes).length\n\tlet out = ''\n\tlet outBytes = 0\n\t// step by code point so a multi-byte character is never cut in half\n\tfor (const char of reason) {\n\t\tconst charBytes = closeReasonEncoder.encode(char).length\n\t\tif (outBytes + charBytes > budget) break\n\t\tout += char\n\t\toutBytes += charBytes\n\t}\n\treturn out + truncatedSuffix(totalBytes - outBytes)\n}\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 * Authorizes a single record write from a client: any per-record, per-session rule the host wants\n * to enforce server-side \u2014 veto writes the session isn't allowed to make, or rewrite the record on\n * create. The session's `meta` carries whatever the rule needs (identity, roles, \u2026); for example,\n * force a comment's `authorId` to the signed-in user so nobody can post in someone else's name.\n *\n * Called on **create**, **update**, and **delete** of records whose `typeName` it's registered for\n * (see {@link TLRecordAuthorizers}), and only for client pushes \u2014 never for server-initiated writes.\n *\n * `prev` and `next` are always at the **server's** schema version: client writes are migrated\n * before the authorizer runs, so guarding or stamping a field never requires knowing what older\n * clients call it. On create, the record you return is what gets stored (after validation) \u2014 no\n * migration runs afterwards, so stamped fields can't be clobbered.\n *\n * Return `null` to reject the write \u2014 it's skipped and the client self-corrects, exactly like the\n * `objectAccess` gate. Otherwise the write is allowed, and:\n *\n * - on **create**, the record you return is what gets stored, so stamp identity fields here (e.g.\n * set `authorId` from `session.meta`);\n * - on **update** and **delete**, only allow-vs-reject is used (the returned record's contents are\n * ignored), so use them to veto changes to immutable fields or unauthorized deletes \u2014 return\n * `next`/`prev` to allow, `null` to reject.\n *\n * \u26A0\uFE0E Runs synchronously inside the commit transaction, on the same path as every document edit \u2014 it\n * must be fast and do **no** I/O. `next`/`prev` are client-controlled records, so treat their\n * contents as untrusted; prefer returning `null` to reject over throwing, though a throw is\n * caught, logged, and treated as a rejection (fail closed) rather than crashing the push. For\n * expensive, async checks (e.g. resolving mentions against who can access a file), react after the\n * fact via `onCommittedChanges`.\n *\n * @public\n */\nexport type TLRecordAuthorizer<Rec extends UnknownRecord, SessionMeta> = (\n\targs: {\n\t\t/** The session performing the write: its host-provided `meta` (e.g. the authenticated user\n\t\t * id) and the canvas-lane `isReadonly` state, for hosts whose object-lane policy follows\n\t\t * canvas access. */\n\t\tsession: { sessionId: string; isReadonly: boolean; meta: SessionMeta }\n\t} & (\n\t\t| { type: 'create'; prev: null; next: Rec }\n\t\t| { type: 'update'; prev: Rec; next: Rec }\n\t\t| { type: 'delete'; prev: Rec; next: null }\n\t)\n) => Rec | null\n\n/**\n * A map from record `typeName` to a {@link TLRecordAuthorizer} for that record type. Only listed\n * types are authorized; every other record writes through untouched, so this stays off the hot path\n * for the vast majority of writes (shape drags etc.).\n *\n * Each authorizer is typed to its record \u2014 e.g. `next` in the `comment` entry is a `TLComment` \u2014 so\n * renaming a field on the record makes the authorizer that reads it fail to compile, rather than\n * silently stamp or guard the wrong field.\n *\n * Presence records are never authorized (presence is per-session and ephemeral); registering the\n * presence typeName is a construction-time error.\n *\n * @public\n */\nexport type TLRecordAuthorizers<R extends UnknownRecord, SessionMeta> = {\n\t[K in R['typeName']]?: TLRecordAuthorizer<Extract<R, { typeName: K }>, SessionMeta>\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\t// don't leave a stray timer on a room the host has already torn down\n\t\tif (this._isClosed || 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._isClosed = true\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.sessions.forEach((session) => {\n\t\t\tthis.clearDebounceTimer(session)\n\t\t\ttry {\n\t\t\t\tsession.socket.close()\n\t\t\t} catch {\n\t\t\t\t// noop, one bad socket must not leave the rest open\n\t\t\t}\n\t\t})\n\t\t// forgetting the sessions is what makes late socket close/error events no-ops, so\n\t\t// nothing can emit session_removed / room_became_empty on a closed room\n\t\tthis.sessions.clear()\n\t}\n\n\tprivate clearDebounceTimer(session: RoomSession<R, SessionMeta>) {\n\t\tif (session.state === RoomSessionState.Connected && session.debounceTimer !== null) {\n\t\t\tclearTimeout(session.debounceTimer)\n\t\t\tsession.debounceTimer = null\n\t\t\tsession.outstandingDataMessages = []\n\t\t}\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 authorizeRecord?: TLRecordAuthorizers<R, SessionMeta>\n\tprivate readonly sessionIdleTimeout: number\n\n\t/**\n\t * The authorizer registered for a record type, widened to the room's record union. Each entry in\n\t * `authorizeRecord` is typed to its specific record; the cast here is the one place we can't\n\t * statically correlate a runtime `typeName` with its record type, so it lives in the library\n\t * rather than in every consumer.\n\t */\n\tprivate authorizerFor(typeName: R['typeName']): TLRecordAuthorizer<R, SessionMeta> | undefined {\n\t\tconst authorize = this.authorizeRecord?.[typeName] as\n\t\t\t| TLRecordAuthorizer<R, SessionMeta>\n\t\t\t| undefined\n\t\tif (!authorize) return undefined\n\t\t// Fail closed: an authorizer that throws rejects the write (and is logged) rather than\n\t\t// aborting the whole push. Authorizers are security-sensitive, so a bug must never let a\n\t\t// write through.\n\t\treturn (args) => {\n\t\t\ttry {\n\t\t\t\treturn authorize(args)\n\t\t\t} catch (e) {\n\t\t\t\tthis.log?.error?.('record authorizer threw; rejecting the write', e)\n\t\t\t\treturn null\n\t\t\t}\n\t\t}\n\t}\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\t/**\n\t\t * Per-type authorizers for client record writes (create, update, delete): veto or, on\n\t\t * create, rewrite. See {@link TLRecordAuthorizers}.\n\t\t */\n\t\tauthorizeRecord?: TLRecordAuthorizers<R, SessionMeta>\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.authorizeRecord = opts.authorizeRecord\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\t// The presence lane never consults authorizers, so a presence key in `authorizeRecord`\n\t\t// would be a silent no-op \u2014 fail loudly at construction instead.\n\t\tif (this.presenceType && this.authorizeRecord) {\n\t\t\tassert(\n\t\t\t\t!getOwnProperty(this.authorizeRecord, this.presenceType.typeName),\n\t\t\t\t`TLSyncRoom: authorizeRecord['${this.presenceType.typeName}'] is a presence type; presence records are not authorized`\n\t\t\t)\n\t\t}\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\tif (!session.socket.isOpen) {\n\t\t\t\t// same as the immediate-send path: send() into a closed socket throws on\n\t\t\t\t// some runtimes (Cloudflare), and here that would be from inside a timer\n\t\t\t\tthis.cancelSession(sessionId)\n\t\t\t\treturn\n\t\t\t}\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\t\tthis.clearDebounceTimer(session)\n\n\t\ttry {\n\t\t\tif (fatalReason) {\n\t\t\t\t// the session is already gone, so a throwing close() would leave its socket open\n\t\t\t\tsession.socket.close(TLSyncErrorCloseEventCode, truncateCloseReason(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.clearDebounceTimer(session)\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\t// a connect racing close() would otherwise create a session nothing ever prunes\n\t\tif (this._isClosed) {\n\t\t\tsocket.close()\n\t\t\treturn this\n\t\t}\n\t\tconst existing = this.sessions.get(sessionId)\n\t\tif (existing) this.clearDebounceTimer(existing)\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\tif (this._isClosed) {\n\t\t\tsocket.close()\n\t\t\treturn\n\t\t}\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). WebSocket close reasons are capped\n\t * at 123 UTF-8 bytes, so a longer reason is truncated and ends with `... (+N bytes)`.\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\t// Only connected clients hold state at a clock we can no longer diff from. A session\n\t\t\t// mid-handshake gets hydrated from its own lastServerClock in the same transaction, so\n\t\t\t// removing it would close its socket and then re-add it as Connected (resurrected).\n\t\t\tif (session.state !== RoomSessionState.Connected) continue\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 = (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\tauthorize?: (prev: R | null, next: R) => R | null,\n\t\t\t// The existing document if the caller already fetched it; `null` for fetched-and-absent,\n\t\t\t// `undefined` for not fetched. Saves a second SELECT + JSON.parse on the push hot path.\n\t\t\tprevDoc?: R | null\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\tlet { value: state } = res\n\n\t\t\t// Get the existing document, if any\n\t\t\tconst doc =\n\t\t\t\tprevDoc !== undefined ? (prevDoc ?? undefined) : (storage.get(id) as R | undefined)\n\n\t\t\t// Authorize on the up-migrated record; on create the authorizer's return is stored as-is,\n\t\t\t// so no later migration can clobber stamped fields.\n\t\t\tif (authorize) {\n\t\t\t\tconst result = authorize(doc ?? null, state)\n\t\t\t\tif (!result) return Result.ok(undefined) // vetoed: skip the op, the client self-corrects\n\t\t\t\tif (!doc) state = result // create: store the authorizer's (stamped) record\n\t\t\t}\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\tauthorize?: (prev: R, next: R) => R | null,\n\t\t\t// The existing document if the caller already fetched it (see `addDocument`).\n\t\t\tprevDoc?: R\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 = prevDoc ?? (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\t// Authorize on the committed candidate \u2014 the record that will actually be stored.\n\t\t\t\t\tif (authorize && !authorize(doc, diff[1])) return\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\t// Authorize on the committed candidate \u2014 the upgraded record, not a raw preview.\n\t\t\t\t\tif (authorize && !authorize(doc, upgraded.value)) return\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\tconst record = op[1]\n\t\t\t\t\t\t\t\t// A put must not change the record's typeName. Two reasons, and the first\n\t\t\t\t\t\t\t\t// applies whether or not authorizers are configured: the write gate above\n\t\t\t\t\t\t\t\t// keyed off the *incoming* typeName, so a swap between lanes (document <->\n\t\t\t\t\t\t\t\t// object) would let a session denied on one lane write through the other by\n\t\t\t\t\t\t\t\t// relabeling an existing record. The second is that the authorizer lookup\n\t\t\t\t\t\t\t\t// also keys off the incoming typeName while the replace path validates\n\t\t\t\t\t\t\t\t// against the stored one, so a swap would consult the wrong authorizer (or\n\t\t\t\t\t\t\t\t// none). Skip it like a veto; the client self-corrects.\n\t\t\t\t\t\t\t\t// `undefined` (no session) means the guard didn't fetch, so addDocument will.\n\t\t\t\t\t\t\t\tconst prevRecord = session ? ((txn.get(id) as R | undefined) ?? null) : undefined\n\t\t\t\t\t\t\t\tif (session && prevRecord && prevRecord.typeName !== record.typeName) {\n\t\t\t\t\t\t\t\t\tthis.log?.warn?.(\n\t\t\t\t\t\t\t\t\t\t'skipping put that changes typeName',\n\t\t\t\t\t\t\t\t\t\t`${prevRecord.typeName} -> ${record.typeName}`,\n\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t'session:',\n\t\t\t\t\t\t\t\t\t\tsession.sessionId\n\t\t\t\t\t\t\t\t\t)\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\t// Per-type authorizer: stamp/veto the write from the session's identity.\n\t\t\t\t\t\t\t\t// Client pushes only; runs inside `addDocument`, on the up-migrated record.\n\t\t\t\t\t\t\t\tlet authorize: ((prev: R | null, next: R) => R | null) | undefined\n\t\t\t\t\t\t\t\tif (session && this.authorizeRecord) {\n\t\t\t\t\t\t\t\t\tconst authorizePut = this.authorizerFor(record.typeName)\n\t\t\t\t\t\t\t\t\tif (authorizePut) {\n\t\t\t\t\t\t\t\t\t\tauthorize = (prevRec, next) => {\n\t\t\t\t\t\t\t\t\t\t\tconst result = authorizePut(\n\t\t\t\t\t\t\t\t\t\t\t\tprevRec\n\t\t\t\t\t\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsession: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsessionId: session.sessionId,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: 'update',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tprev: prevRec,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsession: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsessionId: session.sessionId,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttype: 'create',\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tprev: null,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\tif (!result) {\n\t\t\t\t\t\t\t\t\t\t\t\tthis.log?.warn?.(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'authorizer vetoed put',\n\t\t\t\t\t\t\t\t\t\t\t\t\trecord.typeName,\n\t\t\t\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t\t\t\t'session:',\n\t\t\t\t\t\t\t\t\t\t\t\t\tsession.sessionId\n\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// create: store the stamped record; update: allow/veto only\n\t\t\t\t\t\t\t\t\t\t\treturn prevRec ? next : result\n\t\t\t\t\t\t\t\t\t\t}\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\t// The guard above already fetched this, so don't hit storage again.\n\t\t\t\t\t\t\t\taddDocument(txn, docChanges, id, record, authorize, prevRecord)\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// Per-type authorizer (update, allow/veto only): runs inside `patchDocument`\n\t\t\t\t\t\t\t\t// on the committed candidate, never on a raw client-version preview.\n\t\t\t\t\t\t\t\tconst authorizePatch = session && this.authorizerFor(doc.typeName)\n\t\t\t\t\t\t\t\tconst authorize = authorizePatch\n\t\t\t\t\t\t\t\t\t? (prev: R, next: R) => {\n\t\t\t\t\t\t\t\t\t\t\tconst result = authorizePatch({\n\t\t\t\t\t\t\t\t\t\t\t\tsession: {\n\t\t\t\t\t\t\t\t\t\t\t\t\tsessionId: session.sessionId,\n\t\t\t\t\t\t\t\t\t\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\t\t\t\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\ttype: 'update',\n\t\t\t\t\t\t\t\t\t\t\t\tprev,\n\t\t\t\t\t\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\tif (!result) {\n\t\t\t\t\t\t\t\t\t\t\t\tthis.log?.warn?.(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'authorizer vetoed patch',\n\t\t\t\t\t\t\t\t\t\t\t\t\tdoc.typeName,\n\t\t\t\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t\t\t\t'session:',\n\t\t\t\t\t\t\t\t\t\t\t\t\tsession.sessionId\n\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\treturn result\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t// Try to patch the document. If it fails, stop here. The write gate above\n\t\t\t\t\t\t\t\t// already fetched the record, so pass it through.\n\t\t\t\t\t\t\t\tpatchDocument(txn, docChanges, id, op[1], authorize, doc)\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) as R | undefined\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\t\t\t\t\t\t\t\t// Per-type authorizer (delete): veto deletes the session isn't allowed to make,\n\t\t\t\t\t\t\t\t// e.g. deleting someone else's comment. Allow/veto only.\n\t\t\t\t\t\t\t\tconst authorizeRemove = session && this.authorizerFor(doc.typeName)\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\tauthorizeRemove &&\n\t\t\t\t\t\t\t\t\t!authorizeRemove({\n\t\t\t\t\t\t\t\t\t\tsession: {\n\t\t\t\t\t\t\t\t\t\t\tsessionId: session.sessionId,\n\t\t\t\t\t\t\t\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\t\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\ttype: 'delete',\n\t\t\t\t\t\t\t\t\t\tprev: doc,\n\t\t\t\t\t\t\t\t\t\tnext: null,\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\t\tthis.log?.warn?.(\n\t\t\t\t\t\t\t\t\t\t'authorizer vetoed delete',\n\t\t\t\t\t\t\t\t\t\tdoc.typeName,\n\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t'session:',\n\t\t\t\t\t\t\t\t\t\tsession.sessionId\n\t\t\t\t\t\t\t\t\t)\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\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\ttype ThrottledFunction,\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// WebSocket close frames cap the reason at 123 UTF-8 bytes; a longer one makes `close()` throw\nconst MAX_CLOSE_REASON_BYTES = 123\nconst closeReasonEncoder = new TextEncoder()\n\nconst truncatedSuffix = (droppedBytes: number) => `... (+${droppedBytes} bytes)`\n\nfunction truncateCloseReason(reason: string) {\n\tconst totalBytes = closeReasonEncoder.encode(reason).length\n\tif (totalBytes <= MAX_CLOSE_REASON_BYTES) return reason\n\t// the dropped count can't have more digits than the total, so reserving room for it always fits\n\tconst budget = MAX_CLOSE_REASON_BYTES - truncatedSuffix(totalBytes).length\n\tlet out = ''\n\tlet outBytes = 0\n\t// step by code point so a multi-byte character is never cut in half\n\tfor (const char of reason) {\n\t\tconst charBytes = closeReasonEncoder.encode(char).length\n\t\tif (outBytes + charBytes > budget) break\n\t\tout += char\n\t\toutBytes += charBytes\n\t}\n\treturn out + truncatedSuffix(totalBytes - outBytes)\n}\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 * Authorizes a single record write from a client: any per-record, per-session rule the host wants\n * to enforce server-side \u2014 veto writes the session isn't allowed to make, or rewrite the record on\n * create. The session's `meta` carries whatever the rule needs (identity, roles, \u2026); for example,\n * force a comment's `authorId` to the signed-in user so nobody can post in someone else's name.\n *\n * Called on **create**, **update**, and **delete** of records whose `typeName` it's registered for\n * (see {@link TLRecordAuthorizers}), and only for client pushes \u2014 never for server-initiated writes.\n *\n * `prev` and `next` are always at the **server's** schema version: client writes are migrated\n * before the authorizer runs, so guarding or stamping a field never requires knowing what older\n * clients call it. On create, the record you return is what gets stored (after validation) \u2014 no\n * migration runs afterwards, so stamped fields can't be clobbered.\n *\n * Return `null` to reject the write \u2014 it's skipped and the client self-corrects, exactly like the\n * `objectAccess` gate. Otherwise the write is allowed, and:\n *\n * - on **create**, the record you return is what gets stored, so stamp identity fields here (e.g.\n * set `authorId` from `session.meta`);\n * - on **update** and **delete**, only allow-vs-reject is used (the returned record's contents are\n * ignored), so use them to veto changes to immutable fields or unauthorized deletes \u2014 return\n * `next`/`prev` to allow, `null` to reject.\n *\n * \u26A0\uFE0E Runs synchronously inside the commit transaction, on the same path as every document edit \u2014 it\n * must be fast and do **no** I/O. `next`/`prev` are client-controlled records, so treat their\n * contents as untrusted; prefer returning `null` to reject over throwing, though a throw is\n * caught, logged, and treated as a rejection (fail closed) rather than crashing the push. For\n * expensive, async checks (e.g. resolving mentions against who can access a file), react after the\n * fact via `onCommittedChanges`.\n *\n * @public\n */\nexport type TLRecordAuthorizer<Rec extends UnknownRecord, SessionMeta> = (\n\targs: {\n\t\t/** The session performing the write: its host-provided `meta` (e.g. the authenticated user\n\t\t * id) and the canvas-lane `isReadonly` state, for hosts whose object-lane policy follows\n\t\t * canvas access. */\n\t\tsession: { sessionId: string; isReadonly: boolean; meta: SessionMeta }\n\t} & (\n\t\t| { type: 'create'; prev: null; next: Rec }\n\t\t| { type: 'update'; prev: Rec; next: Rec }\n\t\t| { type: 'delete'; prev: Rec; next: null }\n\t)\n) => Rec | null\n\n/**\n * A map from record `typeName` to a {@link TLRecordAuthorizer} for that record type. Only listed\n * types are authorized; every other record writes through untouched, so this stays off the hot path\n * for the vast majority of writes (shape drags etc.).\n *\n * Each authorizer is typed to its record \u2014 e.g. `next` in the `comment` entry is a `TLComment` \u2014 so\n * renaming a field on the record makes the authorizer that reads it fail to compile, rather than\n * silently stamp or guard the wrong field.\n *\n * Presence records are never authorized (presence is per-session and ephemeral); registering the\n * presence typeName is a construction-time error.\n *\n * @public\n */\nexport type TLRecordAuthorizers<R extends UnknownRecord, SessionMeta> = {\n\t[K in R['typeName']]?: TLRecordAuthorizer<Extract<R, { typeName: K }>, SessionMeta>\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: ThrottledFunction<() => void> = 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\t// don't leave a stray timer on a room the host has already torn down\n\t\tif (this._isClosed || 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._isClosed = true\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.sessions.forEach((session) => {\n\t\t\tthis.clearDebounceTimer(session)\n\t\t\ttry {\n\t\t\t\tsession.socket.close()\n\t\t\t} catch {\n\t\t\t\t// noop, one bad socket must not leave the rest open\n\t\t\t}\n\t\t})\n\t\t// forgetting the sessions is what makes late socket close/error events no-ops, so\n\t\t// nothing can emit session_removed / room_became_empty on a closed room\n\t\tthis.sessions.clear()\n\t}\n\n\tprivate clearDebounceTimer(session: RoomSession<R, SessionMeta>) {\n\t\tif (session.state === RoomSessionState.Connected && session.debounceTimer !== null) {\n\t\t\tclearTimeout(session.debounceTimer)\n\t\t\tsession.debounceTimer = null\n\t\t\tsession.outstandingDataMessages = []\n\t\t}\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 authorizeRecord?: TLRecordAuthorizers<R, SessionMeta>\n\tprivate readonly sessionIdleTimeout: number\n\n\t/**\n\t * The authorizer registered for a record type, widened to the room's record union. Each entry in\n\t * `authorizeRecord` is typed to its specific record; the cast here is the one place we can't\n\t * statically correlate a runtime `typeName` with its record type, so it lives in the library\n\t * rather than in every consumer.\n\t */\n\tprivate authorizerFor(typeName: R['typeName']): TLRecordAuthorizer<R, SessionMeta> | undefined {\n\t\tconst authorize = this.authorizeRecord?.[typeName] as\n\t\t\t| TLRecordAuthorizer<R, SessionMeta>\n\t\t\t| undefined\n\t\tif (!authorize) return undefined\n\t\t// Fail closed: an authorizer that throws rejects the write (and is logged) rather than\n\t\t// aborting the whole push. Authorizers are security-sensitive, so a bug must never let a\n\t\t// write through.\n\t\treturn (args) => {\n\t\t\ttry {\n\t\t\t\treturn authorize(args)\n\t\t\t} catch (e) {\n\t\t\t\tthis.log?.error?.('record authorizer threw; rejecting the write', e)\n\t\t\t\treturn null\n\t\t\t}\n\t\t}\n\t}\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\t/**\n\t\t * Per-type authorizers for client record writes (create, update, delete): veto or, on\n\t\t * create, rewrite. See {@link TLRecordAuthorizers}.\n\t\t */\n\t\tauthorizeRecord?: TLRecordAuthorizers<R, SessionMeta>\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.authorizeRecord = opts.authorizeRecord\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\t// The presence lane never consults authorizers, so a presence key in `authorizeRecord`\n\t\t// would be a silent no-op \u2014 fail loudly at construction instead.\n\t\tif (this.presenceType && this.authorizeRecord) {\n\t\t\tassert(\n\t\t\t\t!getOwnProperty(this.authorizeRecord, this.presenceType.typeName),\n\t\t\t\t`TLSyncRoom: authorizeRecord['${this.presenceType.typeName}'] is a presence type; presence records are not authorized`\n\t\t\t)\n\t\t}\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\tif (!session.socket.isOpen) {\n\t\t\t\t// same as the immediate-send path: send() into a closed socket throws on\n\t\t\t\t// some runtimes (Cloudflare), and here that would be from inside a timer\n\t\t\t\tthis.cancelSession(sessionId)\n\t\t\t\treturn\n\t\t\t}\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\t\tthis.clearDebounceTimer(session)\n\n\t\ttry {\n\t\t\tif (fatalReason) {\n\t\t\t\t// the session is already gone, so a throwing close() would leave its socket open\n\t\t\t\tsession.socket.close(TLSyncErrorCloseEventCode, truncateCloseReason(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.clearDebounceTimer(session)\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// Computed once and shared by every session that needs no down-migration; the push path\n\t\t// hands us the diff it already computed (in legacy append mode when needed), and re-deriving\n\t\t// it per session would both repeat the diffing work and lose that legacy handling.\n\t\tconst legacyAppendMode = !this.getCanEmitStringAppend()\n\t\tconst unmigrated = networkDiff ?? toNetworkDiff(diff, legacyAppendMode)\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\tunmigrated,\n\t\t\t\tlegacyAppendMode\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\t// a connect racing close() would otherwise create a session nothing ever prunes\n\t\tif (this._isClosed) {\n\t\t\tsocket.close()\n\t\t\treturn this\n\t\t}\n\t\tconst existing = this.sessions.get(sessionId)\n\t\tif (existing) this.clearDebounceTimer(existing)\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\tif (this._isClosed) {\n\t\t\tsocket.close()\n\t\t\treturn\n\t\t}\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\t// Restore presence before the schema checks: other clients still hold it from before the\n\t\t// socket slept, and a rejection only broadcasts its removal if it's in the store\n\t\tif (presenceRecord && presenceId) {\n\t\t\tthis.presenceStore.set(presenceId, presenceRecord as R)\n\t\t}\n\n\t\t// The server may have changed builds while the socket slept, so re-run the handshake's\n\t\t// schema checks (HS3): a schema we can no longer reconcile must not be served raw diffs.\n\t\tif (!migrations.ok) {\n\t\t\tthis.rejectSession(sessionId, this.getVersionMismatchReason(serializedSchema))\n\t\t\treturn\n\t\t}\n\t\tif (migrations.value.some((m) => m.scope !== 'record' || !m.down)) {\n\t\t\tthis.rejectSession(sessionId, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)\n\t\t\treturn\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 * @param legacyAppendMode - Emit string appends as puts (SES5); defaults to the room-wide state\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\tlegacyAppendMode = !this.getCanEmitStringAppend()\n\t): Result<NetworkDiff<R>, MigrationFailureReason> {\n\t\tif (!requiresDownMigrations) {\n\t\t\treturn Result.ok(unmigrated ?? toNetworkDiff(diff, legacyAppendMode))\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, legacyAppendMode)\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). WebSocket close reasons are capped\n\t * at 123 UTF-8 bytes, so a longer reason is truncated and ends with `... (+N bytes)`.\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\t// Only connected clients hold state at a clock we can no longer diff from. A session\n\t\t\t// mid-handshake gets hydrated from its own lastServerClock in the same transaction, so\n\t\t\t// removing it would close its socket and then re-add it as Connected (resurrected).\n\t\t\tif (session.state !== RoomSessionState.Connected) continue\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 (\n\t\t\ttheirSchema.schemaVersion === 2 &&\n\t\t\tourSchema.schemaVersion === 2 &&\n\t\t\t// a malformed client schema must reject the session, not throw\n\t\t\ttypeof theirSchema.sequences === 'object' &&\n\t\t\ttheirSchema.sequences !== null\n\t\t) {\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 = (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\tauthorize?: (prev: R | null, next: R) => R | null,\n\t\t\t// The existing document if the caller already fetched it; `null` for fetched-and-absent,\n\t\t\t// `undefined` for not fetched. Saves a second SELECT + JSON.parse on the push hot path.\n\t\t\tprevDoc?: R | null\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\tlet { value: state } = res\n\n\t\t\tconst doc =\n\t\t\t\tprevDoc !== undefined ? (prevDoc ?? undefined) : (storage.get(id) as R | undefined)\n\n\t\t\t// Authorize on the up-migrated record; on create the authorizer's return is stored as-is,\n\t\t\t// so no later migration can clobber stamped fields.\n\t\t\tif (authorize) {\n\t\t\t\tconst result = authorize(doc ?? null, state)\n\t\t\t\tif (!result) return Result.ok(undefined) // vetoed: skip the op, the client self-corrects\n\t\t\t\tif (!doc) state = result // create: store the authorizer's (stamped) record\n\t\t\t}\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, legacyAppendMode)\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\tauthorize?: (prev: R, next: R) => R | null,\n\t\t\t// The existing document if the caller already fetched it (see `addDocument`).\n\t\t\tprevDoc?: R\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 = prevDoc ?? (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\t// Authorize on the committed candidate \u2014 the record that will actually be stored.\n\t\t\t\t\tif (authorize && !authorize(doc, diff[1])) return\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\t// Authorize on the committed candidate \u2014 the upgraded record, not a raw preview.\n\t\t\t\t\tif (authorize && !authorize(doc, upgraded.value)) return\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\t// what authorizers see of the pushing session, shared by every authorized op in this push\n\t\t\t\tconst authSession = session\n\t\t\t\t\t? { sessionId: session.sessionId, isReadonly: session.isReadonly, meta: session.meta }\n\t\t\t\t\t: null\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\tconst record = op[1]\n\t\t\t\t\t\t\t\t// A put must not change the record's typeName. Two reasons, and the first\n\t\t\t\t\t\t\t\t// applies whether or not authorizers are configured: the write gate above\n\t\t\t\t\t\t\t\t// keyed off the *incoming* typeName, so a swap between lanes (document <->\n\t\t\t\t\t\t\t\t// object) would let a session denied on one lane write through the other by\n\t\t\t\t\t\t\t\t// relabeling an existing record. The second is that the authorizer lookup\n\t\t\t\t\t\t\t\t// also keys off the incoming typeName while the replace path validates\n\t\t\t\t\t\t\t\t// against the stored one, so a swap would consult the wrong authorizer (or\n\t\t\t\t\t\t\t\t// none). Skip it like a veto; the client self-corrects.\n\t\t\t\t\t\t\t\t// `undefined` (no session) means the guard didn't fetch, so addDocument will.\n\t\t\t\t\t\t\t\tconst prevRecord = session ? ((txn.get(id) as R | undefined) ?? null) : undefined\n\t\t\t\t\t\t\t\tif (session && prevRecord && prevRecord.typeName !== record.typeName) {\n\t\t\t\t\t\t\t\t\tthis.log?.warn?.(\n\t\t\t\t\t\t\t\t\t\t'skipping put that changes typeName',\n\t\t\t\t\t\t\t\t\t\t`${prevRecord.typeName} -> ${record.typeName}`,\n\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t'session:',\n\t\t\t\t\t\t\t\t\t\tsession.sessionId\n\t\t\t\t\t\t\t\t\t)\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\t// Per-type authorizer: stamp/veto the write from the session's identity.\n\t\t\t\t\t\t\t\t// Client pushes only; runs inside `addDocument`, on the up-migrated record.\n\t\t\t\t\t\t\t\tlet authorize: ((prev: R | null, next: R) => R | null) | undefined\n\t\t\t\t\t\t\t\tif (session && this.authorizeRecord) {\n\t\t\t\t\t\t\t\t\tconst authorizePut = this.authorizerFor(record.typeName)\n\t\t\t\t\t\t\t\t\tif (authorizePut) {\n\t\t\t\t\t\t\t\t\t\tauthorize = (prevRec, next) => {\n\t\t\t\t\t\t\t\t\t\t\tconst result = authorizePut(\n\t\t\t\t\t\t\t\t\t\t\t\tprevRec\n\t\t\t\t\t\t\t\t\t\t\t\t\t? { session: authSession!, type: 'update', prev: prevRec, next }\n\t\t\t\t\t\t\t\t\t\t\t\t\t: { session: authSession!, type: 'create', prev: null, next }\n\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\tif (!result) {\n\t\t\t\t\t\t\t\t\t\t\t\tthis.log?.warn?.(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'authorizer vetoed put',\n\t\t\t\t\t\t\t\t\t\t\t\t\trecord.typeName,\n\t\t\t\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t\t\t\t'session:',\n\t\t\t\t\t\t\t\t\t\t\t\t\tsession.sessionId\n\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t// create: store the stamped record; update: allow/veto only\n\t\t\t\t\t\t\t\t\t\t\treturn prevRec ? next : result\n\t\t\t\t\t\t\t\t\t\t}\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\t// The guard above already fetched this, so don't hit storage again.\n\t\t\t\t\t\t\t\taddDocument(txn, docChanges, id, record, authorize, prevRecord)\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// Per-type authorizer (update, allow/veto only): runs inside `patchDocument`\n\t\t\t\t\t\t\t\t// on the committed candidate, never on a raw client-version preview.\n\t\t\t\t\t\t\t\tconst authorizePatch = session && this.authorizerFor(doc.typeName)\n\t\t\t\t\t\t\t\tconst authorize = authorizePatch\n\t\t\t\t\t\t\t\t\t? (prev: R, next: R) => {\n\t\t\t\t\t\t\t\t\t\t\tconst result = authorizePatch({\n\t\t\t\t\t\t\t\t\t\t\t\tsession: authSession!,\n\t\t\t\t\t\t\t\t\t\t\t\ttype: 'update',\n\t\t\t\t\t\t\t\t\t\t\t\tprev,\n\t\t\t\t\t\t\t\t\t\t\t\tnext,\n\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\tif (!result) {\n\t\t\t\t\t\t\t\t\t\t\t\tthis.log?.warn?.(\n\t\t\t\t\t\t\t\t\t\t\t\t\t'authorizer vetoed patch',\n\t\t\t\t\t\t\t\t\t\t\t\t\tdoc.typeName,\n\t\t\t\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t\t\t\t'session:',\n\t\t\t\t\t\t\t\t\t\t\t\t\tsession.sessionId\n\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\treturn result\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t\t// Try to patch the document. If it fails, stop here. The write gate above\n\t\t\t\t\t\t\t\t// already fetched the record, so pass it through.\n\t\t\t\t\t\t\t\tpatchDocument(txn, docChanges, id, op[1], authorize, doc)\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) as R | undefined\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\t\t\t\t\t\t\t\t// Per-type authorizer (delete): veto deletes the session isn't allowed to make,\n\t\t\t\t\t\t\t\t// e.g. deleting someone else's comment. Allow/veto only.\n\t\t\t\t\t\t\t\tconst authorizeRemove = session && this.authorizerFor(doc.typeName)\n\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\tauthorizeRemove &&\n\t\t\t\t\t\t\t\t\t!authorizeRemove({ session: authSession!, type: 'delete', prev: doc, next: null })\n\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\tthis.log?.warn?.(\n\t\t\t\t\t\t\t\t\t\t'authorizer vetoed delete',\n\t\t\t\t\t\t\t\t\t\tdoc.typeName,\n\t\t\t\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\t\t\t\t'session:',\n\t\t\t\t\t\t\t\t\t\tsession.sessionId\n\t\t\t\t\t\t\t\t\t)\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 = {\n\t\t\t\tnetworkDiff: toNetworkDiff(changes, legacyAppendMode),\n\t\t\t\tdiff: changes,\n\t\t\t}\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\tlegacyAppendMode\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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",
6
6
  "names": ["changes", "result"]
7
7
  }
@@ -25,11 +25,11 @@ __export(TLSyncStorage_exports, {
25
25
  module.exports = __toCommonJS(TLSyncStorage_exports);
26
26
  var import_utils = require("@tldraw/utils");
27
27
  var import_diff = require("./diff");
28
- function toNetworkDiff(diff) {
28
+ function toNetworkDiff(diff, legacyAppendMode = false) {
29
29
  const networkDiff = {};
30
30
  for (const [id, put] of (0, import_utils.objectMapEntriesIterable)(diff.puts)) {
31
31
  if (Array.isArray(put)) {
32
- const patch = (0, import_diff.diffRecord)(put[0], put[1]);
32
+ const patch = (0, import_diff.diffRecord)(put[0], put[1], legacyAppendMode);
33
33
  if (patch) {
34
34
  networkDiff[id] = [import_diff.RecordOpType.Patch, patch];
35
35
  }