@tldraw/sync-core 4.2.1 → 4.2.2
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 +483 -58
- package/dist-cjs/index.js +13 -3
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js +55 -0
- package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js.map +7 -0
- package/dist-cjs/lib/InMemorySyncStorage.js +287 -0
- package/dist-cjs/lib/InMemorySyncStorage.js.map +7 -0
- package/dist-cjs/lib/MicrotaskNotifier.js +50 -0
- package/dist-cjs/lib/MicrotaskNotifier.js.map +7 -0
- package/dist-cjs/lib/NodeSqliteWrapper.js +48 -0
- package/dist-cjs/lib/NodeSqliteWrapper.js.map +7 -0
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/SQLiteSyncStorage.js +428 -0
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +7 -0
- package/dist-cjs/lib/TLSocketRoom.js +117 -69
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +7 -0
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +357 -688
- package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
- package/dist-cjs/lib/TLSyncStorage.js +76 -0
- package/dist-cjs/lib/TLSyncStorage.js.map +7 -0
- package/dist-cjs/lib/chunk.js +2 -2
- package/dist-cjs/lib/chunk.js.map +1 -1
- package/dist-cjs/lib/recordDiff.js +52 -0
- package/dist-cjs/lib/recordDiff.js.map +7 -0
- package/dist-esm/index.d.mts +483 -58
- package/dist-esm/index.mjs +20 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs +35 -0
- package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs.map +7 -0
- package/dist-esm/lib/InMemorySyncStorage.mjs +272 -0
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +7 -0
- package/dist-esm/lib/MicrotaskNotifier.mjs +30 -0
- package/dist-esm/lib/MicrotaskNotifier.mjs.map +7 -0
- package/dist-esm/lib/NodeSqliteWrapper.mjs +28 -0
- package/dist-esm/lib/NodeSqliteWrapper.mjs.map +7 -0
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/SQLiteSyncStorage.mjs +414 -0
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +7 -0
- package/dist-esm/lib/TLSocketRoom.mjs +121 -70
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +7 -0
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +370 -702
- package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
- package/dist-esm/lib/TLSyncStorage.mjs +56 -0
- package/dist-esm/lib/TLSyncStorage.mjs.map +7 -0
- package/dist-esm/lib/chunk.mjs +2 -2
- package/dist-esm/lib/chunk.mjs.map +1 -1
- package/dist-esm/lib/recordDiff.mjs +32 -0
- package/dist-esm/lib/recordDiff.mjs.map +7 -0
- package/package.json +12 -11
- package/src/index.ts +32 -3
- package/src/lib/ClientWebSocketAdapter.test.ts +3 -0
- package/src/lib/DurableObjectSqliteSyncWrapper.ts +95 -0
- package/src/lib/InMemorySyncStorage.ts +387 -0
- package/src/lib/MicrotaskNotifier.test.ts +429 -0
- package/src/lib/MicrotaskNotifier.ts +38 -0
- package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +270 -0
- package/src/lib/NodeSqliteSyncWrapper.test.ts +272 -0
- package/src/lib/NodeSqliteWrapper.ts +99 -0
- package/src/lib/RoomSession.test.ts +1 -0
- package/src/lib/RoomSession.ts +2 -0
- package/src/lib/SQLiteSyncStorage.ts +627 -0
- package/src/lib/TLSocketRoom.ts +228 -114
- package/src/lib/TLSyncClient.ts +12 -0
- package/src/lib/TLSyncRoom.ts +473 -913
- package/src/lib/TLSyncStorage.ts +216 -0
- package/src/lib/chunk.ts +2 -2
- package/src/lib/computeTombstonePruning.test.ts +352 -0
- package/src/lib/recordDiff.ts +73 -0
- package/src/test/FuzzEditor.ts +4 -5
- package/src/test/InMemorySyncStorage.test.ts +1684 -0
- package/src/test/SQLiteSyncStorage.test.ts +1378 -0
- package/src/test/TLSocketRoom.test.ts +255 -49
- package/src/test/TLSyncRoom.test.ts +1024 -534
- package/src/test/TestServer.ts +12 -1
- package/src/test/customMessages.test.ts +1 -1
- package/src/test/presenceMode.test.ts +6 -6
- package/src/test/syncFuzz.test.ts +2 -4
- package/src/test/upgradeDowngrade.test.ts +290 -8
- package/src/test/validation.test.ts +15 -10
- package/src/test/pruneTombstones.test.ts +0 -178
|
@@ -23,10 +23,12 @@ __export(TLSocketRoom_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(TLSocketRoom_exports);
|
|
24
24
|
var import_tlschema = require("@tldraw/tlschema");
|
|
25
25
|
var import_utils = require("@tldraw/utils");
|
|
26
|
+
var import_InMemorySyncStorage = require("./InMemorySyncStorage");
|
|
26
27
|
var import_RoomSession = require("./RoomSession");
|
|
27
28
|
var import_ServerSocketAdapter = require("./ServerSocketAdapter");
|
|
28
29
|
var import_TLSyncClient = require("./TLSyncClient");
|
|
29
30
|
var import_TLSyncRoom = require("./TLSyncRoom");
|
|
31
|
+
var import_TLSyncStorage = require("./TLSyncStorage");
|
|
30
32
|
var import_chunk = require("./chunk");
|
|
31
33
|
class TLSocketRoom {
|
|
32
34
|
/**
|
|
@@ -45,17 +47,29 @@ class TLSocketRoom {
|
|
|
45
47
|
*/
|
|
46
48
|
constructor(opts) {
|
|
47
49
|
this.opts = opts;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if (opts.storage && opts.initialSnapshot) {
|
|
51
|
+
throw new Error("Cannot provide both storage and initialSnapshot options");
|
|
52
|
+
}
|
|
53
|
+
const storage = opts.storage ? opts.storage : new import_InMemorySyncStorage.InMemorySyncStorage({
|
|
54
|
+
snapshot: (0, import_TLSyncStorage.convertStoreSnapshotToRoomSnapshot)(
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
56
|
+
opts.initialSnapshot ?? import_InMemorySyncStorage.DEFAULT_INITIAL_SNAPSHOT
|
|
57
|
+
)
|
|
58
|
+
});
|
|
59
|
+
if ("onDataChange" in opts && opts.onDataChange) {
|
|
60
|
+
this.disposables.add(
|
|
61
|
+
storage.onChange(() => {
|
|
62
|
+
opts.onDataChange?.();
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
}
|
|
53
66
|
this.room = new import_TLSyncRoom.TLSyncRoom({
|
|
54
|
-
|
|
67
|
+
onPresenceChange: opts.onPresenceChange,
|
|
55
68
|
schema: opts.schema ?? (0, import_tlschema.createTLSchema)(),
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
log: opts.log,
|
|
70
|
+
storage
|
|
58
71
|
});
|
|
72
|
+
this.storage = storage;
|
|
59
73
|
this.room.events.on("session_removed", (args) => {
|
|
60
74
|
this.sessions.delete(args.sessionId);
|
|
61
75
|
if (this.opts.onSessionRemoved) {
|
|
@@ -71,7 +85,8 @@ class TLSocketRoom {
|
|
|
71
85
|
room;
|
|
72
86
|
sessions = /* @__PURE__ */ new Map();
|
|
73
87
|
log;
|
|
74
|
-
|
|
88
|
+
storage;
|
|
89
|
+
disposables = /* @__PURE__ */ new Set();
|
|
75
90
|
/**
|
|
76
91
|
* Returns the number of active sessions.
|
|
77
92
|
* Note that this is not the same as the number of connected sockets!
|
|
@@ -255,7 +270,7 @@ class TLSocketRoom {
|
|
|
255
270
|
* ```
|
|
256
271
|
*/
|
|
257
272
|
getCurrentDocumentClock() {
|
|
258
|
-
return this.
|
|
273
|
+
return this.storage.getClock();
|
|
259
274
|
}
|
|
260
275
|
/**
|
|
261
276
|
* Retrieves a deeply cloned copy of a record from the document store.
|
|
@@ -276,7 +291,9 @@ class TLSocketRoom {
|
|
|
276
291
|
* ```
|
|
277
292
|
*/
|
|
278
293
|
getRecord(id) {
|
|
279
|
-
return
|
|
294
|
+
return this.storage.transaction((txn) => {
|
|
295
|
+
return (0, import_utils.structuredClone)(txn.get(id));
|
|
296
|
+
}).result;
|
|
280
297
|
}
|
|
281
298
|
/**
|
|
282
299
|
* Returns information about all active sessions in the room. Each session
|
|
@@ -317,6 +334,7 @@ class TLSocketRoom {
|
|
|
317
334
|
* to restore the room state later or revert to a previous version.
|
|
318
335
|
*
|
|
319
336
|
* @returns Complete room snapshot including documents, clock values, and tombstones
|
|
337
|
+
* @deprecated if you need to do this use
|
|
320
338
|
*
|
|
321
339
|
* @example
|
|
322
340
|
* ```ts
|
|
@@ -330,7 +348,10 @@ class TLSocketRoom {
|
|
|
330
348
|
* ```
|
|
331
349
|
*/
|
|
332
350
|
getCurrentSnapshot() {
|
|
333
|
-
|
|
351
|
+
if (this.storage.getSnapshot) {
|
|
352
|
+
return this.storage.getSnapshot();
|
|
353
|
+
}
|
|
354
|
+
throw new Error("getCurrentSnapshot is not supported for this storage type");
|
|
334
355
|
}
|
|
335
356
|
/**
|
|
336
357
|
* Retrieves all presence records from the document store. Presence records
|
|
@@ -341,23 +362,11 @@ class TLSocketRoom {
|
|
|
341
362
|
*/
|
|
342
363
|
getPresenceRecords() {
|
|
343
364
|
const result = {};
|
|
344
|
-
for (const
|
|
345
|
-
|
|
346
|
-
result[document.state.id] = document.state;
|
|
347
|
-
}
|
|
365
|
+
for (const presence of this.room.presenceStore.values()) {
|
|
366
|
+
result[presence.id] = presence;
|
|
348
367
|
}
|
|
349
368
|
return result;
|
|
350
369
|
}
|
|
351
|
-
/**
|
|
352
|
-
* Returns a JSON-serialized snapshot of the current document state. This is
|
|
353
|
-
* equivalent to JSON.stringify(getCurrentSnapshot()) but provided as a convenience.
|
|
354
|
-
*
|
|
355
|
-
* @returns JSON string representation of the room snapshot
|
|
356
|
-
* @internal
|
|
357
|
-
*/
|
|
358
|
-
getCurrentSerializedSnapshot() {
|
|
359
|
-
return JSON.stringify(this.room.getSnapshot());
|
|
360
|
-
}
|
|
361
370
|
/**
|
|
362
371
|
* Loads a document snapshot, completely replacing the current room state.
|
|
363
372
|
* This will disconnect all current clients and update the document to match
|
|
@@ -377,39 +386,9 @@ class TLSocketRoom {
|
|
|
377
386
|
* ```
|
|
378
387
|
*/
|
|
379
388
|
loadSnapshot(snapshot) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
const oldRoom = this.room;
|
|
384
|
-
const oldRoomSnapshot = oldRoom.getSnapshot();
|
|
385
|
-
const oldIds = oldRoomSnapshot.documents.map((d) => d.state.id);
|
|
386
|
-
const newIds = new Set(snapshot.documents.map((d) => d.state.id));
|
|
387
|
-
const removedIds = oldIds.filter((id) => !newIds.has(id));
|
|
388
|
-
const tombstones = { ...oldRoomSnapshot.tombstones };
|
|
389
|
-
removedIds.forEach((id) => {
|
|
390
|
-
tombstones[id] = oldRoom.clock + 1;
|
|
391
|
-
});
|
|
392
|
-
newIds.forEach((id) => {
|
|
393
|
-
delete tombstones[id];
|
|
394
|
-
});
|
|
395
|
-
const newRoom = new import_TLSyncRoom.TLSyncRoom({
|
|
396
|
-
...this.syncCallbacks,
|
|
397
|
-
schema: oldRoom.schema,
|
|
398
|
-
snapshot: {
|
|
399
|
-
clock: oldRoom.clock + 1,
|
|
400
|
-
documentClock: oldRoom.clock + 1,
|
|
401
|
-
documents: snapshot.documents.map((d) => ({
|
|
402
|
-
lastChangedClock: oldRoom.clock + 1,
|
|
403
|
-
state: d.state
|
|
404
|
-
})),
|
|
405
|
-
schema: snapshot.schema,
|
|
406
|
-
tombstones,
|
|
407
|
-
tombstoneHistoryStartsAtClock: oldRoomSnapshot.tombstoneHistoryStartsAtClock
|
|
408
|
-
},
|
|
409
|
-
log: this.log
|
|
389
|
+
this.storage.transaction((txn) => {
|
|
390
|
+
(0, import_TLSyncStorage.loadSnapshotIntoStorage)(txn, this.room.schema, snapshot);
|
|
410
391
|
});
|
|
411
|
-
this.room = newRoom;
|
|
412
|
-
oldRoom.close();
|
|
413
392
|
}
|
|
414
393
|
/**
|
|
415
394
|
* Executes a transaction to modify the document store. Changes made within the
|
|
@@ -453,9 +432,31 @@ class TLSocketRoom {
|
|
|
453
432
|
* }
|
|
454
433
|
* })
|
|
455
434
|
* ```
|
|
435
|
+
* @deprecated use the storage.transaction method instead
|
|
456
436
|
*/
|
|
437
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
457
438
|
async updateStore(updater) {
|
|
458
|
-
|
|
439
|
+
if (this.isClosed()) {
|
|
440
|
+
throw new Error("Cannot update store on a closed room");
|
|
441
|
+
}
|
|
442
|
+
const ctx = new StoreUpdateContext(
|
|
443
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
444
|
+
Object.fromEntries(this.getCurrentSnapshot().documents.map((d) => [d.state.id, d.state])),
|
|
445
|
+
this.room.schema
|
|
446
|
+
);
|
|
447
|
+
try {
|
|
448
|
+
await updater(ctx);
|
|
449
|
+
} finally {
|
|
450
|
+
ctx.close();
|
|
451
|
+
}
|
|
452
|
+
this.storage.transaction((txn) => {
|
|
453
|
+
for (const [id, record] of Object.entries(ctx.updates.puts)) {
|
|
454
|
+
txn.set(id, record);
|
|
455
|
+
}
|
|
456
|
+
for (const id of ctx.updates.deletes) {
|
|
457
|
+
txn.delete(id);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
459
460
|
}
|
|
460
461
|
/**
|
|
461
462
|
* Sends a custom message to a specific client session. This allows sending
|
|
@@ -529,6 +530,8 @@ class TLSocketRoom {
|
|
|
529
530
|
*/
|
|
530
531
|
close() {
|
|
531
532
|
this.room.close();
|
|
533
|
+
this.disposables.forEach((d) => d());
|
|
534
|
+
this.disposables.clear();
|
|
532
535
|
}
|
|
533
536
|
/**
|
|
534
537
|
* Checks whether the room has been permanently closed. Closed rooms cannot
|
|
@@ -551,16 +554,61 @@ class TLSocketRoom {
|
|
|
551
554
|
return this.room.isClosed();
|
|
552
555
|
}
|
|
553
556
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
schema: snapshot.schema,
|
|
563
|
-
tombstones: {}
|
|
557
|
+
class StoreUpdateContext {
|
|
558
|
+
constructor(snapshot, schema) {
|
|
559
|
+
this.snapshot = snapshot;
|
|
560
|
+
this.schema = schema;
|
|
561
|
+
}
|
|
562
|
+
updates = {
|
|
563
|
+
puts: {},
|
|
564
|
+
deletes: /* @__PURE__ */ new Set()
|
|
564
565
|
};
|
|
566
|
+
put(record) {
|
|
567
|
+
if (this._isClosed) throw new Error("StoreUpdateContext is closed");
|
|
568
|
+
const recordType = (0, import_utils.getOwnProperty)(this.schema.types, record.typeName);
|
|
569
|
+
if (!recordType) {
|
|
570
|
+
throw new Error(`Missing definition for record type ${record.typeName}`);
|
|
571
|
+
}
|
|
572
|
+
const recordBefore = this.snapshot[record.id] ?? void 0;
|
|
573
|
+
recordType.validate(record, recordBefore);
|
|
574
|
+
if (record.id in this.snapshot && (0, import_utils.isEqual)(this.snapshot[record.id], record)) {
|
|
575
|
+
delete this.updates.puts[record.id];
|
|
576
|
+
} else {
|
|
577
|
+
this.updates.puts[record.id] = (0, import_utils.structuredClone)(record);
|
|
578
|
+
}
|
|
579
|
+
this.updates.deletes.delete(record.id);
|
|
580
|
+
}
|
|
581
|
+
delete(recordOrId) {
|
|
582
|
+
if (this._isClosed) throw new Error("StoreUpdateContext is closed");
|
|
583
|
+
const id = typeof recordOrId === "string" ? recordOrId : recordOrId.id;
|
|
584
|
+
delete this.updates.puts[id];
|
|
585
|
+
if (this.snapshot[id]) {
|
|
586
|
+
this.updates.deletes.add(id);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
get(id) {
|
|
590
|
+
if (this._isClosed) throw new Error("StoreUpdateContext is closed");
|
|
591
|
+
if ((0, import_utils.hasOwnProperty)(this.updates.puts, id)) {
|
|
592
|
+
return (0, import_utils.structuredClone)(this.updates.puts[id]);
|
|
593
|
+
}
|
|
594
|
+
if (this.updates.deletes.has(id)) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
return (0, import_utils.structuredClone)(this.snapshot[id] ?? null);
|
|
598
|
+
}
|
|
599
|
+
getAll() {
|
|
600
|
+
if (this._isClosed) throw new Error("StoreUpdateContext is closed");
|
|
601
|
+
const result = Object.values(this.updates.puts);
|
|
602
|
+
for (const [id, record] of Object.entries(this.snapshot)) {
|
|
603
|
+
if (!this.updates.deletes.has(id) && !(0, import_utils.hasOwnProperty)(this.updates.puts, id)) {
|
|
604
|
+
result.push(record);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return (0, import_utils.structuredClone)(result);
|
|
608
|
+
}
|
|
609
|
+
_isClosed = false;
|
|
610
|
+
close() {
|
|
611
|
+
this._isClosed = true;
|
|
612
|
+
}
|
|
565
613
|
}
|
|
566
614
|
//# sourceMappingURL=TLSocketRoom.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/TLSocketRoom.ts"],
|
|
4
|
-
"sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { TLStoreSnapshot, createTLSchema } from '@tldraw/tlschema'\nimport { objectMapValues, structuredClone } from '@tldraw/utils'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, RoomStoreMethods, TLSyncRoom } from './TLSyncRoom'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n/**\n * Logging interface for TLSocketRoom operations. Provides optional methods\n * for warning and error logging during synchronization operations.\n *\n * @example\n * ```ts\n * const logger: TLSyncLog = {\n * warn: (...args) => console.warn('[SYNC]', ...args),\n * error: (...args) => console.error('[SYNC]', ...args)\n * }\n *\n * const room = new TLSocketRoom({ log: logger })\n * ```\n *\n * @public\n */\nexport interface TLSyncLog {\n\t/**\n\t * Optional warning logger for non-fatal sync issues\n\t * @param args - Arguments to log\n\t */\n\twarn?(...args: any[]): void\n\t/**\n\t * Optional error logger for sync errors and failures\n\t * @param args - Arguments to log\n\t */\n\terror?(...args: any[]): void\n}\n\n/**\n * A server-side room that manages WebSocket connections and synchronizes tldraw document state\n * between multiple clients in real-time. Each room represents a collaborative document space\n * where users can work together on drawings with automatic conflict resolution.\n *\n * TLSocketRoom handles:\n * - WebSocket connection lifecycle management\n * - Real-time synchronization of document changes\n * - Session management and presence tracking\n * - Message chunking for large payloads\n * - Automatic client timeout and cleanup\n *\n * @example\n * ```ts\n * // Basic room setup\n * const room = new TLSocketRoom({\n * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {\n * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)\n * if (numSessionsRemaining === 0) {\n * room.close()\n * }\n * },\n * onDataChange: () => {\n * console.log('Document data changed, consider persisting')\n * }\n * })\n *\n * // Handle new client connections\n * room.handleSocketConnect({\n * sessionId: 'user-session-123',\n * socket: webSocket,\n * isReadonly: false\n * })\n * ```\n *\n * @example\n * ```ts\n * // Room with initial snapshot and schema\n * const room = new TLSocketRoom({\n * initialSnapshot: existingSnapshot,\n * schema: myCustomSchema,\n * clientTimeout: 30000,\n * log: {\n * warn: (...args) => logger.warn('SYNC:', ...args),\n * error: (...args) => logger.error('SYNC:', ...args)\n * }\n * })\n *\n * // Update document programmatically\n * await room.updateStore(store => {\n * const shape = store.get('shape:abc123')\n * if (shape) {\n * shape.x = 100\n * store.put(shape)\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\tprivate readonly syncCallbacks: {\n\t\tonDataChange?(): void\n\t\tonPresenceChange?(): void\n\t}\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(\n\t\tpublic readonly opts: {\n\t\t\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t\t\tschema?: StoreSchema<R, any>\n\t\t\t// how long to wait for a client to communicate before disconnecting them\n\t\t\tclientTimeout?: number\n\t\t\tlog?: TLSyncLog\n\t\t\t// a callback that is called when a client is disconnected\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonSessionRemoved?: (\n\t\t\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\t\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t\t\t) => void\n\t\t\t// a callback that is called whenever a message is sent\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonBeforeSendMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonAfterReceiveMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\tonDataChange?(): void\n\t\t\t/** @internal */\n\t\t\tonPresenceChange?(): void\n\t\t}\n\t) {\n\t\tconst initialSnapshot =\n\t\t\topts.initialSnapshot && 'store' in opts.initialSnapshot\n\t\t\t\t? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot!)\n\t\t\t\t: opts.initialSnapshot\n\n\t\tthis.syncCallbacks = {\n\t\t\tonDataChange: opts.onDataChange,\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t}\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tsnapshot: initialSnapshot,\n\t\t\tlog: opts.log,\n\t\t})\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Handles a new client WebSocket connection, creating a session within the room.\n\t * This should be called whenever a client establishes a WebSocket connection to join\n\t * the collaborative document.\n\t *\n\t * @param opts - Connection options\n\t * - sessionId - Unique identifier for the client session (typically from browser tab)\n\t * - socket - WebSocket-like object for client communication\n\t * - isReadonly - Whether the client can modify the document (defaults to false)\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // Handle new WebSocket connection\n\t * room.handleSocketConnect({\n\t * sessionId: 'user-session-abc123',\n\t * socket: webSocketConnection,\n\t * isReadonly: !userHasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // With session metadata\n\t * room.handleSocketConnect({\n\t * sessionId: 'session-xyz',\n\t * socket: ws,\n\t * meta: { userId: 'user-123', name: 'Alice' }\n\t * })\n\t * ```\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * Processes a message received from a client WebSocket. Use this method in server\n\t * environments where WebSocket event listeners cannot be attached directly to socket\n\t * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).\n\t *\n\t * The method handles message chunking/reassembly and forwards complete messages\n\t * to the underlying sync room for processing.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t * @param message - Raw message data from the client (string or binary)\n\t *\n\t * @example\n\t * ```ts\n\t * // In a Bun.serve handler\n\t * server.upgrade(req, {\n\t * data: { sessionId, room },\n\t * upgrade(res, req) {\n\t * // Connection established\n\t * },\n\t * message(ws, message) {\n\t * const { sessionId, room } = ws.data\n\t * room.handleSocketMessage(sessionId, message)\n\t * }\n\t * })\n\t * ```\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * Handles a WebSocket error for the specified session. Use this in server environments\n\t * where socket event listeners cannot be attached directly. This will initiate cleanup\n\t * and session removal for the affected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('error', () => {\n\t * room.handleSocketError(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Handles a WebSocket close event for the specified session. Use this in server\n\t * environments where socket event listeners cannot be attached directly. This will\n\t * initiate cleanup and session removal for the disconnected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('close', () => {\n\t * room.handleSocketClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current document clock value. The clock is a monotonically increasing\n\t * integer that increments with each document change, providing a consistent ordering\n\t * of changes across the distributed system.\n\t *\n\t * @returns The current document clock value\n\t *\n\t * @example\n\t * ```ts\n\t * const clock = room.getCurrentDocumentClock()\n\t * console.log(`Document is at version ${clock}`)\n\t * ```\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.room.documentClock\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 structuredClone(this.room.documents.get(id)?.state)\n\t}\n\n\t/**\n\t * Returns information about all active sessions in the room. Each session\n\t * represents a connected client with their current connection status and metadata.\n\t *\n\t * @returns Array of session information objects containing:\n\t * - sessionId - Unique session identifier\n\t * - isConnected - Whether the session has an active WebSocket connection\n\t * - isReadonly - Whether the session can modify the document\n\t * - meta - Custom session metadata\n\t *\n\t * @example\n\t * ```ts\n\t * const sessions = room.getSessions()\n\t * console.log(`Room has ${sessions.length} active sessions`)\n\t *\n\t * for (const session of sessions) {\n\t * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)\n\t * if (session.isReadonly) {\n\t * console.log(' (read-only access)')\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Creates a complete snapshot of the current document state, including all records\n\t * and synchronization metadata. This snapshot can be persisted to storage and used\n\t * to restore the room state later or revert to a previous version.\n\t *\n\t * @returns Complete room snapshot including documents, clock values, and tombstones\n\t *\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\treturn this.room.getSnapshot()\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 document of this.room.documents.values()) {\n\t\t\tif (document.state.typeName === this.room.presenceType?.typeName) {\n\t\t\t\tresult[document.state.id] = document.state\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Returns a JSON-serialized snapshot of the current document state. This is\n\t * equivalent to JSON.stringify(getCurrentSnapshot()) but provided as a convenience.\n\t *\n\t * @returns JSON string representation of the room snapshot\n\t * @internal\n\t */\n\tgetCurrentSerializedSnapshot() {\n\t\treturn JSON.stringify(this.room.getSnapshot())\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\tif ('store' in snapshot) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\t\t}\n\t\tconst oldRoom = this.room\n\t\tconst oldRoomSnapshot = oldRoom.getSnapshot()\n\t\tconst oldIds = oldRoomSnapshot.documents.map((d) => d.state.id)\n\t\tconst newIds = new Set(snapshot.documents.map((d) => d.state.id))\n\t\tconst removedIds = oldIds.filter((id) => !newIds.has(id))\n\n\t\tconst tombstones: RoomSnapshot['tombstones'] = { ...oldRoomSnapshot.tombstones }\n\t\tremovedIds.forEach((id) => {\n\t\t\ttombstones[id] = oldRoom.clock + 1\n\t\t})\n\t\tnewIds.forEach((id) => {\n\t\t\tdelete tombstones[id]\n\t\t})\n\n\t\tconst newRoom = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: oldRoom.schema,\n\t\t\tsnapshot: {\n\t\t\t\tclock: oldRoom.clock + 1,\n\t\t\t\tdocumentClock: oldRoom.clock + 1,\n\t\t\t\tdocuments: snapshot.documents.map((d) => ({\n\t\t\t\t\tlastChangedClock: oldRoom.clock + 1,\n\t\t\t\t\tstate: d.state,\n\t\t\t\t})),\n\t\t\t\tschema: snapshot.schema,\n\t\t\t\ttombstones,\n\t\t\t\ttombstoneHistoryStartsAtClock: oldRoomSnapshot.tombstoneHistoryStartsAtClock,\n\t\t\t},\n\t\t\tlog: this.log,\n\t\t})\n\t\t// replace room with new one and kick out all the clients\n\t\tthis.room = newRoom\n\t\toldRoom.close()\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 */\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\treturn this.room.updateStore(updater)\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}\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\nfunction convertStoreSnapshotToRoomSnapshot(snapshot: TLStoreSnapshot): RoomSnapshot {\n\treturn {\n\t\tclock: 0,\n\t\tdocumentClock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,sBAAgD;AAChD,
|
|
4
|
+
"sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { createTLSchema, TLStoreSnapshot } from '@tldraw/tlschema'\nimport { getOwnProperty, hasOwnProperty, isEqual, structuredClone } from '@tldraw/utils'\nimport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './InMemorySyncStorage'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, TLSyncRoom } from './TLSyncRoom'\nimport {\n\tconvertStoreSnapshotToRoomSnapshot,\n\tloadSnapshotIntoStorage,\n\tTLSyncStorage,\n} from './TLSyncStorage'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n/**\n * Logging interface for TLSocketRoom operations. Provides optional methods\n * for warning and error logging during synchronization operations.\n *\n * @example\n * ```ts\n * const logger: TLSyncLog = {\n * warn: (...args) => console.warn('[SYNC]', ...args),\n * error: (...args) => console.error('[SYNC]', ...args)\n * }\n *\n * const room = new TLSocketRoom({ log: logger })\n * ```\n *\n * @public\n */\nexport interface TLSyncLog {\n\t/**\n\t * Optional warning logger for non-fatal sync issues\n\t * @param args - Arguments to log\n\t */\n\twarn?(...args: any[]): void\n\t/**\n\t * Optional error logger for sync errors and failures\n\t * @param args - Arguments to log\n\t */\n\terror?(...args: any[]): void\n}\n\n/**\n * Base options for TLSocketRoom.\n * @public\n */\nexport interface TLSocketRoomOptions<R extends UnknownRecord, SessionMeta> {\n\tstorage?: TLSyncStorage<R>\n\t/**\n\t * @deprecated use the storage option instead\n\t */\n\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t/**\n\t * @deprecated use the storage option with an onChange callback instead\n\t */\n\tonDataChange?(): void\n\tschema?: StoreSchema<R, any>\n\t// how long to wait for a client to communicate before disconnecting them\n\tclientTimeout?: number\n\tlog?: TLSyncLog\n\t// a callback that is called when a client is disconnected\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonSessionRemoved?: (\n\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t) => void\n\t// a callback that is called whenever a message is sent\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonBeforeSendMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\tonAfterReceiveMessage?: (args: {\n\t\tsessionId: string\n\t\t/** @internal keep the protocol private for now */\n\t\tmessage: TLSocketServerSentEvent<R>\n\t\tstringified: string\n\t\tmeta: SessionMeta\n\t}) => void\n\t/** @internal */\n\tonPresenceChange?(): void\n}\n\n/**\n * A server-side room that manages WebSocket connections and synchronizes tldraw document state\n * between multiple clients in real-time. Each room represents a collaborative document space\n * where users can work together on drawings with automatic conflict resolution.\n *\n * TLSocketRoom handles:\n * - WebSocket connection lifecycle management\n * - Real-time synchronization of document changes\n * - Session management and presence tracking\n * - Message chunking for large payloads\n * - Automatic client timeout and cleanup\n *\n * @example\n * ```ts\n * // Basic room setup\n * const room = new TLSocketRoom({\n * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {\n * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)\n * if (numSessionsRemaining === 0) {\n * room.close()\n * }\n * },\n * onDataChange: () => {\n * console.log('Document data changed, consider persisting')\n * }\n * })\n *\n * // Handle new client connections\n * room.handleSocketConnect({\n * sessionId: 'user-session-123',\n * socket: webSocket,\n * isReadonly: false\n * })\n * ```\n *\n * @example\n * ```ts\n * // Room with initial snapshot and schema\n * const room = new TLSocketRoom({\n * initialSnapshot: existingSnapshot,\n * schema: myCustomSchema,\n * clientTimeout: 30000,\n * log: {\n * warn: (...args) => logger.warn('SYNC:', ...args),\n * error: (...args) => logger.error('SYNC:', ...args)\n * }\n * })\n *\n * // Update document programmatically\n * await room.updateStore(store => {\n * const shape = store.get('shape:abc123')\n * if (shape) {\n * shape.x = 100\n * store.put(shape)\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\n\tpublic storage: TLSyncStorage<R>\n\n\tprivate disposables = new Set<() => void>()\n\n\t/**\n\t * Creates a new TLSocketRoom instance for managing collaborative document synchronization.\n\t *\n\t * opts - Configuration options for the room\n\t * - initialSnapshot - Optional initial document state to load\n\t * - schema - Store schema defining record types and validation\n\t * - clientTimeout - Milliseconds to wait before disconnecting inactive clients\n\t * - log - Optional logger for warnings and errors\n\t * - onSessionRemoved - Called when a client session is removed\n\t * - onBeforeSendMessage - Called before sending messages to clients\n\t * - onAfterReceiveMessage - Called after receiving messages from clients\n\t * - onDataChange - Called when document data changes\n\t * - onPresenceChange - Called when presence data changes\n\t */\n\tconstructor(public readonly opts: TLSocketRoomOptions<R, SessionMeta>) {\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif (opts.storage && opts.initialSnapshot) {\n\t\t\tthrow new Error('Cannot provide both storage and initialSnapshot options')\n\t\t}\n\t\tconst storage = opts.storage\n\t\t\t? opts.storage\n\t\t\t: new InMemorySyncStorage<R>({\n\t\t\t\t\tsnapshot: convertStoreSnapshotToRoomSnapshot(\n\t\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\t\topts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT\n\t\t\t\t\t),\n\t\t\t\t})\n\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tif ('onDataChange' in opts && opts.onDataChange) {\n\t\t\tthis.disposables.add(\n\t\t\t\tstorage.onChange(() => {\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\t\t\topts.onDataChange?.()\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tlog: opts.log,\n\t\t\tstorage,\n\t\t})\n\t\tthis.storage = storage\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Handles a new client WebSocket connection, creating a session within the room.\n\t * This should be called whenever a client establishes a WebSocket connection to join\n\t * the collaborative document.\n\t *\n\t * @param opts - Connection options\n\t * - sessionId - Unique identifier for the client session (typically from browser tab)\n\t * - socket - WebSocket-like object for client communication\n\t * - isReadonly - Whether the client can modify the document (defaults to false)\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // Handle new WebSocket connection\n\t * room.handleSocketConnect({\n\t * sessionId: 'user-session-abc123',\n\t * socket: webSocketConnection,\n\t * isReadonly: !userHasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // With session metadata\n\t * room.handleSocketConnect({\n\t * sessionId: 'session-xyz',\n\t * socket: ws,\n\t * meta: { userId: 'user-123', name: 'Alice' }\n\t * })\n\t * ```\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * Processes a message received from a client WebSocket. Use this method in server\n\t * environments where WebSocket event listeners cannot be attached directly to socket\n\t * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).\n\t *\n\t * The method handles message chunking/reassembly and forwards complete messages\n\t * to the underlying sync room for processing.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t * @param message - Raw message data from the client (string or binary)\n\t *\n\t * @example\n\t * ```ts\n\t * // In a Bun.serve handler\n\t * server.upgrade(req, {\n\t * data: { sessionId, room },\n\t * upgrade(res, req) {\n\t * // Connection established\n\t * },\n\t * message(ws, message) {\n\t * const { sessionId, room } = ws.data\n\t * room.handleSocketMessage(sessionId, message)\n\t * }\n\t * })\n\t * ```\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * Handles a WebSocket error for the specified session. Use this in server environments\n\t * where socket event listeners cannot be attached directly. This will initiate cleanup\n\t * and session removal for the affected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('error', () => {\n\t * room.handleSocketError(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Handles a WebSocket close event for the specified session. Use this in server\n\t * environments where socket event listeners cannot be attached directly. This will\n\t * initiate cleanup and session removal for the disconnected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('close', () => {\n\t * room.handleSocketClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current document clock value. The clock is a monotonically increasing\n\t * integer that increments with each document change, providing a consistent ordering\n\t * of changes across the distributed system.\n\t *\n\t * @returns The current document clock value\n\t *\n\t * @example\n\t * ```ts\n\t * const clock = room.getCurrentDocumentClock()\n\t * console.log(`Document is at version ${clock}`)\n\t * ```\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.storage.getClock()\n\t}\n\n\t/**\n\t * Retrieves a deeply cloned copy of a record from the document store.\n\t * Returns undefined if the record doesn't exist. The returned record is\n\t * safe to mutate without affecting the original store data.\n\t *\n\t * @param id - Unique identifier of the record to retrieve\n\t * @returns Deep clone of the record, or undefined if not found\n\t *\n\t * @example\n\t * ```ts\n\t * const shape = room.getRecord('shape:abc123')\n\t * if (shape) {\n\t * console.log('Shape position:', shape.x, shape.y)\n\t * // Safe to modify without affecting store\n\t * shape.x = 100\n\t * }\n\t * ```\n\t */\n\tgetRecord(id: string) {\n\t\treturn this.storage.transaction((txn) => {\n\t\t\treturn structuredClone(txn.get(id)) as any\n\t\t}).result as R\n\t}\n\n\t/**\n\t * Returns information about all active sessions in the room. Each session\n\t * represents a connected client with their current connection status and metadata.\n\t *\n\t * @returns Array of session information objects containing:\n\t * - sessionId - Unique session identifier\n\t * - isConnected - Whether the session has an active WebSocket connection\n\t * - isReadonly - Whether the session can modify the document\n\t * - meta - Custom session metadata\n\t *\n\t * @example\n\t * ```ts\n\t * const sessions = room.getSessions()\n\t * console.log(`Room has ${sessions.length} active sessions`)\n\t *\n\t * for (const session of sessions) {\n\t * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)\n\t * if (session.isReadonly) {\n\t * console.log(' (read-only access)')\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Creates a complete snapshot of the current document state, including all records\n\t * and synchronization metadata. This snapshot can be persisted to storage and used\n\t * to restore the room state later or revert to a previous version.\n\t *\n\t * @returns Complete room snapshot including documents, clock values, and tombstones\n\t * @deprecated if you need to do this use\n\t *\n\t * @example\n\t * ```ts\n\t * // Capture current state for persistence\n\t * const snapshot = room.getCurrentSnapshot()\n\t * await saveToDatabase(roomId, JSON.stringify(snapshot))\n\t *\n\t * // Later, restore from snapshot\n\t * const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))\n\t * const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })\n\t * ```\n\t */\n\tgetCurrentSnapshot() {\n\t\tif (this.storage.getSnapshot) {\n\t\t\treturn this.storage.getSnapshot()\n\t\t}\n\t\tthrow new Error('getCurrentSnapshot is not supported for this storage type')\n\t}\n\n\t/**\n\t * Retrieves all presence records from the document store. Presence records\n\t * contain ephemeral user state like cursor positions and selections.\n\t *\n\t * @returns Object mapping record IDs to presence record data\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const presence of this.room.presenceStore.values()) {\n\t\t\tresult[presence.id] = presence\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Loads a document snapshot, completely replacing the current room state.\n\t * This will disconnect all current clients and update the document to match\n\t * the provided snapshot. Use this for restoring from backups or implementing\n\t * document versioning.\n\t *\n\t * @param snapshot - Room or store snapshot to load\n\t *\n\t * @example\n\t * ```ts\n\t * // Restore from a saved snapshot\n\t * const backup = JSON.parse(await loadBackup(roomId))\n\t * room.loadSnapshot(backup)\n\t *\n\t * // All clients will be disconnected and need to reconnect\n\t * // to see the restored document state\n\t * ```\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tthis.storage.transaction((txn) => {\n\t\t\tloadSnapshotIntoStorage(txn, this.room.schema, snapshot)\n\t\t})\n\t}\n\n\t/**\n\t * Executes a transaction to modify the document store. Changes made within the\n\t * transaction are atomic and will be synchronized to all connected clients.\n\t * The transaction provides isolation from concurrent changes until it commits.\n\t *\n\t * @param updater - Function that receives store methods to make changes\n\t * - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)\n\t * - store.put(record) - Save a modified record\n\t * - store.getAll() - Get all records in the store\n\t * - store.delete(id) - Remove a record from the store\n\t * @returns Promise that resolves when the transaction completes\n\t *\n\t * @example\n\t * ```ts\n\t * // Update multiple shapes in a single transaction\n\t * await room.updateStore(store => {\n\t * const shape1 = store.get('shape:abc123')\n\t * const shape2 = store.get('shape:def456')\n\t *\n\t * if (shape1) {\n\t * shape1.x = 100\n\t * store.put(shape1)\n\t * }\n\t *\n\t * if (shape2) {\n\t * shape2.meta.approved = true\n\t * store.put(shape2)\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // Async transaction with external API call\n\t * await room.updateStore(async store => {\n\t * const doc = store.get('document:main')\n\t * if (doc) {\n\t * doc.lastModified = await getCurrentTimestamp()\n\t * store.put(doc)\n\t * }\n\t * })\n\t * ```\n\t * @deprecated use the storage.transaction method instead\n\t */\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\tif (this.isClosed()) {\n\t\t\tthrow new Error('Cannot update store on a closed room')\n\t\t}\n\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\tconst ctx = new StoreUpdateContext<R>(\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\t\t\tObject.fromEntries(this.getCurrentSnapshot().documents.map((d) => [d.state.id, d.state])),\n\t\t\tthis.room.schema\n\t\t)\n\t\ttry {\n\t\t\tawait updater(ctx)\n\t\t} finally {\n\t\t\tctx.close()\n\t\t}\n\t\tthis.storage.transaction((txn) => {\n\t\t\tfor (const [id, record] of Object.entries(ctx.updates.puts)) {\n\t\t\t\ttxn.set(id, record as R)\n\t\t\t}\n\t\t\tfor (const id of ctx.updates.deletes) {\n\t\t\t\ttxn.delete(id)\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Sends a custom message to a specific client session. This allows sending\n\t * application-specific data that doesn't modify the document state, such as\n\t * notifications, chat messages, or custom commands.\n\t *\n\t * @param sessionId - Target session identifier\n\t * @param data - Custom payload to send (will be JSON serialized)\n\t *\n\t * @example\n\t * ```ts\n\t * // Send a notification to a specific user\n\t * room.sendCustomMessage('session-123', {\n\t * type: 'notification',\n\t * message: 'Your changes have been saved'\n\t * })\n\t *\n\t * // Send a chat message\n\t * room.sendCustomMessage('session-456', {\n\t * type: 'chat',\n\t * from: 'Alice',\n\t * text: 'Great work on this design!'\n\t * })\n\t * ```\n\t */\n\tsendCustomMessage(sessionId: string, data: any) {\n\t\tthis.room.sendCustomMessage(sessionId, data)\n\t}\n\n\t/**\n\t * Immediately removes a session from the room and closes its WebSocket connection.\n\t * The client will attempt to reconnect automatically unless a fatal reason is provided.\n\t *\n\t * @param sessionId - Session identifier to remove\n\t * @param fatalReason - Optional fatal error reason that prevents reconnection\n\t *\n\t * @example\n\t * ```ts\n\t * // Kick a user (they can reconnect)\n\t * room.closeSession('session-troublemaker')\n\t *\n\t * // Permanently ban a user\n\t * room.closeSession('session-banned', 'PERMISSION_DENIED')\n\t *\n\t * // Close session due to inactivity\n\t * room.closeSession('session-idle', 'TIMEOUT')\n\t * ```\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Closes the room and disconnects all connected clients. This should be called\n\t * when shutting down the room permanently, such as during server shutdown or\n\t * when the room is no longer needed. Once closed, the room cannot be reopened.\n\t *\n\t * @example\n\t * ```ts\n\t * // Clean shutdown when no users remain\n\t * if (room.getNumActiveSessions() === 0) {\n\t * await persistSnapshot(room.getCurrentSnapshot())\n\t * room.close()\n\t * }\n\t *\n\t * // Server shutdown\n\t * process.on('SIGTERM', () => {\n\t * for (const room of activeRooms.values()) {\n\t * room.close()\n\t * }\n\t * })\n\t * ```\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.disposables.clear()\n\t}\n\n\t/**\n\t * Checks whether the room has been permanently closed. Closed rooms cannot\n\t * accept new connections or process further changes.\n\t *\n\t * @returns True if the room is closed, false if still active\n\t *\n\t * @example\n\t * ```ts\n\t * if (room.isClosed()) {\n\t * console.log('Room has been shut down')\n\t * // Create a new room or redirect users\n\t * } else {\n\t * // Room is still accepting connections\n\t * room.handleSocketConnect({ sessionId, socket })\n\t * }\n\t * ```\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/**\n * Utility type that removes properties with void values from an object type.\n * This is used internally to conditionally require session metadata based on\n * whether SessionMeta extends void.\n *\n * @example\n * ```ts\n * type Example = { a: string, b: void, c: number }\n * type Result = OmitVoid<Example> // { a: string, c: number }\n * ```\n *\n * @public\n */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\n/**\n * Interface for making transactional changes to room store data. Used within\n * updateStore transactions to modify documents atomically.\n *\n * @example\n * ```ts\n * await room.updateStore((store) => {\n * const shape = store.get('shape:123')\n * if (shape) {\n * store.put({ ...shape, x: shape.x + 10 })\n * }\n * store.delete('shape:456')\n * })\n * ```\n *\n * @public\n * @deprecated use the storage.transaction method instead\n */\nexport interface RoomStoreMethods<R extends UnknownRecord = UnknownRecord> {\n\t/**\n\t * Add or update a record in the store.\n\t *\n\t * @param record - The record to store\n\t */\n\tput(record: R): void\n\t/**\n\t * Delete a record from the store.\n\t *\n\t * @param recordOrId - The record or record ID to delete\n\t */\n\tdelete(recordOrId: R | string): void\n\t/**\n\t * Get a record by its ID.\n\t *\n\t * @param id - The record ID\n\t * @returns The record or null if not found\n\t */\n\tget(id: string): R | null\n\t/**\n\t * Get all records in the store.\n\t *\n\t * @returns Array of all records\n\t */\n\tgetAll(): R[]\n}\n\n/**\n * @deprecated use the storage.transaction method instead\n */\n// eslint-disable-next-line @typescript-eslint/no-deprecated\nclass StoreUpdateContext<R extends UnknownRecord> implements RoomStoreMethods<R> {\n\tconstructor(\n\t\tprivate readonly snapshot: Record<string, UnknownRecord>,\n\t\tprivate readonly schema: StoreSchema<R, any>\n\t) {}\n\treadonly updates = {\n\t\tputs: {} as Record<string, UnknownRecord>,\n\t\tdeletes: new Set<string>(),\n\t}\n\tput(record: R): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst recordType = getOwnProperty(this.schema.types, record.typeName)\n\t\tif (!recordType) {\n\t\t\tthrow new Error(`Missing definition for record type ${record.typeName}`)\n\t\t}\n\t\tconst recordBefore = this.snapshot[record.id] ?? undefined\n\t\trecordType.validate(record, recordBefore as R)\n\n\t\tif (record.id in this.snapshot && isEqual(this.snapshot[record.id], record)) {\n\t\t\tdelete this.updates.puts[record.id]\n\t\t} else {\n\t\t\tthis.updates.puts[record.id] = structuredClone(record)\n\t\t}\n\t\tthis.updates.deletes.delete(record.id)\n\t}\n\tdelete(recordOrId: R | string): void {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst id = typeof recordOrId === 'string' ? recordOrId : recordOrId.id\n\t\tdelete this.updates.puts[id]\n\t\tif (this.snapshot[id]) {\n\t\t\tthis.updates.deletes.add(id)\n\t\t}\n\t}\n\tget(id: string): R | null {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tif (hasOwnProperty(this.updates.puts, id)) {\n\t\t\treturn structuredClone(this.updates.puts[id]) as R\n\t\t}\n\t\tif (this.updates.deletes.has(id)) {\n\t\t\treturn null\n\t\t}\n\t\treturn structuredClone(this.snapshot[id] ?? null) as R\n\t}\n\n\tgetAll(): R[] {\n\t\tif (this._isClosed) throw new Error('StoreUpdateContext is closed')\n\t\tconst result = Object.values(this.updates.puts)\n\t\tfor (const [id, record] of Object.entries(this.snapshot)) {\n\t\t\tif (!this.updates.deletes.has(id) && !hasOwnProperty(this.updates.puts, id)) {\n\t\t\t\tresult.push(record)\n\t\t\t}\n\t\t}\n\t\treturn structuredClone(result) as R[]\n\t}\n\n\tprivate _isClosed = false\n\tclose() {\n\t\tthis._isClosed = true\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -19,6 +19,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
19
19
|
var TLSyncClient_exports = {};
|
|
20
20
|
__export(TLSyncClient_exports, {
|
|
21
21
|
TLSyncClient: () => TLSyncClient,
|
|
22
|
+
TLSyncError: () => TLSyncError,
|
|
22
23
|
TLSyncErrorCloseEventCode: () => TLSyncErrorCloseEventCode,
|
|
23
24
|
TLSyncErrorCloseEventReason: () => TLSyncErrorCloseEventReason
|
|
24
25
|
});
|
|
@@ -50,6 +51,12 @@ const TLSyncErrorCloseEventReason = {
|
|
|
50
51
|
/** Room has reached maximum capacity */
|
|
51
52
|
ROOM_FULL: "ROOM_FULL"
|
|
52
53
|
};
|
|
54
|
+
class TLSyncError extends Error {
|
|
55
|
+
constructor(message, reason) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.reason = reason;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
53
60
|
const PING_INTERVAL = 5e3;
|
|
54
61
|
const MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION = PING_INTERVAL * 2;
|
|
55
62
|
class TLSyncClient {
|