@tldraw/sync-core 4.3.0-canary.c7096a59bf3b → 4.3.0-canary.d039f3a1ab8f

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 +239 -57
  2. package/dist-cjs/index.js +7 -3
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/InMemorySyncStorage.js +289 -0
  5. package/dist-cjs/lib/InMemorySyncStorage.js.map +7 -0
  6. package/dist-cjs/lib/RoomSession.js.map +1 -1
  7. package/dist-cjs/lib/TLSocketRoom.js +117 -69
  8. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  9. package/dist-cjs/lib/TLSyncClient.js +7 -0
  10. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  11. package/dist-cjs/lib/TLSyncRoom.js +357 -688
  12. package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
  13. package/dist-cjs/lib/TLSyncStorage.js +76 -0
  14. package/dist-cjs/lib/TLSyncStorage.js.map +7 -0
  15. package/dist-cjs/lib/recordDiff.js +52 -0
  16. package/dist-cjs/lib/recordDiff.js.map +7 -0
  17. package/dist-esm/index.d.mts +239 -57
  18. package/dist-esm/index.mjs +12 -5
  19. package/dist-esm/index.mjs.map +2 -2
  20. package/dist-esm/lib/InMemorySyncStorage.mjs +274 -0
  21. package/dist-esm/lib/InMemorySyncStorage.mjs.map +7 -0
  22. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  23. package/dist-esm/lib/TLSocketRoom.mjs +121 -70
  24. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  25. package/dist-esm/lib/TLSyncClient.mjs +7 -0
  26. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  27. package/dist-esm/lib/TLSyncRoom.mjs +370 -702
  28. package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
  29. package/dist-esm/lib/TLSyncStorage.mjs +56 -0
  30. package/dist-esm/lib/TLSyncStorage.mjs.map +7 -0
  31. package/dist-esm/lib/recordDiff.mjs +32 -0
  32. package/dist-esm/lib/recordDiff.mjs.map +7 -0
  33. package/package.json +6 -6
  34. package/src/index.ts +21 -3
  35. package/src/lib/InMemorySyncStorage.ts +357 -0
  36. package/src/lib/RoomSession.test.ts +1 -0
  37. package/src/lib/RoomSession.ts +2 -0
  38. package/src/lib/TLSocketRoom.ts +228 -114
  39. package/src/lib/TLSyncClient.ts +12 -0
  40. package/src/lib/TLSyncRoom.ts +473 -913
  41. package/src/lib/TLSyncStorage.ts +216 -0
  42. package/src/lib/recordDiff.ts +73 -0
  43. package/src/test/InMemorySyncStorage.test.ts +1674 -0
  44. package/src/test/TLSocketRoom.test.ts +255 -49
  45. package/src/test/TLSyncRoom.test.ts +1021 -533
  46. package/src/test/TestServer.ts +12 -1
  47. package/src/test/customMessages.test.ts +1 -1
  48. package/src/test/presenceMode.test.ts +6 -6
  49. package/src/test/upgradeDowngrade.test.ts +282 -8
  50. package/src/test/validation.test.ts +10 -10
  51. package/src/test/pruneTombstones.test.ts +0 -178
@@ -0,0 +1,289 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var InMemorySyncStorage_exports = {};
20
+ __export(InMemorySyncStorage_exports, {
21
+ DEFAULT_INITIAL_SNAPSHOT: () => DEFAULT_INITIAL_SNAPSHOT,
22
+ InMemorySyncStorage: () => InMemorySyncStorage,
23
+ MAX_TOMBSTONES: () => MAX_TOMBSTONES,
24
+ TOMBSTONE_PRUNE_BUFFER_SIZE: () => TOMBSTONE_PRUNE_BUFFER_SIZE
25
+ });
26
+ module.exports = __toCommonJS(InMemorySyncStorage_exports);
27
+ var import_state = require("@tldraw/state");
28
+ var import_store = require("@tldraw/store");
29
+ var import_tlschema = require("@tldraw/tlschema");
30
+ var import_utils = require("@tldraw/utils");
31
+ const TOMBSTONE_PRUNE_BUFFER_SIZE = 1e3;
32
+ const MAX_TOMBSTONES = 5e3;
33
+ const DEFAULT_INITIAL_SNAPSHOT = {
34
+ documentClock: 0,
35
+ tombstoneHistoryStartsAtClock: 0,
36
+ schema: (0, import_tlschema.createTLSchema)().serialize(),
37
+ documents: [
38
+ {
39
+ state: import_tlschema.DocumentRecordType.create({ id: import_tlschema.TLDOCUMENT_ID }),
40
+ lastChangedClock: 0
41
+ },
42
+ {
43
+ state: import_tlschema.PageRecordType.create({
44
+ id: "page:page",
45
+ name: "Page 1",
46
+ index: "a1"
47
+ }),
48
+ lastChangedClock: 0
49
+ }
50
+ ]
51
+ };
52
+ class InMemorySyncStorage {
53
+ /** @internal */
54
+ documents;
55
+ /** @internal */
56
+ tombstones;
57
+ /** @internal */
58
+ schema;
59
+ /** @internal */
60
+ documentClock;
61
+ /** @internal */
62
+ tombstoneHistoryStartsAtClock;
63
+ listeners = /* @__PURE__ */ new Set();
64
+ onChange(callback) {
65
+ let didDelete = false;
66
+ queueMicrotask(() => {
67
+ if (didDelete) return;
68
+ this.listeners.add(callback);
69
+ });
70
+ return () => {
71
+ if (didDelete) return;
72
+ didDelete = true;
73
+ this.listeners.delete(callback);
74
+ };
75
+ }
76
+ constructor({
77
+ snapshot = DEFAULT_INITIAL_SNAPSHOT,
78
+ onChange
79
+ } = {}) {
80
+ const maxClockValue = Math.max(
81
+ 0,
82
+ ...Object.values(snapshot.tombstones ?? {}),
83
+ ...Object.values(snapshot.documents.map((d) => d.lastChangedClock))
84
+ );
85
+ this.documents = new import_store.AtomMap(
86
+ "room documents",
87
+ snapshot.documents.map((d) => [
88
+ d.state.id,
89
+ { state: (0, import_store.devFreeze)(d.state), lastChangedClock: d.lastChangedClock }
90
+ ])
91
+ );
92
+ const documentClock = Math.max(maxClockValue, snapshot.documentClock ?? snapshot.clock ?? 0);
93
+ this.documentClock = (0, import_state.atom)("document clock", documentClock);
94
+ const tombstoneHistoryStartsAtClock = Math.min(
95
+ snapshot.tombstoneHistoryStartsAtClock ?? documentClock,
96
+ documentClock
97
+ );
98
+ this.tombstoneHistoryStartsAtClock = (0, import_state.atom)(
99
+ "tombstone history starts at clock",
100
+ tombstoneHistoryStartsAtClock
101
+ );
102
+ this.schema = (0, import_state.atom)("schema", snapshot.schema ?? (0, import_tlschema.createTLSchema)().serializeEarliestVersion());
103
+ this.tombstones = new import_store.AtomMap(
104
+ "room tombstones",
105
+ // If the tombstone history starts now (or we didn't have the
106
+ // tombstoneHistoryStartsAtClock) then there are no tombstones
107
+ tombstoneHistoryStartsAtClock === documentClock ? [] : (0, import_utils.objectMapEntries)(snapshot.tombstones ?? {})
108
+ );
109
+ if (onChange) {
110
+ this.onChange(onChange);
111
+ }
112
+ }
113
+ transaction(callback, opts) {
114
+ const clockBefore = this.documentClock.get();
115
+ const trackChanges = opts?.emitChanges === "always";
116
+ const txn = new InMemorySyncStorageTransaction(this);
117
+ let result;
118
+ let changes;
119
+ try {
120
+ result = (0, import_state.transaction)(() => {
121
+ return callback(txn);
122
+ });
123
+ if (trackChanges) {
124
+ changes = txn.getChangesSince(clockBefore)?.diff;
125
+ }
126
+ } catch (error) {
127
+ console.error("Error in transaction", error);
128
+ throw error;
129
+ } finally {
130
+ txn.close();
131
+ }
132
+ if (typeof result === "object" && result && "then" in result && typeof result.then === "function") {
133
+ const err = new Error("Transaction must return a value, not a promise");
134
+ console.error(err);
135
+ throw err;
136
+ }
137
+ const clockAfter = this.documentClock.get();
138
+ const didChange = clockAfter > clockBefore;
139
+ if (didChange) {
140
+ queueMicrotask(() => {
141
+ const props = {
142
+ id: opts?.id,
143
+ documentClock: clockAfter
144
+ };
145
+ for (const listener of this.listeners) {
146
+ try {
147
+ listener(props);
148
+ } catch (error) {
149
+ console.error("Error in onChange callback", error);
150
+ }
151
+ }
152
+ });
153
+ }
154
+ return { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes };
155
+ }
156
+ getClock() {
157
+ return this.documentClock.get();
158
+ }
159
+ /** @internal */
160
+ pruneTombstones = (0, import_utils.throttle)(
161
+ () => {
162
+ if (this.tombstones.size > MAX_TOMBSTONES) {
163
+ const tombstones = Array.from(this.tombstones);
164
+ tombstones.sort((a, b) => a[1] - b[1]);
165
+ let cutoff = TOMBSTONE_PRUNE_BUFFER_SIZE + this.tombstones.size - MAX_TOMBSTONES;
166
+ while (cutoff < tombstones.length && tombstones[cutoff - 1][1] === tombstones[cutoff][1]) {
167
+ cutoff++;
168
+ }
169
+ const oldestRemaining = tombstones[cutoff];
170
+ this.tombstoneHistoryStartsAtClock.set(oldestRemaining?.[1] ?? this.documentClock.get());
171
+ const toDelete = tombstones.slice(0, cutoff);
172
+ this.tombstones.deleteMany(toDelete.map(([id]) => id));
173
+ }
174
+ },
175
+ 1e3,
176
+ // prevent this from running synchronously to avoid blocking requests
177
+ { leading: false }
178
+ );
179
+ getSnapshot() {
180
+ return {
181
+ tombstoneHistoryStartsAtClock: this.tombstoneHistoryStartsAtClock.get(),
182
+ documentClock: this.documentClock.get(),
183
+ documents: Array.from(this.documents.values()),
184
+ tombstones: Object.fromEntries(this.tombstones.entries()),
185
+ schema: this.schema.get()
186
+ };
187
+ }
188
+ }
189
+ class InMemorySyncStorageTransaction {
190
+ constructor(storage) {
191
+ this.storage = storage;
192
+ this._clock = this.storage.documentClock.get();
193
+ }
194
+ _clock;
195
+ _closed = false;
196
+ /** @internal */
197
+ close() {
198
+ this._closed = true;
199
+ }
200
+ assertNotClosed() {
201
+ (0, import_utils.assert)(!this._closed, "Transaction has ended, iterator cannot be consumed");
202
+ }
203
+ getClock() {
204
+ return this._clock;
205
+ }
206
+ didIncrementClock = false;
207
+ getNextClock() {
208
+ if (!this.didIncrementClock) {
209
+ this.didIncrementClock = true;
210
+ this._clock = this.storage.documentClock.set(this.storage.documentClock.get() + 1);
211
+ }
212
+ return this._clock;
213
+ }
214
+ get(id) {
215
+ this.assertNotClosed();
216
+ return this.storage.documents.get(id)?.state;
217
+ }
218
+ set(id, record) {
219
+ this.assertNotClosed();
220
+ (0, import_utils.assert)(id === record.id, `Record id mismatch: key does not match record.id`);
221
+ const clock = this.getNextClock();
222
+ if (this.storage.tombstones.has(id)) {
223
+ this.storage.tombstones.delete(id);
224
+ }
225
+ this.storage.documents.set(id, {
226
+ state: (0, import_store.devFreeze)(record),
227
+ lastChangedClock: clock
228
+ });
229
+ }
230
+ delete(id) {
231
+ this.assertNotClosed();
232
+ if (!this.storage.documents.has(id)) return;
233
+ const clock = this.getNextClock();
234
+ this.storage.documents.delete(id);
235
+ this.storage.tombstones.set(id, clock);
236
+ this.storage.pruneTombstones();
237
+ }
238
+ *entries() {
239
+ this.assertNotClosed();
240
+ for (const [id, record] of this.storage.documents.entries()) {
241
+ this.assertNotClosed();
242
+ yield [id, record.state];
243
+ }
244
+ }
245
+ *keys() {
246
+ this.assertNotClosed();
247
+ for (const key of this.storage.documents.keys()) {
248
+ this.assertNotClosed();
249
+ yield key;
250
+ }
251
+ }
252
+ *values() {
253
+ this.assertNotClosed();
254
+ for (const record of this.storage.documents.values()) {
255
+ this.assertNotClosed();
256
+ yield record.state;
257
+ }
258
+ }
259
+ getSchema() {
260
+ this.assertNotClosed();
261
+ return this.storage.schema.get();
262
+ }
263
+ setSchema(schema) {
264
+ this.assertNotClosed();
265
+ this.storage.schema.set(schema);
266
+ }
267
+ getChangesSince(sinceClock) {
268
+ this.assertNotClosed();
269
+ const clock = this.storage.documentClock.get();
270
+ if (sinceClock === clock) return void 0;
271
+ if (sinceClock > clock) {
272
+ sinceClock = -1;
273
+ }
274
+ const diff = { puts: {}, deletes: [] };
275
+ const wipeAll = sinceClock < this.storage.tombstoneHistoryStartsAtClock.get();
276
+ for (const doc of this.storage.documents.values()) {
277
+ if (wipeAll || doc.lastChangedClock > sinceClock) {
278
+ diff.puts[doc.state.id] = doc.state;
279
+ }
280
+ }
281
+ for (const [id, clock2] of this.storage.tombstones.entries()) {
282
+ if (clock2 > sinceClock) {
283
+ diff.deletes.push(id);
284
+ }
285
+ }
286
+ return { diff, wipeAll };
287
+ }
288
+ }
289
+ //# sourceMappingURL=InMemorySyncStorage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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 { 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 * 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 listeners = new Set<(arg: TLSyncStorageOnChangeCallbackProps) => unknown>()\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void {\n\t\tlet didDelete = false\n\t\t// we put the callback registration in a microtask because the callback is invoked\n\t\t// in a microtask, and so this makes sure the callback is invoked after all the updates\n\t\t// that happened in the current callstack before this onChange registration have been processed.\n\t\tqueueMicrotask(() => {\n\t\t\tif (didDelete) return\n\t\t\tthis.listeners.add(callback)\n\t\t})\n\t\treturn () => {\n\t\t\tif (didDelete) return\n\t\t\tdidDelete = true\n\t\t\tthis.listeners.delete(callback)\n\t\t}\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\t// todo: batch these updates\n\t\t\tqueueMicrotask(() => {\n\t\t\t\tconst props: TLSyncStorageOnChangeCallbackProps = {\n\t\t\t\t\tid: opts?.id,\n\t\t\t\t\tdocumentClock: clockAfter,\n\t\t\t\t}\n\t\t\t\tfor (const listener of this.listeners) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tlistener(props)\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error('Error in onChange callback', error)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\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\tconst tombstones = Array.from(this.tombstones)\n\t\t\t\t// sort entries in ascending order by clock (oldest first)\n\t\t\t\ttombstones.sort((a, b) => a[1] - b[1])\n\t\t\t\t// determine how many to delete, avoiding partial history for a clock value\n\t\t\t\tlet cutoff = TOMBSTONE_PRUNE_BUFFER_SIZE + this.tombstones.size - MAX_TOMBSTONES\n\t\t\t\twhile (cutoff < tombstones.length && tombstones[cutoff - 1][1] === tombstones[cutoff][1]) {\n\t\t\t\t\tcutoff++\n\t\t\t\t}\n\n\t\t\t\t// Set history start to the oldest remaining tombstone's clock\n\t\t\t\t// (or documentClock if we're deleting everything)\n\t\t\t\tconst oldestRemaining = tombstones[cutoff]\n\t\t\t\tthis.tombstoneHistoryStartsAtClock.set(oldestRemaining?.[1] ?? this.documentClock.get())\n\n\t\t\t\t// Delete the oldest tombstones (first cutoff entries)\n\t\t\t\tconst toDelete = tombstones.slice(0, cutoff)\n\t\t\t\tthis.tombstones.deleteMany(toDelete.map(([id]) => id))\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<R extends UnknownRecord>\n\timplements TLSyncStorageTransaction<R>\n{\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\tfor (const [id, clock] of this.storage.tombstones.entries()) {\n\t\t\tif (clock > sinceClock) {\n\t\t\t\t// For tombstones, we don't have the removed record, use placeholder\n\t\t\t\tdiff.deletes.push(id)\n\t\t\t}\n\t\t}\n\t\treturn { diff, wipeAll }\n\t}\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["clock"]
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 { TLRoomSocket } from './TLSyncRoom'\nimport { TLSocketServerSentDataEvent } from './protocol'\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/** 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 { TLRoomSocket } from './TLSyncRoom'\nimport { TLSocketServerSentDataEvent } from './protocol'\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"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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
  }
@@ -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
- const initialSnapshot = opts.initialSnapshot && "store" in opts.initialSnapshot ? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot) : opts.initialSnapshot;
49
- this.syncCallbacks = {
50
- onDataChange: opts.onDataChange,
51
- onPresenceChange: opts.onPresenceChange
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
- ...this.syncCallbacks,
67
+ onPresenceChange: opts.onPresenceChange,
55
68
  schema: opts.schema ?? (0, import_tlschema.createTLSchema)(),
56
- snapshot: initialSnapshot,
57
- log: opts.log
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
- syncCallbacks;
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.room.documentClock;
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 (0, import_utils.structuredClone)(this.room.documents.get(id)?.state);
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
- return this.room.getSnapshot();
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 document of this.room.documents.values()) {
345
- if (document.state.typeName === this.room.presenceType?.typeName) {
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
- if ("store" in snapshot) {
381
- snapshot = convertStoreSnapshotToRoomSnapshot(snapshot);
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
- return this.room.updateStore(updater);
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
- function convertStoreSnapshotToRoomSnapshot(snapshot) {
555
- return {
556
- clock: 0,
557
- documentClock: 0,
558
- documents: (0, import_utils.objectMapValues)(snapshot.store).map((state) => ({
559
- state,
560
- lastChangedClock: 0
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