@tldraw/sync-core 5.3.0-canary.fceaae5e9feb → 5.3.0-internal.fba91ed55f6c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist-cjs/index.d.ts +154 -3
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/InMemorySyncStorage.js +55 -20
  5. package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
  6. package/dist-cjs/lib/RoomSession.js.map +1 -1
  7. package/dist-cjs/lib/SQLiteSyncStorage.js +115 -52
  8. package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
  9. package/dist-cjs/lib/TLSocketRoom.js +27 -2
  10. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  11. package/dist-cjs/lib/TLSyncClient.js +6 -1
  12. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  13. package/dist-cjs/lib/TLSyncRoom.js +160 -10
  14. package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
  15. package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
  16. package/dist-cjs/lib/protocol.js.map +2 -2
  17. package/dist-esm/index.d.mts +154 -3
  18. package/dist-esm/index.mjs +1 -1
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/InMemorySyncStorage.mjs +55 -20
  21. package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
  22. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  23. package/dist-esm/lib/SQLiteSyncStorage.mjs +115 -52
  24. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
  25. package/dist-esm/lib/TLSocketRoom.mjs +27 -2
  26. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  27. package/dist-esm/lib/TLSyncClient.mjs +6 -1
  28. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  29. package/dist-esm/lib/TLSyncRoom.mjs +160 -10
  30. package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
  31. package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
  32. package/dist-esm/lib/protocol.mjs.map +2 -2
  33. package/package.json +6 -6
  34. package/src/index.ts +3 -0
  35. package/src/lib/InMemorySyncStorage.ts +74 -21
  36. package/src/lib/RoomSession.ts +7 -2
  37. package/src/lib/SQLiteSyncStorage.test.ts +29 -2
  38. package/src/lib/SQLiteSyncStorage.ts +158 -59
  39. package/src/lib/TLSocketRoom.ts +61 -3
  40. package/src/lib/TLSyncClient.test.ts +9 -2
  41. package/src/lib/TLSyncClient.ts +15 -3
  42. package/src/lib/TLSyncRoom.ts +294 -10
  43. package/src/lib/TLSyncStorage.ts +8 -0
  44. package/src/lib/protocol.ts +17 -0
  45. package/src/test/TLSocketRoom.test.ts +37 -2
  46. package/src/test/TLSyncClientRebase.test.ts +285 -0
  47. package/src/test/TLSyncRoom.test.ts +25 -0
  48. package/src/test/TestServer.ts +14 -4
  49. package/src/test/objectStore.test.ts +630 -0
  50. package/src/test/storageContractSuite.ts +79 -0
  51. package/src/test/upgradeDowngrade.test.ts +144 -2
@@ -108,13 +108,20 @@ export declare class DurableObjectSqliteSyncWrapper implements TLSyncSqliteWrapp
108
108
  */
109
109
  export declare class InMemorySyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {
110
110
  /* Excluded from this release type: documents */
111
+ /* Excluded from this release type: objects */
112
+ /* Excluded from this release type: objectTypes */
111
113
  /* Excluded from this release type: tombstones */
112
114
  /* Excluded from this release type: schema */
113
115
  /* Excluded from this release type: documentClock */
114
116
  /* Excluded from this release type: tombstoneHistoryStartsAtClock */
115
117
  private notifier;
116
118
  onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void;
117
- constructor({ snapshot, onChange }?: {
119
+ constructor({ snapshot, objectTypes, onChange }?: {
120
+ /**
121
+ * Record type names stored in the object-store lane. Records of these types are routed to
122
+ * a separate partition: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.
123
+ */
124
+ objectTypes?: readonly string[];
118
125
  onChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown;
119
126
  snapshot?: RoomSnapshot;
120
127
  });
@@ -122,6 +129,7 @@ export declare class InMemorySyncStorage<R extends UnknownRecord> implements TLS
122
129
  getClock(): number;
123
130
  /* Excluded from this release type: pruneTombstones */
124
131
  getSnapshot(): RoomSnapshot;
132
+ getObjectsSnapshot(): RoomSnapshot['documents'];
125
133
  }
126
134
 
127
135
  /**
@@ -383,6 +391,11 @@ export declare interface RoomStoreMethods<R extends UnknownRecord = UnknownRecor
383
391
  export declare interface SessionStateSnapshot {
384
392
  serializedSchema: SerializedSchema;
385
393
  isReadonly: boolean;
394
+ /**
395
+ * Write access for the record types listed in `objectTypes`. Optional so snapshots persisted
396
+ * before this field existed resume cleanly (they fail closed to 'read').
397
+ */
398
+ objectAccess?: TLObjectStoreAccess;
386
399
  presenceId: null | string;
387
400
  presenceRecord: null | UnknownRecord;
388
401
  requiresLegacyRejection: boolean;
@@ -438,7 +451,14 @@ export declare class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyn
438
451
  static getDocumentClock(storage: TLSyncSqliteWrapper): null | number;
439
452
  private readonly stmts;
440
453
  private readonly sql;
441
- constructor({ sql, snapshot, onChange }: {
454
+ /* Excluded from this release type: objectTypes */
455
+ constructor({ sql, snapshot, objectTypes, onChange }: {
456
+ /**
457
+ * Record type names stored in the object-store lane. Records of these types are routed to
458
+ * a separate table: excluded from `getSnapshot()`, returned by `getObjectsSnapshot()`.
459
+ * They share the documents' clock, tombstones, and transactions.
460
+ */
461
+ objectTypes?: readonly string[];
442
462
  onChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown;
443
463
  snapshot?: RoomSnapshot | StoreSnapshot<R>;
444
464
  sql: TLSyncSqliteWrapper;
@@ -452,7 +472,8 @@ export declare class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyn
452
472
  /* Excluded from this release type: _setSchema */
453
473
  /* Excluded from this release type: pruneTombstones */
454
474
  getSnapshot(): RoomSnapshot;
455
- private _iterateDocuments;
475
+ getObjectsSnapshot(): RoomSnapshot['documents'];
476
+ private _iterateRecords;
456
477
  private _iterateTombstones;
457
478
  }
458
479
 
@@ -520,6 +541,17 @@ export declare type TLCustomMessageHandler = (this: null, data: any) => void;
520
541
 
521
542
  /* Excluded from this release type: TLIncompatibilityReason */
522
543
 
544
+ /**
545
+ * Access level for object-store lane record types (see `objectTypes` on the room).
546
+ *
547
+ * Object-lane records (e.g. comments) are gated by this per-session value instead of the
548
+ * document-lane `isReadonly` flag, so a session can be allowed to write objects without
549
+ * being allowed to edit the document ("can comment but not edit"), or vice versa.
550
+ *
551
+ * @public
552
+ */
553
+ export declare type TLObjectStoreAccess = 'read' | 'write';
554
+
523
555
  /**
524
556
  * Interface for persistent WebSocket-like connections used by TLSyncClient.
525
557
  * Handles automatic reconnection and provides event-based communication with the sync server.
@@ -598,6 +630,78 @@ export declare type TLPresenceMode =
598
630
 
599
631
  /* Excluded from this release type: TLPushRequest */
600
632
 
633
+ /**
634
+ * Authorizes a single record write from a client: any per-record, per-session rule the host wants
635
+ * to enforce server-side — veto writes the session isn't allowed to make, or rewrite the record on
636
+ * create. The session's `meta` carries whatever the rule needs (identity, roles, …); for example,
637
+ * force a comment's `authorId` to the signed-in user so nobody can post in someone else's name.
638
+ *
639
+ * Called on **create**, **update**, and **delete** of records whose `typeName` it's registered for
640
+ * (see {@link TLRecordAuthorizers}), and only for client pushes — never for server-initiated writes.
641
+ *
642
+ * `prev` and `next` are always at the **server's** schema version: client writes are migrated
643
+ * before the authorizer runs, so guarding or stamping a field never requires knowing what older
644
+ * clients call it. On create, the record you return is what gets stored (after validation) — no
645
+ * migration runs afterwards, so stamped fields can't be clobbered.
646
+ *
647
+ * Return `null` to reject the write — it's skipped and the client self-corrects, exactly like the
648
+ * `objectAccess` gate. Otherwise the write is allowed, and:
649
+ *
650
+ * - on **create**, the record you return is what gets stored, so stamp identity fields here (e.g.
651
+ * set `authorId` from `session.meta`);
652
+ * - on **update** and **delete**, only allow-vs-reject is used (the returned record's contents are
653
+ * ignored), so use them to veto changes to immutable fields or unauthorized deletes — return
654
+ * `next`/`prev` to allow, `null` to reject.
655
+ *
656
+ * ⚠︎ Runs synchronously inside the commit transaction, on the same path as every document edit — it
657
+ * must be fast and do **no** I/O. `next`/`prev` are client-controlled records, so treat their
658
+ * contents as untrusted; prefer returning `null` to reject over throwing, though a throw is
659
+ * caught, logged, and treated as a rejection (fail closed) rather than crashing the push. For
660
+ * expensive, async checks (e.g. resolving mentions against who can access a file), react after the
661
+ * fact via `onCommittedChanges`.
662
+ *
663
+ * @public
664
+ */
665
+ export declare type TLRecordAuthorizer<Rec extends UnknownRecord, SessionMeta> = (args: {
666
+ /** The session performing the write, including its host-provided `meta` (e.g. the authenticated user id). */
667
+ session: {
668
+ meta: SessionMeta;
669
+ sessionId: string;
670
+ };
671
+ } & ({
672
+ next: null;
673
+ prev: Rec;
674
+ type: 'delete';
675
+ } | {
676
+ next: Rec;
677
+ prev: null;
678
+ type: 'create';
679
+ } | {
680
+ next: Rec;
681
+ prev: Rec;
682
+ type: 'update';
683
+ })) => null | Rec;
684
+
685
+ /**
686
+ * A map from record `typeName` to a {@link TLRecordAuthorizer} for that record type. Only listed
687
+ * types are authorized; every other record writes through untouched, so this stays off the hot path
688
+ * for the vast majority of writes (shape drags etc.).
689
+ *
690
+ * Each authorizer is typed to its record — e.g. `next` in the `comment` entry is a `TLComment` — so
691
+ * renaming a field on the record makes the authorizer that reads it fail to compile, rather than
692
+ * silently stamp or guard the wrong field.
693
+ *
694
+ * Presence records are never authorized (presence is per-session and ephemeral); registering the
695
+ * presence typeName is a construction-time error.
696
+ *
697
+ * @public
698
+ */
699
+ export declare type TLRecordAuthorizers<R extends UnknownRecord, SessionMeta> = {
700
+ [K in R['typeName']]?: TLRecordAuthorizer<Extract<R, {
701
+ typeName: K;
702
+ }>, SessionMeta>;
703
+ };
704
+
601
705
  /**
602
706
  * Specialized error class for synchronization-related failures in tldraw collaboration.
603
707
  *
@@ -757,6 +861,9 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
757
861
  * - sessionId - Unique identifier for the client session (typically from browser tab)
758
862
  * - socket - WebSocket-like object for client communication
759
863
  * - isReadonly - Whether the client can modify the document (defaults to false)
864
+ * - objectAccess - Write access for object-store lane record types (defaults to 'write').
865
+ * Independent of isReadonly, so a session can be allowed to write objects (e.g. comments)
866
+ * without being allowed to edit the document.
760
867
  * - meta - Additional session metadata (required if SessionMeta is not void)
761
868
  *
762
869
  * @example
@@ -781,6 +888,7 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
781
888
  */
782
889
  handleSocketConnect(opts: {
783
890
  isReadonly?: boolean;
891
+ objectAccess?: TLObjectStoreAccess;
784
892
  sessionId: string;
785
893
  socket: WebSocketMinimal;
786
894
  } & (SessionMeta extends void ? object : {
@@ -964,6 +1072,7 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
964
1072
  isConnected: boolean;
965
1073
  isReadonly: boolean;
966
1074
  meta: SessionMeta;
1075
+ objectAccess: TLObjectStoreAccess;
967
1076
  sessionId: string;
968
1077
  }>;
969
1078
  /**
@@ -986,6 +1095,12 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
986
1095
  * ```
987
1096
  */
988
1097
  getCurrentSnapshot(): RoomSnapshot;
1098
+ /**
1099
+ * Returns a snapshot of the object-store lane (see `objectTypes`), for persisting
1100
+ * object-lane records separately from the document. Same entry shape as
1101
+ * {@link RoomSnapshot.documents}.
1102
+ */
1103
+ getCurrentObjectsSnapshot(): RoomSnapshot['documents'];
989
1104
  /* Excluded from this release type: getPresenceRecords */
990
1105
  /**
991
1106
  * Loads a document snapshot, completely replacing the current room state.
@@ -1173,6 +1288,32 @@ export declare interface TLSocketRoomOptions<R extends UnknownRecord, SessionMet
1173
1288
  stringified: string;
1174
1289
  }) => void;
1175
1290
  /* Excluded from this release type: onPresenceChange */
1291
+ /**
1292
+ * Called once after a client push commits, with the committed document diff. Use to react to
1293
+ * document changes as soon as they commit — e.g. project certain record types to an external
1294
+ * store. Best-effort: do not throw and do not block.
1295
+ *
1296
+ * Only fires for client pushes. Server-initiated changes — `updateStore`, `loadSnapshot`, or
1297
+ * writing to the storage directly — do not trigger it, so anything mirroring room state through
1298
+ * this callback must handle those paths itself.
1299
+ */
1300
+ onCommittedChanges?: (args: {
1301
+ diff: TLSyncForwardDiff<R>;
1302
+ documentClock: number;
1303
+ }) => void;
1304
+ /**
1305
+ * Record type names to serve through the object-store lane instead of the document lane.
1306
+ * Each must be a document-scoped type registered in the schema. Object-lane writes are gated
1307
+ * per session by `objectAccess` (see {@link TLSocketRoom.handleSocketConnect}) rather than
1308
+ * `isReadonly`, so e.g. a session can be allowed to comment without being allowed to edit.
1309
+ */
1310
+ objectTypes?: readonly string[];
1311
+ /**
1312
+ * Per-type authorizers for client record writes (create, update, delete): veto writes the
1313
+ * session isn't allowed to make, or rewrite the record on create — e.g. force a comment's
1314
+ * `authorId` to the signed-in user. See {@link TLRecordAuthorizers}.
1315
+ */
1316
+ authorizeRecord?: TLRecordAuthorizers<R, SessionMeta>;
1176
1317
  /**
1177
1318
  * When set, the room will call {@link TLSocketRoom.getSessionSnapshot} after
1178
1319
  * no message activity for a session for 5s and pass the result to this callback.
@@ -1327,6 +1468,8 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
1327
1468
  * @param self - The TLSyncClient instance that connected
1328
1469
  * @param details - Connection details
1329
1470
  * - isReadonly - Whether the connection is in read-only mode
1471
+ * - objectAccess - Write access for object-store lane record types (defaults to 'write'
1472
+ * when the server doesn't send it, e.g. older servers or rooms with no object lane)
1330
1473
  */
1331
1474
  private readonly onAfterConnect?;
1332
1475
  private readonly onCustomMessageReceived?;
@@ -1354,6 +1497,7 @@ export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> =
1354
1497
  didCancel?(): boolean;
1355
1498
  onAfterConnect?(self: TLSyncClient<R, S>, details: {
1356
1499
  isReadonly: boolean;
1500
+ objectAccess: TLObjectStoreAccess;
1357
1501
  }): void;
1358
1502
  onCustomMessageReceived?: TLCustomMessageHandler;
1359
1503
  onLoad(self: TLSyncClient<R, S>): void;
@@ -1580,7 +1724,14 @@ export declare interface TLSyncStorage<R extends UnknownRecord> {
1580
1724
  transaction<T>(callback: TLSyncStorageTransactionCallback<R, T>, opts?: TLSyncStorageTransactionOptions): TLSyncStorageTransactionResult<T, R>;
1581
1725
  getClock(): number;
1582
1726
  onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void;
1727
+ /** A snapshot of the document lane only — never contains object-store lane records. */
1583
1728
  getSnapshot?(): RoomSnapshot;
1729
+ /**
1730
+ * A snapshot of the object-store lane (see `objectTypes` on the storage), for hosts that
1731
+ * persist object-lane records separately from the document. Same entry shape as
1732
+ * `RoomSnapshot['documents']` so a lane can be persisted and re-seeded via a merged snapshot.
1733
+ */
1734
+ getObjectsSnapshot?(): RoomSnapshot['documents'];
1584
1735
  }
1585
1736
 
1586
1737
  /**
@@ -36,7 +36,7 @@ import {
36
36
  } from "./lib/TLSyncStorage.mjs";
37
37
  registerTldrawLibraryVersion(
38
38
  "@tldraw/sync-core",
39
- "5.3.0-canary.fceaae5e9feb",
39
+ "5.3.0-internal.fba91ed55f6c",
40
40
  "esm"
41
41
  );
42
42
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport { DurableObjectSqliteSyncWrapper } from './lib/DurableObjectSqliteSyncWrapper'\nexport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './lib/InMemorySyncStorage'\nexport { NodeSqliteWrapper, type SyncSqliteDatabase } from './lib/NodeSqliteWrapper'\nexport {\n\tgetTlsyncProtocolVersion,\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport {\n\tSQLiteSyncStorage,\n\ttype TLSqliteInputValue,\n\ttype TLSqliteOutputValue,\n\ttype TLSqliteRow,\n\ttype TLSyncSqliteStatement,\n\ttype TLSyncSqliteWrapper,\n\ttype TLSyncSqliteWrapperConfig,\n} from './lib/SQLiteSyncStorage'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport {\n\tTLSocketRoom,\n\ttype OmitVoid,\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\ttype RoomStoreMethods,\n\ttype SessionStateSnapshot,\n\ttype TLSocketRoomOptions,\n\ttype TLSyncLog,\n} from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tTLSyncRoom,\n\ttype MinimalDocStore,\n\ttype PresenceStore,\n\ttype RoomSnapshot,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\nexport {\n\tloadSnapshotIntoStorage,\n\ttype TLSyncForwardDiff,\n\ttype TLSyncStorage,\n\ttype TLSyncStorageGetChangesSinceResult,\n\ttype TLSyncStorageOnChangeCallbackProps,\n\ttype TLSyncStorageTransaction,\n\ttype TLSyncStorageTransactionCallback,\n\ttype TLSyncStorageTransactionOptions,\n\ttype TLSyncStorageTransactionResult,\n} from './lib/TLSyncStorage'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
- "mappings": "AAAA,SAAS,oCAAoC;AAC7C,SAAS,OAAO,0BAA0B;AAC1C,SAAS,wBAAwB,wBAAwB;AACzD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASM;AACP,SAAS,sCAAsC;AAC/C,SAAS,0BAA0B,2BAA2B;AAC9D,SAAS,yBAAkD;AAC3D;AAAA,EACC;AAAA,EAEA;AAAA,OAOM;AACP,SAAS,wBAAgE;AAGzE;AAAA,EACC;AAAA,OAOM;AACP,SAAS,yBAAyB;AAClC;AAAA,EACC;AAAA,OAOM;AACP;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,OAQM;AACP;AAAA,EACC;AAAA,OAKM;AACP;AAAA,EACC;AAAA,OASM;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
4
+ "sourcesContent": ["import { registerTldrawLibraryVersion } from '@tldraw/utils'\nexport { chunk, JsonChunkAssembler } from './lib/chunk'\nexport { ClientWebSocketAdapter, ReconnectManager } from './lib/ClientWebSocketAdapter'\nexport {\n\tapplyObjectDiff,\n\tdiffRecord,\n\tgetNetworkDiff,\n\tRecordOpType,\n\tValueOpType,\n\ttype AppendOp,\n\ttype DeleteOp,\n\ttype NetworkDiff,\n\ttype ObjectDiff,\n\ttype PatchOp,\n\ttype PutOp,\n\ttype RecordOp,\n\ttype ValueOp,\n} from './lib/diff'\nexport { DurableObjectSqliteSyncWrapper } from './lib/DurableObjectSqliteSyncWrapper'\nexport { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './lib/InMemorySyncStorage'\nexport { NodeSqliteWrapper, type SyncSqliteDatabase } from './lib/NodeSqliteWrapper'\nexport {\n\tgetTlsyncProtocolVersion,\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\tTLIncompatibilityReason,\n\ttype TLConnectRequest,\n\ttype TLObjectStoreAccess,\n\ttype TLPingRequest,\n\ttype TLPushRequest,\n\ttype TLSocketClientSentEvent,\n\ttype TLSocketServerSentDataEvent,\n\ttype TLSocketServerSentEvent,\n} from './lib/protocol'\nexport { RoomSessionState, type RoomSession, type RoomSessionBase } from './lib/RoomSession'\nexport type { PersistedRoomSnapshotForSupabase } from './lib/server-types'\nexport type { WebSocketMinimal } from './lib/ServerSocketAdapter'\nexport {\n\tSQLiteSyncStorage,\n\ttype TLSqliteInputValue,\n\ttype TLSqliteOutputValue,\n\ttype TLSqliteRow,\n\ttype TLSyncSqliteStatement,\n\ttype TLSyncSqliteWrapper,\n\ttype TLSyncSqliteWrapperConfig,\n} from './lib/SQLiteSyncStorage'\nexport { TLRemoteSyncError } from './lib/TLRemoteSyncError'\nexport {\n\tTLSocketRoom,\n\ttype OmitVoid,\n\t// eslint-disable-next-line @typescript-eslint/no-deprecated\n\ttype RoomStoreMethods,\n\ttype SessionStateSnapshot,\n\ttype TLSocketRoomOptions,\n\ttype TLSyncLog,\n} from './lib/TLSocketRoom'\nexport {\n\tTLSyncClient,\n\tTLSyncErrorCloseEventCode,\n\tTLSyncErrorCloseEventReason,\n\ttype SubscribingFn,\n\ttype TLCustomMessageHandler,\n\ttype TLPersistentClientSocket,\n\ttype TLPersistentClientSocketStatus,\n\ttype TLPresenceMode,\n\ttype TLSocketStatusChangeEvent,\n\ttype TLSocketStatusListener,\n} from './lib/TLSyncClient'\nexport {\n\tTLSyncRoom,\n\ttype MinimalDocStore,\n\ttype PresenceStore,\n\ttype RoomSnapshot,\n\ttype TLRecordAuthorizer,\n\ttype TLRecordAuthorizers,\n\ttype TLRoomSocket,\n} from './lib/TLSyncRoom'\nexport {\n\tloadSnapshotIntoStorage,\n\ttype TLSyncForwardDiff,\n\ttype TLSyncStorage,\n\ttype TLSyncStorageGetChangesSinceResult,\n\ttype TLSyncStorageOnChangeCallbackProps,\n\ttype TLSyncStorageTransaction,\n\ttype TLSyncStorageTransactionCallback,\n\ttype TLSyncStorageTransactionOptions,\n\ttype TLSyncStorageTransactionResult,\n} from './lib/TLSyncStorage'\n\nregisterTldrawLibraryVersion(\n\t(globalThis as any).TLDRAW_LIBRARY_NAME,\n\t(globalThis as any).TLDRAW_LIBRARY_VERSION,\n\t(globalThis as any).TLDRAW_LIBRARY_MODULES\n)\n"],
5
+ "mappings": "AAAA,SAAS,oCAAoC;AAC7C,SAAS,OAAO,0BAA0B;AAC1C,SAAS,wBAAwB,wBAAwB;AACzD;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OASM;AACP,SAAS,sCAAsC;AAC/C,SAAS,0BAA0B,2BAA2B;AAC9D,SAAS,yBAAkD;AAC3D;AAAA,EACC;AAAA,EAEA;AAAA,OAQM;AACP,SAAS,wBAAgE;AAGzE;AAAA,EACC;AAAA,OAOM;AACP,SAAS,yBAAyB;AAClC;AAAA,EACC;AAAA,OAOM;AACP;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,OAQM;AACP;AAAA,EACC;AAAA,OAOM;AACP;AAAA,EACC;AAAA,OASM;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AACF;",
6
6
  "names": []
7
7
  }
@@ -50,6 +50,14 @@ const DEFAULT_INITIAL_SNAPSHOT = {
50
50
  class InMemorySyncStorage {
51
51
  /** @internal */
52
52
  documents;
53
+ /**
54
+ * Object-store lane records, partitioned out of `documents` so they never appear in the
55
+ * document snapshot. They share the same clock, tombstones, and transactions as documents.
56
+ * @internal
57
+ */
58
+ objects;
59
+ /** @internal */
60
+ objectTypes;
53
61
  /** @internal */
54
62
  tombstones;
55
63
  /** @internal */
@@ -64,19 +72,26 @@ class InMemorySyncStorage {
64
72
  }
65
73
  constructor({
66
74
  snapshot = DEFAULT_INITIAL_SNAPSHOT,
75
+ objectTypes,
67
76
  onChange
68
77
  } = {}) {
78
+ this.objectTypes = new Set(objectTypes ?? []);
69
79
  const maxClockValue = Math.max(
70
80
  0,
71
81
  ...Object.values(snapshot.tombstones ?? {}),
72
82
  ...Object.values(snapshot.documents.map((d) => d.lastChangedClock))
73
83
  );
84
+ const toEntry = (d) => [
85
+ d.state.id,
86
+ { state: devFreeze(d.state), lastChangedClock: d.lastChangedClock }
87
+ ];
74
88
  this.documents = new AtomMap(
75
89
  "room documents",
76
- snapshot.documents.map((d) => [
77
- d.state.id,
78
- { state: devFreeze(d.state), lastChangedClock: d.lastChangedClock }
79
- ])
90
+ snapshot.documents.filter((d) => !this.objectTypes.has(d.state.typeName)).map(toEntry)
91
+ );
92
+ this.objects = new AtomMap(
93
+ "room objects",
94
+ snapshot.documents.filter((d) => this.objectTypes.has(d.state.typeName)).map(toEntry)
80
95
  );
81
96
  const documentClock = Math.max(maxClockValue, snapshot.documentClock ?? snapshot.clock ?? 0);
82
97
  this.documentClock = atom("document clock", documentClock);
@@ -161,6 +176,9 @@ class InMemorySyncStorage {
161
176
  schema: this.schema.get()
162
177
  };
163
178
  }
179
+ getObjectsSnapshot() {
180
+ return Array.from(this.objects.values());
181
+ }
164
182
  }
165
183
  class InMemorySyncStorageTransaction {
166
184
  constructor(storage) {
@@ -188,9 +206,15 @@ class InMemorySyncStorageTransaction {
188
206
  }
189
207
  return this._clock;
190
208
  }
209
+ /** The partition a record with this id currently lives in, if any. */
210
+ mapContaining(id) {
211
+ if (this.storage.documents.has(id)) return this.storage.documents;
212
+ if (this.storage.objects.has(id)) return this.storage.objects;
213
+ return void 0;
214
+ }
191
215
  get(id) {
192
216
  this.assertNotClosed();
193
- return this.storage.documents.get(id)?.state;
217
+ return (this.storage.documents.get(id) ?? this.storage.objects.get(id))?.state;
194
218
  }
195
219
  set(id, record) {
196
220
  this.assertNotClosed();
@@ -199,38 +223,47 @@ class InMemorySyncStorageTransaction {
199
223
  if (this.storage.tombstones.has(id)) {
200
224
  this.storage.tombstones.delete(id);
201
225
  }
202
- this.storage.documents.set(id, {
226
+ const map = this.storage.objectTypes.has(record.typeName) ? this.storage.objects : this.storage.documents;
227
+ map.set(id, {
203
228
  state: devFreeze(record),
204
229
  lastChangedClock: clock
205
230
  });
206
231
  }
207
232
  delete(id) {
208
233
  this.assertNotClosed();
209
- if (!this.storage.documents.has(id)) return;
234
+ const map = this.mapContaining(id);
235
+ if (!map) return;
210
236
  const clock = this.getNextClock();
211
- this.storage.documents.delete(id);
237
+ map.delete(id);
212
238
  this.storage.tombstones.set(id, clock);
213
239
  this.storage.pruneTombstones();
214
240
  }
241
+ // iteration spans both partitions so schema migrations cover object-lane records too
215
242
  *entries() {
216
243
  this.assertNotClosed();
217
- for (const [id, record] of this.storage.documents.entries()) {
218
- this.assertNotClosed();
219
- yield [id, record.state];
244
+ for (const map of [this.storage.documents, this.storage.objects]) {
245
+ for (const [id, record] of map.entries()) {
246
+ this.assertNotClosed();
247
+ yield [id, record.state];
248
+ }
220
249
  }
221
250
  }
222
251
  *keys() {
223
252
  this.assertNotClosed();
224
- for (const key of this.storage.documents.keys()) {
225
- this.assertNotClosed();
226
- yield key;
253
+ for (const map of [this.storage.documents, this.storage.objects]) {
254
+ for (const key of map.keys()) {
255
+ this.assertNotClosed();
256
+ yield key;
257
+ }
227
258
  }
228
259
  }
229
260
  *values() {
230
261
  this.assertNotClosed();
231
- for (const record of this.storage.documents.values()) {
232
- this.assertNotClosed();
233
- yield record.state;
262
+ for (const map of [this.storage.documents, this.storage.objects]) {
263
+ for (const record of map.values()) {
264
+ this.assertNotClosed();
265
+ yield record.state;
266
+ }
234
267
  }
235
268
  }
236
269
  getSchema() {
@@ -250,9 +283,11 @@ class InMemorySyncStorageTransaction {
250
283
  }
251
284
  const diff = { puts: {}, deletes: [] };
252
285
  const wipeAll = sinceClock < this.storage.tombstoneHistoryStartsAtClock.get();
253
- for (const doc of this.storage.documents.values()) {
254
- if (wipeAll || doc.lastChangedClock > sinceClock) {
255
- diff.puts[doc.state.id] = doc.state;
286
+ for (const map of [this.storage.documents, this.storage.objects]) {
287
+ for (const doc of map.values()) {
288
+ if (wipeAll || doc.lastChangedClock > sinceClock) {
289
+ diff.puts[doc.state.id] = doc.state;
290
+ }
256
291
  }
257
292
  }
258
293
  if (!wipeAll) {
@@ -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/** @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\tonChange,\n\t}: {\n\t\tsnapshot?: RoomSnapshot\n\t\tonChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown\n\t} = {}) {\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\tthis.documents = new AtomMap(\n\t\t\t'room documents',\n\t\t\tsnapshot.documents.map((d) => [\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 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\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\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\tget(id: string): R | undefined {\n\t\tthis.assertNotClosed()\n\t\treturn this.storage.documents.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\tthis.storage.documents.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\tif (!this.storage.documents.has(id)) return\n\t\tconst clock = this.getNextClock()\n\t\tthis.storage.documents.delete(id)\n\t\tthis.storage.tombstones.set(id, clock)\n\t\tthis.storage.pruneTombstones()\n\t}\n\n\t*entries(): IterableIterator<[string, R]> {\n\t\tthis.assertNotClosed()\n\t\tfor (const [id, record] of this.storage.documents.entries()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield [id, record.state]\n\t\t}\n\t}\n\n\t*keys(): IterableIterator<string> {\n\t\tthis.assertNotClosed()\n\t\tfor (const key of this.storage.documents.keys()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield key\n\t\t}\n\t}\n\n\t*values(): IterableIterator<R> {\n\t\tthis.assertNotClosed()\n\t\tfor (const record of this.storage.documents.values()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield record.state\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\tfor (const doc of this.storage.documents.values()) {\n\t\t\tif (wipeAll || doc.lastChangedClock > sinceClock) {\n\t\t\t\t// For historical changes, we don't have \"from\" state, so use added\n\t\t\t\tdiff.puts[doc.state.id] = doc.state as R\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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",
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\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,SAAS,MAAY,mBAAmB;AACxC,SAAS,SAAS,iBAAkD;AACpE;AAAA,EACC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEM;AACP,SAAS,QAAkB,kBAAkB,gBAAgB;AAC7D,SAAS,yBAAyB;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,QAAQ,eAAe,EAAE,UAAU;AAAA,EACnC,WAAW;AAAA,IACV;AAAA,MACC,OAAO,mBAAmB,OAAO,EAAE,IAAI,cAAc,CAAC;AAAA,MACtD,kBAAkB;AAAA,IACnB;AAAA,IACA;AAAA,MACC,OAAO,eAAe,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,kBAAwD;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;AAC5C,UAAM,gBAAgB,KAAK;AAAA,MAC1B;AAAA,MACA,GAAG,OAAO,OAAO,SAAS,cAAc,CAAC,CAAC;AAAA,MAC1C,GAAG,OAAO,OAAO,SAAS,UAAU,IAAI,CAAC,MAAM,EAAE,gBAAgB,CAAC;AAAA,IACnE;AAGA,UAAM,UAAU,CACf,MACsD;AAAA,MACtD,EAAE,MAAM;AAAA,MACR,EAAE,OAAO,UAAU,EAAE,KAAK,GAAQ,kBAAkB,EAAE,iBAAiB;AAAA,IACxE;AACA,SAAK,YAAY,IAAI;AAAA,MACpB;AAAA,MACA,SAAS,UAAU,OAAO,CAAC,MAAM,CAAC,KAAK,YAAY,IAAI,EAAE,MAAM,QAAQ,CAAC,EAAE,IAAI,OAAO;AAAA,IACtF;AACA,SAAK,UAAU,IAAI;AAAA,MAClB;AAAA,MACA,SAAS,UAAU,OAAO,CAAC,MAAM,KAAK,YAAY,IAAI,EAAE,MAAM,QAAQ,CAAC,EAAE,IAAI,OAAO;AAAA,IACrF;AACA,UAAM,gBAAgB,KAAK,IAAI,eAAe,SAAS,iBAAiB,SAAS,SAAS,CAAC;AAE3F,SAAK,gBAAgB,KAAK,kBAAkB,aAAa;AAEzD,UAAM,gCAAgC,KAAK;AAAA,MAC1C,SAAS,iCAAiC;AAAA,MAC1C;AAAA,IACD;AACA,SAAK,gCAAgC;AAAA,MACpC;AAAA,MACA;AAAA,IACD;AAEA,SAAK,SAAS,KAAK,UAAU,SAAS,UAAU,eAAe,EAAE,yBAAyB,CAAC;AAC3F,SAAK,aAAa,IAAI;AAAA,MACrB;AAAA;AAAA;AAAA,MAGA,kCAAkC,gBAC/B,CAAC,IACD,iBAAiB,SAAS,cAAc,CAAC,CAAC;AAAA,IAC9C;AACA,QAAI,UAAU;AACb,WAAK,SAAS,QAAQ;AAAA,IACvB;AAAA,EACD;AAAA,EAEA,YACC,UACA,MACuC;AACvC,UAAM,cAAc,KAAK,cAAc,IAAI;AAC3C,UAAM,eAAe,MAAM,gBAAgB;AAC3C,UAAM,MAAM,IAAI,+BAAkC,IAAI;AACtD,QAAI;AACJ,QAAI;AACJ,QAAI;AACH,eAAS,YAAY,MAAM;AAC1B,eAAO,SAAS,GAAU;AAAA,MAC3B,CAAC;AACD,UAAI,cAAc;AACjB,kBAAU,IAAI,gBAAgB,WAAW,GAAG;AAAA,MAC7C;AAAA,IACD,SAAS,OAAO;AACf,cAAQ,MAAM,wBAAwB,KAAK;AAC3C,YAAM;AAAA,IACP,UAAE;AACD,UAAI,MAAM;AAAA,IACX;AACA,QACC,OAAO,WAAW,YAClB,UACA,UAAU,UACV,OAAO,OAAO,SAAS,YACtB;AACD,YAAM,MAAM,IAAI,MAAM,gDAAgD;AACtE,cAAQ,MAAM,GAAG;AACjB,YAAM;AAAA,IACP;AAEA,UAAM,aAAa,KAAK,cAAc,IAAI;AAC1C,UAAM,YAAY,aAAa;AAC/B,QAAI,WAAW;AACd,WAAK,SAAS,OAAO,EAAE,IAAI,MAAM,IAAI,eAAe,WAAW,CAAC;AAAA,IACjE;AAGA,WAAO,EAAE,eAAe,YAAY,WAAW,aAAa,aAAa,QAAQ,QAAQ;AAAA,EAC1F;AAAA,EAEA,WAAmB;AAClB,WAAO,KAAK,cAAc,IAAI;AAAA,EAC/B;AAAA;AAAA,EAGA,kBAAkB;AAAA,IACjB,MAAM;AACL,UAAI,KAAK,WAAW,OAAO,gBAAgB;AAE1C,cAAM,aAAa,MAAM,KAAK,KAAK,WAAW,QAAQ,CAAC,EACrD,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,EAAE,IAAI,MAAM,EAAE,EACpC,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAElC,cAAM,SAAS,wBAAwB;AAAA,UACtC;AAAA,UACA,eAAe,KAAK,cAAc,IAAI;AAAA,QACvC,CAAC;AACD,YAAI,QAAQ;AACX,eAAK,8BAA8B,IAAI,OAAO,gCAAgC;AAC9E,eAAK,WAAW,WAAW,OAAO,WAAW;AAAA,QAC9C;AAAA,MACD;AAAA,IACD;AAAA,IACA;AAAA;AAAA,IAEA,EAAE,SAAS,MAAM;AAAA,EAClB;AAAA,EAEA,cAA4B;AAG3B,WAAO;AAAA,MACN,+BAA+B,KAAK,8BAA8B,IAAI;AAAA,MACtE,eAAe,KAAK,cAAc,IAAI;AAAA,MACtC,WAAW,MAAM,KAAK,KAAK,UAAU,OAAO,CAAC;AAAA,MAC7C,YAAY,OAAO,YAAY,KAAK,WAAW,QAAQ,CAAC;AAAA,MACxD,QAAQ,KAAK,OAAO,IAAI;AAAA,IACzB;AAAA,EACD;AAAA,EAEA,qBAAgD;AAC/C,WAAO,MAAM,KAAK,KAAK,QAAQ,OAAO,CAAC;AAAA,EACxC;AACD;AAQA,MAAM,+BAEmC;AAAA,EAIxC,YAAoB,SAAiC;AAAjC;AACnB,SAAK,SAAS,KAAK,QAAQ,cAAc,IAAI;AAAA,EAC9C;AAAA,EAFoB;AAAA,EAHZ;AAAA,EACA,UAAU;AAAA;AAAA,EAOlB,QAAQ;AACP,SAAK,UAAU;AAAA,EAChB;AAAA,EAEQ,kBAAkB;AACzB,WAAO,CAAC,KAAK,SAAS,oDAAoD;AAAA,EAC3E;AAAA,EAEA,WAAmB;AAClB,WAAO,KAAK;AAAA,EACb;AAAA,EAEQ,oBAA6B;AAAA,EAC7B,eAAuB;AAC9B,QAAI,CAAC,KAAK,mBAAmB;AAC5B,WAAK,oBAAoB;AACzB,WAAK,SAAS,KAAK,QAAQ,cAAc,IAAI,KAAK,QAAQ,cAAc,IAAI,IAAI,CAAC;AAAA,IAClF;AACA,WAAO,KAAK;AAAA,EACb;AAAA;AAAA,EAGQ,cAAc,IAAY;AACjC,QAAI,KAAK,QAAQ,UAAU,IAAI,EAAE,EAAG,QAAO,KAAK,QAAQ;AACxD,QAAI,KAAK,QAAQ,QAAQ,IAAI,EAAE,EAAG,QAAO,KAAK,QAAQ;AACtD,WAAO;AAAA,EACR;AAAA,EAEA,IAAI,IAA2B;AAC9B,SAAK,gBAAgB;AACrB,YAAQ,KAAK,QAAQ,UAAU,IAAI,EAAE,KAAK,KAAK,QAAQ,QAAQ,IAAI,EAAE,IAAI;AAAA,EAC1E;AAAA,EAEA,IAAI,IAAY,QAAiB;AAChC,SAAK,gBAAgB;AACrB,WAAO,OAAO,OAAO,IAAI,kDAAkD;AAC3E,UAAM,QAAQ,KAAK,aAAa;AAEhC,QAAI,KAAK,QAAQ,WAAW,IAAI,EAAE,GAAG;AACpC,WAAK,QAAQ,WAAW,OAAO,EAAE;AAAA,IAClC;AAEA,UAAM,MAAM,KAAK,QAAQ,YAAY,IAAI,OAAO,QAAQ,IACrD,KAAK,QAAQ,UACb,KAAK,QAAQ;AAChB,QAAI,IAAI,IAAI;AAAA,MACX,OAAO,UAAU,MAAM;AAAA,MACvB,kBAAkB;AAAA,IACnB,CAAC;AAAA,EACF;AAAA,EAEA,OAAO,IAAkB;AACxB,SAAK,gBAAgB;AAErB,UAAM,MAAM,KAAK,cAAc,EAAE;AACjC,QAAI,CAAC,IAAK;AACV,UAAM,QAAQ,KAAK,aAAa;AAChC,QAAI,OAAO,EAAE;AACb,SAAK,QAAQ,WAAW,IAAI,IAAI,KAAK;AACrC,SAAK,QAAQ,gBAAgB;AAAA,EAC9B;AAAA;AAAA,EAIA,CAAC,UAAyC;AACzC,SAAK,gBAAgB;AACrB,eAAW,OAAO,CAAC,KAAK,QAAQ,WAAW,KAAK,QAAQ,OAAO,GAAG;AACjE,iBAAW,CAAC,IAAI,MAAM,KAAK,IAAI,QAAQ,GAAG;AACzC,aAAK,gBAAgB;AACrB,cAAM,CAAC,IAAI,OAAO,KAAK;AAAA,MACxB;AAAA,IACD;AAAA,EACD;AAAA,EAEA,CAAC,OAAiC;AACjC,SAAK,gBAAgB;AACrB,eAAW,OAAO,CAAC,KAAK,QAAQ,WAAW,KAAK,QAAQ,OAAO,GAAG;AACjE,iBAAW,OAAO,IAAI,KAAK,GAAG;AAC7B,aAAK,gBAAgB;AACrB,cAAM;AAAA,MACP;AAAA,IACD;AAAA,EACD;AAAA,EAEA,CAAC,SAA8B;AAC9B,SAAK,gBAAgB;AACrB,eAAW,OAAO,CAAC,KAAK,QAAQ,WAAW,KAAK,QAAQ,OAAO,GAAG;AACjE,iBAAW,UAAU,IAAI,OAAO,GAAG;AAClC,aAAK,gBAAgB;AACrB,cAAM,OAAO;AAAA,MACd;AAAA,IACD;AAAA,EACD;AAAA,EAEA,YAA8B;AAC7B,SAAK,gBAAgB;AACrB,WAAO,KAAK,QAAQ,OAAO,IAAI;AAAA,EAChC;AAAA,EAEA,UAAU,QAAgC;AACzC,SAAK,gBAAgB;AACrB,SAAK,QAAQ,OAAO,IAAI,MAAM;AAAA,EAC/B;AAAA,EAEA,gBAAgB,YAAuE;AACtF,SAAK,gBAAgB;AACrB,UAAM,QAAQ,KAAK,QAAQ,cAAc,IAAI;AAC7C,QAAI,eAAe,MAAO,QAAO;AACjC,QAAI,aAAa,OAAO;AAEvB,mBAAa;AAAA,IACd;AACA,UAAM,OAA6B,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,EAAE;AAC3D,UAAM,UAAU,aAAa,KAAK,QAAQ,8BAA8B,IAAI;AAE5E,eAAW,OAAO,CAAC,KAAK,QAAQ,WAAW,KAAK,QAAQ,OAAO,GAAG;AACjE,iBAAW,OAAO,IAAI,OAAO,GAAG;AAC/B,YAAI,WAAW,IAAI,mBAAmB,YAAY;AAEjD,eAAK,KAAK,IAAI,MAAM,EAAE,IAAI,IAAI;AAAA,QAC/B;AAAA,MACD;AAAA,IACD;AACA,QAAI,CAAC,SAAS;AACb,iBAAW,CAAC,IAAIA,MAAK,KAAK,KAAK,QAAQ,WAAW,QAAQ,GAAG;AAC5D,YAAIA,SAAQ,YAAY;AAEvB,eAAK,QAAQ,KAAK,EAAE;AAAA,QACrB;AAAA,MACD;AAAA,IACD;AACA,WAAO,EAAE,MAAM,QAAQ;AAAA,EACxB;AACD;",
6
6
  "names": ["clock"]
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/RoomSession.ts"],
4
- "sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { TLSocketServerSentDataEvent } from './protocol'\nimport { TLRoomSocket } from './TLSyncRoom'\n\n/**\n * Enumeration of possible states for a room session during its lifecycle.\n *\n * Room sessions progress through these states as clients connect, authenticate,\n * and disconnect from collaborative rooms.\n *\n * @internal\n */\nexport const RoomSessionState = {\n\t/** Session is waiting for the initial connect message from the client */\n\tAwaitingConnectMessage: 'awaiting-connect-message',\n\t/** Session is disconnected but waiting for final cleanup before removal */\n\tAwaitingRemoval: 'awaiting-removal',\n\t/** Session is fully connected and actively synchronizing */\n\tConnected: 'connected',\n} as const\n\n/**\n * Type representing the possible states a room session can be in.\n *\n * @example\n * ```ts\n * const sessionState: RoomSessionState = RoomSessionState.Connected\n * if (sessionState === RoomSessionState.AwaitingConnectMessage) {\n * console.log('Session waiting for connect message')\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]\n\n/**\n * Maximum time in milliseconds to wait for a connect message after socket connection.\n *\n * If a client connects but doesn't send a connect message within this time,\n * the session will be terminated.\n *\n * @public\n */\nexport const SESSION_START_WAIT_TIME = 10000\n\n/**\n * Time in milliseconds to wait before completely removing a disconnected session.\n *\n * This grace period allows for quick reconnections without losing session state,\n * which is especially helpful for brief network interruptions.\n *\n * @public\n */\nexport const SESSION_REMOVAL_WAIT_TIME = 5000\n\n/**\n * Maximum time in milliseconds a connected session can remain idle before cleanup.\n *\n * Sessions that don't receive any messages or interactions for this duration\n * may be considered for cleanup to free server resources.\n *\n * @public\n */\nexport const SESSION_IDLE_TIMEOUT = 20000\n\n/**\n * Base properties shared by all room session states.\n *\n * @internal\n */\nexport interface RoomSessionBase<R extends UnknownRecord, Meta> {\n\t/** Unique identifier for this session */\n\tsessionId: string\n\t/** Presence identifier for live cursor/selection tracking, if available */\n\tpresenceId: string | null\n\t/** WebSocket connection wrapper for this session */\n\tsocket: TLRoomSocket<R>\n\t/** Custom metadata associated with this session */\n\tmeta: Meta\n\t/** Whether this session has read-only permissions */\n\tisReadonly: boolean\n\t/** Whether this session requires legacy protocol rejection handling */\n\trequiresLegacyRejection: boolean\n\t/** Whether this session supports string append operations */\n\tsupportsStringAppend: boolean\n}\n\n/**\n * Represents a client session within a collaborative room, tracking the connection\n * state, permissions, and synchronization details for a single user.\n *\n * Each session corresponds to one WebSocket connection and progresses through\n * different states during its lifecycle. The session type is a discriminated union\n * based on the current state, ensuring type safety when accessing state-specific properties.\n *\n * @example\n * ```ts\n * // Check session state and access appropriate properties\n * function handleSession(session: RoomSession<MyRecord, UserMeta>) {\n * switch (session.state) {\n * case RoomSessionState.AwaitingConnectMessage:\n * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)\n * break\n * case RoomSessionState.Connected:\n * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)\n * break\n * case RoomSessionState.AwaitingRemoval:\n * console.log(`Session will be removed at ${session.cancellationTime}`)\n * break\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSession<R extends UnknownRecord, Meta> =\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingConnectMessage\n\t\t\t/** Timestamp when the session was created */\n\t\t\tsessionStartTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingRemoval\n\t\t\t/** Timestamp when the session was marked for removal */\n\t\t\tcancellationTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.Connected\n\t\t\t/** Serialized schema information for this connected session */\n\t\t\tserializedSchema: SerializedSchema\n\t\t\t/** Whether this session requires down migrations */\n\t\t\trequiresDownMigrations: boolean\n\t\t\t/** Timestamp of the last interaction or message from this session */\n\t\t\tlastInteractionTime: number\n\t\t\t/** Timer for debouncing operations, if active */\n\t\t\tdebounceTimer: ReturnType<typeof setTimeout> | null\n\t\t\t/** Queue of data messages waiting to be sent to this session */\n\t\t\toutstandingDataMessages: TLSocketServerSentDataEvent<R>[]\n\t })\n"],
4
+ "sourcesContent": ["import { SerializedSchema, UnknownRecord } from '@tldraw/store'\nimport { TLObjectStoreAccess, TLSocketServerSentDataEvent } from './protocol'\nimport { TLRoomSocket } from './TLSyncRoom'\n\n/**\n * Enumeration of possible states for a room session during its lifecycle.\n *\n * Room sessions progress through these states as clients connect, authenticate,\n * and disconnect from collaborative rooms.\n *\n * @internal\n */\nexport const RoomSessionState = {\n\t/** Session is waiting for the initial connect message from the client */\n\tAwaitingConnectMessage: 'awaiting-connect-message',\n\t/** Session is disconnected but waiting for final cleanup before removal */\n\tAwaitingRemoval: 'awaiting-removal',\n\t/** Session is fully connected and actively synchronizing */\n\tConnected: 'connected',\n} as const\n\n/**\n * Type representing the possible states a room session can be in.\n *\n * @example\n * ```ts\n * const sessionState: RoomSessionState = RoomSessionState.Connected\n * if (sessionState === RoomSessionState.AwaitingConnectMessage) {\n * console.log('Session waiting for connect message')\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]\n\n/**\n * Maximum time in milliseconds to wait for a connect message after socket connection.\n *\n * If a client connects but doesn't send a connect message within this time,\n * the session will be terminated.\n *\n * @public\n */\nexport const SESSION_START_WAIT_TIME = 10000\n\n/**\n * Time in milliseconds to wait before completely removing a disconnected session.\n *\n * This grace period allows for quick reconnections without losing session state,\n * which is especially helpful for brief network interruptions.\n *\n * @public\n */\nexport const SESSION_REMOVAL_WAIT_TIME = 5000\n\n/**\n * Maximum time in milliseconds a connected session can remain idle before cleanup.\n *\n * Sessions that don't receive any messages or interactions for this duration\n * may be considered for cleanup to free server resources.\n *\n * @public\n */\nexport const SESSION_IDLE_TIMEOUT = 20000\n\n/**\n * Base properties shared by all room session states.\n *\n * @internal\n */\nexport interface RoomSessionBase<R extends UnknownRecord, Meta> {\n\t/** Unique identifier for this session */\n\tsessionId: string\n\t/** Presence identifier for live cursor/selection tracking, if available */\n\tpresenceId: string | null\n\t/** WebSocket connection wrapper for this session */\n\tsocket: TLRoomSocket<R>\n\t/** Custom metadata associated with this session */\n\tmeta: Meta\n\t/** Whether this session has read-only permissions for document-lane records */\n\tisReadonly: boolean\n\t/**\n\t * Write access for object-store lane record types (see `objectTypes` on the room).\n\t * Deliberately independent of `isReadonly` so \"can comment but not edit\" is expressible.\n\t */\n\tobjectAccess: TLObjectStoreAccess\n\t/** Whether this session requires legacy protocol rejection handling */\n\trequiresLegacyRejection: boolean\n\t/** Whether this session supports string append operations */\n\tsupportsStringAppend: boolean\n}\n\n/**\n * Represents a client session within a collaborative room, tracking the connection\n * state, permissions, and synchronization details for a single user.\n *\n * Each session corresponds to one WebSocket connection and progresses through\n * different states during its lifecycle. The session type is a discriminated union\n * based on the current state, ensuring type safety when accessing state-specific properties.\n *\n * @example\n * ```ts\n * // Check session state and access appropriate properties\n * function handleSession(session: RoomSession<MyRecord, UserMeta>) {\n * switch (session.state) {\n * case RoomSessionState.AwaitingConnectMessage:\n * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)\n * break\n * case RoomSessionState.Connected:\n * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)\n * break\n * case RoomSessionState.AwaitingRemoval:\n * console.log(`Session will be removed at ${session.cancellationTime}`)\n * break\n * }\n * }\n * ```\n *\n * @internal\n */\nexport type RoomSession<R extends UnknownRecord, Meta> =\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingConnectMessage\n\t\t\t/** Timestamp when the session was created */\n\t\t\tsessionStartTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.AwaitingRemoval\n\t\t\t/** Timestamp when the session was marked for removal */\n\t\t\tcancellationTime: number\n\t })\n\t| (RoomSessionBase<R, Meta> & {\n\t\t\t/** Current state of the session */\n\t\t\tstate: typeof RoomSessionState.Connected\n\t\t\t/** Serialized schema information for this connected session */\n\t\t\tserializedSchema: SerializedSchema\n\t\t\t/** Whether this session requires down migrations */\n\t\t\trequiresDownMigrations: boolean\n\t\t\t/** Timestamp of the last interaction or message from this session */\n\t\t\tlastInteractionTime: number\n\t\t\t/** Timer for debouncing operations, if active */\n\t\t\tdebounceTimer: ReturnType<typeof setTimeout> | null\n\t\t\t/** Queue of data messages waiting to be sent to this session */\n\t\t\toutstandingDataMessages: TLSocketServerSentDataEvent<R>[]\n\t })\n"],
5
5
  "mappings": "AAYO,MAAM,mBAAmB;AAAA;AAAA,EAE/B,wBAAwB;AAAA;AAAA,EAExB,iBAAiB;AAAA;AAAA,EAEjB,WAAW;AACZ;AAyBO,MAAM,0BAA0B;AAUhC,MAAM,4BAA4B;AAUlC,MAAM,uBAAuB;",
6
6
  "names": []
7
7
  }