@tldraw/sync-core 4.2.2 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +58 -483
- package/dist-cjs/index.js +3 -13
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/TLSocketRoom.js +69 -117
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +0 -7
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +688 -357
- package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
- package/dist-cjs/lib/chunk.js +2 -2
- package/dist-cjs/lib/chunk.js.map +1 -1
- package/dist-esm/index.d.mts +58 -483
- package/dist-esm/index.mjs +5 -20
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/TLSocketRoom.mjs +70 -121
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +0 -7
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +702 -370
- package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
- package/dist-esm/lib/chunk.mjs +2 -2
- package/dist-esm/lib/chunk.mjs.map +1 -1
- package/package.json +11 -12
- package/src/index.ts +3 -32
- package/src/lib/ClientWebSocketAdapter.test.ts +0 -3
- package/src/lib/RoomSession.test.ts +0 -1
- package/src/lib/RoomSession.ts +0 -2
- package/src/lib/TLSocketRoom.ts +114 -228
- package/src/lib/TLSyncClient.ts +0 -12
- package/src/lib/TLSyncRoom.ts +913 -473
- package/src/lib/chunk.ts +2 -2
- package/src/test/FuzzEditor.ts +5 -4
- package/src/test/TLSocketRoom.test.ts +49 -255
- package/src/test/TLSyncRoom.test.ts +534 -1024
- package/src/test/TestServer.ts +1 -12
- package/src/test/customMessages.test.ts +1 -1
- package/src/test/presenceMode.test.ts +6 -6
- package/src/test/pruneTombstones.test.ts +178 -0
- package/src/test/syncFuzz.test.ts +4 -2
- package/src/test/upgradeDowngrade.test.ts +8 -290
- package/src/test/validation.test.ts +10 -15
- package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js +0 -55
- package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js.map +0 -7
- package/dist-cjs/lib/InMemorySyncStorage.js +0 -287
- package/dist-cjs/lib/InMemorySyncStorage.js.map +0 -7
- package/dist-cjs/lib/MicrotaskNotifier.js +0 -50
- package/dist-cjs/lib/MicrotaskNotifier.js.map +0 -7
- package/dist-cjs/lib/NodeSqliteWrapper.js +0 -48
- package/dist-cjs/lib/NodeSqliteWrapper.js.map +0 -7
- package/dist-cjs/lib/SQLiteSyncStorage.js +0 -428
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +0 -7
- package/dist-cjs/lib/TLSyncStorage.js +0 -76
- package/dist-cjs/lib/TLSyncStorage.js.map +0 -7
- package/dist-cjs/lib/recordDiff.js +0 -52
- package/dist-cjs/lib/recordDiff.js.map +0 -7
- package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs +0 -35
- package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs.map +0 -7
- package/dist-esm/lib/InMemorySyncStorage.mjs +0 -272
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +0 -7
- package/dist-esm/lib/MicrotaskNotifier.mjs +0 -30
- package/dist-esm/lib/MicrotaskNotifier.mjs.map +0 -7
- package/dist-esm/lib/NodeSqliteWrapper.mjs +0 -28
- package/dist-esm/lib/NodeSqliteWrapper.mjs.map +0 -7
- package/dist-esm/lib/SQLiteSyncStorage.mjs +0 -414
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +0 -7
- package/dist-esm/lib/TLSyncStorage.mjs +0 -56
- package/dist-esm/lib/TLSyncStorage.mjs.map +0 -7
- package/dist-esm/lib/recordDiff.mjs +0 -32
- package/dist-esm/lib/recordDiff.mjs.map +0 -7
- package/src/lib/DurableObjectSqliteSyncWrapper.ts +0 -95
- package/src/lib/InMemorySyncStorage.ts +0 -387
- package/src/lib/MicrotaskNotifier.test.ts +0 -429
- package/src/lib/MicrotaskNotifier.ts +0 -38
- package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
- package/src/lib/NodeSqliteSyncWrapper.test.ts +0 -272
- package/src/lib/NodeSqliteWrapper.ts +0 -99
- package/src/lib/SQLiteSyncStorage.ts +0 -627
- package/src/lib/TLSyncStorage.ts +0 -216
- package/src/lib/computeTombstonePruning.test.ts +0 -352
- package/src/lib/recordDiff.ts +0 -73
- package/src/test/InMemorySyncStorage.test.ts +0 -1684
- package/src/test/SQLiteSyncStorage.test.ts +0 -1378
|
@@ -1,414 +0,0 @@
|
|
|
1
|
-
import { transaction } from "@tldraw/state";
|
|
2
|
-
import { assert, objectMapEntries, throttle } from "@tldraw/utils";
|
|
3
|
-
import {
|
|
4
|
-
computeTombstonePruning,
|
|
5
|
-
DEFAULT_INITIAL_SNAPSHOT,
|
|
6
|
-
MAX_TOMBSTONES
|
|
7
|
-
} from "./InMemorySyncStorage.mjs";
|
|
8
|
-
import { MicrotaskNotifier } from "./MicrotaskNotifier.mjs";
|
|
9
|
-
import {
|
|
10
|
-
convertStoreSnapshotToRoomSnapshot
|
|
11
|
-
} from "./TLSyncStorage.mjs";
|
|
12
|
-
function migrateSqliteSyncStorage(storage, {
|
|
13
|
-
documentsTable = "documents",
|
|
14
|
-
tombstonesTable = "tombstones",
|
|
15
|
-
metadataTable = "metadata"
|
|
16
|
-
} = {}) {
|
|
17
|
-
let migrationVersion = 0;
|
|
18
|
-
try {
|
|
19
|
-
const row = storage.prepare(`SELECT migrationVersion FROM ${metadataTable} LIMIT 1`).all()[0];
|
|
20
|
-
migrationVersion = row?.migrationVersion ?? 0;
|
|
21
|
-
} catch (_e) {
|
|
22
|
-
}
|
|
23
|
-
if (migrationVersion === 0) {
|
|
24
|
-
migrationVersion++;
|
|
25
|
-
storage.exec(`
|
|
26
|
-
CREATE TABLE ${documentsTable} (
|
|
27
|
-
id TEXT PRIMARY KEY,
|
|
28
|
-
state BLOB NOT NULL,
|
|
29
|
-
lastChangedClock INTEGER NOT NULL
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
CREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);
|
|
33
|
-
|
|
34
|
-
CREATE TABLE ${tombstonesTable} (
|
|
35
|
-
id TEXT PRIMARY KEY,
|
|
36
|
-
clock INTEGER NOT NULL
|
|
37
|
-
);
|
|
38
|
-
CREATE INDEX idx_${tombstonesTable}_clock ON ${tombstonesTable}(clock);
|
|
39
|
-
|
|
40
|
-
-- This table is used to store the metadata for the sync storage.
|
|
41
|
-
-- There should only be one row in this table.
|
|
42
|
-
CREATE TABLE ${metadataTable} (
|
|
43
|
-
migrationVersion INTEGER NOT NULL,
|
|
44
|
-
documentClock INTEGER NOT NULL,
|
|
45
|
-
tombstoneHistoryStartsAtClock INTEGER NOT NULL,
|
|
46
|
-
schema TEXT NOT NULL
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
INSERT INTO ${metadataTable} (migrationVersion, documentClock, tombstoneHistoryStartsAtClock, schema) VALUES (2, 0, 0, '')
|
|
50
|
-
`);
|
|
51
|
-
migrationVersion++;
|
|
52
|
-
}
|
|
53
|
-
if (migrationVersion === 1) {
|
|
54
|
-
migrationVersion++;
|
|
55
|
-
storage.exec(`
|
|
56
|
-
CREATE TABLE ${documentsTable}_new (
|
|
57
|
-
id TEXT PRIMARY KEY,
|
|
58
|
-
state BLOB NOT NULL,
|
|
59
|
-
lastChangedClock INTEGER NOT NULL
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
INSERT INTO ${documentsTable}_new (id, state, lastChangedClock)
|
|
63
|
-
SELECT id, CAST(state AS BLOB), lastChangedClock FROM ${documentsTable};
|
|
64
|
-
|
|
65
|
-
DROP TABLE ${documentsTable};
|
|
66
|
-
|
|
67
|
-
ALTER TABLE ${documentsTable}_new RENAME TO ${documentsTable};
|
|
68
|
-
|
|
69
|
-
CREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);
|
|
70
|
-
`);
|
|
71
|
-
}
|
|
72
|
-
storage.exec(`UPDATE ${metadataTable} SET migrationVersion = ${migrationVersion}`);
|
|
73
|
-
}
|
|
74
|
-
const textEncoder = new TextEncoder();
|
|
75
|
-
const textDecoder = new TextDecoder();
|
|
76
|
-
function encodeState(state) {
|
|
77
|
-
return textEncoder.encode(JSON.stringify(state));
|
|
78
|
-
}
|
|
79
|
-
function decodeState(state) {
|
|
80
|
-
return JSON.parse(textDecoder.decode(state));
|
|
81
|
-
}
|
|
82
|
-
class SQLiteSyncStorage {
|
|
83
|
-
/**
|
|
84
|
-
* Check if the storage has been initialized (has data in the clock table).
|
|
85
|
-
* Useful for determining whether to load from an external source on first access.
|
|
86
|
-
*/
|
|
87
|
-
static hasBeenInitialized(storage) {
|
|
88
|
-
const prefix = storage.config?.tablePrefix ?? "";
|
|
89
|
-
try {
|
|
90
|
-
const schema = storage.prepare(`SELECT schema FROM ${prefix}metadata LIMIT 1`).all()[0]?.schema;
|
|
91
|
-
return !!schema;
|
|
92
|
-
} catch (_e) {
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Get the current document clock value from storage without fully initializing.
|
|
98
|
-
* Returns null if storage has not been initialized.
|
|
99
|
-
* Useful for comparing storage freshness against external sources.
|
|
100
|
-
*/
|
|
101
|
-
static getDocumentClock(storage) {
|
|
102
|
-
const prefix = storage.config?.tablePrefix ?? "";
|
|
103
|
-
try {
|
|
104
|
-
const row = storage.prepare(`SELECT documentClock FROM ${prefix}metadata LIMIT 1`).all()[0];
|
|
105
|
-
if (row && SQLiteSyncStorage.hasBeenInitialized(storage)) {
|
|
106
|
-
return row.documentClock;
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
} catch (_e) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
// Prepared statements - created once, reused many times
|
|
114
|
-
stmts;
|
|
115
|
-
sql;
|
|
116
|
-
constructor({
|
|
117
|
-
sql,
|
|
118
|
-
snapshot,
|
|
119
|
-
onChange
|
|
120
|
-
}) {
|
|
121
|
-
this.sql = sql;
|
|
122
|
-
const prefix = sql.config?.tablePrefix ?? "";
|
|
123
|
-
const documentsTable = `${prefix}documents`;
|
|
124
|
-
const tombstonesTable = `${prefix}tombstones`;
|
|
125
|
-
const metadataTable = `${prefix}metadata`;
|
|
126
|
-
migrateSqliteSyncStorage(this.sql, { documentsTable, tombstonesTable, metadataTable });
|
|
127
|
-
this.stmts = {
|
|
128
|
-
// Metadata
|
|
129
|
-
getDocumentClock: this.sql.prepare(
|
|
130
|
-
`SELECT documentClock FROM ${metadataTable} LIMIT 1`
|
|
131
|
-
),
|
|
132
|
-
getTombstoneHistoryStartsAtClock: this.sql.prepare(
|
|
133
|
-
`SELECT tombstoneHistoryStartsAtClock FROM ${metadataTable}`
|
|
134
|
-
),
|
|
135
|
-
getSchema: this.sql.prepare(`SELECT schema FROM ${metadataTable}`),
|
|
136
|
-
setSchema: this.sql.prepare(`UPDATE ${metadataTable} SET schema = ?`),
|
|
137
|
-
setTombstoneHistoryStartsAtClock: this.sql.prepare(
|
|
138
|
-
`UPDATE ${metadataTable} SET tombstoneHistoryStartsAtClock = ?`
|
|
139
|
-
),
|
|
140
|
-
incrementDocumentClock: this.sql.prepare(
|
|
141
|
-
`UPDATE ${metadataTable} SET documentClock = documentClock + 1`
|
|
142
|
-
),
|
|
143
|
-
// Documents
|
|
144
|
-
getDocument: this.sql.prepare(
|
|
145
|
-
`SELECT state FROM ${documentsTable} WHERE id = ?`
|
|
146
|
-
),
|
|
147
|
-
insertDocument: this.sql.prepare(`INSERT OR REPLACE INTO ${documentsTable} (id, state, lastChangedClock) VALUES (?, ?, ?)`),
|
|
148
|
-
deleteDocument: this.sql.prepare(
|
|
149
|
-
`DELETE FROM ${documentsTable} WHERE id = ?`
|
|
150
|
-
),
|
|
151
|
-
documentExists: this.sql.prepare(
|
|
152
|
-
`SELECT id FROM ${documentsTable} WHERE id = ?`
|
|
153
|
-
),
|
|
154
|
-
iterateDocuments: this.sql.prepare(
|
|
155
|
-
`SELECT state, lastChangedClock FROM ${documentsTable}`
|
|
156
|
-
),
|
|
157
|
-
iterateDocumentEntries: this.sql.prepare(
|
|
158
|
-
`SELECT id, state FROM ${documentsTable}`
|
|
159
|
-
),
|
|
160
|
-
iterateDocumentKeys: this.sql.prepare(`SELECT id FROM ${documentsTable}`),
|
|
161
|
-
iterateDocumentValues: this.sql.prepare(
|
|
162
|
-
`SELECT state FROM ${documentsTable}`
|
|
163
|
-
),
|
|
164
|
-
getDocumentsChangedSince: this.sql.prepare(
|
|
165
|
-
`SELECT state FROM ${documentsTable} WHERE lastChangedClock > ?`
|
|
166
|
-
),
|
|
167
|
-
// Tombstones
|
|
168
|
-
insertTombstone: this.sql.prepare(
|
|
169
|
-
`INSERT OR REPLACE INTO ${tombstonesTable} (id, clock) VALUES (?, ?)`
|
|
170
|
-
),
|
|
171
|
-
deleteTombstone: this.sql.prepare(
|
|
172
|
-
`DELETE FROM ${tombstonesTable} WHERE id = ?`
|
|
173
|
-
),
|
|
174
|
-
deleteTombstonesBefore: this.sql.prepare(
|
|
175
|
-
`DELETE FROM ${tombstonesTable} WHERE clock < ?`
|
|
176
|
-
),
|
|
177
|
-
countTombstones: this.sql.prepare(
|
|
178
|
-
`SELECT count(*) as count FROM ${tombstonesTable}`
|
|
179
|
-
),
|
|
180
|
-
iterateTombstones: this.sql.prepare(
|
|
181
|
-
`SELECT id, clock FROM ${tombstonesTable} ORDER BY clock ASC`
|
|
182
|
-
),
|
|
183
|
-
getTombstonesChangedSince: this.sql.prepare(
|
|
184
|
-
`SELECT id FROM ${tombstonesTable} WHERE clock > ?`
|
|
185
|
-
),
|
|
186
|
-
// Initial setup (only used when loading a snapshot)
|
|
187
|
-
updateMetadata: this.sql.prepare(
|
|
188
|
-
`UPDATE ${metadataTable} SET documentClock = ?, tombstoneHistoryStartsAtClock = ?, schema = ?`
|
|
189
|
-
)
|
|
190
|
-
};
|
|
191
|
-
const hasData = SQLiteSyncStorage.hasBeenInitialized(sql);
|
|
192
|
-
if (snapshot || !hasData) {
|
|
193
|
-
snapshot = convertStoreSnapshotToRoomSnapshot(snapshot ?? DEFAULT_INITIAL_SNAPSHOT);
|
|
194
|
-
const documentClock = snapshot.documentClock ?? snapshot.clock ?? 0;
|
|
195
|
-
const tombstoneHistoryStartsAtClock = snapshot.tombstoneHistoryStartsAtClock ?? documentClock;
|
|
196
|
-
this.sql.exec(`
|
|
197
|
-
DELETE FROM ${documentsTable};
|
|
198
|
-
DELETE FROM ${tombstonesTable};
|
|
199
|
-
`);
|
|
200
|
-
for (const doc of snapshot.documents) {
|
|
201
|
-
this.stmts.insertDocument.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock);
|
|
202
|
-
}
|
|
203
|
-
if (snapshot.tombstones) {
|
|
204
|
-
for (const [id, clock] of objectMapEntries(snapshot.tombstones)) {
|
|
205
|
-
this.stmts.insertTombstone.run(id, clock);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
this.stmts.updateMetadata.run(
|
|
209
|
-
documentClock,
|
|
210
|
-
tombstoneHistoryStartsAtClock,
|
|
211
|
-
JSON.stringify(snapshot.schema)
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
if (onChange) {
|
|
215
|
-
this.onChange(onChange);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
notifier = new MicrotaskNotifier();
|
|
219
|
-
onChange(callback) {
|
|
220
|
-
return this.notifier.register(callback);
|
|
221
|
-
}
|
|
222
|
-
transaction(callback, opts) {
|
|
223
|
-
const clockBefore = this.getClock();
|
|
224
|
-
const trackChanges = opts?.emitChanges === "always";
|
|
225
|
-
return this.sql.transaction(() => {
|
|
226
|
-
const txn = new SQLiteSyncStorageTransaction(this, this.stmts);
|
|
227
|
-
let result;
|
|
228
|
-
let changes;
|
|
229
|
-
try {
|
|
230
|
-
result = transaction(() => {
|
|
231
|
-
return callback(txn);
|
|
232
|
-
});
|
|
233
|
-
if (trackChanges) {
|
|
234
|
-
changes = txn.getChangesSince(clockBefore)?.diff;
|
|
235
|
-
}
|
|
236
|
-
} finally {
|
|
237
|
-
txn.close();
|
|
238
|
-
}
|
|
239
|
-
if (typeof result === "object" && result && "then" in result && typeof result.then === "function") {
|
|
240
|
-
throw new Error("Transaction must return a value, not a promise");
|
|
241
|
-
}
|
|
242
|
-
const clockAfter = this.getClock();
|
|
243
|
-
const didChange = clockAfter > clockBefore;
|
|
244
|
-
if (didChange) {
|
|
245
|
-
this.notifier.notify({ id: opts?.id, documentClock: clockAfter });
|
|
246
|
-
}
|
|
247
|
-
return { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes };
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
getClock() {
|
|
251
|
-
const clockRow = this.stmts.getDocumentClock.all()[0];
|
|
252
|
-
return clockRow?.documentClock ?? 0;
|
|
253
|
-
}
|
|
254
|
-
/** @internal */
|
|
255
|
-
_getTombstoneHistoryStartsAtClock() {
|
|
256
|
-
const clockRow = this.stmts.getTombstoneHistoryStartsAtClock.all()[0];
|
|
257
|
-
return clockRow?.tombstoneHistoryStartsAtClock ?? 0;
|
|
258
|
-
}
|
|
259
|
-
/** @internal */
|
|
260
|
-
_getSchema() {
|
|
261
|
-
const clockRow = this.stmts.getSchema.all()[0];
|
|
262
|
-
assert(clockRow, "Storage not initialized - clock row missing");
|
|
263
|
-
return JSON.parse(clockRow.schema);
|
|
264
|
-
}
|
|
265
|
-
/** @internal */
|
|
266
|
-
_setSchema(schema) {
|
|
267
|
-
this.stmts.setSchema.run(JSON.stringify(schema));
|
|
268
|
-
}
|
|
269
|
-
/** @internal */
|
|
270
|
-
pruneTombstones = throttle(
|
|
271
|
-
() => {
|
|
272
|
-
const tombstoneCount = this.stmts.countTombstones.all()[0].count;
|
|
273
|
-
if (tombstoneCount > MAX_TOMBSTONES) {
|
|
274
|
-
const tombstones = this.stmts.iterateTombstones.all();
|
|
275
|
-
const result = computeTombstonePruning({ tombstones, documentClock: this.getClock() });
|
|
276
|
-
if (result) {
|
|
277
|
-
this.stmts.setTombstoneHistoryStartsAtClock.run(result.newTombstoneHistoryStartsAtClock);
|
|
278
|
-
this.stmts.deleteTombstonesBefore.run(result.newTombstoneHistoryStartsAtClock);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
},
|
|
282
|
-
1e3,
|
|
283
|
-
// prevent this from running synchronously to avoid blocking requests
|
|
284
|
-
{ leading: false }
|
|
285
|
-
);
|
|
286
|
-
getSnapshot() {
|
|
287
|
-
return {
|
|
288
|
-
tombstoneHistoryStartsAtClock: this._getTombstoneHistoryStartsAtClock(),
|
|
289
|
-
documentClock: this.getClock(),
|
|
290
|
-
documents: Array.from(this._iterateDocuments()),
|
|
291
|
-
tombstones: Object.fromEntries(this._iterateTombstones()),
|
|
292
|
-
schema: this._getSchema()
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
*_iterateDocuments() {
|
|
296
|
-
for (const row of this.stmts.iterateDocuments.iterate()) {
|
|
297
|
-
yield { state: decodeState(row.state), lastChangedClock: row.lastChangedClock };
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
*_iterateTombstones() {
|
|
301
|
-
for (const row of this.stmts.iterateTombstones.iterate()) {
|
|
302
|
-
yield [row.id, row.clock];
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
class SQLiteSyncStorageTransaction {
|
|
307
|
-
constructor(storage, stmts) {
|
|
308
|
-
this.storage = storage;
|
|
309
|
-
this.stmts = stmts;
|
|
310
|
-
this._clock = this.storage.getClock();
|
|
311
|
-
}
|
|
312
|
-
_clock;
|
|
313
|
-
_closed = false;
|
|
314
|
-
_didIncrementClock = false;
|
|
315
|
-
/** @internal */
|
|
316
|
-
close() {
|
|
317
|
-
this._closed = true;
|
|
318
|
-
}
|
|
319
|
-
assertNotClosed() {
|
|
320
|
-
assert(!this._closed, "Transaction has ended, iterator cannot be consumed");
|
|
321
|
-
}
|
|
322
|
-
getClock() {
|
|
323
|
-
return this._clock;
|
|
324
|
-
}
|
|
325
|
-
getNextClock() {
|
|
326
|
-
if (!this._didIncrementClock) {
|
|
327
|
-
this._didIncrementClock = true;
|
|
328
|
-
this.stmts.incrementDocumentClock.run();
|
|
329
|
-
this._clock = this.storage.getClock();
|
|
330
|
-
}
|
|
331
|
-
return this._clock;
|
|
332
|
-
}
|
|
333
|
-
get(id) {
|
|
334
|
-
this.assertNotClosed();
|
|
335
|
-
const row = this.stmts.getDocument.all(id)[0];
|
|
336
|
-
if (!row) return void 0;
|
|
337
|
-
return decodeState(row.state);
|
|
338
|
-
}
|
|
339
|
-
set(id, record) {
|
|
340
|
-
this.assertNotClosed();
|
|
341
|
-
assert(id === record.id, `Record id mismatch: key does not match record.id`);
|
|
342
|
-
const clock = this.getNextClock();
|
|
343
|
-
this.stmts.deleteTombstone.run(id);
|
|
344
|
-
this.stmts.insertDocument.run(id, encodeState(record), clock);
|
|
345
|
-
}
|
|
346
|
-
delete(id) {
|
|
347
|
-
this.assertNotClosed();
|
|
348
|
-
const exists = this.stmts.documentExists.all(id)[0];
|
|
349
|
-
if (!exists) return;
|
|
350
|
-
const clock = this.getNextClock();
|
|
351
|
-
this.stmts.deleteDocument.run(id);
|
|
352
|
-
this.stmts.insertTombstone.run(id, clock);
|
|
353
|
-
this.storage.pruneTombstones();
|
|
354
|
-
}
|
|
355
|
-
*entries() {
|
|
356
|
-
this.assertNotClosed();
|
|
357
|
-
for (const row of this.stmts.iterateDocumentEntries.iterate()) {
|
|
358
|
-
this.assertNotClosed();
|
|
359
|
-
yield [row.id, decodeState(row.state)];
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
*keys() {
|
|
363
|
-
this.assertNotClosed();
|
|
364
|
-
for (const row of this.stmts.iterateDocumentKeys.iterate()) {
|
|
365
|
-
this.assertNotClosed();
|
|
366
|
-
yield row.id;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
*values() {
|
|
370
|
-
this.assertNotClosed();
|
|
371
|
-
for (const row of this.stmts.iterateDocumentValues.iterate()) {
|
|
372
|
-
this.assertNotClosed();
|
|
373
|
-
yield decodeState(row.state);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
getSchema() {
|
|
377
|
-
this.assertNotClosed();
|
|
378
|
-
return this.storage._getSchema();
|
|
379
|
-
}
|
|
380
|
-
setSchema(schema) {
|
|
381
|
-
this.assertNotClosed();
|
|
382
|
-
this.storage._setSchema(schema);
|
|
383
|
-
}
|
|
384
|
-
getChangesSince(sinceClock) {
|
|
385
|
-
this.assertNotClosed();
|
|
386
|
-
const clock = this.storage.getClock();
|
|
387
|
-
if (sinceClock === clock) return void 0;
|
|
388
|
-
if (sinceClock > clock) {
|
|
389
|
-
sinceClock = -1;
|
|
390
|
-
}
|
|
391
|
-
const diff = { puts: {}, deletes: [] };
|
|
392
|
-
const wipeAll = sinceClock < this.storage._getTombstoneHistoryStartsAtClock();
|
|
393
|
-
if (wipeAll) {
|
|
394
|
-
for (const row of this.stmts.iterateDocumentValues.iterate()) {
|
|
395
|
-
const state = decodeState(row.state);
|
|
396
|
-
diff.puts[state.id] = state;
|
|
397
|
-
}
|
|
398
|
-
} else {
|
|
399
|
-
for (const row of this.stmts.getDocumentsChangedSince.iterate(sinceClock)) {
|
|
400
|
-
const state = decodeState(row.state);
|
|
401
|
-
diff.puts[state.id] = state;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
for (const row of this.stmts.getTombstonesChangedSince.iterate(sinceClock)) {
|
|
405
|
-
diff.deletes.push(row.id);
|
|
406
|
-
}
|
|
407
|
-
return { diff, wipeAll };
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
export {
|
|
411
|
-
SQLiteSyncStorage,
|
|
412
|
-
migrateSqliteSyncStorage
|
|
413
|
-
};
|
|
414
|
-
//# sourceMappingURL=SQLiteSyncStorage.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/lib/SQLiteSyncStorage.ts"],
|
|
4
|
-
"sourcesContent": ["import { transaction } from '@tldraw/state'\nimport { SerializedSchema, StoreSnapshot, UnknownRecord } from '@tldraw/store'\nimport { assert, objectMapEntries, throttle } from '@tldraw/utils'\nimport {\n\tcomputeTombstonePruning,\n\tDEFAULT_INITIAL_SNAPSHOT,\n\tMAX_TOMBSTONES,\n} from './InMemorySyncStorage'\nimport { MicrotaskNotifier } from './MicrotaskNotifier'\nimport { RoomSnapshot } from './TLSyncRoom'\nimport {\n\tconvertStoreSnapshotToRoomSnapshot,\n\tTLSyncForwardDiff,\n\tTLSyncStorage,\n\tTLSyncStorageGetChangesSinceResult,\n\tTLSyncStorageOnChangeCallbackProps,\n\tTLSyncStorageTransaction,\n\tTLSyncStorageTransactionCallback,\n\tTLSyncStorageTransactionOptions,\n\tTLSyncStorageTransactionResult,\n} from './TLSyncStorage'\n\n/**\n * Valid input value types for SQLite query parameters.\n * These are the types that can be passed as bindings to prepared statements.\n * @public\n */\nexport type TLSqliteInputValue = null | number | bigint | string | Uint8Array\n\n/**\n * Possible output value types returned from SQLite queries.\n * Includes all input types plus Uint8Array for BLOB columns.\n * @public\n */\nexport type TLSqliteOutputValue = null | number | bigint | string | Uint8Array\n\n/**\n * A row returned from a SQLite query, mapping column names to their values.\n * @public\n */\nexport type TLSqliteRow = Record<string, TLSqliteOutputValue>\n\n/**\n * A prepared statement that can be executed multiple times with different bindings.\n * @public\n */\nexport interface TLSyncSqliteStatement<\n\tTResult extends TLSqliteRow | void,\n\tTParams extends TLSqliteInputValue[] = [],\n> {\n\t/** Execute the statement and iterate over results one at a time */\n\titerate(...bindings: TParams): IterableIterator<TResult>\n\t/** Execute the statement and return all results as an array */\n\tall(...bindings: TParams): TResult[]\n\t/** Execute the statement without returning results (for DML) */\n\trun(...bindings: TParams): void\n}\n\n/**\n * Configuration for SQLiteSyncStorage.\n * @public\n */\nexport interface TLSyncSqliteWrapperConfig {\n\t/** Prefix for all table names (default: ''). E.g. 'sync_' creates tables 'sync_documents', 'sync_tombstones', 'sync_metadata' */\n\ttablePrefix?: string\n}\n\n/**\n * Interface for SQLite storage with prepare, exec and transaction capabilities.\n * @public\n */\nexport interface TLSyncSqliteWrapper {\n\t/** Optional configuration for table names. If not provided, defaults are used. */\n\treadonly config?: TLSyncSqliteWrapperConfig\n\t/** Prepare a SQL statement for execution */\n\tprepare<TResult extends TLSqliteRow | void, TParams extends TLSqliteInputValue[] = []>(\n\t\tsql: string\n\t): TLSyncSqliteStatement<TResult, TParams>\n\t/** Execute raw SQL (for DDL, multi-statement scripts) */\n\texec(sql: string): void\n\t/** Execute a callback within a transaction */\n\ttransaction<T>(callback: () => T): T\n}\n\nexport function migrateSqliteSyncStorage(\n\tstorage: TLSyncSqliteWrapper,\n\t{\n\t\tdocumentsTable = 'documents',\n\t\ttombstonesTable = 'tombstones',\n\t\tmetadataTable = 'metadata',\n\t}: { documentsTable?: string; tombstonesTable?: string; metadataTable?: string } = {}\n): void {\n\tlet migrationVersion = 0\n\ttry {\n\t\tconst row = storage\n\t\t\t.prepare<{\n\t\t\t\tmigrationVersion: number\n\t\t\t}>(`SELECT migrationVersion FROM ${metadataTable} LIMIT 1`)\n\t\t\t.all()[0]\n\t\tmigrationVersion = row?.migrationVersion ?? 0\n\t} catch (_e) {\n\t\t// noop\n\t}\n\n\tif (migrationVersion === 0) {\n\t\tmigrationVersion++\n\t\tstorage.exec(`\n\t\t\tCREATE TABLE ${documentsTable} (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tstate BLOB NOT NULL,\n\t\t\t\tlastChangedClock INTEGER NOT NULL\n\t\t\t);\n\n\t\t\tCREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);\n\n\t\t\tCREATE TABLE ${tombstonesTable} (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tclock INTEGER NOT NULL\n\t\t\t);\n\t\t\tCREATE INDEX idx_${tombstonesTable}_clock ON ${tombstonesTable}(clock);\n\n\t\t\t-- This table is used to store the metadata for the sync storage.\n\t\t\t-- There should only be one row in this table.\n\t\t\tCREATE TABLE ${metadataTable} (\n\t\t\t migrationVersion INTEGER NOT NULL,\n\t\t\t\tdocumentClock INTEGER NOT NULL,\n\t\t\t\ttombstoneHistoryStartsAtClock INTEGER NOT NULL,\n\t\t\t\tschema TEXT NOT NULL\n\t\t\t);\n\t\t\t\n\t\t\tINSERT INTO ${metadataTable} (migrationVersion, documentClock, tombstoneHistoryStartsAtClock, schema) VALUES (2, 0, 0, '')\n\t\t`)\n\t\t// Skip migration 2 since we created the table with BLOB already\n\t\tmigrationVersion++\n\t}\n\n\tif (migrationVersion === 1) {\n\t\t// Migration 2: Convert state column from TEXT to BLOB\n\t\t// SQLite doesn't support ALTER COLUMN, so we need to recreate the table\n\t\tmigrationVersion++\n\t\tstorage.exec(`\n\t\t\tCREATE TABLE ${documentsTable}_new (\n\t\t\t\tid TEXT PRIMARY KEY,\n\t\t\t\tstate BLOB NOT NULL,\n\t\t\t\tlastChangedClock INTEGER NOT NULL\n\t\t\t);\n\t\t\t\n\t\t\tINSERT INTO ${documentsTable}_new (id, state, lastChangedClock)\n\t\t\tSELECT id, CAST(state AS BLOB), lastChangedClock FROM ${documentsTable};\n\t\t\t\n\t\t\tDROP TABLE ${documentsTable};\n\t\t\t\n\t\t\tALTER TABLE ${documentsTable}_new RENAME TO ${documentsTable};\n\t\t\t\n\t\t\tCREATE INDEX idx_${documentsTable}_lastChangedClock ON ${documentsTable}(lastChangedClock);\n\t\t`)\n\t}\n\n\t// add more migrations here if and when needed\n\n\tstorage.exec(`UPDATE ${metadataTable} SET migrationVersion = ${migrationVersion}`)\n}\n\nconst textEncoder = new TextEncoder()\nconst textDecoder = new TextDecoder()\n\nfunction encodeState(state: unknown): Uint8Array {\n\treturn textEncoder.encode(JSON.stringify(state))\n}\n\nfunction decodeState<T>(state: Uint8Array): T {\n\treturn JSON.parse(textDecoder.decode(state))\n}\n\n/**\n * SQLite-based implementation of TLSyncStorage.\n * Stores documents, tombstones, metadata, and clock values in SQLite tables.\n *\n * This storage backend provides persistent synchronization state that survives\n * process restarts, unlike InMemorySyncStorage which loses data when the process ends.\n *\n * @example\n * ```ts\n * // With Cloudflare Durable Objects\n * import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'\n *\n * const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // With Node.js sqlite (Node 22.5+)\n * import { DatabaseSync } from 'node:sqlite'\n * import { SQLiteSyncStorage, NodeSqliteWrapper } from '@tldraw/sync-core'\n *\n * const db = new DatabaseSync('sync-state.db')\n * const sql = new NodeSqliteWrapper(db)\n * const storage = new SQLiteSyncStorage({ sql })\n * ```\n *\n * @example\n * ```ts\n * // Initialize with an existing snapshot\n * const storage = new SQLiteSyncStorage({ sql, snapshot: existingSnapshot })\n * ```\n *\n * @public\n */\nexport class SQLiteSyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {\n\t/**\n\t * Check if the storage has been initialized (has data in the clock table).\n\t * Useful for determining whether to load from an external source on first access.\n\t */\n\tstatic hasBeenInitialized(storage: TLSyncSqliteWrapper): boolean {\n\t\tconst prefix = storage.config?.tablePrefix ?? ''\n\t\ttry {\n\t\t\tconst schema = storage\n\t\t\t\t.prepare<{ schema: string }>(`SELECT schema FROM ${prefix}metadata LIMIT 1`)\n\t\t\t\t.all()[0]?.schema\n\t\t\treturn !!schema\n\t\t} catch (_e) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t/**\n\t * Get the current document clock value from storage without fully initializing.\n\t * Returns null if storage has not been initialized.\n\t * Useful for comparing storage freshness against external sources.\n\t */\n\tstatic getDocumentClock(storage: TLSyncSqliteWrapper): number | null {\n\t\tconst prefix = storage.config?.tablePrefix ?? ''\n\t\ttry {\n\t\t\tconst row = storage\n\t\t\t\t.prepare<{ documentClock: number }>(`SELECT documentClock FROM ${prefix}metadata LIMIT 1`)\n\t\t\t\t.all()[0]\n\t\t\t// documentClock exists but could be 0, so we check if the storage is initialized\n\t\t\tif (row && SQLiteSyncStorage.hasBeenInitialized(storage)) {\n\t\t\t\treturn row.documentClock\n\t\t\t}\n\t\t\treturn null\n\t\t} catch (_e) {\n\t\t\treturn null\n\t\t}\n\t}\n\n\t// Prepared statements - created once, reused many times\n\tprivate readonly stmts\n\n\tprivate readonly sql: TLSyncSqliteWrapper\n\n\tconstructor({\n\t\tsql,\n\t\tsnapshot,\n\t\tonChange,\n\t}: {\n\t\tsql: TLSyncSqliteWrapper\n\t\tsnapshot?: RoomSnapshot | StoreSnapshot<R>\n\t\tonChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown\n\t}) {\n\t\tthis.sql = sql\n\t\tconst prefix = sql.config?.tablePrefix ?? ''\n\t\tconst documentsTable = `${prefix}documents`\n\t\tconst tombstonesTable = `${prefix}tombstones`\n\t\tconst metadataTable = `${prefix}metadata`\n\n\t\tmigrateSqliteSyncStorage(this.sql, { documentsTable, tombstonesTable, metadataTable })\n\n\t\t// Prepare all statements once\n\t\tthis.stmts = {\n\t\t\t// Metadata\n\t\t\tgetDocumentClock: this.sql.prepare<{ documentClock: number }>(\n\t\t\t\t`SELECT documentClock FROM ${metadataTable} LIMIT 1`\n\t\t\t),\n\t\t\tgetTombstoneHistoryStartsAtClock: this.sql.prepare<{ tombstoneHistoryStartsAtClock: number }>(\n\t\t\t\t`SELECT tombstoneHistoryStartsAtClock FROM ${metadataTable}`\n\t\t\t),\n\t\t\tgetSchema: this.sql.prepare<{ schema: string }>(`SELECT schema FROM ${metadataTable}`),\n\t\t\tsetSchema: this.sql.prepare<void, [schema: string]>(`UPDATE ${metadataTable} SET schema = ?`),\n\t\t\tsetTombstoneHistoryStartsAtClock: this.sql.prepare<void, [clock: number]>(\n\t\t\t\t`UPDATE ${metadataTable} SET tombstoneHistoryStartsAtClock = ?`\n\t\t\t),\n\t\t\tincrementDocumentClock: this.sql.prepare<void>(\n\t\t\t\t`UPDATE ${metadataTable} SET documentClock = documentClock + 1`\n\t\t\t),\n\n\t\t\t// Documents\n\t\t\tgetDocument: this.sql.prepare<{ state: Uint8Array }, [id: string]>(\n\t\t\t\t`SELECT state FROM ${documentsTable} WHERE id = ?`\n\t\t\t),\n\t\t\tinsertDocument: this.sql.prepare<\n\t\t\t\tvoid,\n\t\t\t\t[id: string, state: Uint8Array, lastChangedClock: number]\n\t\t\t>(`INSERT OR REPLACE INTO ${documentsTable} (id, state, lastChangedClock) VALUES (?, ?, ?)`),\n\t\t\tdeleteDocument: this.sql.prepare<void, [id: string]>(\n\t\t\t\t`DELETE FROM ${documentsTable} WHERE id = ?`\n\t\t\t),\n\t\t\tdocumentExists: this.sql.prepare<{ id: string }, [id: string]>(\n\t\t\t\t`SELECT id FROM ${documentsTable} WHERE id = ?`\n\t\t\t),\n\t\t\titerateDocuments: this.sql.prepare<{ state: Uint8Array; lastChangedClock: number }>(\n\t\t\t\t`SELECT state, lastChangedClock FROM ${documentsTable}`\n\t\t\t),\n\t\t\titerateDocumentEntries: this.sql.prepare<{ id: string; state: Uint8Array }>(\n\t\t\t\t`SELECT id, state FROM ${documentsTable}`\n\t\t\t),\n\t\t\titerateDocumentKeys: this.sql.prepare<{ id: string }>(`SELECT id FROM ${documentsTable}`),\n\t\t\titerateDocumentValues: this.sql.prepare<{ state: Uint8Array }>(\n\t\t\t\t`SELECT state FROM ${documentsTable}`\n\t\t\t),\n\t\t\tgetDocumentsChangedSince: this.sql.prepare<{ state: Uint8Array }, [sinceClock: number]>(\n\t\t\t\t`SELECT state FROM ${documentsTable} WHERE lastChangedClock > ?`\n\t\t\t),\n\n\t\t\t// Tombstones\n\t\t\tinsertTombstone: this.sql.prepare<void, [id: string, clock: number]>(\n\t\t\t\t`INSERT OR REPLACE INTO ${tombstonesTable} (id, clock) VALUES (?, ?)`\n\t\t\t),\n\t\t\tdeleteTombstone: this.sql.prepare<void, [id: string]>(\n\t\t\t\t`DELETE FROM ${tombstonesTable} WHERE id = ?`\n\t\t\t),\n\t\t\tdeleteTombstonesBefore: this.sql.prepare<void, [clock: number]>(\n\t\t\t\t`DELETE FROM ${tombstonesTable} WHERE clock < ?`\n\t\t\t),\n\t\t\tcountTombstones: this.sql.prepare<{ count: number }>(\n\t\t\t\t`SELECT count(*) as count FROM ${tombstonesTable}`\n\t\t\t),\n\t\t\titerateTombstones: this.sql.prepare<{ id: string; clock: number }>(\n\t\t\t\t`SELECT id, clock FROM ${tombstonesTable} ORDER BY clock ASC`\n\t\t\t),\n\t\t\tgetTombstonesChangedSince: this.sql.prepare<{ id: string }, [sinceClock: number]>(\n\t\t\t\t`SELECT id FROM ${tombstonesTable} WHERE clock > ?`\n\t\t\t),\n\n\t\t\t// Initial setup (only used when loading a snapshot)\n\t\t\tupdateMetadata: this.sql.prepare<\n\t\t\t\tvoid,\n\t\t\t\t[documentClock: number, tombstoneHistoryStartsAtClock: number, schema: string]\n\t\t\t>(\n\t\t\t\t`UPDATE ${metadataTable} SET documentClock = ?, tombstoneHistoryStartsAtClock = ?, schema = ?`\n\t\t\t),\n\t\t}\n\n\t\t// Check if we already have data\n\t\tconst hasData = SQLiteSyncStorage.hasBeenInitialized(sql)\n\n\t\tif (snapshot || !hasData) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot ?? DEFAULT_INITIAL_SNAPSHOT)\n\n\t\t\tconst documentClock = snapshot.documentClock ?? snapshot.clock ?? 0\n\t\t\tconst tombstoneHistoryStartsAtClock = snapshot.tombstoneHistoryStartsAtClock ?? documentClock\n\n\t\t\t// Clear existing data\n\t\t\tthis.sql.exec(`\n\t\t\t\tDELETE FROM ${documentsTable};\n\t\t\t\tDELETE FROM ${tombstonesTable};\n\t\t\t`)\n\n\t\t\t// Insert documents\n\t\t\tfor (const doc of snapshot.documents) {\n\t\t\t\tthis.stmts.insertDocument.run(doc.state.id, encodeState(doc.state), doc.lastChangedClock)\n\t\t\t}\n\n\t\t\t// Insert tombstones\n\t\t\tif (snapshot.tombstones) {\n\t\t\t\tfor (const [id, clock] of objectMapEntries(snapshot.tombstones)) {\n\t\t\t\t\tthis.stmts.insertTombstone.run(id, clock)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Insert metadata row\n\t\t\tthis.stmts.updateMetadata.run(\n\t\t\t\tdocumentClock,\n\t\t\t\ttombstoneHistoryStartsAtClock,\n\t\t\t\tJSON.stringify(snapshot.schema)\n\t\t\t)\n\t\t}\n\t\tif (onChange) {\n\t\t\tthis.onChange(onChange)\n\t\t}\n\t}\n\n\tprivate notifier = new MicrotaskNotifier<[TLSyncStorageOnChangeCallbackProps]>()\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => void): () => void {\n\t\treturn this.notifier.register(callback)\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.getClock()\n\t\tconst trackChanges = opts?.emitChanges === 'always'\n\t\treturn this.sql.transaction(() => {\n\t\t\tconst txn = new SQLiteSyncStorageTransaction<R>(this, this.stmts)\n\t\t\tlet result: T\n\t\t\tlet changes: TLSyncForwardDiff<R> | undefined\n\t\t\ttry {\n\t\t\t\tresult = transaction(() => {\n\t\t\t\t\treturn callback(txn)\n\t\t\t\t}) as T\n\t\t\t\tif (trackChanges) {\n\t\t\t\t\tchanges = txn.getChangesSince(clockBefore)?.diff\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\ttxn.close()\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttypeof result === 'object' &&\n\t\t\t\tresult &&\n\t\t\t\t'then' in result &&\n\t\t\t\ttypeof result.then === 'function'\n\t\t\t) {\n\t\t\t\tthrow new Error('Transaction must return a value, not a promise')\n\t\t\t}\n\n\t\t\tconst clockAfter = this.getClock()\n\t\t\tconst didChange = clockAfter > clockBefore\n\t\t\tif (didChange) {\n\t\t\t\tthis.notifier.notify({ id: opts?.id, documentClock: clockAfter })\n\t\t\t}\n\t\t\treturn { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes }\n\t\t})\n\t}\n\n\tgetClock(): number {\n\t\tconst clockRow = this.stmts.getDocumentClock.all()[0]\n\t\treturn clockRow?.documentClock ?? 0\n\t}\n\n\t/** @internal */\n\t_getTombstoneHistoryStartsAtClock(): number {\n\t\tconst clockRow = this.stmts.getTombstoneHistoryStartsAtClock.all()[0]\n\t\treturn clockRow?.tombstoneHistoryStartsAtClock ?? 0\n\t}\n\n\t/** @internal */\n\t_getSchema(): SerializedSchema {\n\t\tconst clockRow = this.stmts.getSchema.all()[0]\n\t\tassert(clockRow, 'Storage not initialized - clock row missing')\n\t\treturn JSON.parse(clockRow.schema)\n\t}\n\n\t/** @internal */\n\t_setSchema(schema: SerializedSchema): void {\n\t\tthis.stmts.setSchema.run(JSON.stringify(schema))\n\t}\n\n\t/** @internal */\n\tpruneTombstones = throttle(\n\t\t() => {\n\t\t\tconst tombstoneCount = this.stmts.countTombstones.all()[0].count as number\n\t\t\tif (tombstoneCount > MAX_TOMBSTONES) {\n\t\t\t\t// Get all tombstones sorted by clock ascending (oldest first)\n\t\t\t\tconst tombstones = this.stmts.iterateTombstones.all()\n\n\t\t\t\tconst result = computeTombstonePruning({ tombstones, documentClock: this.getClock() })\n\t\t\t\tif (result) {\n\t\t\t\t\tthis.stmts.setTombstoneHistoryStartsAtClock.run(result.newTombstoneHistoryStartsAtClock)\n\t\t\t\t\t// Delete all tombstones with clock < newTombstoneHistoryStartsAtClock in one operation.\n\t\t\t\t\t// This works because computeTombstonePruning ensures we never split a clock value.\n\t\t\t\t\tthis.stmts.deleteTombstonesBefore.run(result.newTombstoneHistoryStartsAtClock)\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._getTombstoneHistoryStartsAtClock(),\n\t\t\tdocumentClock: this.getClock(),\n\t\t\tdocuments: Array.from(this._iterateDocuments()),\n\t\t\ttombstones: Object.fromEntries(this._iterateTombstones()),\n\t\t\tschema: this._getSchema(),\n\t\t}\n\t}\n\tprivate *_iterateDocuments(): IterableIterator<{ state: R; lastChangedClock: number }> {\n\t\tfor (const row of this.stmts.iterateDocuments.iterate()) {\n\t\t\tyield { state: decodeState<R>(row.state), lastChangedClock: row.lastChangedClock }\n\t\t}\n\t}\n\n\tprivate *_iterateTombstones(): IterableIterator<[string, number]> {\n\t\tfor (const row of this.stmts.iterateTombstones.iterate()) {\n\t\t\tyield [row.id, row.clock]\n\t\t}\n\t}\n}\n\n/**\n * Transaction implementation for SQLiteSyncStorage.\n * Provides access to documents, tombstones, and metadata within a transaction.\n *\n * @internal\n */\nclass SQLiteSyncStorageTransaction<R extends UnknownRecord> implements TLSyncStorageTransaction<R> {\n\tprivate _clock: number\n\tprivate _closed = false\n\tprivate _didIncrementClock: boolean = false\n\n\tconstructor(\n\t\tprivate storage: SQLiteSyncStorage<R>,\n\t\tprivate stmts: SQLiteSyncStorage<R>['stmts']\n\t) {\n\t\tthis._clock = this.storage.getClock()\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 getNextClock(): number {\n\t\tif (!this._didIncrementClock) {\n\t\t\tthis._didIncrementClock = true\n\t\t\tthis.stmts.incrementDocumentClock.run()\n\t\t\tthis._clock = this.storage.getClock()\n\t\t}\n\t\treturn this._clock\n\t}\n\n\tget(id: string): R | undefined {\n\t\tthis.assertNotClosed()\n\t\tconst row = this.stmts.getDocument.all(id)[0]\n\t\tif (!row) return undefined\n\t\treturn decodeState<R>(row.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\tthis.stmts.deleteTombstone.run(id)\n\t\tthis.stmts.insertDocument.run(id, encodeState(record), clock)\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 exists = this.stmts.documentExists.all(id)[0]\n\t\tif (!exists) return\n\t\tconst clock = this.getNextClock()\n\t\tthis.stmts.deleteDocument.run(id)\n\t\tthis.stmts.insertTombstone.run(id, clock)\n\t\tthis.storage.pruneTombstones()\n\t}\n\n\t*entries(): IterableIterator<[string, R]> {\n\t\tthis.assertNotClosed()\n\t\tfor (const row of this.stmts.iterateDocumentEntries.iterate()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield [row.id, decodeState<R>(row.state)]\n\t\t}\n\t}\n\n\t*keys(): IterableIterator<string> {\n\t\tthis.assertNotClosed()\n\t\tfor (const row of this.stmts.iterateDocumentKeys.iterate()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield row.id\n\t\t}\n\t}\n\n\t*values(): IterableIterator<R> {\n\t\tthis.assertNotClosed()\n\t\tfor (const row of this.stmts.iterateDocumentValues.iterate()) {\n\t\t\tthis.assertNotClosed()\n\t\t\tyield decodeState<R>(row.state)\n\t\t}\n\t}\n\n\tgetSchema(): SerializedSchema {\n\t\tthis.assertNotClosed()\n\t\treturn this.storage._getSchema()\n\t}\n\n\tsetSchema(schema: SerializedSchema): void {\n\t\tthis.assertNotClosed()\n\t\tthis.storage._setSchema(schema)\n\t}\n\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined {\n\t\tthis.assertNotClosed()\n\t\tconst clock = this.storage.getClock()\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._getTombstoneHistoryStartsAtClock()\n\n\t\tif (wipeAll) {\n\t\t\t// If wipeAll, include all documents\n\t\t\tfor (const row of this.stmts.iterateDocumentValues.iterate()) {\n\t\t\t\tconst state = decodeState<R>(row.state)\n\t\t\t\tdiff.puts[state.id] = state\n\t\t\t}\n\t\t} else {\n\t\t\t// Get documents changed since clock\n\t\t\tfor (const row of this.stmts.getDocumentsChangedSince.iterate(sinceClock)) {\n\t\t\t\tconst state = decodeState<R>(row.state)\n\t\t\t\tdiff.puts[state.id] = state\n\t\t\t}\n\t\t}\n\n\t\t// Get tombstones changed since clock\n\t\tfor (const row of this.stmts.getTombstonesChangedSince.iterate(sinceClock)) {\n\t\t\tdiff.deletes.push(row.id)\n\t\t}\n\n\t\treturn { diff, wipeAll }\n\t}\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { isEqual, objectMapEntriesIterable, objectMapValues } from "@tldraw/utils";
|
|
2
|
-
import { diffRecord, RecordOpType } from "./diff.mjs";
|
|
3
|
-
function toNetworkDiff(diff) {
|
|
4
|
-
const networkDiff = {};
|
|
5
|
-
for (const [id, put] of objectMapEntriesIterable(diff.puts)) {
|
|
6
|
-
if (Array.isArray(put)) {
|
|
7
|
-
const patch = diffRecord(put[0], put[1]);
|
|
8
|
-
if (patch) {
|
|
9
|
-
networkDiff[id] = [RecordOpType.Patch, patch];
|
|
10
|
-
}
|
|
11
|
-
} else {
|
|
12
|
-
networkDiff[id] = [RecordOpType.Put, put];
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
for (const id of diff.deletes) {
|
|
16
|
-
networkDiff[id] = [RecordOpType.Remove];
|
|
17
|
-
}
|
|
18
|
-
return networkDiff;
|
|
19
|
-
}
|
|
20
|
-
function loadSnapshotIntoStorage(txn, schema, snapshot) {
|
|
21
|
-
snapshot = convertStoreSnapshotToRoomSnapshot(snapshot);
|
|
22
|
-
assert(snapshot.schema, "Schema is required");
|
|
23
|
-
const docIds = /* @__PURE__ */ new Set();
|
|
24
|
-
for (const doc of snapshot.documents) {
|
|
25
|
-
docIds.add(doc.state.id);
|
|
26
|
-
const existing = txn.get(doc.state.id);
|
|
27
|
-
if (isEqual(existing, doc.state)) continue;
|
|
28
|
-
txn.set(doc.state.id, doc.state);
|
|
29
|
-
}
|
|
30
|
-
for (const id of txn.keys()) {
|
|
31
|
-
if (!docIds.has(id)) {
|
|
32
|
-
txn.delete(id);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
txn.setSchema(snapshot.schema);
|
|
36
|
-
schema.migrateStorage(txn);
|
|
37
|
-
}
|
|
38
|
-
function convertStoreSnapshotToRoomSnapshot(snapshot) {
|
|
39
|
-
if ("documents" in snapshot) return snapshot;
|
|
40
|
-
return {
|
|
41
|
-
clock: 0,
|
|
42
|
-
documentClock: 0,
|
|
43
|
-
documents: objectMapValues(snapshot.store).map((state) => ({
|
|
44
|
-
state,
|
|
45
|
-
lastChangedClock: 0
|
|
46
|
-
})),
|
|
47
|
-
schema: snapshot.schema,
|
|
48
|
-
tombstones: {}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
export {
|
|
52
|
-
convertStoreSnapshotToRoomSnapshot,
|
|
53
|
-
loadSnapshotIntoStorage,
|
|
54
|
-
toNetworkDiff
|
|
55
|
-
};
|
|
56
|
-
//# sourceMappingURL=TLSyncStorage.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/lib/TLSyncStorage.ts"],
|
|
4
|
-
"sourcesContent": ["import { StoreSchema, SynchronousStorage, UnknownRecord } from '@tldraw/store'\nimport { isEqual, objectMapEntriesIterable, objectMapValues } from '@tldraw/utils'\nimport { TLStoreSnapshot } from 'tldraw'\nimport { diffRecord, NetworkDiff, RecordOpType } from './diff'\nimport { RoomSnapshot } from './TLSyncRoom'\n\n/**\n * Transaction interface for storage operations. Provides methods to read and modify\n * documents, tombstones, and metadata within a transaction.\n *\n * @public\n */\nexport interface TLSyncStorageTransaction<R extends UnknownRecord> extends SynchronousStorage<R> {\n\t/**\n\t * Get the current clock value.\n\t * If the clock has incremented during the transaction,\n\t * the incremented value will be returned.\n\t *\n\t * @returns The current clock value\n\t */\n\tgetClock(): number\n\n\t/**\n\t * Get all changes (document updates and deletions) since a given clock time.\n\t * This is the main method for calculating diffs for client sync.\n\t *\n\t * @param sinceClock - The clock time to get changes since\n\t * @returns Changes since the specified clock time\n\t */\n\tgetChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined\n}\n\n/**\n * Options for a transaction.\n * @public\n */\nexport interface TLSyncStorageTransactionOptions {\n\t/**\n\t * Use this if you need to identify the transaction for logging or debugging purposes\n\t * or for ignoring certain changes in onChange callbacks\n\t */\n\tid?: string\n\t/**\n\t * Controls when the storage layer should emit the actual changes that occurred during the transaction.\n\t *\n\t * - `'always'` - Always emit the changes, regardless of whether they were applied verbatim\n\t * - `'when-different'` - Only emit changes if the storage layer modified/embellished the records\n\t * (e.g., added server timestamps, normalized data, etc.)\n\t *\n\t * When changes are emitted, they will be available in the `changes` field of the transaction result.\n\t * This is useful when the storage layer may transform records and the caller needs to know\n\t * what actually changed rather than what was requested.\n\t */\n\temitChanges?: 'always' | 'when-different'\n}\n\n/**\n * Callback type for a transaction.\n * The conditional return type ensures that the callback is synchronous.\n * @public\n */\nexport type TLSyncStorageTransactionCallback<R extends UnknownRecord, T> = (\n\ttxn: TLSyncStorageTransaction<R>\n) => T extends Promise<any>\n\t? {\n\t\t\t__error: 'Transaction callbacks cannot be async. Use synchronous operations only.'\n\t\t}\n\t: T\n\n/**\n * Pluggable synchronous transactional storage layer for TLSyncRoom.\n * Provides methods for managing documents, tombstones, and clocks within transactions.\n *\n * @public\n */\nexport interface TLSyncStorage<R extends UnknownRecord> {\n\ttransaction<T>(\n\t\tcallback: TLSyncStorageTransactionCallback<R, T>,\n\t\topts?: TLSyncStorageTransactionOptions\n\t): TLSyncStorageTransactionResult<T, R>\n\n\tgetClock(): number\n\n\tonChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void\n\n\tgetSnapshot?(): RoomSnapshot\n}\n\n/**\n * Properties passed to the onChange callback.\n * @public\n */\nexport interface TLSyncStorageOnChangeCallbackProps {\n\t/**\n\t * The ID of the transaction that caused the change.\n\t * This is useful for ignoring certain changes in onChange callbacks.\n\t */\n\tid?: string\n\tdocumentClock: number\n}\n\n/**\n * Result returned from a storage transaction.\n * @public\n */\nexport interface TLSyncStorageTransactionResult<T, R extends UnknownRecord = UnknownRecord> {\n\tdocumentClock: number\n\tdidChange: boolean\n\tresult: T\n\t/**\n\t * The actual changes that occurred during the transaction, if requested via `emitChanges` option.\n\t * This is a RecordsDiff where:\n\t * - `added` contains records that were put (we don't have \"from\" state for emitted changes)\n\t * - `removed` contains records that were deleted (with placeholder values since we only have IDs)\n\t * - `updated` is empty (emitted changes don't track before/after pairs)\n\t *\n\t * Only populated when:\n\t * - `emitChanges: 'always'` was specified, or\n\t * - `emitChanges: 'when-different'` was specified and the storage layer modified records\n\t */\n\tchanges?: TLSyncForwardDiff<R>\n}\n\n/**\n * Respresents a diff of puts and deletes.\n * @public\n */\nexport interface TLSyncForwardDiff<R extends UnknownRecord> {\n\tputs: Record<string, R | [before: R, after: R]>\n\tdeletes: string[]\n}\n\n/**\n * @internal\n */\nexport function toNetworkDiff<R extends UnknownRecord>(diff: TLSyncForwardDiff<R>): NetworkDiff<R> {\n\tconst networkDiff: NetworkDiff<R> = {}\n\tfor (const [id, put] of objectMapEntriesIterable(diff.puts)) {\n\t\tif (Array.isArray(put)) {\n\t\t\tconst patch = diffRecord(put[0], put[1])\n\t\t\tif (patch) {\n\t\t\t\tnetworkDiff[id] = [RecordOpType.Patch, patch]\n\t\t\t}\n\t\t} else {\n\t\t\tnetworkDiff[id] = [RecordOpType.Put, put]\n\t\t}\n\t}\n\tfor (const id of diff.deletes) {\n\t\tnetworkDiff[id] = [RecordOpType.Remove]\n\t}\n\treturn networkDiff\n}\n\n/**\n * Result returned from getChangesSince, containing all changes since a given clock time.\n * @public\n */\nexport interface TLSyncStorageGetChangesSinceResult<R extends UnknownRecord> {\n\t/**\n\t * The changes as a TLSyncForwardDiff.\n\t */\n\tdiff: TLSyncForwardDiff<R>\n\t/**\n\t * If true, the client should wipe all local data and replace with the server's state.\n\t * This happens when the client's clock is too old and we've lost tombstone history.\n\t */\n\twipeAll: boolean\n}\n\n/**\n * Loads a snapshot into storage during a transaction.\n * Migrates the snapshot to the current schema and loads it into storage.\n *\n * @public\n * @param txn - The transaction to load the snapshot into\n * @param schema - The current schema\n * @param snapshot - The snapshot to load\n */\nexport function loadSnapshotIntoStorage<R extends UnknownRecord>(\n\ttxn: TLSyncStorageTransaction<R>,\n\tschema: StoreSchema<R, any>,\n\tsnapshot: RoomSnapshot | TLStoreSnapshot\n) {\n\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\tassert(snapshot.schema, 'Schema is required')\n\tconst docIds = new Set<string>()\n\tfor (const doc of snapshot.documents) {\n\t\tdocIds.add(doc.state.id)\n\t\tconst existing = txn.get(doc.state.id)\n\t\tif (isEqual(existing, doc.state)) continue\n\t\ttxn.set(doc.state.id, doc.state as R)\n\t}\n\tfor (const id of txn.keys()) {\n\t\tif (!docIds.has(id)) {\n\t\t\ttxn.delete(id)\n\t\t}\n\t}\n\ttxn.setSchema(snapshot.schema)\n\tschema.migrateStorage(txn)\n}\n\nexport function convertStoreSnapshotToRoomSnapshot(\n\tsnapshot: RoomSnapshot | TLStoreSnapshot\n): RoomSnapshot {\n\tif ('documents' in snapshot) return snapshot\n\treturn {\n\t\tclock: 0,\n\t\tdocumentClock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,SAAS,0BAA0B,uBAAuB;AAEnE,SAAS,YAAyB,oBAAoB;AAoI/C,SAAS,cAAuC,MAA4C;AAClG,QAAM,cAA8B,CAAC;AACrC,aAAW,CAAC,IAAI,GAAG,KAAK,yBAAyB,KAAK,IAAI,GAAG;AAC5D,QAAI,MAAM,QAAQ,GAAG,GAAG;AACvB,YAAM,QAAQ,WAAW,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AACvC,UAAI,OAAO;AACV,oBAAY,EAAE,IAAI,CAAC,aAAa,OAAO,KAAK;AAAA,MAC7C;AAAA,IACD,OAAO;AACN,kBAAY,EAAE,IAAI,CAAC,aAAa,KAAK,GAAG;AAAA,IACzC;AAAA,EACD;AACA,aAAW,MAAM,KAAK,SAAS;AAC9B,gBAAY,EAAE,IAAI,CAAC,aAAa,MAAM;AAAA,EACvC;AACA,SAAO;AACR;AA2BO,SAAS,wBACf,KACA,QACA,UACC;AACD,aAAW,mCAAmC,QAAQ;AACtD,SAAO,SAAS,QAAQ,oBAAoB;AAC5C,QAAM,SAAS,oBAAI,IAAY;AAC/B,aAAW,OAAO,SAAS,WAAW;AACrC,WAAO,IAAI,IAAI,MAAM,EAAE;AACvB,UAAM,WAAW,IAAI,IAAI,IAAI,MAAM,EAAE;AACrC,QAAI,QAAQ,UAAU,IAAI,KAAK,EAAG;AAClC,QAAI,IAAI,IAAI,MAAM,IAAI,IAAI,KAAU;AAAA,EACrC;AACA,aAAW,MAAM,IAAI,KAAK,GAAG;AAC5B,QAAI,CAAC,OAAO,IAAI,EAAE,GAAG;AACpB,UAAI,OAAO,EAAE;AAAA,IACd;AAAA,EACD;AACA,MAAI,UAAU,SAAS,MAAM;AAC7B,SAAO,eAAe,GAAG;AAC1B;AAEO,SAAS,mCACf,UACe;AACf,MAAI,eAAe,SAAU,QAAO;AACpC,SAAO;AAAA,IACN,OAAO;AAAA,IACP,eAAe;AAAA,IACf,WAAW,gBAAgB,SAAS,KAAK,EAAE,IAAI,CAAC,WAAW;AAAA,MAC1D;AAAA,MACA,kBAAkB;AAAA,IACnB,EAAE;AAAA,IACF,QAAQ,SAAS;AAAA,IACjB,YAAY,CAAC;AAAA,EACd;AACD;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { applyObjectDiff, diffRecord } from "./diff.mjs";
|
|
2
|
-
import { TLSyncError, TLSyncErrorCloseEventReason } from "./TLSyncClient.mjs";
|
|
3
|
-
function diffAndValidateRecord(prevState, newState, recordType, legacyAppendMode = false) {
|
|
4
|
-
const diff = diffRecord(prevState, newState, legacyAppendMode);
|
|
5
|
-
if (!diff) return;
|
|
6
|
-
try {
|
|
7
|
-
recordType.validate(newState);
|
|
8
|
-
} catch (error) {
|
|
9
|
-
throw new TLSyncError(error.message, TLSyncErrorCloseEventReason.INVALID_RECORD);
|
|
10
|
-
}
|
|
11
|
-
return diff;
|
|
12
|
-
}
|
|
13
|
-
function applyAndDiffRecord(prevState, diff, recordType, legacyAppendMode = false) {
|
|
14
|
-
const newState = applyObjectDiff(prevState, diff);
|
|
15
|
-
if (newState === prevState) return;
|
|
16
|
-
const actualDiff = diffAndValidateRecord(prevState, newState, recordType, legacyAppendMode);
|
|
17
|
-
if (!actualDiff) return;
|
|
18
|
-
return [actualDiff, newState];
|
|
19
|
-
}
|
|
20
|
-
function validateRecord(state, recordType) {
|
|
21
|
-
try {
|
|
22
|
-
recordType.validate(state);
|
|
23
|
-
} catch (error) {
|
|
24
|
-
throw new TLSyncError(error.message, TLSyncErrorCloseEventReason.INVALID_RECORD);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
export {
|
|
28
|
-
applyAndDiffRecord,
|
|
29
|
-
diffAndValidateRecord,
|
|
30
|
-
validateRecord
|
|
31
|
-
};
|
|
32
|
-
//# sourceMappingURL=recordDiff.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/lib/recordDiff.ts"],
|
|
4
|
-
"sourcesContent": ["import { RecordType, UnknownRecord } from '@tldraw/store'\nimport { ObjectDiff, applyObjectDiff, diffRecord } from './diff'\nimport { TLSyncError, TLSyncErrorCloseEventReason } from './TLSyncClient'\n\n/**\n * Validate a record and compute the diff between two states.\n * Returns null if the states are identical.\n *\n * @param prevState - The previous record state\n * @param newState - The new record state\n * @param recordType - The record type definition for validation\n * @param legacyAppendMode - If true, string append operations will be converted to Put operations\n * @returns Result containing the diff and new state, or null if no changes, or validation error\n *\n * @internal\n */\nexport function diffAndValidateRecord<R extends UnknownRecord>(\n\tprevState: R,\n\tnewState: R,\n\trecordType: RecordType<R, any>,\n\tlegacyAppendMode = false\n) {\n\tconst diff = diffRecord(prevState, newState, legacyAppendMode)\n\tif (!diff) return\n\ttry {\n\t\trecordType.validate(newState)\n\t} catch (error: any) {\n\t\tthrow new TLSyncError(error.message, TLSyncErrorCloseEventReason.INVALID_RECORD)\n\t}\n\treturn diff\n}\n\n/**\n * Apply a diff to a record state, validate the result, and compute the final diff.\n * Returns null if the diff produces no changes.\n *\n * @param prevState - The previous record state\n * @param diff - The object diff to apply\n * @param recordType - The record type definition for validation\n * @param legacyAppendMode - If true, string append operations will be converted to Put operations\n * @returns Result containing the final diff and new state, or null if no changes, or validation error\n *\n * @internal\n */\nexport function applyAndDiffRecord<R extends UnknownRecord>(\n\tprevState: R,\n\tdiff: ObjectDiff,\n\trecordType: RecordType<R, any>,\n\tlegacyAppendMode = false\n): [ObjectDiff, R] | undefined {\n\tconst newState = applyObjectDiff(prevState, diff)\n\tif (newState === prevState) return\n\tconst actualDiff = diffAndValidateRecord(prevState, newState, recordType, legacyAppendMode)\n\tif (!actualDiff) return\n\treturn [actualDiff, newState]\n}\n\n/**\n * Validate a record without computing a diff. Used when creating new records.\n *\n * @param state - The record state to validate\n * @param recordType - The record type definition for validation\n * @returns Result indicating success or validation error\n *\n * @internal\n */\nexport function validateRecord<R extends UnknownRecord>(state: R, recordType: RecordType<R, any>) {\n\ttry {\n\t\trecordType.validate(state)\n\t} catch (error: any) {\n\t\tthrow new TLSyncError(error.message, TLSyncErrorCloseEventReason.INVALID_RECORD)\n\t}\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAqB,iBAAiB,kBAAkB;AACxD,SAAS,aAAa,mCAAmC;AAclD,SAAS,sBACf,WACA,UACA,YACA,mBAAmB,OAClB;AACD,QAAM,OAAO,WAAW,WAAW,UAAU,gBAAgB;AAC7D,MAAI,CAAC,KAAM;AACX,MAAI;AACH,eAAW,SAAS,QAAQ;AAAA,EAC7B,SAAS,OAAY;AACpB,UAAM,IAAI,YAAY,MAAM,SAAS,4BAA4B,cAAc;AAAA,EAChF;AACA,SAAO;AACR;AAcO,SAAS,mBACf,WACA,MACA,YACA,mBAAmB,OACW;AAC9B,QAAM,WAAW,gBAAgB,WAAW,IAAI;AAChD,MAAI,aAAa,UAAW;AAC5B,QAAM,aAAa,sBAAsB,WAAW,UAAU,YAAY,gBAAgB;AAC1F,MAAI,CAAC,WAAY;AACjB,SAAO,CAAC,YAAY,QAAQ;AAC7B;AAWO,SAAS,eAAwC,OAAU,YAAgC;AACjG,MAAI;AACH,eAAW,SAAS,KAAK;AAAA,EAC1B,SAAS,OAAY;AACpB,UAAM,IAAI,YAAY,MAAM,SAAS,4BAA4B,cAAc;AAAA,EAChF;AACD;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|