@tldraw/sync-core 5.5.0-canary.efd2cd7e73e0 → 5.5.0-canary.f09b62e6b096
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.
- package/dist-cjs/index.d.ts +2 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/InMemorySyncStorage.js +18 -18
- package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +2 -1
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +20 -2
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-esm/index.d.mts +2 -1
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/InMemorySyncStorage.mjs +18 -18
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +2 -1
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +20 -2
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/package.json +7 -7
- package/src/lib/ClientWebSocketAdapter.test.ts +46 -19
- package/src/lib/InMemorySyncStorage.test.ts +21 -0
- package/src/lib/InMemorySyncStorage.ts +20 -22
- package/src/lib/TLSocketRoom.ts +2 -1
- package/src/lib/TLSyncRoom.ts +27 -2
- package/src/test/TLSyncRoom.test.ts +25 -0
package/dist-cjs/index.d.ts
CHANGED
|
@@ -1200,7 +1200,8 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
|
|
|
1200
1200
|
* The client will attempt to reconnect automatically unless a fatal reason is provided.
|
|
1201
1201
|
*
|
|
1202
1202
|
* @param sessionId - Session identifier to remove
|
|
1203
|
-
* @param fatalReason - Optional fatal error reason that prevents reconnection
|
|
1203
|
+
* @param fatalReason - Optional fatal error reason that prevents reconnection. WebSocket close
|
|
1204
|
+
* reasons are capped at 123 UTF-8 bytes, so a longer reason is truncated and ends with `... (+N bytes)`.
|
|
1204
1205
|
*
|
|
1205
1206
|
* @example
|
|
1206
1207
|
* ```ts
|
package/dist-cjs/index.js
CHANGED
|
@@ -61,7 +61,7 @@ var import_TLSyncRoom = require("./lib/TLSyncRoom");
|
|
|
61
61
|
var import_TLSyncStorage = require("./lib/TLSyncStorage");
|
|
62
62
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
63
63
|
"@tldraw/sync-core",
|
|
64
|
-
"5.5.0-canary.
|
|
64
|
+
"5.5.0-canary.f09b62e6b096",
|
|
65
65
|
"cjs"
|
|
66
66
|
);
|
|
67
67
|
//# sourceMappingURL=index.js.map
|
|
@@ -98,23 +98,23 @@ class InMemorySyncStorage {
|
|
|
98
98
|
onChange
|
|
99
99
|
} = {}) {
|
|
100
100
|
this.objectTypes = new Set(objectTypes ?? []);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
101
|
+
let maxClockValue = 0;
|
|
102
|
+
const documentEntries = [];
|
|
103
|
+
const objectEntries = [];
|
|
104
|
+
for (const d of snapshot.documents) {
|
|
105
|
+
if (d.lastChangedClock > maxClockValue) maxClockValue = d.lastChangedClock;
|
|
106
|
+
const entries = this.objectTypes.has(d.state.typeName) ? objectEntries : documentEntries;
|
|
107
|
+
entries.push([
|
|
108
|
+
d.state.id,
|
|
109
|
+
{ state: (0, import_store.devFreeze)(d.state), lastChangedClock: d.lastChangedClock }
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
const tombstoneEntries = (0, import_utils.objectMapEntries)(snapshot.tombstones ?? {});
|
|
113
|
+
for (const [, clock] of tombstoneEntries) {
|
|
114
|
+
if (clock > maxClockValue) maxClockValue = clock;
|
|
115
|
+
}
|
|
116
|
+
this.documents = new import_store.AtomMap("room documents", documentEntries);
|
|
117
|
+
this.objects = new import_store.AtomMap("room objects", objectEntries);
|
|
118
118
|
const documentClock = Math.max(maxClockValue, snapshot.documentClock ?? snapshot.clock ?? 0);
|
|
119
119
|
this.documentClock = (0, import_state.atom)("document clock", documentClock);
|
|
120
120
|
const tombstoneHistoryStartsAtClock = Math.min(
|
|
@@ -130,7 +130,7 @@ class InMemorySyncStorage {
|
|
|
130
130
|
"room tombstones",
|
|
131
131
|
// If the tombstone history starts now (or we didn't have the
|
|
132
132
|
// tombstoneHistoryStartsAtClock) then there are no tombstones
|
|
133
|
-
tombstoneHistoryStartsAtClock === documentClock ? [] :
|
|
133
|
+
tombstoneHistoryStartsAtClock === documentClock ? [] : tombstoneEntries
|
|
134
134
|
);
|
|
135
135
|
if (onChange) {
|
|
136
136
|
this.onChange(onChange);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/InMemorySyncStorage.ts"],
|
|
4
|
-
"sourcesContent": ["import { atom, Atom, transaction } from '@tldraw/state'\nimport { AtomMap, devFreeze, SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport {\n\tcreateTLSchema,\n\tDocumentRecordType,\n\tPageRecordType,\n\tTLDOCUMENT_ID,\n\tTLPageId,\n} from '@tldraw/tlschema'\nimport { assert, IndexKey, objectMapEntries, throttle } from '@tldraw/utils'\nimport { MicrotaskNotifier } from './MicrotaskNotifier'\nimport { RoomSnapshot } from './TLSyncRoom'\nimport {\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n\tTLSyncStorageGetChangesSinceResult,\n\tTLSyncStorageOnChangeCallbackProps,\n\tTLSyncStorageTransaction,\n\tTLSyncStorageTransactionCallback,\n\tTLSyncStorageTransactionOptions,\n\tTLSyncStorageTransactionResult,\n} from './TLSyncStorage'\n\n/** @internal */\nexport const TOMBSTONE_PRUNE_BUFFER_SIZE = 1000\n/** @internal */\nexport const MAX_TOMBSTONES = 5000\n\n/**\n * Result of computing which tombstones to prune.\n * @internal\n */\nexport interface TombstonePruneResult {\n\t/** The new value for tombstoneHistoryStartsAtClock */\n\tnewTombstoneHistoryStartsAtClock: number\n\t/** IDs of tombstones to delete */\n\tidsToDelete: string[]\n}\n\n/**\n * Computes which tombstones should be pruned, avoiding partial history for any clock value.\n * Returns null if no pruning is needed (tombstone count <= maxTombstones).\n *\n * @param tombstones - Array of tombstones sorted by clock ascending (oldest first)\n * @param documentClock - Current document clock (used as fallback if all tombstones are deleted)\n * @param maxTombstones - Maximum number of tombstones to keep (default: MAX_TOMBSTONES)\n * @param pruneBufferSize - Extra tombstones to prune beyond the threshold (default: TOMBSTONE_PRUNE_BUFFER_SIZE)\n * @returns Pruning result or null if no pruning needed\n *\n * @internal\n */\nexport function computeTombstonePruning({\n\ttombstones,\n\tdocumentClock,\n\tmaxTombstones = MAX_TOMBSTONES,\n\tpruneBufferSize = TOMBSTONE_PRUNE_BUFFER_SIZE,\n}: {\n\ttombstones: Array<{ id: string; clock: number }>\n\tdocumentClock: number\n\tmaxTombstones?: number\n\tpruneBufferSize?: number\n}): TombstonePruneResult | null {\n\tif (tombstones.length <= maxTombstones) {\n\t\treturn null\n\t}\n\n\t// Determine how many to delete, avoiding partial history for a clock value\n\tlet cutoff = pruneBufferSize + tombstones.length - maxTombstones\n\twhile (\n\t\tcutoff < tombstones.length &&\n\t\ttombstones[cutoff - 1]?.clock === tombstones[cutoff]?.clock\n\t) {\n\t\tcutoff++\n\t}\n\n\t// Set history start to the oldest remaining tombstone's clock\n\t// (or documentClock if we're deleting everything)\n\tconst oldestRemaining = tombstones[cutoff]\n\tconst newTombstoneHistoryStartsAtClock = oldestRemaining?.clock ?? documentClock\n\n\t// Collect the oldest tombstones to delete (first cutoff entries)\n\tconst idsToDelete = tombstones.slice(0, cutoff).map((t) => t.id)\n\n\treturn { newTombstoneHistoryStartsAtClock, idsToDelete }\n}\n\n/**\n * Default initial snapshot for a new room.\n * @public\n */\nexport const DEFAULT_INITIAL_SNAPSHOT = {\n\tdocumentClock: 0,\n\ttombstoneHistoryStartsAtClock: 0,\n\tschema: createTLSchema().serialize(),\n\tdocuments: [\n\t\t{\n\t\t\tstate: DocumentRecordType.create({ id: TLDOCUMENT_ID }),\n\t\t\tlastChangedClock: 0,\n\t\t},\n\t\t{\n\t\t\tstate: PageRecordType.create({\n\t\t\t\tid: 'page:page' as TLPageId,\n\t\t\t\tname: 'Page 1',\n\t\t\t\tindex: 'a1' as IndexKey,\n\t\t\t}),\n\t\t\tlastChangedClock: 0,\n\t\t},\n\t],\n}\n\n/**\n * In-memory implementation of TLSyncStorage using AtomMap for documents and tombstones,\n * and atoms for clock values. This is the default storage implementation used by TLSyncRoom.\n *\n * @public\n */\nexport class InMemorySyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {\n\t/** @internal */\n\tdocuments: AtomMap<string, { state: R; lastChangedClock: number }>\n\t/**\n\t * Object-store lane records, partitioned out of `documents` so they never appear in the\n\t * document snapshot. They share the same clock, tombstones, and transactions as documents.\n\t * @internal\n\t */\n\tobjects: AtomMap<string, { state: R; lastChangedClock: number }>\n\t/** @internal */\n\treadonly objectTypes: ReadonlySet<string>\n\t/** @internal */\n\ttombstones: AtomMap<string, number>\n\t/** @internal */\n\tschema: Atom<SerializedSchema>\n\t/** @internal */\n\tdocumentClock: Atom<number>\n\t/** @internal */\n\ttombstoneHistoryStartsAtClock: Atom<number>\n\n\tprivate notifier = new MicrotaskNotifier<[TLSyncStorageOnChangeCallbackProps]>()\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void {\n\t\treturn this.notifier.register(callback)\n\t}\n\n\tconstructor({\n\t\tsnapshot = DEFAULT_INITIAL_SNAPSHOT,\n\t\tobjectTypes,\n\t\tonChange,\n\t}: {\n\t\tsnapshot?: RoomSnapshot\n\t\t/**\n\t\t * Record type names stored in the object-store lane. Records of these types are routed to\n\t\t * a separate partition: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.\n\t\t */\n\t\tobjectTypes?: readonly string[]\n\t\tonChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown\n\t} = {}) {\n\t\tthis.objectTypes = new Set(objectTypes ?? [])\n\t\tconst maxClockValue = Math.max(\n\t\t\t0,\n\t\t\t...Object.values(snapshot.tombstones ?? {}),\n\t\t\t...Object.values(snapshot.documents.map((d) => d.lastChangedClock))\n\t\t)\n\t\t// route snapshot entries into their partitions (a seed snapshot may carry object-lane\n\t\t// records merged in, e.g. loaded from a separate persistence lane)\n\t\tconst toEntry = (\n\t\t\td: RoomSnapshot['documents'][number]\n\t\t): [string, { state: R; lastChangedClock: number }] => [\n\t\t\td.state.id,\n\t\t\t{ state: devFreeze(d.state) as R, lastChangedClock: d.lastChangedClock },\n\t\t]\n\t\tthis.documents = new AtomMap(\n\t\t\t'room documents',\n\t\t\tsnapshot.documents.filter((d) => !this.objectTypes.has(d.state.typeName)).map(toEntry)\n\t\t)\n\t\tthis.objects = new AtomMap(\n\t\t\t'room objects',\n\t\t\tsnapshot.documents.filter((d) => this.objectTypes.has(d.state.typeName)).map(toEntry)\n\t\t)\n\t\tconst documentClock = Math.max(maxClockValue, snapshot.documentClock ?? snapshot.clock ?? 0)\n\n\t\tthis.documentClock = atom('document clock', documentClock)\n\t\t// math.min to make sure the tombstone history starts at or before the document clock\n\t\tconst tombstoneHistoryStartsAtClock = Math.min(\n\t\t\tsnapshot.tombstoneHistoryStartsAtClock ?? documentClock,\n\t\t\tdocumentClock\n\t\t)\n\t\tthis.tombstoneHistoryStartsAtClock = atom(\n\t\t\t'tombstone history starts at clock',\n\t\t\ttombstoneHistoryStartsAtClock\n\t\t)\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tthis.schema = atom('schema', snapshot.schema ?? createTLSchema().serializeEarliestVersion())\n\t\tthis.tombstones = new AtomMap(\n\t\t\t'room tombstones',\n\t\t\t// If the tombstone history starts now (or we didn't have the\n\t\t\t// tombstoneHistoryStartsAtClock) then there are no tombstones\n\t\t\ttombstoneHistoryStartsAtClock === documentClock\n\t\t\t\t? []\n\t\t\t\t: objectMapEntries(snapshot.tombstones ?? {})\n\t\t)\n\t\tif (onChange) {\n\t\t\tthis.onChange(onChange)\n\t\t}\n\t}\n\n\ttransaction<T>(\n\t\tcallback: TLSyncStorageTransactionCallback<R, T>,\n\t\topts?: TLSyncStorageTransactionOptions\n\t): TLSyncStorageTransactionResult<T, R> {\n\t\tconst clockBefore = this.documentClock.get()\n\t\tconst trackChanges = opts?.emitChanges === 'always'\n\t\tconst txn = new InMemorySyncStorageTransaction<R>(this)\n\t\tlet result: T\n\t\tlet changes: TLSyncForwardDiff<R> | undefined\n\t\ttry {\n\t\t\tresult = transaction(() => {\n\t\t\t\treturn callback(txn as any)\n\t\t\t}) as T\n\t\t\tif (trackChanges) {\n\t\t\t\tchanges = txn.getChangesSince(clockBefore)?.diff\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error in transaction', error)\n\t\t\tthrow error\n\t\t} finally {\n\t\t\ttxn.close()\n\t\t}\n\t\tif (\n\t\t\ttypeof result === 'object' &&\n\t\t\tresult &&\n\t\t\t'then' in result &&\n\t\t\ttypeof result.then === 'function'\n\t\t) {\n\t\t\tconst err = new Error('Transaction must return a value, not a promise')\n\t\t\tconsole.error(err)\n\t\t\tthrow err\n\t\t}\n\n\t\tconst clockAfter = this.documentClock.get()\n\t\tconst didChange = clockAfter > clockBefore\n\t\tif (didChange) {\n\t\t\tthis.notifier.notify({ id: opts?.id, documentClock: clockAfter })\n\t\t}\n\t\t// InMemorySyncStorage applies changes verbatim, so we only emit changes\n\t\t// when 'always' is specified (not for 'when-different')\n\t\treturn { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes }\n\t}\n\n\tgetClock(): number {\n\t\treturn this.documentClock.get()\n\t}\n\n\t/** @internal */\n\tpruneTombstones = throttle(\n\t\t() => {\n\t\t\tif (this.tombstones.size > MAX_TOMBSTONES) {\n\t\t\t\t// Convert to array and sort by clock ascending (oldest first)\n\t\t\t\tconst tombstones = Array.from(this.tombstones.entries())\n\t\t\t\t\t.map(([id, clock]) => ({ id, clock }))\n\t\t\t\t\t.sort((a, b) => a.clock - b.clock)\n\n\t\t\t\tconst result = computeTombstonePruning({\n\t\t\t\t\ttombstones,\n\t\t\t\t\tdocumentClock: this.documentClock.get(),\n\t\t\t\t})\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.tombstoneHistoryStartsAtClock.set(result.newTombstoneHistoryStartsAtClock)\n\t\t\t\t\tthis.tombstones.deleteMany(result.idsToDelete)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t1000,\n\t\t// prevent this from running synchronously to avoid blocking requests\n\t\t{ leading: false }\n\t)\n\n\tgetSnapshot(): RoomSnapshot {\n\t\t// object-lane records live in their own partition, so the document snapshot is\n\t\t// pure-document by construction\n\t\treturn {\n\t\t\ttombstoneHistoryStartsAtClock: this.tombstoneHistoryStartsAtClock.get(),\n\t\t\tdocumentClock: this.documentClock.get(),\n\t\t\tdocuments: Array.from(this.documents.values()),\n\t\t\ttombstones: Object.fromEntries(this.tombstones.entries()),\n\t\t\tschema: this.schema.get(),\n\t\t}\n\t}\n\n\tgetObjectsSnapshot(): RoomSnapshot['documents'] {\n\t\treturn Array.from(this.objects.values())\n\t}\n\n\tgetObjectsByIds(ids: Iterable<string>): RoomSnapshot['documents'] {\n\t\tconst result: RoomSnapshot['documents'] = []\n\t\tconst seen = new Set<string>()\n\t\tfor (const id of ids) {\n\t\t\tif (seen.has(id)) continue\n\t\t\tseen.add(id)\n\t\t\tconst doc = this.objects.get(id)\n\t\t\tif (doc) result.push(doc)\n\t\t}\n\t\treturn result\n\t}\n}\n\n/**\n * Transaction implementation for InMemorySyncStorage.\n * Provides access to documents, tombstones, and metadata within a transaction.\n *\n * @internal\n */\nclass InMemorySyncStorageTransaction<\n\tR extends UnknownRecord,\n> implements TLSyncStorageTransaction<R> {\n\tprivate _clock\n\tprivate _closed = false\n\n\tconstructor(private storage: InMemorySyncStorage<R>) {\n\t\tthis._clock = this.storage.documentClock.get()\n\t}\n\n\t/** @internal */\n\tclose() {\n\t\tthis._closed = true\n\t}\n\n\tprivate assertNotClosed() {\n\t\tassert(!this._closed, 'Transaction has ended, iterator cannot be consumed')\n\t}\n\n\tgetClock(): number {\n\t\treturn this._clock\n\t}\n\n\tprivate didIncrementClock: boolean = false\n\tprivate getNextClock(): number {\n\t\tif (!this.didIncrementClock) {\n\t\t\tthis.didIncrementClock = true\n\t\t\tthis._clock = this.storage.documentClock.set(this.storage.documentClock.get() + 1)\n\t\t}\n\t\treturn this._clock\n\t}\n\n\t/** The partition a record with this id currently lives in, if any. */\n\tprivate mapContaining(id: string) {\n\t\tif (this.storage.documents.has(id)) return this.storage.documents\n\t\tif (this.storage.objects.has(id)) return this.storage.objects\n\t\treturn undefined\n\t}\n\n\tget(id: string): R | undefined {\n\t\tthis.assertNotClosed()\n\t\treturn (this.storage.documents.get(id) ?? this.storage.objects.get(id))?.state\n\t}\n\n\tset(id: string, record: R): void {\n\t\tthis.assertNotClosed()\n\t\tassert(id === record.id, `Record id mismatch: key does not match record.id`)\n\t\tconst clock = this.getNextClock()\n\t\t// Automatically clear tombstone if it exists\n\t\tif (this.storage.tombstones.has(id)) {\n\t\t\tthis.storage.tombstones.delete(id)\n\t\t}\n\t\t// route by type: object-lane records live in their own partition\n\t\tconst map = this.storage.objectTypes.has(record.typeName)\n\t\t\t? this.storage.objects\n\t\t\t: this.storage.documents\n\t\tmap.set(id, {\n\t\t\tstate: devFreeze(record) as R,\n\t\t\tlastChangedClock: clock,\n\t\t})\n\t}\n\n\tdelete(id: string): void {\n\t\tthis.assertNotClosed()\n\t\t// Only create a tombstone if the record actually exists\n\t\tconst map = this.mapContaining(id)\n\t\tif (!map) return\n\t\tconst clock = this.getNextClock()\n\t\tmap.delete(id)\n\t\tthis.storage.tombstones.set(id, clock)\n\t\tthis.storage.pruneTombstones()\n\t}\n\n\t// iteration spans both partitions so schema migrations cover object-lane records too\n\n\t*entries(): IterableIterator<[string, R]> {\n\t\tthis.assertNotClosed()\n\t\tfor (const map of [this.storage.documents, this.storage.objects]) {\n\t\t\tfor (const [id, record] of map.entries()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield [id, record.state]\n\t\t\t}\n\t\t}\n\t}\n\n\t*keys(): IterableIterator<string> {\n\t\tthis.assertNotClosed()\n\t\tfor (const map of [this.storage.documents, this.storage.objects]) {\n\t\t\tfor (const key of map.keys()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield key\n\t\t\t}\n\t\t}\n\t}\n\n\t*values(): IterableIterator<R> {\n\t\tthis.assertNotClosed()\n\t\tfor (const map of [this.storage.documents, this.storage.objects]) {\n\t\t\tfor (const record of map.values()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield record.state\n\t\t\t}\n\t\t}\n\t}\n\n\tgetSchema(): SerializedSchema {\n\t\tthis.assertNotClosed()\n\t\treturn this.storage.schema.get()\n\t}\n\n\tsetSchema(schema: SerializedSchema): void {\n\t\tthis.assertNotClosed()\n\t\tthis.storage.schema.set(schema)\n\t}\n\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined {\n\t\tthis.assertNotClosed()\n\t\tconst clock = this.storage.documentClock.get()\n\t\tif (sinceClock === clock) return undefined\n\t\tif (sinceClock > clock) {\n\t\t\t// something went wrong, wipe the slate clean\n\t\t\tsinceClock = -1\n\t\t}\n\t\tconst diff: TLSyncForwardDiff<R> = { puts: {}, deletes: [] }\n\t\tconst wipeAll = sinceClock < this.storage.tombstoneHistoryStartsAtClock.get()\n\t\t// both partitions share the clock and tombstones, so the change feed spans both\n\t\tfor (const map of [this.storage.documents, this.storage.objects]) {\n\t\t\tfor (const doc of map.values()) {\n\t\t\t\tif (wipeAll || doc.lastChangedClock > sinceClock) {\n\t\t\t\t\t// For historical changes, we don't have \"from\" state, so use added\n\t\t\t\t\tdiff.puts[doc.state.id] = doc.state as R\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!wipeAll) {\n\t\t\tfor (const [id, clock] of this.storage.tombstones.entries()) {\n\t\t\t\tif (clock > sinceClock) {\n\t\t\t\t\t// For tombstones, we don't have the removed record, use placeholder\n\t\t\t\t\tdiff.deletes.push(id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn { diff, wipeAll }\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAwC;AACxC,mBAAoE;AACpE,sBAMO;AACP,mBAA6D;AAC7D,+BAAkC;AAc3B,MAAM,8BAA8B;AAEpC,MAAM,iBAAiB;AAyBvB,SAAS,wBAAwB;AAAA,EACvC;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB,kBAAkB;AACnB,GAKgC;AAC/B,MAAI,WAAW,UAAU,eAAe;AACvC,WAAO;AAAA,EACR;AAGA,MAAI,SAAS,kBAAkB,WAAW,SAAS;AACnD,SACC,SAAS,WAAW,UACpB,WAAW,SAAS,CAAC,GAAG,UAAU,WAAW,MAAM,GAAG,OACrD;AACD;AAAA,EACD;AAIA,QAAM,kBAAkB,WAAW,MAAM;AACzC,QAAM,mCAAmC,iBAAiB,SAAS;AAGnE,QAAM,cAAc,WAAW,MAAM,GAAG,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AAE/D,SAAO,EAAE,kCAAkC,YAAY;AACxD;AAMO,MAAM,2BAA2B;AAAA,EACvC,eAAe;AAAA,EACf,+BAA+B;AAAA,EAC/B,YAAQ,gCAAe,EAAE,UAAU;AAAA,EACnC,WAAW;AAAA,IACV;AAAA,MACC,OAAO,mCAAmB,OAAO,EAAE,IAAI,8BAAc,CAAC;AAAA,MACtD,kBAAkB;AAAA,IACnB;AAAA,IACA;AAAA,MACC,OAAO,+BAAe,OAAO;AAAA,QAC5B,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,OAAO;AAAA,MACR,CAAC;AAAA,MACD,kBAAkB;AAAA,IACnB;AAAA,EACD;AACD;AAQO,MAAM,oBAAyE;AAAA;AAAA,EAErF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA;AAAA,EAES;AAAA;AAAA,EAET;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EAEQ,WAAW,IAAI,2CAAwD;AAAA,EAC/E,SAAS,UAA4E;AACpF,WAAO,KAAK,SAAS,SAAS,QAAQ;AAAA,EACvC;AAAA,EAEA,YAAY;AAAA,IACX,WAAW;AAAA,IACX;AAAA,IACA;AAAA,EACD,IAQI,CAAC,GAAG;AACP,SAAK,cAAc,IAAI,IAAI,eAAe,CAAC,CAAC;
|
|
4
|
+
"sourcesContent": ["import { atom, Atom, transaction } from '@tldraw/state'\nimport { AtomMap, devFreeze, SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport {\n\tcreateTLSchema,\n\tDocumentRecordType,\n\tPageRecordType,\n\tTLDOCUMENT_ID,\n\tTLPageId,\n} from '@tldraw/tlschema'\nimport { assert, IndexKey, objectMapEntries, throttle } from '@tldraw/utils'\nimport { MicrotaskNotifier } from './MicrotaskNotifier'\nimport { RoomSnapshot } from './TLSyncRoom'\nimport {\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n\tTLSyncStorageGetChangesSinceResult,\n\tTLSyncStorageOnChangeCallbackProps,\n\tTLSyncStorageTransaction,\n\tTLSyncStorageTransactionCallback,\n\tTLSyncStorageTransactionOptions,\n\tTLSyncStorageTransactionResult,\n} from './TLSyncStorage'\n\n/** @internal */\nexport const TOMBSTONE_PRUNE_BUFFER_SIZE = 1000\n/** @internal */\nexport const MAX_TOMBSTONES = 5000\n\n/**\n * Result of computing which tombstones to prune.\n * @internal\n */\nexport interface TombstonePruneResult {\n\t/** The new value for tombstoneHistoryStartsAtClock */\n\tnewTombstoneHistoryStartsAtClock: number\n\t/** IDs of tombstones to delete */\n\tidsToDelete: string[]\n}\n\n/**\n * Computes which tombstones should be pruned, avoiding partial history for any clock value.\n * Returns null if no pruning is needed (tombstone count <= maxTombstones).\n *\n * @param tombstones - Array of tombstones sorted by clock ascending (oldest first)\n * @param documentClock - Current document clock (used as fallback if all tombstones are deleted)\n * @param maxTombstones - Maximum number of tombstones to keep (default: MAX_TOMBSTONES)\n * @param pruneBufferSize - Extra tombstones to prune beyond the threshold (default: TOMBSTONE_PRUNE_BUFFER_SIZE)\n * @returns Pruning result or null if no pruning needed\n *\n * @internal\n */\nexport function computeTombstonePruning({\n\ttombstones,\n\tdocumentClock,\n\tmaxTombstones = MAX_TOMBSTONES,\n\tpruneBufferSize = TOMBSTONE_PRUNE_BUFFER_SIZE,\n}: {\n\ttombstones: Array<{ id: string; clock: number }>\n\tdocumentClock: number\n\tmaxTombstones?: number\n\tpruneBufferSize?: number\n}): TombstonePruneResult | null {\n\tif (tombstones.length <= maxTombstones) {\n\t\treturn null\n\t}\n\n\t// Determine how many to delete, avoiding partial history for a clock value\n\tlet cutoff = pruneBufferSize + tombstones.length - maxTombstones\n\twhile (\n\t\tcutoff < tombstones.length &&\n\t\ttombstones[cutoff - 1]?.clock === tombstones[cutoff]?.clock\n\t) {\n\t\tcutoff++\n\t}\n\n\t// Set history start to the oldest remaining tombstone's clock\n\t// (or documentClock if we're deleting everything)\n\tconst oldestRemaining = tombstones[cutoff]\n\tconst newTombstoneHistoryStartsAtClock = oldestRemaining?.clock ?? documentClock\n\n\t// Collect the oldest tombstones to delete (first cutoff entries)\n\tconst idsToDelete = tombstones.slice(0, cutoff).map((t) => t.id)\n\n\treturn { newTombstoneHistoryStartsAtClock, idsToDelete }\n}\n\n/**\n * Default initial snapshot for a new room.\n * @public\n */\nexport const DEFAULT_INITIAL_SNAPSHOT = {\n\tdocumentClock: 0,\n\ttombstoneHistoryStartsAtClock: 0,\n\tschema: createTLSchema().serialize(),\n\tdocuments: [\n\t\t{\n\t\t\tstate: DocumentRecordType.create({ id: TLDOCUMENT_ID }),\n\t\t\tlastChangedClock: 0,\n\t\t},\n\t\t{\n\t\t\tstate: PageRecordType.create({\n\t\t\t\tid: 'page:page' as TLPageId,\n\t\t\t\tname: 'Page 1',\n\t\t\t\tindex: 'a1' as IndexKey,\n\t\t\t}),\n\t\t\tlastChangedClock: 0,\n\t\t},\n\t],\n}\n\n/**\n * In-memory implementation of TLSyncStorage using AtomMap for documents and tombstones,\n * and atoms for clock values. This is the default storage implementation used by TLSyncRoom.\n *\n * @public\n */\nexport class InMemorySyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {\n\t/** @internal */\n\tdocuments: AtomMap<string, { state: R; lastChangedClock: number }>\n\t/**\n\t * Object-store lane records, partitioned out of `documents` so they never appear in the\n\t * document snapshot. They share the same clock, tombstones, and transactions as documents.\n\t * @internal\n\t */\n\tobjects: AtomMap<string, { state: R; lastChangedClock: number }>\n\t/** @internal */\n\treadonly objectTypes: ReadonlySet<string>\n\t/** @internal */\n\ttombstones: AtomMap<string, number>\n\t/** @internal */\n\tschema: Atom<SerializedSchema>\n\t/** @internal */\n\tdocumentClock: Atom<number>\n\t/** @internal */\n\ttombstoneHistoryStartsAtClock: Atom<number>\n\n\tprivate notifier = new MicrotaskNotifier<[TLSyncStorageOnChangeCallbackProps]>()\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void {\n\t\treturn this.notifier.register(callback)\n\t}\n\n\tconstructor({\n\t\tsnapshot = DEFAULT_INITIAL_SNAPSHOT,\n\t\tobjectTypes,\n\t\tonChange,\n\t}: {\n\t\tsnapshot?: RoomSnapshot\n\t\t/**\n\t\t * Record type names stored in the object-store lane. Records of these types are routed to\n\t\t * a separate partition: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.\n\t\t */\n\t\tobjectTypes?: readonly string[]\n\t\tonChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown\n\t} = {}) {\n\t\tthis.objectTypes = new Set(objectTypes ?? [])\n\t\t// a loop rather than `Math.max(0, ...clocks)`: spreading a large room's clocks as call\n\t\t// arguments overflows the stack at roughly 100k+ records/tombstones\n\t\tlet maxClockValue = 0\n\t\t// route snapshot entries into their partitions (a seed snapshot may carry object-lane\n\t\t// records merged in, e.g. loaded from a separate persistence lane)\n\t\tconst documentEntries: [string, { state: R; lastChangedClock: number }][] = []\n\t\tconst objectEntries: [string, { state: R; lastChangedClock: number }][] = []\n\t\tfor (const d of snapshot.documents) {\n\t\t\tif (d.lastChangedClock > maxClockValue) maxClockValue = d.lastChangedClock\n\t\t\tconst entries = this.objectTypes.has(d.state.typeName) ? objectEntries : documentEntries\n\t\t\tentries.push([\n\t\t\t\td.state.id,\n\t\t\t\t{ state: devFreeze(d.state) as R, lastChangedClock: d.lastChangedClock },\n\t\t\t])\n\t\t}\n\t\tconst tombstoneEntries = objectMapEntries(snapshot.tombstones ?? {})\n\t\tfor (const [, clock] of tombstoneEntries) {\n\t\t\tif (clock > maxClockValue) maxClockValue = clock\n\t\t}\n\t\tthis.documents = new AtomMap('room documents', documentEntries)\n\t\tthis.objects = new AtomMap('room objects', objectEntries)\n\t\tconst documentClock = Math.max(maxClockValue, snapshot.documentClock ?? snapshot.clock ?? 0)\n\n\t\tthis.documentClock = atom('document clock', documentClock)\n\t\t// math.min to make sure the tombstone history starts at or before the document clock\n\t\tconst tombstoneHistoryStartsAtClock = Math.min(\n\t\t\tsnapshot.tombstoneHistoryStartsAtClock ?? documentClock,\n\t\t\tdocumentClock\n\t\t)\n\t\tthis.tombstoneHistoryStartsAtClock = atom(\n\t\t\t'tombstone history starts at clock',\n\t\t\ttombstoneHistoryStartsAtClock\n\t\t)\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tthis.schema = atom('schema', snapshot.schema ?? createTLSchema().serializeEarliestVersion())\n\t\tthis.tombstones = new AtomMap(\n\t\t\t'room tombstones',\n\t\t\t// If the tombstone history starts now (or we didn't have the\n\t\t\t// tombstoneHistoryStartsAtClock) then there are no tombstones\n\t\t\ttombstoneHistoryStartsAtClock === documentClock ? [] : tombstoneEntries\n\t\t)\n\t\tif (onChange) {\n\t\t\tthis.onChange(onChange)\n\t\t}\n\t}\n\n\ttransaction<T>(\n\t\tcallback: TLSyncStorageTransactionCallback<R, T>,\n\t\topts?: TLSyncStorageTransactionOptions\n\t): TLSyncStorageTransactionResult<T, R> {\n\t\tconst clockBefore = this.documentClock.get()\n\t\tconst trackChanges = opts?.emitChanges === 'always'\n\t\tconst txn = new InMemorySyncStorageTransaction<R>(this)\n\t\tlet result: T\n\t\tlet changes: TLSyncForwardDiff<R> | undefined\n\t\ttry {\n\t\t\tresult = transaction(() => {\n\t\t\t\treturn callback(txn as any)\n\t\t\t}) as T\n\t\t\tif (trackChanges) {\n\t\t\t\tchanges = txn.getChangesSince(clockBefore)?.diff\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('Error in transaction', error)\n\t\t\tthrow error\n\t\t} finally {\n\t\t\ttxn.close()\n\t\t}\n\t\tif (\n\t\t\ttypeof result === 'object' &&\n\t\t\tresult &&\n\t\t\t'then' in result &&\n\t\t\ttypeof result.then === 'function'\n\t\t) {\n\t\t\tconst err = new Error('Transaction must return a value, not a promise')\n\t\t\tconsole.error(err)\n\t\t\tthrow err\n\t\t}\n\n\t\tconst clockAfter = this.documentClock.get()\n\t\tconst didChange = clockAfter > clockBefore\n\t\tif (didChange) {\n\t\t\tthis.notifier.notify({ id: opts?.id, documentClock: clockAfter })\n\t\t}\n\t\t// InMemorySyncStorage applies changes verbatim, so we only emit changes\n\t\t// when 'always' is specified (not for 'when-different')\n\t\treturn { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes }\n\t}\n\n\tgetClock(): number {\n\t\treturn this.documentClock.get()\n\t}\n\n\t/** @internal */\n\tpruneTombstones = throttle(\n\t\t() => {\n\t\t\tif (this.tombstones.size > MAX_TOMBSTONES) {\n\t\t\t\t// Convert to array and sort by clock ascending (oldest first)\n\t\t\t\tconst tombstones = Array.from(this.tombstones.entries())\n\t\t\t\t\t.map(([id, clock]) => ({ id, clock }))\n\t\t\t\t\t.sort((a, b) => a.clock - b.clock)\n\n\t\t\t\tconst result = computeTombstonePruning({\n\t\t\t\t\ttombstones,\n\t\t\t\t\tdocumentClock: this.documentClock.get(),\n\t\t\t\t})\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.tombstoneHistoryStartsAtClock.set(result.newTombstoneHistoryStartsAtClock)\n\t\t\t\t\tthis.tombstones.deleteMany(result.idsToDelete)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t1000,\n\t\t// prevent this from running synchronously to avoid blocking requests\n\t\t{ leading: false }\n\t)\n\n\tgetSnapshot(): RoomSnapshot {\n\t\t// object-lane records live in their own partition, so the document snapshot is\n\t\t// pure-document by construction\n\t\treturn {\n\t\t\ttombstoneHistoryStartsAtClock: this.tombstoneHistoryStartsAtClock.get(),\n\t\t\tdocumentClock: this.documentClock.get(),\n\t\t\tdocuments: Array.from(this.documents.values()),\n\t\t\ttombstones: Object.fromEntries(this.tombstones.entries()),\n\t\t\tschema: this.schema.get(),\n\t\t}\n\t}\n\n\tgetObjectsSnapshot(): RoomSnapshot['documents'] {\n\t\treturn Array.from(this.objects.values())\n\t}\n\n\tgetObjectsByIds(ids: Iterable<string>): RoomSnapshot['documents'] {\n\t\tconst result: RoomSnapshot['documents'] = []\n\t\tconst seen = new Set<string>()\n\t\tfor (const id of ids) {\n\t\t\tif (seen.has(id)) continue\n\t\t\tseen.add(id)\n\t\t\tconst doc = this.objects.get(id)\n\t\t\tif (doc) result.push(doc)\n\t\t}\n\t\treturn result\n\t}\n}\n\n/**\n * Transaction implementation for InMemorySyncStorage.\n * Provides access to documents, tombstones, and metadata within a transaction.\n *\n * @internal\n */\nclass InMemorySyncStorageTransaction<\n\tR extends UnknownRecord,\n> implements TLSyncStorageTransaction<R> {\n\tprivate _clock\n\tprivate _closed = false\n\n\tconstructor(private storage: InMemorySyncStorage<R>) {\n\t\tthis._clock = this.storage.documentClock.get()\n\t}\n\n\t/** @internal */\n\tclose() {\n\t\tthis._closed = true\n\t}\n\n\tprivate assertNotClosed() {\n\t\tassert(!this._closed, 'Transaction has ended, iterator cannot be consumed')\n\t}\n\n\tgetClock(): number {\n\t\treturn this._clock\n\t}\n\n\tprivate didIncrementClock: boolean = false\n\tprivate getNextClock(): number {\n\t\tif (!this.didIncrementClock) {\n\t\t\tthis.didIncrementClock = true\n\t\t\tthis._clock = this.storage.documentClock.set(this.storage.documentClock.get() + 1)\n\t\t}\n\t\treturn this._clock\n\t}\n\n\t/** The partition a record with this id currently lives in, if any. */\n\tprivate mapContaining(id: string) {\n\t\tif (this.storage.documents.has(id)) return this.storage.documents\n\t\tif (this.storage.objects.has(id)) return this.storage.objects\n\t\treturn undefined\n\t}\n\n\tget(id: string): R | undefined {\n\t\tthis.assertNotClosed()\n\t\treturn (this.storage.documents.get(id) ?? this.storage.objects.get(id))?.state\n\t}\n\n\tset(id: string, record: R): void {\n\t\tthis.assertNotClosed()\n\t\tassert(id === record.id, `Record id mismatch: key does not match record.id`)\n\t\tconst clock = this.getNextClock()\n\t\t// Automatically clear tombstone if it exists\n\t\tif (this.storage.tombstones.has(id)) {\n\t\t\tthis.storage.tombstones.delete(id)\n\t\t}\n\t\t// route by type: object-lane records live in their own partition\n\t\tconst map = this.storage.objectTypes.has(record.typeName)\n\t\t\t? this.storage.objects\n\t\t\t: this.storage.documents\n\t\tmap.set(id, {\n\t\t\tstate: devFreeze(record) as R,\n\t\t\tlastChangedClock: clock,\n\t\t})\n\t}\n\n\tdelete(id: string): void {\n\t\tthis.assertNotClosed()\n\t\t// Only create a tombstone if the record actually exists\n\t\tconst map = this.mapContaining(id)\n\t\tif (!map) return\n\t\tconst clock = this.getNextClock()\n\t\tmap.delete(id)\n\t\tthis.storage.tombstones.set(id, clock)\n\t\tthis.storage.pruneTombstones()\n\t}\n\n\t// iteration spans both partitions so schema migrations cover object-lane records too\n\n\t*entries(): IterableIterator<[string, R]> {\n\t\tthis.assertNotClosed()\n\t\tfor (const map of [this.storage.documents, this.storage.objects]) {\n\t\t\tfor (const [id, record] of map.entries()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield [id, record.state]\n\t\t\t}\n\t\t}\n\t}\n\n\t*keys(): IterableIterator<string> {\n\t\tthis.assertNotClosed()\n\t\tfor (const map of [this.storage.documents, this.storage.objects]) {\n\t\t\tfor (const key of map.keys()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield key\n\t\t\t}\n\t\t}\n\t}\n\n\t*values(): IterableIterator<R> {\n\t\tthis.assertNotClosed()\n\t\tfor (const map of [this.storage.documents, this.storage.objects]) {\n\t\t\tfor (const record of map.values()) {\n\t\t\t\tthis.assertNotClosed()\n\t\t\t\tyield record.state\n\t\t\t}\n\t\t}\n\t}\n\n\tgetSchema(): SerializedSchema {\n\t\tthis.assertNotClosed()\n\t\treturn this.storage.schema.get()\n\t}\n\n\tsetSchema(schema: SerializedSchema): void {\n\t\tthis.assertNotClosed()\n\t\tthis.storage.schema.set(schema)\n\t}\n\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined {\n\t\tthis.assertNotClosed()\n\t\tconst clock = this.storage.documentClock.get()\n\t\tif (sinceClock === clock) return undefined\n\t\tif (sinceClock > clock) {\n\t\t\t// something went wrong, wipe the slate clean\n\t\t\tsinceClock = -1\n\t\t}\n\t\tconst diff: TLSyncForwardDiff<R> = { puts: {}, deletes: [] }\n\t\tconst wipeAll = sinceClock < this.storage.tombstoneHistoryStartsAtClock.get()\n\t\t// both partitions share the clock and tombstones, so the change feed spans both\n\t\tfor (const map of [this.storage.documents, this.storage.objects]) {\n\t\t\tfor (const doc of map.values()) {\n\t\t\t\tif (wipeAll || doc.lastChangedClock > sinceClock) {\n\t\t\t\t\t// For historical changes, we don't have \"from\" state, so use added\n\t\t\t\t\tdiff.puts[doc.state.id] = doc.state as R\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!wipeAll) {\n\t\t\tfor (const [id, clock] of this.storage.tombstones.entries()) {\n\t\t\t\tif (clock > sinceClock) {\n\t\t\t\t\t// For tombstones, we don't have the removed record, use placeholder\n\t\t\t\t\tdiff.deletes.push(id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn { diff, wipeAll }\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["clock"]
|
|
7
7
|
}
|
|
@@ -649,7 +649,8 @@ class TLSocketRoom {
|
|
|
649
649
|
* The client will attempt to reconnect automatically unless a fatal reason is provided.
|
|
650
650
|
*
|
|
651
651
|
* @param sessionId - Session identifier to remove
|
|
652
|
-
* @param fatalReason - Optional fatal error reason that prevents reconnection
|
|
652
|
+
* @param fatalReason - Optional fatal error reason that prevents reconnection. WebSocket close
|
|
653
|
+
* reasons are capped at 123 UTF-8 bytes, so a longer reason is truncated and ends with `... (+N bytes)`.
|
|
653
654
|
*
|
|
654
655
|
* @example
|
|
655
656
|
* ```ts
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/TLSocketRoom.ts"],
|
|
4
|
-
"sourcesContent": ["import type { SerializedSchema, StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { createTLSchema, TLInstancePresence, TLStoreSnapshot } from '@tldraw/tlschema'\nimport { getOwnProperty, hasOwnProperty, isEqual, structuredClone } from '@tldraw/utils'\nimport { JsonChunkAssembler } from './chunk'\nimport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './InMemorySyncStorage'\nimport { TLObjectStoreAccess, TLSocketServerSentEvent } from './protocol'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, TLRecordAuthorizers, TLSyncRoom } from './TLSyncRoom'\nimport {\n\tconvertStoreSnapshotToRoomSnapshot,\n\tloadSnapshotIntoStorage,\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n} from './TLSyncStorage'\n\n/**\n * Strip potentially large fields from a tldraw instance_presence record so the\n * snapshot stays small when stored in WebSocket attachments (e.g. for hibernation).\n * Keeps cursor, selection, page, and user identity; clears scribbles, chatMessage, brush.\n */\nfunction stripPresenceForSnapshot(record: UnknownRecord): UnknownRecord {\n\tif (record.typeName !== 'instance_presence') return record\n\tconst stripped = { ...record } as TLInstancePresence\n\tstripped.scribbles = []\n\tstripped.chatMessage = ''\n\tstripped.selectedShapeIds = []\n\tstripped.brush = null\n\n\treturn stripped as unknown as UnknownRecord\n}\n\n/**\n * Logging interface for TLSocketRoom operations. Provides optional methods\n * for warning and error logging during synchronization operations.\n *\n * @example\n * ```ts\n * const logger: TLSyncLog = {\n * warn: (...args) => console.warn('[SYNC]', ...args),\n * error: (...args) => console.error('[SYNC]', ...args)\n * }\n *\n * const room = new TLSocketRoom({ log: logger })\n * ```\n *\n * @public\n */\nexport interface TLSyncLog {\n\t/**\n\t * Optional warning logger for non-fatal sync issues\n\t * @param args - Arguments to log\n\t */\n\twarn?(...args: any[]): void\n\t/**\n\t * Optional error logger for sync errors and failures\n\t * @param args - Arguments to log\n\t */\n\terror?(...args: any[]): void\n}\n\n/**\n * A snapshot of per-session state that can be persisted and used to resume a session\n * after the server restarts (e.g., after Cloudflare Durable Object hibernation).\n *\n * Obtain via {@link TLSocketRoom.getSessionSnapshot} and restore via\n * {@link TLSocketRoom.handleSocketResume}.\n *\n * @public\n */\nexport interface SessionStateSnapshot {\n\tserializedSchema: SerializedSchema\n\tisReadonly: boolean\n\t/**\n\t * Write access for the record types listed in `objectTypes`. Optional so snapshots persisted\n\t * before this field existed resume cleanly (they fail closed to 'read').\n\t */\n\tobjectAccess?: TLObjectStoreAccess\n\tpresenceId: string | null\n\tpresenceRecord: UnknownRecord | null\n\trequiresLegacyRejection: boolean\n\tsupportsStringAppend: boolean\n}\n\n/**\n * Base options for TLSocketRoom.\n * @public\n */\nexport interface TLSocketRoomOptions<R extends UnknownRecord, SessionMeta> {\n\tstorage?: TLSyncStorage<R>\n\t/**\n\t * @deprecated use the storage option instead\n\t */\n\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t/**\n\t * @deprecated use the storage option with an onChange callback instead\n\t */\n\tonDataChange?(): void\n\tschema?: StoreSchema<R, any>\n\t// how long to wait for a client to communicate before disconnecting them\n\tclientTimeout?: number\n\tlog?: TLSyncLog\n\t// a callback that is called when a client is disconnected\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonSessionRemoved?: (\n\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t) => void\n\t// a callback that is called whenever a message is sent\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonBeforeSendMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonAfterReceiveMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t/** @internal */\n\tonPresenceChange?(): void\n\t/**\n\t * Called once after a client push commits, with the committed document diff. Use to react to\n\t * document changes as soon as they commit \u2014 e.g. project certain record types to an external\n\t * store. Best-effort: do not throw and do not block.\n\t *\n\t * Only fires for client pushes. Server-initiated changes \u2014 `updateStore`, `loadSnapshot`, or\n\t * writing to the storage directly \u2014 do not trigger it, so anything mirroring room state through\n\t * this callback must handle those paths itself.\n\t */\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonCommittedChanges?: (args: { diff: TLSyncForwardDiff<R>; documentClock: number }) => void\n\t/**\n\t * Record type names to serve through the object-store lane instead of the document lane.\n\t * Each must be a document-scoped type registered in the schema. Object-lane writes are gated\n\t * per session by `objectAccess` (see {@link TLSocketRoom.handleSocketConnect}) rather than\n\t * `isReadonly`, so e.g. a session can be allowed to comment without being allowed to edit.\n\t */\n\tobjectTypes?: readonly string[]\n\t/**\n\t * Per-type authorizers for client record writes (create, update, delete): veto writes the\n\t * session isn't allowed to make, or rewrite the record on create \u2014 e.g. force a comment's\n\t * `authorId` to the signed-in user. See {@link TLRecordAuthorizers}.\n\t */\n\tauthorizeRecord?: TLRecordAuthorizers<R, SessionMeta>\n\t/**\n\t * When set, the room will call {@link TLSocketRoom.getSessionSnapshot} after\n\t * no message activity for a session for 5s and pass the result to this callback.\n\t * Use for persisting snapshots to WebSocket attachments (e.g. Cloudflare hibernation).\n\t * The room clears any pending snapshot when the session closes.\n\t */\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonSessionSnapshot?: (sessionId: string, snapshot: SessionStateSnapshot) => void\n}\n\n/**\n * A server-side room that manages WebSocket connections and synchronizes tldraw document state\n * between multiple clients in real-time. Each room represents a collaborative document space\n * where users can work together on drawings with automatic conflict resolution.\n *\n * TLSocketRoom handles:\n * - WebSocket connection lifecycle management\n * - Real-time synchronization of document changes\n * - Session management and presence tracking\n * - Message chunking for large payloads\n * - Automatic client timeout and cleanup\n *\n * @example\n * ```ts\n * // Basic room setup\n * const room = new TLSocketRoom({\n * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {\n * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)\n * if (numSessionsRemaining === 0) {\n * room.close()\n * }\n * },\n * onDataChange: () => {\n * console.log('Document data changed, consider persisting')\n * }\n * })\n *\n * // Handle new client connections\n * room.handleSocketConnect({\n * sessionId: 'user-session-123',\n * socket: webSocket,\n * isReadonly: false\n * })\n * ```\n *\n * @example\n * ```ts\n * // Room with initial snapshot and schema\n * const room = new TLSocketRoom({\n * initialSnapshot: existingSnapshot,\n * schema: myCustomSchema,\n * clientTimeout: 30000,\n * log: {\n * warn: (...args) => logger.warn('SYNC:', ...args),\n * error: (...args) => logger.error('SYNC:', ...args)\n * }\n * })\n *\n * // Update document programmatically\n * await room.updateStore(store => {\n * const shape = store.get('shape:abc123')\n * if (shape) {\n * shape.x = 100\n * store.put(shape)\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\n\tpublic storage: TLSyncStorage<R>\n\n\tprivate disposables = new Set<() => void>()\n\tprivate readonly snapshotTimers = new Map<string, ReturnType<typeof setTimeout>>()\n\n\t/**\n\t * Creates a new TLSocketRoom instance for managing collaborative document synchronization.\n\t *\n\t * opts - Configuration options for the room\n\t * - initialSnapshot - Optional initial document state to load\n\t * - schema - Store schema defining record types and validation\n\t * - clientTimeout - Milliseconds to wait before disconnecting inactive clients\n\t * - log - Optional logger for warnings and errors\n\t * - onSessionRemoved - Called when a client session is removed\n\t * - onBeforeSendMessage - Called before sending messages to clients\n\t * - onAfterReceiveMessage - Called after receiving messages from clients\n\t * - onDataChange - Called when document data changes\n\t * - onPresenceChange - Called when presence data changes\n\t */\n\tconstructor(public readonly opts: TLSocketRoomOptions<R, SessionMeta>) {\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif (opts.storage && opts.initialSnapshot) {\n\t\t\tthrow new Error('Cannot provide both storage and initialSnapshot options')\n\t\t}\n\t\tconst storage = opts.storage\n\t\t\t? opts.storage\n\t\t\t: new InMemorySyncStorage<R>({\n\t\t\t\t\tsnapshot: convertStoreSnapshotToRoomSnapshot(\n\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\topts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT\n\t\t\t\t\t),\n\t\t\t\t\t// keep the storage partition in step with the room's object lane\n\t\t\t\t\tobjectTypes: opts.objectTypes,\n\t\t\t\t})\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif ('onDataChange' in opts && opts.onDataChange) {\n\t\t\tthis.disposables.add(\n\t\t\t\tstorage.onChange(() => {\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\topts.onDataChange?.()\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t\t// The default logger belongs to both layers: TLSyncRoom is where authorizer throws are\n\t\t// logged, and a host that passes no `log` would otherwise never see them.\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t\tonCommittedChanges: opts.onCommittedChanges,\n\t\t\tobjectTypes: opts.objectTypes,\n\t\t\tauthorizeRecord: opts.authorizeRecord,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tlog: this.log,\n\t\t\tstorage,\n\t\t\tclientTimeout: opts.clientTimeout,\n\t\t})\n\t\tthis.storage = storage\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.clearSnapshotTimer(args.sessionId)\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Handles a new client WebSocket connection, creating a session within the room.\n\t * This should be called whenever a client establishes a WebSocket connection to join\n\t * the collaborative document.\n\t *\n\t * @param opts - Connection options\n\t * - sessionId - Unique identifier for the client session (typically from browser tab)\n\t * - socket - WebSocket-like object for client communication\n\t * - isReadonly - Whether the client can modify the document (defaults to false)\n\t * - objectAccess - Write access for object-store lane record types (defaults to 'write').\n\t * Independent of isReadonly, so a session can be allowed to write objects (e.g. comments)\n\t * without being allowed to edit the document.\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // Handle new WebSocket connection\n\t * room.handleSocketConnect({\n\t * sessionId: 'user-session-abc123',\n\t * socket: webSocketConnection,\n\t * isReadonly: !userHasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // With session metadata\n\t * room.handleSocketConnect({\n\t * sessionId: 'session-xyz',\n\t * socket: ws,\n\t * meta: { userId: 'user-123', name: 'Alice' }\n\t * })\n\t * ```\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t\tobjectAccess?: TLObjectStoreAccess\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false, objectAccess = 'write' } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tobjectAccess,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\tprivate clearSnapshotTimer(sessionId: string) {\n\t\tconst t = this.snapshotTimers.get(sessionId)\n\t\tif (t) {\n\t\t\tclearTimeout(t)\n\t\t\tthis.snapshotTimers.delete(sessionId)\n\t\t}\n\t}\n\n\tprivate scheduleDebouncedSnapshot(sessionId: string) {\n\t\tif (!this.opts.onSessionSnapshot) return\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.snapshotTimers.set(\n\t\t\tsessionId,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.snapshotTimers.delete(sessionId)\n\t\t\t\tconst snapshot = this.getSessionSnapshot(sessionId)\n\t\t\t\tif (snapshot) this.opts.onSessionSnapshot!(sessionId, snapshot)\n\t\t\t}, 5000)\n\t\t)\n\t}\n\n\t/**\n\t * Processes a message received from a client WebSocket. Use this method in server\n\t * environments where WebSocket event listeners cannot be attached directly to socket\n\t * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).\n\t *\n\t * The method handles message chunking/reassembly and forwards complete messages\n\t * to the underlying sync room for processing.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t * @param message - Raw message data from the client (string or binary)\n\t *\n\t * @example\n\t * ```ts\n\t * // In a Bun.serve handler\n\t * server.upgrade(req, {\n\t * data: { sessionId, room },\n\t * upgrade(res, req) {\n\t * // Connection established\n\t * },\n\t * message(ws, message) {\n\t * const { sessionId, room } = ws.data\n\t * room.handleSocketMessage(sessionId, message)\n\t * }\n\t * })\n\t * ```\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// handleMessage is async but does not await internally; catch rejections\n\t\t\t\t// so an error thrown while handling the message still rejects the session\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any).catch((error) => {\n\t\t\t\t\tthis.log?.error?.(error)\n\t\t\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t\t\t})\n\t\t\t\tthis.room.pruneSessions()\n\t\t\t\tthis.scheduleDebouncedSnapshot(sessionId)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * Handles a WebSocket error for the specified session. Use this in server environments\n\t * where socket event listeners cannot be attached directly. This will initiate cleanup\n\t * and session removal for the affected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('error', () => {\n\t * room.handleSocketError(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Handles a WebSocket close event for the specified session. Use this in server\n\t * environments where socket event listeners cannot be attached directly. This will\n\t * initiate cleanup and session removal for the disconnected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('close', () => {\n\t * room.handleSocketClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Resumes a previously-connected session directly into `Connected` state, bypassing\n\t * the connect handshake. Use this after server hibernation (e.g., Cloudflare Durable\n\t * Object hibernation) when WebSocket connections survived but all in-memory state was lost.\n\t *\n\t * The session is restored using a {@link SessionStateSnapshot} previously obtained\n\t * via {@link TLSocketRoom.getSessionSnapshot}. The client is unaware the server restarted and\n\t * continues sending messages normally.\n\t *\n\t * Unlike {@link TLSocketRoom.handleSocketConnect}, this method does NOT attach WebSocket event\n\t * listeners. In hibernation environments, events are delivered via class methods\n\t * (e.g., `webSocketMessage`) rather than `addEventListener`.\n\t *\n\t * @param opts - Resume options\n\t * - sessionId - Unique identifier for the client session\n\t * - socket - WebSocket-like object for client communication\n\t * - snapshot - Session state snapshot from {@link TLSocketRoom.getSessionSnapshot}\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // After Cloudflare DO hibernation wake\n\t * for (const ws of ctx.getWebSockets()) {\n\t * const data = ws.deserializeAttachment()\n\t * room.handleSocketResume({\n\t * sessionId: data.sessionId,\n\t * socket: ws,\n\t * snapshot: data.snapshot,\n\t * })\n\t * }\n\t * ```\n\t */\n\thandleSocketResume(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tsnapshot: SessionStateSnapshot\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, snapshot } = opts\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\t// no-op: hibernation environments use class methods, not addEventListener\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleResumedSession({\n\t\t\tsessionId,\n\t\t\tisReadonly: snapshot.isReadonly,\n\t\t\t// snapshots persisted before this field existed fail closed \u2014 those sessions predate\n\t\t\t// the record types gated by objectAccess, so they have nothing to write anyway\n\t\t\tobjectAccess: snapshot.objectAccess ?? 'read',\n\t\t\tserializedSchema: snapshot.serializedSchema,\n\t\t\tpresenceId: snapshot.presenceId,\n\t\t\tpresenceRecord: snapshot.presenceRecord,\n\t\t\trequiresLegacyRejection: snapshot.requiresLegacyRejection,\n\t\t\tsupportsStringAppend: snapshot.supportsStringAppend,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\t}\n\n\t/**\n\t * Returns a snapshot of a connected session's state that can be persisted and later\n\t * used with {@link TLSocketRoom.handleSocketResume} to restore the session after hibernation.\n\t *\n\t * Returns `null` if the session doesn't exist or isn't in the `Connected` state.\n\t *\n\t * @param sessionId - The session to snapshot\n\t *\n\t * @example\n\t * ```ts\n\t * // Store snapshot in a Cloudflare WebSocket attachment\n\t * const snapshot = room.getSessionSnapshot(sessionId)\n\t * if (snapshot) {\n\t * ws.serializeAttachment({ sessionId, snapshot })\n\t * }\n\t * ```\n\t */\n\tgetSessionSnapshot(sessionId: string): SessionStateSnapshot | null {\n\t\tconst session = this.room.sessions.get(sessionId)\n\t\tif (!session || session.state !== RoomSessionState.Connected) {\n\t\t\treturn null\n\t\t}\n\n\t\tlet presenceRecord: UnknownRecord | null = null\n\t\tif (session.presenceId) {\n\t\t\tconst record = this.room.presenceStore.get(session.presenceId)\n\t\t\tif (record) {\n\t\t\t\tpresenceRecord = stripPresenceForSnapshot(record as UnknownRecord)\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tserializedSchema: session.serializedSchema,\n\t\t\tisReadonly: session.isReadonly,\n\t\t\tobjectAccess: session.objectAccess,\n\t\t\tpresenceId: session.presenceId,\n\t\t\tpresenceRecord,\n\t\t\trequiresLegacyRejection: session.requiresLegacyRejection,\n\t\t\tsupportsStringAppend: session.supportsStringAppend,\n\t\t}\n\t}\n\n\t/**\n\t * Returns the current document clock value. The clock is a monotonically increasing\n\t * integer that increments with each document change, providing a consistent ordering\n\t * of changes across the distributed system.\n\t *\n\t * @returns The current document clock value\n\t *\n\t * @example\n\t * ```ts\n\t * const clock = room.getCurrentDocumentClock()\n\t * console.log(`Document is at version ${clock}`)\n\t * ```\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.storage.getClock()\n\t}\n\n\t/**\n\t * Retrieves a deeply cloned copy of a record from the document store.\n\t * Returns undefined if the record doesn't exist. The returned record is\n\t * safe to mutate without affecting the original store data.\n\t *\n\t * @param id - Unique identifier of the record to retrieve\n\t * @returns Deep clone of the record, or undefined if not found\n\t *\n\t * @example\n\t * ```ts\n\t * const shape = room.getRecord('shape:abc123')\n\t * if (shape) {\n\t * console.log('Shape position:', shape.x, shape.y)\n\t * // Safe to modify without affecting store\n\t * shape.x = 100\n\t * }\n\t * ```\n\t */\n\tgetRecord(id: string) {\n\t\treturn this.storage.transaction((txn) => {\n\t\t\treturn structuredClone(txn.get(id)) as any\n\t\t}).result as R\n\t}\n\n\t/**\n\t * Returns information about all active sessions in the room. Each session\n\t * represents a connected client with their current connection status and metadata.\n\t *\n\t * @returns Array of session information objects containing:\n\t * - sessionId - Unique session identifier\n\t * - isConnected - Whether the session has an active WebSocket connection\n\t * - isReadonly - Whether the session can modify the document\n\t * - meta - Custom session metadata\n\t *\n\t * @example\n\t * ```ts\n\t * const sessions = room.getSessions()\n\t * console.log(`Room has ${sessions.length} active sessions`)\n\t *\n\t * for (const session of sessions) {\n\t * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)\n\t * if (session.isReadonly) {\n\t * console.log(' (read-only access)')\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tobjectAccess: TLObjectStoreAccess\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tobjectAccess: session.objectAccess,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Creates a complete snapshot of the current document state, including all records\n\t * and synchronization metadata. This snapshot can be persisted to storage and used\n\t * to restore the room state later or revert to a previous version.\n\t *\n\t * @returns Complete room snapshot including documents, clock values, and tombstones\n\t * @deprecated if you need to do this use\n\t *\n\t * @example\n\t * ```ts\n\t * // Capture current state for persistence\n\t * const snapshot = room.getCurrentSnapshot()\n\t * await saveToDatabase(roomId, JSON.stringify(snapshot))\n\t *\n\t * // Later, restore from snapshot\n\t * const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))\n\t * const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })\n\t * ```\n\t */\n\tgetCurrentSnapshot() {\n\t\tif (this.storage.getSnapshot) {\n\t\t\treturn this.storage.getSnapshot()\n\t\t}\n\t\tthrow new Error('getCurrentSnapshot is not supported for this storage type')\n\t}\n\n\t/**\n\t * Returns a snapshot of the object-store lane (see `objectTypes`), for persisting\n\t * object-lane records separately from the document. Same entry shape as\n\t * {@link RoomSnapshot.documents}.\n\t */\n\tgetCurrentObjectsSnapshot(): RoomSnapshot['documents'] {\n\t\tif (this.storage.getObjectsSnapshot) {\n\t\t\treturn this.storage.getObjectsSnapshot()\n\t\t}\n\t\tthrow new Error('getCurrentObjectsSnapshot is not supported for this storage type')\n\t}\n\n\t/**\n\t * Retrieves all presence records from the document store. Presence records\n\t * contain ephemeral user state like cursor positions and selections.\n\t *\n\t * @returns Object mapping record IDs to presence record data\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const presence of this.room.presenceStore.values()) {\n\t\t\tresult[presence.id] = presence\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Loads a document snapshot, completely replacing the current room state.\n\t * This will disconnect all current clients and update the document to match\n\t * the provided snapshot. Use this for restoring from backups or implementing\n\t * document versioning.\n\t *\n\t * @param snapshot - Room or store snapshot to load\n\t *\n\t * @example\n\t * ```ts\n\t * // Restore from a saved snapshot\n\t * const backup = JSON.parse(await loadBackup(roomId))\n\t * room.loadSnapshot(backup)\n\t *\n\t * // All clients will be disconnected and need to reconnect\n\t * // to see the restored document state\n\t * ```\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tthis.storage.transaction((txn) => {\n\t\t\tloadSnapshotIntoStorage(txn, this.room.schema, snapshot)\n\t\t})\n\t}\n\n\t/**\n\t * Executes a transaction to modify the document store. Changes made within the\n\t * transaction are atomic and will be synchronized to all connected clients.\n\t * The transaction provides isolation from concurrent changes until it commits.\n\t *\n\t * @param updater - Function that receives store methods to make changes\n\t * - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)\n\t * - store.put(record) - Save a modified record\n\t * - store.getAll() - Get all records in the store\n\t * - store.delete(id) - Remove a record from the store\n\t * @returns Promise that resolves when the transaction completes\n\t *\n\t * @example\n\t * ```ts\n\t * // Update multiple shapes in a single transaction\n\t * await room.updateStore(store => {\n\t * const shape1 = store.get('shape:abc123')\n\t * const shape2 = store.get('shape:def456')\n\t *\n\t * if (shape1) {\n\t * shape1.x = 100\n\t * store.put(shape1)\n\t * }\n\t *\n\t * if (shape2) {\n\t * shape2.meta.approved = true\n\t * store.put(shape2)\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // Async transaction with external API call\n\t * await room.updateStore(async store => {\n\t * const doc = store.get('document:main')\n\t * if (doc) {\n\t * doc.lastModified = await getCurrentTimestamp()\n\t * store.put(doc)\n\t * }\n\t * })\n\t * ```\n\t * @deprecated use the storage.transaction method instead\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\tif (this.isClosed()) {\n\t\t\tthrow new Error('Cannot update store on a closed room')\n\t\t}\n\t\t// read through the transaction rather than getSnapshot(): the document snapshot excludes the\n\t\t// object-store lane, which would make object-lane records invisible (and undeletable) here\n\t\tconst records = this.storage.transaction((txn) => Object.fromEntries(txn.entries())).result\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tconst ctx = new StoreUpdateContext<R>(records, this.room.schema)\n\t\ttry {\n\t\t\tawait updater(ctx)\n\t\t} finally {\n\t\t\tctx.close()\n\t\t}\n\t\tthis.storage.transaction((txn) => {\n\t\t\tfor (const [id, record] of Object.entries(ctx.updates.puts)) {\n\t\t\t\ttxn.set(id, record as R)\n\t\t\t}\n\t\t\tfor (const id of ctx.updates.deletes) {\n\t\t\t\ttxn.delete(id)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Sends a custom message to a specific client session. This allows sending\n\t * application-specific data that doesn't modify the document state, such as\n\t * notifications, chat messages, or custom commands.\n\t *\n\t * @param sessionId - Target session identifier\n\t * @param data - Custom payload to send (will be JSON serialized)\n\t *\n\t * @example\n\t * ```ts\n\t * // Send a notification to a specific user\n\t * room.sendCustomMessage('session-123', {\n\t * type: 'notification',\n\t * message: 'Your changes have been saved'\n\t * })\n\t *\n\t * // Send a chat message\n\t * room.sendCustomMessage('session-456', {\n\t * type: 'chat',\n\t * from: 'Alice',\n\t * text: 'Great work on this design!'\n\t * })\n\t * ```\n\t */\n\tsendCustomMessage(sessionId: string, data: any) {\n\t\tthis.room.sendCustomMessage(sessionId, data)\n\t}\n\n\t/**\n\t * Immediately removes a session from the room and closes its WebSocket connection.\n\t * The client will attempt to reconnect automatically unless a fatal reason is provided.\n\t *\n\t * @param sessionId - Session identifier to remove\n\t * @param fatalReason - Optional fatal error reason that prevents reconnection\n\t *\n\t * @example\n\t * ```ts\n\t * // Kick a user (they can reconnect)\n\t * room.closeSession('session-troublemaker')\n\t *\n\t * // Permanently ban a user\n\t * room.closeSession('session-banned', 'PERMISSION_DENIED')\n\t *\n\t * // Close session due to inactivity\n\t * room.closeSession('session-idle', 'TIMEOUT')\n\t * ```\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Closes the room and disconnects all connected clients. This should be called\n\t * when shutting down the room permanently, such as during server shutdown or\n\t * when the room is no longer needed. Once closed, the room cannot be reopened.\n\t *\n\t * @example\n\t * ```ts\n\t * // Clean shutdown when no users remain\n\t * if (room.getNumActiveSessions() === 0) {\n\t * await persistSnapshot(room.getCurrentSnapshot())\n\t * room.close()\n\t * }\n\t *\n\t * // Server shutdown\n\t * process.on('SIGTERM', () => {\n\t * for (const room of activeRooms.values()) {\n\t * room.close()\n\t * }\n\t * })\n\t * ```\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t\tfor (const sessionId of this.snapshotTimers.keys()) {\n\t\t\tthis.clearSnapshotTimer(sessionId)\n\t\t}\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.disposables.clear()\n\t}\n\n\t/**\n\t * Checks whether the room has been permanently closed. Closed rooms cannot\n\t * accept new connections or process further changes.\n\t *\n\t * @returns True if the room is closed, false if still active\n\t *\n\t * @example\n\t * ```ts\n\t * if (room.isClosed()) {\n\t * console.log('Room has been shut down')\n\t * // Create a new room or redirect users\n\t * } else {\n\t * // Room is still accepting connections\n\t * room.handleSocketConnect({ sessionId, socket })\n\t * }\n\t * ```\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/**\n * Utility type that removes properties with void values from an object type.\n * This is used internally to conditionally require session metadata based on\n * whether SessionMeta extends void.\n *\n * @example\n * ```ts\n * type Example = { a: string, b: void, c: number }\n * type Result = OmitVoid<Example> // { a: string, c: number }\n * ```\n *\n * @public\n */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\n/**\n * Interface for making transactional changes to room store data. Used within\n * updateStore transactions to modify documents atomically.\n *\n * @example\n * ```ts\n * await room.updateStore((store) => {\n * const shape = store.get('shape:123')\n * if (shape) {\n * store.put({ ...shape, x: shape.x + 10 })\n * }\n * store.delete('shape:456')\n * })\n * ```\n *\n * @public\n * @deprecated use the storage.transaction method instead\n */\nexport interface RoomStoreMethods<R extends UnknownRecord = UnknownRecord> {\n\t/**\n\t * Add or update a record in the store.\n\t *\n\t * @param record - The record to store\n\t */\n\tput(record: R): void\n\t/**\n\t * Delete a record from the store.\n\t *\n\t * @param recordOrId - The record or record ID to delete\n\t */\n\tdelete(recordOrId: R | string): void\n\t/**\n\t * Get a record by its ID.\n\t *\n\t * @param id - The record ID\n\t * @returns The record or null if not found\n\t */\n\tget(id: string): R | null\n\t/**\n\t * Get all records in the store.\n\t *\n\t * @returns Array of all records\n\t */\n\tgetAll(): R[]\n}\n\n/**\n * @deprecated use the storage.transaction method instead\n */\n// eslint-disable-next-line @typescript-eslint/no-deprecated\nclass StoreUpdateContext<R extends UnknownRecord> implements RoomStoreMethods<R> {\n\tconstructor(\n\t\tprivate readonly snapshot: Record<string, UnknownRecord>,\n\t\tprivate readonly schema: StoreSchema<R, any>\n\t) {}\n\treadonly updates = {\n\t\tputs: {} as Record<string, UnknownRecord>,\n\t\tdeletes: new Set<string>(),\n\t}\n\tput(record: R): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst recordType = getOwnProperty(this.schema.types, record.typeName)\n\t\tif (!recordType) {\n\t\t\tthrow new Error(`Missing definition for record type ${record.typeName}`)\n\t\t}\n\t\tconst recordBefore = this.snapshot[record.id] ?? undefined\n\t\trecordType.validate(record, recordBefore as R)\n\n\t\tif (record.id in this.snapshot && isEqual(this.snapshot[record.id], record)) {\n\t\t\tdelete this.updates.puts[record.id]\n\t\t} else {\n\t\t\tthis.updates.puts[record.id] = structuredClone(record)\n\t\t}\n\t\tthis.updates.deletes.delete(record.id)\n\t}\n\tdelete(recordOrId: R | string): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst id = typeof recordOrId === 'string' ? recordOrId : recordOrId.id\n\t\tdelete this.updates.puts[id]\n\t\tif (this.snapshot[id]) {\n\t\t\tthis.updates.deletes.add(id)\n\t\t}\n\t}\n\tget(id: string): R | null {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tif (hasOwnProperty(this.updates.puts, id)) {\n\t\t\treturn structuredClone(this.updates.puts[id]) as R\n\t\t}\n\t\tif (this.updates.deletes.has(id)) {\n\t\t\treturn null\n\t\t}\n\t\treturn structuredClone(this.snapshot[id] ?? null) as R\n\t}\n\n\tgetAll(): R[] {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst result = Object.values(this.updates.puts)\n\t\tfor (const [id, record] of Object.entries(this.snapshot)) {\n\t\t\tif (!this.updates.deletes.has(id) && !hasOwnProperty(this.updates.puts, id)) {\n\t\t\t\tresult.push(record)\n\t\t\t}\n\t\t}\n\t\treturn structuredClone(result) as R[]\n\t}\n\n\tprivate _isClosed = false\n\tclose() {\n\t\tthis._isClosed = true\n\t}\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { SerializedSchema, StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { createTLSchema, TLInstancePresence, TLStoreSnapshot } from '@tldraw/tlschema'\nimport { getOwnProperty, hasOwnProperty, isEqual, structuredClone } from '@tldraw/utils'\nimport { JsonChunkAssembler } from './chunk'\nimport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './InMemorySyncStorage'\nimport { TLObjectStoreAccess, TLSocketServerSentEvent } from './protocol'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, TLRecordAuthorizers, TLSyncRoom } from './TLSyncRoom'\nimport {\n\tconvertStoreSnapshotToRoomSnapshot,\n\tloadSnapshotIntoStorage,\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n} from './TLSyncStorage'\n\n/**\n * Strip potentially large fields from a tldraw instance_presence record so the\n * snapshot stays small when stored in WebSocket attachments (e.g. for hibernation).\n * Keeps cursor, selection, page, and user identity; clears scribbles, chatMessage, brush.\n */\nfunction stripPresenceForSnapshot(record: UnknownRecord): UnknownRecord {\n\tif (record.typeName !== 'instance_presence') return record\n\tconst stripped = { ...record } as TLInstancePresence\n\tstripped.scribbles = []\n\tstripped.chatMessage = ''\n\tstripped.selectedShapeIds = []\n\tstripped.brush = null\n\n\treturn stripped as unknown as UnknownRecord\n}\n\n/**\n * Logging interface for TLSocketRoom operations. Provides optional methods\n * for warning and error logging during synchronization operations.\n *\n * @example\n * ```ts\n * const logger: TLSyncLog = {\n * warn: (...args) => console.warn('[SYNC]', ...args),\n * error: (...args) => console.error('[SYNC]', ...args)\n * }\n *\n * const room = new TLSocketRoom({ log: logger })\n * ```\n *\n * @public\n */\nexport interface TLSyncLog {\n\t/**\n\t * Optional warning logger for non-fatal sync issues\n\t * @param args - Arguments to log\n\t */\n\twarn?(...args: any[]): void\n\t/**\n\t * Optional error logger for sync errors and failures\n\t * @param args - Arguments to log\n\t */\n\terror?(...args: any[]): void\n}\n\n/**\n * A snapshot of per-session state that can be persisted and used to resume a session\n * after the server restarts (e.g., after Cloudflare Durable Object hibernation).\n *\n * Obtain via {@link TLSocketRoom.getSessionSnapshot} and restore via\n * {@link TLSocketRoom.handleSocketResume}.\n *\n * @public\n */\nexport interface SessionStateSnapshot {\n\tserializedSchema: SerializedSchema\n\tisReadonly: boolean\n\t/**\n\t * Write access for the record types listed in `objectTypes`. Optional so snapshots persisted\n\t * before this field existed resume cleanly (they fail closed to 'read').\n\t */\n\tobjectAccess?: TLObjectStoreAccess\n\tpresenceId: string | null\n\tpresenceRecord: UnknownRecord | null\n\trequiresLegacyRejection: boolean\n\tsupportsStringAppend: boolean\n}\n\n/**\n * Base options for TLSocketRoom.\n * @public\n */\nexport interface TLSocketRoomOptions<R extends UnknownRecord, SessionMeta> {\n\tstorage?: TLSyncStorage<R>\n\t/**\n\t * @deprecated use the storage option instead\n\t */\n\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t/**\n\t * @deprecated use the storage option with an onChange callback instead\n\t */\n\tonDataChange?(): void\n\tschema?: StoreSchema<R, any>\n\t// how long to wait for a client to communicate before disconnecting them\n\tclientTimeout?: number\n\tlog?: TLSyncLog\n\t// a callback that is called when a client is disconnected\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonSessionRemoved?: (\n\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t) => void\n\t// a callback that is called whenever a message is sent\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonBeforeSendMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonAfterReceiveMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t/** @internal */\n\tonPresenceChange?(): void\n\t/**\n\t * Called once after a client push commits, with the committed document diff. Use to react to\n\t * document changes as soon as they commit \u2014 e.g. project certain record types to an external\n\t * store. Best-effort: do not throw and do not block.\n\t *\n\t * Only fires for client pushes. Server-initiated changes \u2014 `updateStore`, `loadSnapshot`, or\n\t * writing to the storage directly \u2014 do not trigger it, so anything mirroring room state through\n\t * this callback must handle those paths itself.\n\t */\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonCommittedChanges?: (args: { diff: TLSyncForwardDiff<R>; documentClock: number }) => void\n\t/**\n\t * Record type names to serve through the object-store lane instead of the document lane.\n\t * Each must be a document-scoped type registered in the schema. Object-lane writes are gated\n\t * per session by `objectAccess` (see {@link TLSocketRoom.handleSocketConnect}) rather than\n\t * `isReadonly`, so e.g. a session can be allowed to comment without being allowed to edit.\n\t */\n\tobjectTypes?: readonly string[]\n\t/**\n\t * Per-type authorizers for client record writes (create, update, delete): veto writes the\n\t * session isn't allowed to make, or rewrite the record on create \u2014 e.g. force a comment's\n\t * `authorId` to the signed-in user. See {@link TLRecordAuthorizers}.\n\t */\n\tauthorizeRecord?: TLRecordAuthorizers<R, SessionMeta>\n\t/**\n\t * When set, the room will call {@link TLSocketRoom.getSessionSnapshot} after\n\t * no message activity for a session for 5s and pass the result to this callback.\n\t * Use for persisting snapshots to WebSocket attachments (e.g. Cloudflare hibernation).\n\t * The room clears any pending snapshot when the session closes.\n\t */\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tonSessionSnapshot?: (sessionId: string, snapshot: SessionStateSnapshot) => void\n}\n\n/**\n * A server-side room that manages WebSocket connections and synchronizes tldraw document state\n * between multiple clients in real-time. Each room represents a collaborative document space\n * where users can work together on drawings with automatic conflict resolution.\n *\n * TLSocketRoom handles:\n * - WebSocket connection lifecycle management\n * - Real-time synchronization of document changes\n * - Session management and presence tracking\n * - Message chunking for large payloads\n * - Automatic client timeout and cleanup\n *\n * @example\n * ```ts\n * // Basic room setup\n * const room = new TLSocketRoom({\n * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {\n * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)\n * if (numSessionsRemaining === 0) {\n * room.close()\n * }\n * },\n * onDataChange: () => {\n * console.log('Document data changed, consider persisting')\n * }\n * })\n *\n * // Handle new client connections\n * room.handleSocketConnect({\n * sessionId: 'user-session-123',\n * socket: webSocket,\n * isReadonly: false\n * })\n * ```\n *\n * @example\n * ```ts\n * // Room with initial snapshot and schema\n * const room = new TLSocketRoom({\n * initialSnapshot: existingSnapshot,\n * schema: myCustomSchema,\n * clientTimeout: 30000,\n * log: {\n * warn: (...args) => logger.warn('SYNC:', ...args),\n * error: (...args) => logger.error('SYNC:', ...args)\n * }\n * })\n *\n * // Update document programmatically\n * await room.updateStore(store => {\n * const shape = store.get('shape:abc123')\n * if (shape) {\n * shape.x = 100\n * store.put(shape)\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\n\tpublic storage: TLSyncStorage<R>\n\n\tprivate disposables = new Set<() => void>()\n\tprivate readonly snapshotTimers = new Map<string, ReturnType<typeof setTimeout>>()\n\n\t/**\n\t * Creates a new TLSocketRoom instance for managing collaborative document synchronization.\n\t *\n\t * opts - Configuration options for the room\n\t * - initialSnapshot - Optional initial document state to load\n\t * - schema - Store schema defining record types and validation\n\t * - clientTimeout - Milliseconds to wait before disconnecting inactive clients\n\t * - log - Optional logger for warnings and errors\n\t * - onSessionRemoved - Called when a client session is removed\n\t * - onBeforeSendMessage - Called before sending messages to clients\n\t * - onAfterReceiveMessage - Called after receiving messages from clients\n\t * - onDataChange - Called when document data changes\n\t * - onPresenceChange - Called when presence data changes\n\t */\n\tconstructor(public readonly opts: TLSocketRoomOptions<R, SessionMeta>) {\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif (opts.storage && opts.initialSnapshot) {\n\t\t\tthrow new Error('Cannot provide both storage and initialSnapshot options')\n\t\t}\n\t\tconst storage = opts.storage\n\t\t\t? opts.storage\n\t\t\t: new InMemorySyncStorage<R>({\n\t\t\t\t\tsnapshot: convertStoreSnapshotToRoomSnapshot(\n\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\topts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT\n\t\t\t\t\t),\n\t\t\t\t\t// keep the storage partition in step with the room's object lane\n\t\t\t\t\tobjectTypes: opts.objectTypes,\n\t\t\t\t})\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif ('onDataChange' in opts && opts.onDataChange) {\n\t\t\tthis.disposables.add(\n\t\t\t\tstorage.onChange(() => {\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\topts.onDataChange?.()\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t\t// The default logger belongs to both layers: TLSyncRoom is where authorizer throws are\n\t\t// logged, and a host that passes no `log` would otherwise never see them.\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t\tonCommittedChanges: opts.onCommittedChanges,\n\t\t\tobjectTypes: opts.objectTypes,\n\t\t\tauthorizeRecord: opts.authorizeRecord,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tlog: this.log,\n\t\t\tstorage,\n\t\t\tclientTimeout: opts.clientTimeout,\n\t\t})\n\t\tthis.storage = storage\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.clearSnapshotTimer(args.sessionId)\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Handles a new client WebSocket connection, creating a session within the room.\n\t * This should be called whenever a client establishes a WebSocket connection to join\n\t * the collaborative document.\n\t *\n\t * @param opts - Connection options\n\t * - sessionId - Unique identifier for the client session (typically from browser tab)\n\t * - socket - WebSocket-like object for client communication\n\t * - isReadonly - Whether the client can modify the document (defaults to false)\n\t * - objectAccess - Write access for object-store lane record types (defaults to 'write').\n\t * Independent of isReadonly, so a session can be allowed to write objects (e.g. comments)\n\t * without being allowed to edit the document.\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // Handle new WebSocket connection\n\t * room.handleSocketConnect({\n\t * sessionId: 'user-session-abc123',\n\t * socket: webSocketConnection,\n\t * isReadonly: !userHasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // With session metadata\n\t * room.handleSocketConnect({\n\t * sessionId: 'session-xyz',\n\t * socket: ws,\n\t * meta: { userId: 'user-123', name: 'Alice' }\n\t * })\n\t * ```\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t\tobjectAccess?: TLObjectStoreAccess\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false, objectAccess = 'write' } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tobjectAccess,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\tprivate clearSnapshotTimer(sessionId: string) {\n\t\tconst t = this.snapshotTimers.get(sessionId)\n\t\tif (t) {\n\t\t\tclearTimeout(t)\n\t\t\tthis.snapshotTimers.delete(sessionId)\n\t\t}\n\t}\n\n\tprivate scheduleDebouncedSnapshot(sessionId: string) {\n\t\tif (!this.opts.onSessionSnapshot) return\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.snapshotTimers.set(\n\t\t\tsessionId,\n\t\t\tsetTimeout(() => {\n\t\t\t\tthis.snapshotTimers.delete(sessionId)\n\t\t\t\tconst snapshot = this.getSessionSnapshot(sessionId)\n\t\t\t\tif (snapshot) this.opts.onSessionSnapshot!(sessionId, snapshot)\n\t\t\t}, 5000)\n\t\t)\n\t}\n\n\t/**\n\t * Processes a message received from a client WebSocket. Use this method in server\n\t * environments where WebSocket event listeners cannot be attached directly to socket\n\t * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).\n\t *\n\t * The method handles message chunking/reassembly and forwards complete messages\n\t * to the underlying sync room for processing.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t * @param message - Raw message data from the client (string or binary)\n\t *\n\t * @example\n\t * ```ts\n\t * // In a Bun.serve handler\n\t * server.upgrade(req, {\n\t * data: { sessionId, room },\n\t * upgrade(res, req) {\n\t * // Connection established\n\t * },\n\t * message(ws, message) {\n\t * const { sessionId, room } = ws.data\n\t * room.handleSocketMessage(sessionId, message)\n\t * }\n\t * })\n\t * ```\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// handleMessage is async but does not await internally; catch rejections\n\t\t\t\t// so an error thrown while handling the message still rejects the session\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any).catch((error) => {\n\t\t\t\t\tthis.log?.error?.(error)\n\t\t\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t\t\t})\n\t\t\t\tthis.room.pruneSessions()\n\t\t\t\tthis.scheduleDebouncedSnapshot(sessionId)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * Handles a WebSocket error for the specified session. Use this in server environments\n\t * where socket event listeners cannot be attached directly. This will initiate cleanup\n\t * and session removal for the affected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('error', () => {\n\t * room.handleSocketError(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Handles a WebSocket close event for the specified session. Use this in server\n\t * environments where socket event listeners cannot be attached directly. This will\n\t * initiate cleanup and session removal for the disconnected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('close', () => {\n\t * room.handleSocketClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.clearSnapshotTimer(sessionId)\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Resumes a previously-connected session directly into `Connected` state, bypassing\n\t * the connect handshake. Use this after server hibernation (e.g., Cloudflare Durable\n\t * Object hibernation) when WebSocket connections survived but all in-memory state was lost.\n\t *\n\t * The session is restored using a {@link SessionStateSnapshot} previously obtained\n\t * via {@link TLSocketRoom.getSessionSnapshot}. The client is unaware the server restarted and\n\t * continues sending messages normally.\n\t *\n\t * Unlike {@link TLSocketRoom.handleSocketConnect}, this method does NOT attach WebSocket event\n\t * listeners. In hibernation environments, events are delivered via class methods\n\t * (e.g., `webSocketMessage`) rather than `addEventListener`.\n\t *\n\t * @param opts - Resume options\n\t * - sessionId - Unique identifier for the client session\n\t * - socket - WebSocket-like object for client communication\n\t * - snapshot - Session state snapshot from {@link TLSocketRoom.getSessionSnapshot}\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // After Cloudflare DO hibernation wake\n\t * for (const ws of ctx.getWebSockets()) {\n\t * const data = ws.deserializeAttachment()\n\t * room.handleSocketResume({\n\t * sessionId: data.sessionId,\n\t * socket: ws,\n\t * snapshot: data.snapshot,\n\t * })\n\t * }\n\t * ```\n\t */\n\thandleSocketResume(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tsnapshot: SessionStateSnapshot\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, snapshot } = opts\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\t// no-op: hibernation environments use class methods, not addEventListener\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleResumedSession({\n\t\t\tsessionId,\n\t\t\tisReadonly: snapshot.isReadonly,\n\t\t\t// snapshots persisted before this field existed fail closed \u2014 those sessions predate\n\t\t\t// the record types gated by objectAccess, so they have nothing to write anyway\n\t\t\tobjectAccess: snapshot.objectAccess ?? 'read',\n\t\t\tserializedSchema: snapshot.serializedSchema,\n\t\t\tpresenceId: snapshot.presenceId,\n\t\t\tpresenceRecord: snapshot.presenceRecord,\n\t\t\trequiresLegacyRejection: snapshot.requiresLegacyRejection,\n\t\t\tsupportsStringAppend: snapshot.supportsStringAppend,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\t}\n\n\t/**\n\t * Returns a snapshot of a connected session's state that can be persisted and later\n\t * used with {@link TLSocketRoom.handleSocketResume} to restore the session after hibernation.\n\t *\n\t * Returns `null` if the session doesn't exist or isn't in the `Connected` state.\n\t *\n\t * @param sessionId - The session to snapshot\n\t *\n\t * @example\n\t * ```ts\n\t * // Store snapshot in a Cloudflare WebSocket attachment\n\t * const snapshot = room.getSessionSnapshot(sessionId)\n\t * if (snapshot) {\n\t * ws.serializeAttachment({ sessionId, snapshot })\n\t * }\n\t * ```\n\t */\n\tgetSessionSnapshot(sessionId: string): SessionStateSnapshot | null {\n\t\tconst session = this.room.sessions.get(sessionId)\n\t\tif (!session || session.state !== RoomSessionState.Connected) {\n\t\t\treturn null\n\t\t}\n\n\t\tlet presenceRecord: UnknownRecord | null = null\n\t\tif (session.presenceId) {\n\t\t\tconst record = this.room.presenceStore.get(session.presenceId)\n\t\t\tif (record) {\n\t\t\t\tpresenceRecord = stripPresenceForSnapshot(record as UnknownRecord)\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tserializedSchema: session.serializedSchema,\n\t\t\tisReadonly: session.isReadonly,\n\t\t\tobjectAccess: session.objectAccess,\n\t\t\tpresenceId: session.presenceId,\n\t\t\tpresenceRecord,\n\t\t\trequiresLegacyRejection: session.requiresLegacyRejection,\n\t\t\tsupportsStringAppend: session.supportsStringAppend,\n\t\t}\n\t}\n\n\t/**\n\t * Returns the current document clock value. The clock is a monotonically increasing\n\t * integer that increments with each document change, providing a consistent ordering\n\t * of changes across the distributed system.\n\t *\n\t * @returns The current document clock value\n\t *\n\t * @example\n\t * ```ts\n\t * const clock = room.getCurrentDocumentClock()\n\t * console.log(`Document is at version ${clock}`)\n\t * ```\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.storage.getClock()\n\t}\n\n\t/**\n\t * Retrieves a deeply cloned copy of a record from the document store.\n\t * Returns undefined if the record doesn't exist. The returned record is\n\t * safe to mutate without affecting the original store data.\n\t *\n\t * @param id - Unique identifier of the record to retrieve\n\t * @returns Deep clone of the record, or undefined if not found\n\t *\n\t * @example\n\t * ```ts\n\t * const shape = room.getRecord('shape:abc123')\n\t * if (shape) {\n\t * console.log('Shape position:', shape.x, shape.y)\n\t * // Safe to modify without affecting store\n\t * shape.x = 100\n\t * }\n\t * ```\n\t */\n\tgetRecord(id: string) {\n\t\treturn this.storage.transaction((txn) => {\n\t\t\treturn structuredClone(txn.get(id)) as any\n\t\t}).result as R\n\t}\n\n\t/**\n\t * Returns information about all active sessions in the room. Each session\n\t * represents a connected client with their current connection status and metadata.\n\t *\n\t * @returns Array of session information objects containing:\n\t * - sessionId - Unique session identifier\n\t * - isConnected - Whether the session has an active WebSocket connection\n\t * - isReadonly - Whether the session can modify the document\n\t * - meta - Custom session metadata\n\t *\n\t * @example\n\t * ```ts\n\t * const sessions = room.getSessions()\n\t * console.log(`Room has ${sessions.length} active sessions`)\n\t *\n\t * for (const session of sessions) {\n\t * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)\n\t * if (session.isReadonly) {\n\t * console.log(' (read-only access)')\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tobjectAccess: TLObjectStoreAccess\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tobjectAccess: session.objectAccess,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Creates a complete snapshot of the current document state, including all records\n\t * and synchronization metadata. This snapshot can be persisted to storage and used\n\t * to restore the room state later or revert to a previous version.\n\t *\n\t * @returns Complete room snapshot including documents, clock values, and tombstones\n\t * @deprecated if you need to do this use\n\t *\n\t * @example\n\t * ```ts\n\t * // Capture current state for persistence\n\t * const snapshot = room.getCurrentSnapshot()\n\t * await saveToDatabase(roomId, JSON.stringify(snapshot))\n\t *\n\t * // Later, restore from snapshot\n\t * const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))\n\t * const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })\n\t * ```\n\t */\n\tgetCurrentSnapshot() {\n\t\tif (this.storage.getSnapshot) {\n\t\t\treturn this.storage.getSnapshot()\n\t\t}\n\t\tthrow new Error('getCurrentSnapshot is not supported for this storage type')\n\t}\n\n\t/**\n\t * Returns a snapshot of the object-store lane (see `objectTypes`), for persisting\n\t * object-lane records separately from the document. Same entry shape as\n\t * {@link RoomSnapshot.documents}.\n\t */\n\tgetCurrentObjectsSnapshot(): RoomSnapshot['documents'] {\n\t\tif (this.storage.getObjectsSnapshot) {\n\t\t\treturn this.storage.getObjectsSnapshot()\n\t\t}\n\t\tthrow new Error('getCurrentObjectsSnapshot is not supported for this storage type')\n\t}\n\n\t/**\n\t * Retrieves all presence records from the document store. Presence records\n\t * contain ephemeral user state like cursor positions and selections.\n\t *\n\t * @returns Object mapping record IDs to presence record data\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const presence of this.room.presenceStore.values()) {\n\t\t\tresult[presence.id] = presence\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Loads a document snapshot, completely replacing the current room state.\n\t * This will disconnect all current clients and update the document to match\n\t * the provided snapshot. Use this for restoring from backups or implementing\n\t * document versioning.\n\t *\n\t * @param snapshot - Room or store snapshot to load\n\t *\n\t * @example\n\t * ```ts\n\t * // Restore from a saved snapshot\n\t * const backup = JSON.parse(await loadBackup(roomId))\n\t * room.loadSnapshot(backup)\n\t *\n\t * // All clients will be disconnected and need to reconnect\n\t * // to see the restored document state\n\t * ```\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tthis.storage.transaction((txn) => {\n\t\t\tloadSnapshotIntoStorage(txn, this.room.schema, snapshot)\n\t\t})\n\t}\n\n\t/**\n\t * Executes a transaction to modify the document store. Changes made within the\n\t * transaction are atomic and will be synchronized to all connected clients.\n\t * The transaction provides isolation from concurrent changes until it commits.\n\t *\n\t * @param updater - Function that receives store methods to make changes\n\t * - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)\n\t * - store.put(record) - Save a modified record\n\t * - store.getAll() - Get all records in the store\n\t * - store.delete(id) - Remove a record from the store\n\t * @returns Promise that resolves when the transaction completes\n\t *\n\t * @example\n\t * ```ts\n\t * // Update multiple shapes in a single transaction\n\t * await room.updateStore(store => {\n\t * const shape1 = store.get('shape:abc123')\n\t * const shape2 = store.get('shape:def456')\n\t *\n\t * if (shape1) {\n\t * shape1.x = 100\n\t * store.put(shape1)\n\t * }\n\t *\n\t * if (shape2) {\n\t * shape2.meta.approved = true\n\t * store.put(shape2)\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // Async transaction with external API call\n\t * await room.updateStore(async store => {\n\t * const doc = store.get('document:main')\n\t * if (doc) {\n\t * doc.lastModified = await getCurrentTimestamp()\n\t * store.put(doc)\n\t * }\n\t * })\n\t * ```\n\t * @deprecated use the storage.transaction method instead\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\tif (this.isClosed()) {\n\t\t\tthrow new Error('Cannot update store on a closed room')\n\t\t}\n\t\t// read through the transaction rather than getSnapshot(): the document snapshot excludes the\n\t\t// object-store lane, which would make object-lane records invisible (and undeletable) here\n\t\tconst records = this.storage.transaction((txn) => Object.fromEntries(txn.entries())).result\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tconst ctx = new StoreUpdateContext<R>(records, this.room.schema)\n\t\ttry {\n\t\t\tawait updater(ctx)\n\t\t} finally {\n\t\t\tctx.close()\n\t\t}\n\t\tthis.storage.transaction((txn) => {\n\t\t\tfor (const [id, record] of Object.entries(ctx.updates.puts)) {\n\t\t\t\ttxn.set(id, record as R)\n\t\t\t}\n\t\t\tfor (const id of ctx.updates.deletes) {\n\t\t\t\ttxn.delete(id)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Sends a custom message to a specific client session. This allows sending\n\t * application-specific data that doesn't modify the document state, such as\n\t * notifications, chat messages, or custom commands.\n\t *\n\t * @param sessionId - Target session identifier\n\t * @param data - Custom payload to send (will be JSON serialized)\n\t *\n\t * @example\n\t * ```ts\n\t * // Send a notification to a specific user\n\t * room.sendCustomMessage('session-123', {\n\t * type: 'notification',\n\t * message: 'Your changes have been saved'\n\t * })\n\t *\n\t * // Send a chat message\n\t * room.sendCustomMessage('session-456', {\n\t * type: 'chat',\n\t * from: 'Alice',\n\t * text: 'Great work on this design!'\n\t * })\n\t * ```\n\t */\n\tsendCustomMessage(sessionId: string, data: any) {\n\t\tthis.room.sendCustomMessage(sessionId, data)\n\t}\n\n\t/**\n\t * Immediately removes a session from the room and closes its WebSocket connection.\n\t * The client will attempt to reconnect automatically unless a fatal reason is provided.\n\t *\n\t * @param sessionId - Session identifier to remove\n\t * @param fatalReason - Optional fatal error reason that prevents reconnection. WebSocket close\n\t * reasons are capped at 123 UTF-8 bytes, so a longer reason is truncated and ends with `... (+N bytes)`.\n\t *\n\t * @example\n\t * ```ts\n\t * // Kick a user (they can reconnect)\n\t * room.closeSession('session-troublemaker')\n\t *\n\t * // Permanently ban a user\n\t * room.closeSession('session-banned', 'PERMISSION_DENIED')\n\t *\n\t * // Close session due to inactivity\n\t * room.closeSession('session-idle', 'TIMEOUT')\n\t * ```\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Closes the room and disconnects all connected clients. This should be called\n\t * when shutting down the room permanently, such as during server shutdown or\n\t * when the room is no longer needed. Once closed, the room cannot be reopened.\n\t *\n\t * @example\n\t * ```ts\n\t * // Clean shutdown when no users remain\n\t * if (room.getNumActiveSessions() === 0) {\n\t * await persistSnapshot(room.getCurrentSnapshot())\n\t * room.close()\n\t * }\n\t *\n\t * // Server shutdown\n\t * process.on('SIGTERM', () => {\n\t * for (const room of activeRooms.values()) {\n\t * room.close()\n\t * }\n\t * })\n\t * ```\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t\tfor (const sessionId of this.snapshotTimers.keys()) {\n\t\t\tthis.clearSnapshotTimer(sessionId)\n\t\t}\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.disposables.clear()\n\t}\n\n\t/**\n\t * Checks whether the room has been permanently closed. Closed rooms cannot\n\t * accept new connections or process further changes.\n\t *\n\t * @returns True if the room is closed, false if still active\n\t *\n\t * @example\n\t * ```ts\n\t * if (room.isClosed()) {\n\t * console.log('Room has been shut down')\n\t * // Create a new room or redirect users\n\t * } else {\n\t * // Room is still accepting connections\n\t * room.handleSocketConnect({ sessionId, socket })\n\t * }\n\t * ```\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/**\n * Utility type that removes properties with void values from an object type.\n * This is used internally to conditionally require session metadata based on\n * whether SessionMeta extends void.\n *\n * @example\n * ```ts\n * type Example = { a: string, b: void, c: number }\n * type Result = OmitVoid<Example> // { a: string, c: number }\n * ```\n *\n * @public\n */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\n/**\n * Interface for making transactional changes to room store data. Used within\n * updateStore transactions to modify documents atomically.\n *\n * @example\n * ```ts\n * await room.updateStore((store) => {\n * const shape = store.get('shape:123')\n * if (shape) {\n * store.put({ ...shape, x: shape.x + 10 })\n * }\n * store.delete('shape:456')\n * })\n * ```\n *\n * @public\n * @deprecated use the storage.transaction method instead\n */\nexport interface RoomStoreMethods<R extends UnknownRecord = UnknownRecord> {\n\t/**\n\t * Add or update a record in the store.\n\t *\n\t * @param record - The record to store\n\t */\n\tput(record: R): void\n\t/**\n\t * Delete a record from the store.\n\t *\n\t * @param recordOrId - The record or record ID to delete\n\t */\n\tdelete(recordOrId: R | string): void\n\t/**\n\t * Get a record by its ID.\n\t *\n\t * @param id - The record ID\n\t * @returns The record or null if not found\n\t */\n\tget(id: string): R | null\n\t/**\n\t * Get all records in the store.\n\t *\n\t * @returns Array of all records\n\t */\n\tgetAll(): R[]\n}\n\n/**\n * @deprecated use the storage.transaction method instead\n */\n// eslint-disable-next-line @typescript-eslint/no-deprecated\nclass StoreUpdateContext<R extends UnknownRecord> implements RoomStoreMethods<R> {\n\tconstructor(\n\t\tprivate readonly snapshot: Record<string, UnknownRecord>,\n\t\tprivate readonly schema: StoreSchema<R, any>\n\t) {}\n\treadonly updates = {\n\t\tputs: {} as Record<string, UnknownRecord>,\n\t\tdeletes: new Set<string>(),\n\t}\n\tput(record: R): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst recordType = getOwnProperty(this.schema.types, record.typeName)\n\t\tif (!recordType) {\n\t\t\tthrow new Error(`Missing definition for record type ${record.typeName}`)\n\t\t}\n\t\tconst recordBefore = this.snapshot[record.id] ?? undefined\n\t\trecordType.validate(record, recordBefore as R)\n\n\t\tif (record.id in this.snapshot && isEqual(this.snapshot[record.id], record)) {\n\t\t\tdelete this.updates.puts[record.id]\n\t\t} else {\n\t\t\tthis.updates.puts[record.id] = structuredClone(record)\n\t\t}\n\t\tthis.updates.deletes.delete(record.id)\n\t}\n\tdelete(recordOrId: R | string): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst id = typeof recordOrId === 'string' ? recordOrId : recordOrId.id\n\t\tdelete this.updates.puts[id]\n\t\tif (this.snapshot[id]) {\n\t\t\tthis.updates.deletes.add(id)\n\t\t}\n\t}\n\tget(id: string): R | null {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tif (hasOwnProperty(this.updates.puts, id)) {\n\t\t\treturn structuredClone(this.updates.puts[id]) as R\n\t\t}\n\t\tif (this.updates.deletes.has(id)) {\n\t\t\treturn null\n\t\t}\n\t\treturn structuredClone(this.snapshot[id] ?? null) as R\n\t}\n\n\tgetAll(): R[] {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst result = Object.values(this.updates.puts)\n\t\tfor (const [id, record] of Object.entries(this.snapshot)) {\n\t\t\tif (!this.updates.deletes.has(id) && !hasOwnProperty(this.updates.puts, id)) {\n\t\t\t\tresult.push(record)\n\t\t\t}\n\t\t}\n\t\treturn structuredClone(result) as R[]\n\t}\n\n\tprivate _isClosed = false\n\tclose() {\n\t\tthis._isClosed = true\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -35,6 +35,23 @@ var import_TLSyncClient = require("./TLSyncClient");
|
|
|
35
35
|
var import_TLSyncStorage = require("./TLSyncStorage");
|
|
36
36
|
const DATA_MESSAGE_DEBOUNCE_INTERVAL = 1e3 / 60;
|
|
37
37
|
const timeSince = (time) => Date.now() - time;
|
|
38
|
+
const MAX_CLOSE_REASON_BYTES = 123;
|
|
39
|
+
const closeReasonEncoder = new TextEncoder();
|
|
40
|
+
const truncatedSuffix = (droppedBytes) => `... (+${droppedBytes} bytes)`;
|
|
41
|
+
function truncateCloseReason(reason) {
|
|
42
|
+
const totalBytes = closeReasonEncoder.encode(reason).length;
|
|
43
|
+
if (totalBytes <= MAX_CLOSE_REASON_BYTES) return reason;
|
|
44
|
+
const budget = MAX_CLOSE_REASON_BYTES - truncatedSuffix(totalBytes).length;
|
|
45
|
+
let out = "";
|
|
46
|
+
let outBytes = 0;
|
|
47
|
+
for (const char of reason) {
|
|
48
|
+
const charBytes = closeReasonEncoder.encode(char).length;
|
|
49
|
+
if (outBytes + charBytes > budget) break;
|
|
50
|
+
out += char;
|
|
51
|
+
outBytes += charBytes;
|
|
52
|
+
}
|
|
53
|
+
return out + truncatedSuffix(totalBytes - outBytes);
|
|
54
|
+
}
|
|
38
55
|
class TLSyncRoom {
|
|
39
56
|
// A table of connected clients
|
|
40
57
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -285,7 +302,7 @@ class TLSyncRoom {
|
|
|
285
302
|
this.clearDebounceTimer(session);
|
|
286
303
|
try {
|
|
287
304
|
if (fatalReason) {
|
|
288
|
-
session.socket.close(import_TLSyncClient.TLSyncErrorCloseEventCode, fatalReason);
|
|
305
|
+
session.socket.close(import_TLSyncClient.TLSyncErrorCloseEventCode, truncateCloseReason(fatalReason));
|
|
289
306
|
} else {
|
|
290
307
|
session.socket.close();
|
|
291
308
|
}
|
|
@@ -603,7 +620,8 @@ class TLSyncRoom {
|
|
|
603
620
|
* Sends appropriate error messages before closing the connection.
|
|
604
621
|
*
|
|
605
622
|
* @param sessionId - The session to reject
|
|
606
|
-
* @param fatalReason - The reason for rejection (optional)
|
|
623
|
+
* @param fatalReason - The reason for rejection (optional). WebSocket close reasons are capped
|
|
624
|
+
* at 123 UTF-8 bytes, so a longer reason is truncated and ends with `... (+N bytes)`.
|
|
607
625
|
* @example
|
|
608
626
|
* ```ts
|
|
609
627
|
* // Reject due to version mismatch
|