@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,95 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type TLSqliteInputValue,
|
|
3
|
-
type TLSqliteRow,
|
|
4
|
-
type TLSyncSqliteStatement,
|
|
5
|
-
type TLSyncSqliteWrapper,
|
|
6
|
-
type TLSyncSqliteWrapperConfig,
|
|
7
|
-
} from './SQLiteSyncStorage'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Mimics a prepared statement interface for Durable Objects SQLite.
|
|
11
|
-
* Rather than actually preparing the statement, it just stores the SQL and
|
|
12
|
-
* executes it fresh each time. This is still fast because DO SQLite maintains
|
|
13
|
-
* an internal LRU cache of prepared statements.
|
|
14
|
-
*/
|
|
15
|
-
class DurableObjectStatement<
|
|
16
|
-
TResult extends TLSqliteRow | void,
|
|
17
|
-
TParams extends TLSqliteInputValue[],
|
|
18
|
-
> implements TLSyncSqliteStatement<TResult, TParams>
|
|
19
|
-
{
|
|
20
|
-
constructor(
|
|
21
|
-
private sql: {
|
|
22
|
-
exec(sql: string, ...bindings: unknown[]): Iterable<any> & { toArray(): any[] }
|
|
23
|
-
},
|
|
24
|
-
private query: string
|
|
25
|
-
) {}
|
|
26
|
-
|
|
27
|
-
iterate(...bindings: TParams): IterableIterator<TResult> {
|
|
28
|
-
const result = this.sql.exec(this.query, ...bindings)
|
|
29
|
-
return result[Symbol.iterator]() as IterableIterator<TResult>
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
all(...bindings: TParams): TResult[] {
|
|
33
|
-
return this.sql.exec(this.query, ...bindings).toArray()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
run(...bindings: TParams): void {
|
|
37
|
-
this.sql.exec(this.query, ...bindings)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* A wrapper around Cloudflare Durable Object's SqlStorage that implements TLSyncSqliteWrapper.
|
|
43
|
-
*
|
|
44
|
-
* Use this wrapper with SQLiteSyncStorage to persist tldraw sync state using
|
|
45
|
-
* Cloudflare Durable Object's built-in SQLite storage. This provides automatic
|
|
46
|
-
* persistence that survives Durable Object hibernation and restarts.
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```ts
|
|
50
|
-
* import { SQLiteSyncStorage, DurableObjectSqliteSyncWrapper } from '@tldraw/sync-core'
|
|
51
|
-
*
|
|
52
|
-
* // In your Durable Object class:
|
|
53
|
-
* class MyDurableObject extends DurableObject {
|
|
54
|
-
* private storage: SQLiteSyncStorage
|
|
55
|
-
*
|
|
56
|
-
* constructor(ctx: DurableObjectState, env: Env) {
|
|
57
|
-
* super(ctx, env)
|
|
58
|
-
* const sql = new DurableObjectSqliteSyncWrapper(ctx.storage)
|
|
59
|
-
* this.storage = new SQLiteSyncStorage({ sql })
|
|
60
|
-
* }
|
|
61
|
-
* }
|
|
62
|
-
* ```
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```ts
|
|
66
|
-
* // With table prefix to avoid conflicts with other tables
|
|
67
|
-
* const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage, { tablePrefix: 'tldraw_' })
|
|
68
|
-
* // Creates tables: tldraw_documents, tldraw_tombstones, tldraw_metadata
|
|
69
|
-
* ```
|
|
70
|
-
*
|
|
71
|
-
* @public
|
|
72
|
-
*/
|
|
73
|
-
export class DurableObjectSqliteSyncWrapper implements TLSyncSqliteWrapper {
|
|
74
|
-
constructor(
|
|
75
|
-
private storage: {
|
|
76
|
-
sql: { exec(sql: string, ...bindings: unknown[]): Iterable<any> & { toArray(): any[] } }
|
|
77
|
-
transactionSync(callback: () => any): any
|
|
78
|
-
},
|
|
79
|
-
public config?: TLSyncSqliteWrapperConfig
|
|
80
|
-
) {}
|
|
81
|
-
|
|
82
|
-
exec(sql: string): void {
|
|
83
|
-
this.storage.sql.exec(sql)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
prepare<TResult extends TLSqliteRow | void = void, TParams extends TLSqliteInputValue[] = []>(
|
|
87
|
-
sql: string
|
|
88
|
-
): TLSyncSqliteStatement<TResult, TParams> {
|
|
89
|
-
return new DurableObjectStatement<TResult, TParams>(this.storage.sql, sql)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
transaction<T>(callback: () => T): T {
|
|
93
|
-
return this.storage.transactionSync(callback)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import { atom, Atom, transaction } from '@tldraw/state'
|
|
2
|
-
import { AtomMap, devFreeze, SerializedSchema, UnknownRecord } from '@tldraw/store'
|
|
3
|
-
import {
|
|
4
|
-
createTLSchema,
|
|
5
|
-
DocumentRecordType,
|
|
6
|
-
PageRecordType,
|
|
7
|
-
TLDOCUMENT_ID,
|
|
8
|
-
TLPageId,
|
|
9
|
-
} from '@tldraw/tlschema'
|
|
10
|
-
import { assert, IndexKey, objectMapEntries, throttle } from '@tldraw/utils'
|
|
11
|
-
import { MicrotaskNotifier } from './MicrotaskNotifier'
|
|
12
|
-
import { RoomSnapshot } from './TLSyncRoom'
|
|
13
|
-
import {
|
|
14
|
-
TLSyncForwardDiff,
|
|
15
|
-
TLSyncStorage,
|
|
16
|
-
TLSyncStorageGetChangesSinceResult,
|
|
17
|
-
TLSyncStorageOnChangeCallbackProps,
|
|
18
|
-
TLSyncStorageTransaction,
|
|
19
|
-
TLSyncStorageTransactionCallback,
|
|
20
|
-
TLSyncStorageTransactionOptions,
|
|
21
|
-
TLSyncStorageTransactionResult,
|
|
22
|
-
} from './TLSyncStorage'
|
|
23
|
-
|
|
24
|
-
/** @internal */
|
|
25
|
-
export const TOMBSTONE_PRUNE_BUFFER_SIZE = 1000
|
|
26
|
-
/** @internal */
|
|
27
|
-
export const MAX_TOMBSTONES = 5000
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Result of computing which tombstones to prune.
|
|
31
|
-
* @internal
|
|
32
|
-
*/
|
|
33
|
-
export interface TombstonePruneResult {
|
|
34
|
-
/** The new value for tombstoneHistoryStartsAtClock */
|
|
35
|
-
newTombstoneHistoryStartsAtClock: number
|
|
36
|
-
/** IDs of tombstones to delete */
|
|
37
|
-
idsToDelete: string[]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Computes which tombstones should be pruned, avoiding partial history for any clock value.
|
|
42
|
-
* Returns null if no pruning is needed (tombstone count <= maxTombstones).
|
|
43
|
-
*
|
|
44
|
-
* @param tombstones - Array of tombstones sorted by clock ascending (oldest first)
|
|
45
|
-
* @param documentClock - Current document clock (used as fallback if all tombstones are deleted)
|
|
46
|
-
* @param maxTombstones - Maximum number of tombstones to keep (default: MAX_TOMBSTONES)
|
|
47
|
-
* @param pruneBufferSize - Extra tombstones to prune beyond the threshold (default: TOMBSTONE_PRUNE_BUFFER_SIZE)
|
|
48
|
-
* @returns Pruning result or null if no pruning needed
|
|
49
|
-
*
|
|
50
|
-
* @internal
|
|
51
|
-
*/
|
|
52
|
-
export function computeTombstonePruning({
|
|
53
|
-
tombstones,
|
|
54
|
-
documentClock,
|
|
55
|
-
maxTombstones = MAX_TOMBSTONES,
|
|
56
|
-
pruneBufferSize = TOMBSTONE_PRUNE_BUFFER_SIZE,
|
|
57
|
-
}: {
|
|
58
|
-
tombstones: Array<{ id: string; clock: number }>
|
|
59
|
-
documentClock: number
|
|
60
|
-
maxTombstones?: number
|
|
61
|
-
pruneBufferSize?: number
|
|
62
|
-
}): TombstonePruneResult | null {
|
|
63
|
-
if (tombstones.length <= maxTombstones) {
|
|
64
|
-
return null
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Determine how many to delete, avoiding partial history for a clock value
|
|
68
|
-
let cutoff = pruneBufferSize + tombstones.length - maxTombstones
|
|
69
|
-
while (
|
|
70
|
-
cutoff < tombstones.length &&
|
|
71
|
-
tombstones[cutoff - 1]?.clock === tombstones[cutoff]?.clock
|
|
72
|
-
) {
|
|
73
|
-
cutoff++
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Set history start to the oldest remaining tombstone's clock
|
|
77
|
-
// (or documentClock if we're deleting everything)
|
|
78
|
-
const oldestRemaining = tombstones[cutoff]
|
|
79
|
-
const newTombstoneHistoryStartsAtClock = oldestRemaining?.clock ?? documentClock
|
|
80
|
-
|
|
81
|
-
// Collect the oldest tombstones to delete (first cutoff entries)
|
|
82
|
-
const idsToDelete = tombstones.slice(0, cutoff).map((t) => t.id)
|
|
83
|
-
|
|
84
|
-
return { newTombstoneHistoryStartsAtClock, idsToDelete }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Default initial snapshot for a new room.
|
|
89
|
-
* @public
|
|
90
|
-
*/
|
|
91
|
-
export const DEFAULT_INITIAL_SNAPSHOT = {
|
|
92
|
-
documentClock: 0,
|
|
93
|
-
tombstoneHistoryStartsAtClock: 0,
|
|
94
|
-
schema: createTLSchema().serialize(),
|
|
95
|
-
documents: [
|
|
96
|
-
{
|
|
97
|
-
state: DocumentRecordType.create({ id: TLDOCUMENT_ID }),
|
|
98
|
-
lastChangedClock: 0,
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
state: PageRecordType.create({
|
|
102
|
-
id: 'page:page' as TLPageId,
|
|
103
|
-
name: 'Page 1',
|
|
104
|
-
index: 'a1' as IndexKey,
|
|
105
|
-
}),
|
|
106
|
-
lastChangedClock: 0,
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* In-memory implementation of TLSyncStorage using AtomMap for documents and tombstones,
|
|
113
|
-
* and atoms for clock values. This is the default storage implementation used by TLSyncRoom.
|
|
114
|
-
*
|
|
115
|
-
* @public
|
|
116
|
-
*/
|
|
117
|
-
export class InMemorySyncStorage<R extends UnknownRecord> implements TLSyncStorage<R> {
|
|
118
|
-
/** @internal */
|
|
119
|
-
documents: AtomMap<string, { state: R; lastChangedClock: number }>
|
|
120
|
-
/** @internal */
|
|
121
|
-
tombstones: AtomMap<string, number>
|
|
122
|
-
/** @internal */
|
|
123
|
-
schema: Atom<SerializedSchema>
|
|
124
|
-
/** @internal */
|
|
125
|
-
documentClock: Atom<number>
|
|
126
|
-
/** @internal */
|
|
127
|
-
tombstoneHistoryStartsAtClock: Atom<number>
|
|
128
|
-
|
|
129
|
-
private notifier = new MicrotaskNotifier<[TLSyncStorageOnChangeCallbackProps]>()
|
|
130
|
-
onChange(callback: (arg: TLSyncStorageOnChangeCallbackProps) => unknown): () => void {
|
|
131
|
-
return this.notifier.register(callback)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
constructor({
|
|
135
|
-
snapshot = DEFAULT_INITIAL_SNAPSHOT,
|
|
136
|
-
onChange,
|
|
137
|
-
}: {
|
|
138
|
-
snapshot?: RoomSnapshot
|
|
139
|
-
onChange?(arg: TLSyncStorageOnChangeCallbackProps): unknown
|
|
140
|
-
} = {}) {
|
|
141
|
-
const maxClockValue = Math.max(
|
|
142
|
-
0,
|
|
143
|
-
...Object.values(snapshot.tombstones ?? {}),
|
|
144
|
-
...Object.values(snapshot.documents.map((d) => d.lastChangedClock))
|
|
145
|
-
)
|
|
146
|
-
this.documents = new AtomMap(
|
|
147
|
-
'room documents',
|
|
148
|
-
snapshot.documents.map((d) => [
|
|
149
|
-
d.state.id,
|
|
150
|
-
{ state: devFreeze(d.state) as R, lastChangedClock: d.lastChangedClock },
|
|
151
|
-
])
|
|
152
|
-
)
|
|
153
|
-
const documentClock = Math.max(maxClockValue, snapshot.documentClock ?? snapshot.clock ?? 0)
|
|
154
|
-
|
|
155
|
-
this.documentClock = atom('document clock', documentClock)
|
|
156
|
-
// math.min to make sure the tombstone history starts at or before the document clock
|
|
157
|
-
const tombstoneHistoryStartsAtClock = Math.min(
|
|
158
|
-
snapshot.tombstoneHistoryStartsAtClock ?? documentClock,
|
|
159
|
-
documentClock
|
|
160
|
-
)
|
|
161
|
-
this.tombstoneHistoryStartsAtClock = atom(
|
|
162
|
-
'tombstone history starts at clock',
|
|
163
|
-
tombstoneHistoryStartsAtClock
|
|
164
|
-
)
|
|
165
|
-
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
166
|
-
this.schema = atom('schema', snapshot.schema ?? createTLSchema().serializeEarliestVersion())
|
|
167
|
-
this.tombstones = new AtomMap(
|
|
168
|
-
'room tombstones',
|
|
169
|
-
// If the tombstone history starts now (or we didn't have the
|
|
170
|
-
// tombstoneHistoryStartsAtClock) then there are no tombstones
|
|
171
|
-
tombstoneHistoryStartsAtClock === documentClock
|
|
172
|
-
? []
|
|
173
|
-
: objectMapEntries(snapshot.tombstones ?? {})
|
|
174
|
-
)
|
|
175
|
-
if (onChange) {
|
|
176
|
-
this.onChange(onChange)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
transaction<T>(
|
|
181
|
-
callback: TLSyncStorageTransactionCallback<R, T>,
|
|
182
|
-
opts?: TLSyncStorageTransactionOptions
|
|
183
|
-
): TLSyncStorageTransactionResult<T, R> {
|
|
184
|
-
const clockBefore = this.documentClock.get()
|
|
185
|
-
const trackChanges = opts?.emitChanges === 'always'
|
|
186
|
-
const txn = new InMemorySyncStorageTransaction<R>(this)
|
|
187
|
-
let result: T
|
|
188
|
-
let changes: TLSyncForwardDiff<R> | undefined
|
|
189
|
-
try {
|
|
190
|
-
result = transaction(() => {
|
|
191
|
-
return callback(txn as any)
|
|
192
|
-
}) as T
|
|
193
|
-
if (trackChanges) {
|
|
194
|
-
changes = txn.getChangesSince(clockBefore)?.diff
|
|
195
|
-
}
|
|
196
|
-
} catch (error) {
|
|
197
|
-
console.error('Error in transaction', error)
|
|
198
|
-
throw error
|
|
199
|
-
} finally {
|
|
200
|
-
txn.close()
|
|
201
|
-
}
|
|
202
|
-
if (
|
|
203
|
-
typeof result === 'object' &&
|
|
204
|
-
result &&
|
|
205
|
-
'then' in result &&
|
|
206
|
-
typeof result.then === 'function'
|
|
207
|
-
) {
|
|
208
|
-
const err = new Error('Transaction must return a value, not a promise')
|
|
209
|
-
console.error(err)
|
|
210
|
-
throw err
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const clockAfter = this.documentClock.get()
|
|
214
|
-
const didChange = clockAfter > clockBefore
|
|
215
|
-
if (didChange) {
|
|
216
|
-
this.notifier.notify({ id: opts?.id, documentClock: clockAfter })
|
|
217
|
-
}
|
|
218
|
-
// InMemorySyncStorage applies changes verbatim, so we only emit changes
|
|
219
|
-
// when 'always' is specified (not for 'when-different')
|
|
220
|
-
return { documentClock: clockAfter, didChange: clockAfter > clockBefore, result, changes }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
getClock(): number {
|
|
224
|
-
return this.documentClock.get()
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** @internal */
|
|
228
|
-
pruneTombstones = throttle(
|
|
229
|
-
() => {
|
|
230
|
-
if (this.tombstones.size > MAX_TOMBSTONES) {
|
|
231
|
-
// Convert to array and sort by clock ascending (oldest first)
|
|
232
|
-
const tombstones = Array.from(this.tombstones.entries())
|
|
233
|
-
.map(([id, clock]) => ({ id, clock }))
|
|
234
|
-
.sort((a, b) => a.clock - b.clock)
|
|
235
|
-
|
|
236
|
-
const result = computeTombstonePruning({
|
|
237
|
-
tombstones,
|
|
238
|
-
documentClock: this.documentClock.get(),
|
|
239
|
-
})
|
|
240
|
-
if (result) {
|
|
241
|
-
this.tombstoneHistoryStartsAtClock.set(result.newTombstoneHistoryStartsAtClock)
|
|
242
|
-
this.tombstones.deleteMany(result.idsToDelete)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
1000,
|
|
247
|
-
// prevent this from running synchronously to avoid blocking requests
|
|
248
|
-
{ leading: false }
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
getSnapshot(): RoomSnapshot {
|
|
252
|
-
return {
|
|
253
|
-
tombstoneHistoryStartsAtClock: this.tombstoneHistoryStartsAtClock.get(),
|
|
254
|
-
documentClock: this.documentClock.get(),
|
|
255
|
-
documents: Array.from(this.documents.values()),
|
|
256
|
-
tombstones: Object.fromEntries(this.tombstones.entries()),
|
|
257
|
-
schema: this.schema.get(),
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Transaction implementation for InMemorySyncStorage.
|
|
264
|
-
* Provides access to documents, tombstones, and metadata within a transaction.
|
|
265
|
-
*
|
|
266
|
-
* @internal
|
|
267
|
-
*/
|
|
268
|
-
class InMemorySyncStorageTransaction<R extends UnknownRecord>
|
|
269
|
-
implements TLSyncStorageTransaction<R>
|
|
270
|
-
{
|
|
271
|
-
private _clock
|
|
272
|
-
private _closed = false
|
|
273
|
-
|
|
274
|
-
constructor(private storage: InMemorySyncStorage<R>) {
|
|
275
|
-
this._clock = this.storage.documentClock.get()
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** @internal */
|
|
279
|
-
close() {
|
|
280
|
-
this._closed = true
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private assertNotClosed() {
|
|
284
|
-
assert(!this._closed, 'Transaction has ended, iterator cannot be consumed')
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
getClock(): number {
|
|
288
|
-
return this._clock
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private didIncrementClock: boolean = false
|
|
292
|
-
private getNextClock(): number {
|
|
293
|
-
if (!this.didIncrementClock) {
|
|
294
|
-
this.didIncrementClock = true
|
|
295
|
-
this._clock = this.storage.documentClock.set(this.storage.documentClock.get() + 1)
|
|
296
|
-
}
|
|
297
|
-
return this._clock
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
get(id: string): R | undefined {
|
|
301
|
-
this.assertNotClosed()
|
|
302
|
-
return this.storage.documents.get(id)?.state
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
set(id: string, record: R): void {
|
|
306
|
-
this.assertNotClosed()
|
|
307
|
-
assert(id === record.id, `Record id mismatch: key does not match record.id`)
|
|
308
|
-
const clock = this.getNextClock()
|
|
309
|
-
// Automatically clear tombstone if it exists
|
|
310
|
-
if (this.storage.tombstones.has(id)) {
|
|
311
|
-
this.storage.tombstones.delete(id)
|
|
312
|
-
}
|
|
313
|
-
this.storage.documents.set(id, {
|
|
314
|
-
state: devFreeze(record) as R,
|
|
315
|
-
lastChangedClock: clock,
|
|
316
|
-
})
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
delete(id: string): void {
|
|
320
|
-
this.assertNotClosed()
|
|
321
|
-
// Only create a tombstone if the record actually exists
|
|
322
|
-
if (!this.storage.documents.has(id)) return
|
|
323
|
-
const clock = this.getNextClock()
|
|
324
|
-
this.storage.documents.delete(id)
|
|
325
|
-
this.storage.tombstones.set(id, clock)
|
|
326
|
-
this.storage.pruneTombstones()
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
*entries(): IterableIterator<[string, R]> {
|
|
330
|
-
this.assertNotClosed()
|
|
331
|
-
for (const [id, record] of this.storage.documents.entries()) {
|
|
332
|
-
this.assertNotClosed()
|
|
333
|
-
yield [id, record.state]
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
*keys(): IterableIterator<string> {
|
|
338
|
-
this.assertNotClosed()
|
|
339
|
-
for (const key of this.storage.documents.keys()) {
|
|
340
|
-
this.assertNotClosed()
|
|
341
|
-
yield key
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
*values(): IterableIterator<R> {
|
|
346
|
-
this.assertNotClosed()
|
|
347
|
-
for (const record of this.storage.documents.values()) {
|
|
348
|
-
this.assertNotClosed()
|
|
349
|
-
yield record.state
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
getSchema(): SerializedSchema {
|
|
354
|
-
this.assertNotClosed()
|
|
355
|
-
return this.storage.schema.get()
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
setSchema(schema: SerializedSchema): void {
|
|
359
|
-
this.assertNotClosed()
|
|
360
|
-
this.storage.schema.set(schema)
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
getChangesSince(sinceClock: number): TLSyncStorageGetChangesSinceResult<R> | undefined {
|
|
364
|
-
this.assertNotClosed()
|
|
365
|
-
const clock = this.storage.documentClock.get()
|
|
366
|
-
if (sinceClock === clock) return undefined
|
|
367
|
-
if (sinceClock > clock) {
|
|
368
|
-
// something went wrong, wipe the slate clean
|
|
369
|
-
sinceClock = -1
|
|
370
|
-
}
|
|
371
|
-
const diff: TLSyncForwardDiff<R> = { puts: {}, deletes: [] }
|
|
372
|
-
const wipeAll = sinceClock < this.storage.tombstoneHistoryStartsAtClock.get()
|
|
373
|
-
for (const doc of this.storage.documents.values()) {
|
|
374
|
-
if (wipeAll || doc.lastChangedClock > sinceClock) {
|
|
375
|
-
// For historical changes, we don't have "from" state, so use added
|
|
376
|
-
diff.puts[doc.state.id] = doc.state as R
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
for (const [id, clock] of this.storage.tombstones.entries()) {
|
|
380
|
-
if (clock > sinceClock) {
|
|
381
|
-
// For tombstones, we don't have the removed record, use placeholder
|
|
382
|
-
diff.deletes.push(id)
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
return { diff, wipeAll }
|
|
386
|
-
}
|
|
387
|
-
}
|