@tldraw/sync-core 5.2.0-canary.4a316fdfb2bb → 5.2.0-canary.4e00299c3e28
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/DOCS.md +662 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/TLSocketRoom.js +4 -1
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +2 -3
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +3 -2
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/TLSocketRoom.mjs +4 -1
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +2 -4
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +3 -2
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/package.json +9 -8
- package/src/lib/ClientWebSocketAdapter.test.ts +486 -508
- package/src/lib/DurableObjectSqliteSyncWrapper.test.ts +102 -0
- package/src/lib/InMemorySyncStorage.test.ts +294 -0
- package/src/lib/MicrotaskNotifier.test.ts +79 -254
- package/src/lib/{NodeSqliteSyncWrapper.test.ts → NodeSqliteWrapper.test.ts} +29 -25
- package/src/lib/SQLiteSyncStorage.test.ts +239 -0
- package/src/lib/ServerSocketAdapter.test.ts +60 -199
- package/src/lib/TLSocketRoom.ts +6 -1
- package/src/lib/TLSyncClient.test.ts +1127 -846
- package/src/lib/TLSyncClient.ts +4 -4
- package/src/lib/TLSyncRoom.ts +5 -2
- package/src/lib/TLSyncStorage.test.ts +225 -0
- package/src/lib/chunk.test.ts +372 -0
- package/src/lib/diff.test.ts +885 -0
- package/src/lib/interval.test.ts +43 -0
- package/src/lib/protocol.test.ts +43 -0
- package/src/lib/recordDiff.test.ts +159 -0
- package/src/test/TLSocketRoom.test.ts +1109 -894
- package/src/test/TLSyncRoom.test.ts +1735 -893
- package/src/test/storageContractSuite.ts +618 -0
- package/src/test/upgradeDowngrade.test.ts +137 -37
- package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
- package/src/lib/RoomSession.test.ts +0 -101
- package/src/lib/computeTombstonePruning.test.ts +0 -352
- package/src/lib/server-types.test.ts +0 -44
- package/src/test/InMemorySyncStorage.test.ts +0 -1780
- package/src/test/SQLiteSyncStorage.test.ts +0 -1485
- package/src/test/chunk.test.ts +0 -385
- package/src/test/customMessages.test.ts +0 -36
- package/src/test/diff.test.ts +0 -784
- package/src/test/presenceMode.test.ts +0 -149
- package/src/test/validation.test.ts +0 -186
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { DurableObjectSqliteSyncWrapper } from './DurableObjectSqliteSyncWrapper'
|
|
3
|
+
|
|
4
|
+
// A minimal fake of the Durable Object storage API: `sql.exec` records each
|
|
5
|
+
// call and returns an iterable cursor with `toArray()`, and `transactionSync`
|
|
6
|
+
// just invokes the callback.
|
|
7
|
+
function makeFakeStorage(rows: unknown[] = []) {
|
|
8
|
+
const calls: { sql: string; bindings: unknown[] }[] = []
|
|
9
|
+
const storage = {
|
|
10
|
+
sql: {
|
|
11
|
+
exec(sql: string, ...bindings: unknown[]) {
|
|
12
|
+
calls.push({ sql, bindings })
|
|
13
|
+
return {
|
|
14
|
+
[Symbol.iterator]() {
|
|
15
|
+
return rows[Symbol.iterator]()
|
|
16
|
+
},
|
|
17
|
+
toArray() {
|
|
18
|
+
return [...rows]
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
transactionSync: vi.fn(<T>(callback: () => T): T => callback()),
|
|
24
|
+
}
|
|
25
|
+
return { storage, calls }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('DurableObjectSqliteSyncWrapper', () => {
|
|
29
|
+
describe('prepare', () => {
|
|
30
|
+
it('[DO1] iterate re-executes the stored SQL with the given bindings on every call', () => {
|
|
31
|
+
const { storage, calls } = makeFakeStorage([{ id: 1 }, { id: 2 }])
|
|
32
|
+
const wrapper = new DurableObjectSqliteSyncWrapper(storage)
|
|
33
|
+
|
|
34
|
+
const stmt = wrapper.prepare<{ id: number }, [number]>('SELECT id FROM test WHERE value > ?')
|
|
35
|
+
expect(calls).toHaveLength(0) // nothing executed at prepare time
|
|
36
|
+
|
|
37
|
+
expect([...stmt.iterate(100)]).toEqual([{ id: 1 }, { id: 2 }])
|
|
38
|
+
expect([...stmt.iterate(200)]).toEqual([{ id: 1 }, { id: 2 }])
|
|
39
|
+
|
|
40
|
+
expect(calls).toEqual([
|
|
41
|
+
{ sql: 'SELECT id FROM test WHERE value > ?', bindings: [100] },
|
|
42
|
+
{ sql: 'SELECT id FROM test WHERE value > ?', bindings: [200] },
|
|
43
|
+
])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('[DO1] all re-executes the stored SQL with the given bindings and returns toArray()', () => {
|
|
47
|
+
const { storage, calls } = makeFakeStorage([{ name: 'alice' }])
|
|
48
|
+
const wrapper = new DurableObjectSqliteSyncWrapper(storage)
|
|
49
|
+
|
|
50
|
+
const stmt = wrapper.prepare<{ name: string }, [number]>('SELECT name FROM test WHERE id = ?')
|
|
51
|
+
expect(stmt.all(1)).toEqual([{ name: 'alice' }])
|
|
52
|
+
expect(stmt.all(2)).toEqual([{ name: 'alice' }])
|
|
53
|
+
|
|
54
|
+
expect(calls).toEqual([
|
|
55
|
+
{ sql: 'SELECT name FROM test WHERE id = ?', bindings: [1] },
|
|
56
|
+
{ sql: 'SELECT name FROM test WHERE id = ?', bindings: [2] },
|
|
57
|
+
])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('[DO1] run re-executes the stored SQL with the given bindings', () => {
|
|
61
|
+
const { storage, calls } = makeFakeStorage()
|
|
62
|
+
const wrapper = new DurableObjectSqliteSyncWrapper(storage)
|
|
63
|
+
|
|
64
|
+
const stmt = wrapper.prepare<void, [number, string]>(
|
|
65
|
+
'INSERT INTO test (id, name) VALUES (?, ?)'
|
|
66
|
+
)
|
|
67
|
+
stmt.run(1, 'alice')
|
|
68
|
+
stmt.run(2, 'bob')
|
|
69
|
+
|
|
70
|
+
expect(calls).toEqual([
|
|
71
|
+
{ sql: 'INSERT INTO test (id, name) VALUES (?, ?)', bindings: [1, 'alice'] },
|
|
72
|
+
{ sql: 'INSERT INTO test (id, name) VALUES (?, ?)', bindings: [2, 'bob'] },
|
|
73
|
+
])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe('exec', () => {
|
|
78
|
+
it('[DO1] forwards to sql.exec', () => {
|
|
79
|
+
const { storage, calls } = makeFakeStorage()
|
|
80
|
+
const wrapper = new DurableObjectSqliteSyncWrapper(storage)
|
|
81
|
+
|
|
82
|
+
wrapper.exec('CREATE TABLE test (id INTEGER PRIMARY KEY)')
|
|
83
|
+
|
|
84
|
+
expect(calls).toEqual([{ sql: 'CREATE TABLE test (id INTEGER PRIMARY KEY)', bindings: [] }])
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('transaction', () => {
|
|
89
|
+
it('[DO1] delegates to transactionSync and returns its result', () => {
|
|
90
|
+
const { storage } = makeFakeStorage()
|
|
91
|
+
const wrapper = new DurableObjectSqliteSyncWrapper(storage)
|
|
92
|
+
|
|
93
|
+
const callback = vi.fn(() => 'done')
|
|
94
|
+
const result = wrapper.transaction(callback)
|
|
95
|
+
|
|
96
|
+
expect(result).toBe('done')
|
|
97
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
98
|
+
expect(storage.transactionSync).toHaveBeenCalledTimes(1)
|
|
99
|
+
expect(storage.transactionSync).toHaveBeenCalledWith(callback)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { TLDOCUMENT_ID, TLRecord } from '@tldraw/tlschema'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
contractRecords,
|
|
5
|
+
makeContractSnapshot,
|
|
6
|
+
makePage,
|
|
7
|
+
registerStorageContractTests,
|
|
8
|
+
} from '../test/storageContractSuite'
|
|
9
|
+
import {
|
|
10
|
+
computeTombstonePruning,
|
|
11
|
+
InMemorySyncStorage,
|
|
12
|
+
MAX_TOMBSTONES,
|
|
13
|
+
TOMBSTONE_PRUNE_BUFFER_SIZE,
|
|
14
|
+
} from './InMemorySyncStorage'
|
|
15
|
+
|
|
16
|
+
// Helper to create a sorted tombstone array for computeTombstonePruning
|
|
17
|
+
function makeTombstones(
|
|
18
|
+
count: number,
|
|
19
|
+
clockFn: (i: number) => number = (i) => i + 1
|
|
20
|
+
): Array<{ id: string; clock: number }> {
|
|
21
|
+
return Array.from({ length: count }, (_, i) => ({ id: `doc${i}`, clock: clockFn(i) }))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('InMemorySyncStorage', () => {
|
|
25
|
+
registerStorageContractTests({
|
|
26
|
+
create: (opts) => new InMemorySyncStorage<TLRecord>(opts),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('constructor clock handling', () => {
|
|
30
|
+
it('[IM1] clamps the document clock up to the max lastChangedClock in the snapshot', () => {
|
|
31
|
+
const storage = new InMemorySyncStorage<TLRecord>({
|
|
32
|
+
snapshot: makeContractSnapshot(contractRecords, {
|
|
33
|
+
documents: [
|
|
34
|
+
{ state: contractRecords[0], lastChangedClock: 10 },
|
|
35
|
+
{ state: contractRecords[1], lastChangedClock: 3 },
|
|
36
|
+
],
|
|
37
|
+
documentClock: 3,
|
|
38
|
+
}),
|
|
39
|
+
})
|
|
40
|
+
expect(storage.getClock()).toBe(10)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('[IM1] clamps the document clock up to the max tombstone clock in the snapshot', () => {
|
|
44
|
+
const storage = new InMemorySyncStorage<TLRecord>({
|
|
45
|
+
snapshot: makeContractSnapshot(contractRecords, {
|
|
46
|
+
tombstones: { 'shape:gone': 25 },
|
|
47
|
+
documentClock: 3,
|
|
48
|
+
tombstoneHistoryStartsAtClock: 0,
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
expect(storage.getClock()).toBe(25)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('[IM2] clamps tombstoneHistoryStartsAtClock down to the document clock', () => {
|
|
55
|
+
const storage = new InMemorySyncStorage<TLRecord>({
|
|
56
|
+
snapshot: makeContractSnapshot(contractRecords, {
|
|
57
|
+
documentClock: 5,
|
|
58
|
+
tombstoneHistoryStartsAtClock: 10,
|
|
59
|
+
}),
|
|
60
|
+
})
|
|
61
|
+
expect(storage.tombstoneHistoryStartsAtClock.get()).toBe(5)
|
|
62
|
+
expect(storage.getClock()).toBe(5)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('[IM3] discards snapshot tombstones when tombstoneHistoryStartsAtClock equals the document clock', () => {
|
|
66
|
+
const storage = new InMemorySyncStorage<TLRecord>({
|
|
67
|
+
snapshot: makeContractSnapshot(contractRecords, {
|
|
68
|
+
tombstones: { 'shape:deleted1': 5 },
|
|
69
|
+
tombstoneHistoryStartsAtClock: 15,
|
|
70
|
+
documentClock: 15,
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
expect(storage.tombstones.size).toBe(0)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('[IM3] keeps snapshot tombstones when there is usable history', () => {
|
|
77
|
+
const storage = new InMemorySyncStorage<TLRecord>({
|
|
78
|
+
snapshot: makeContractSnapshot(contractRecords, {
|
|
79
|
+
tombstones: { 'shape:deleted1': 5, 'shape:deleted2': 10 },
|
|
80
|
+
tombstoneHistoryStartsAtClock: 0,
|
|
81
|
+
documentClock: 15,
|
|
82
|
+
}),
|
|
83
|
+
})
|
|
84
|
+
expect(storage.tombstones.size).toBe(2)
|
|
85
|
+
expect(storage.tombstones.get('shape:deleted1')).toBe(5)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('record freezing', () => {
|
|
90
|
+
it('[IM4] freezes records stored via set()', () => {
|
|
91
|
+
const storage = new InMemorySyncStorage<TLRecord>({
|
|
92
|
+
snapshot: makeContractSnapshot(contractRecords),
|
|
93
|
+
})
|
|
94
|
+
const page = makePage('mutable')
|
|
95
|
+
storage.transaction((txn) => txn.set(page.id, page))
|
|
96
|
+
expect(Object.isFrozen(storage.documents.get(page.id)?.state)).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('[IM4] freezes records loaded from the constructor snapshot', () => {
|
|
100
|
+
const storage = new InMemorySyncStorage<TLRecord>({
|
|
101
|
+
snapshot: makeContractSnapshot(contractRecords),
|
|
102
|
+
})
|
|
103
|
+
expect(Object.isFrozen(storage.documents.get(TLDOCUMENT_ID)?.state)).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('schema default', () => {
|
|
108
|
+
it('[IM5] assumes the earliest tldraw schema when the snapshot has none', () => {
|
|
109
|
+
const snapshot = makeContractSnapshot(contractRecords) as any
|
|
110
|
+
delete snapshot.schema
|
|
111
|
+
const storage = new InMemorySyncStorage<TLRecord>({ snapshot })
|
|
112
|
+
// the earliest serialized schema pins every sequence to version 0
|
|
113
|
+
const schema = storage.schema.get() as any
|
|
114
|
+
expect(Object.values(schema.sequences).every((v) => v === 0)).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('duplicate ids', () => {
|
|
119
|
+
it('[IM6] the last entry wins for duplicate document ids in a snapshot', () => {
|
|
120
|
+
const page1 = makePage('dupe', 'First', 'a1')
|
|
121
|
+
const page2 = makePage('dupe', 'Second', 'a2')
|
|
122
|
+
const storage = new InMemorySyncStorage<TLRecord>({
|
|
123
|
+
snapshot: {
|
|
124
|
+
documents: [
|
|
125
|
+
{ state: page1, lastChangedClock: 1 },
|
|
126
|
+
{ state: page2, lastChangedClock: 2 },
|
|
127
|
+
],
|
|
128
|
+
documentClock: 5,
|
|
129
|
+
schema: makeContractSnapshot([]).schema,
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
expect(storage.documents.get('page:dupe')).toEqual({ state: page2, lastChangedClock: 2 })
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('computeTombstonePruning', () => {
|
|
137
|
+
describe('threshold', () => {
|
|
138
|
+
it('[TP1] returns null when the count is below maxTombstones', () => {
|
|
139
|
+
expect(
|
|
140
|
+
computeTombstonePruning({
|
|
141
|
+
tombstones: makeTombstones(100),
|
|
142
|
+
documentClock: 1000,
|
|
143
|
+
maxTombstones: 500,
|
|
144
|
+
})
|
|
145
|
+
).toBeNull()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('[TP1] returns null when the count equals maxTombstones exactly', () => {
|
|
149
|
+
expect(
|
|
150
|
+
computeTombstonePruning({
|
|
151
|
+
tombstones: makeTombstones(500),
|
|
152
|
+
documentClock: 1000,
|
|
153
|
+
maxTombstones: 500,
|
|
154
|
+
})
|
|
155
|
+
).toBeNull()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('[TP1] returns null for an empty array', () => {
|
|
159
|
+
expect(computeTombstonePruning({ tombstones: [], documentClock: 1000 })).toBeNull()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('[TP1] does not prune at exactly MAX_TOMBSTONES with default constants', () => {
|
|
163
|
+
expect(
|
|
164
|
+
computeTombstonePruning({
|
|
165
|
+
tombstones: makeTombstones(MAX_TOMBSTONES),
|
|
166
|
+
documentClock: 100000,
|
|
167
|
+
})
|
|
168
|
+
).toBeNull()
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('cutoff calculation', () => {
|
|
173
|
+
it('[TP2] deletes the oldest pruneBufferSize + excess tombstones', () => {
|
|
174
|
+
// 10 tombstones, max 5, buffer 2 => delete 2 + 10 - 5 = 7
|
|
175
|
+
const result = computeTombstonePruning({
|
|
176
|
+
tombstones: makeTombstones(10),
|
|
177
|
+
documentClock: 100,
|
|
178
|
+
maxTombstones: 5,
|
|
179
|
+
pruneBufferSize: 2,
|
|
180
|
+
})
|
|
181
|
+
expect(result).toEqual({
|
|
182
|
+
idsToDelete: ['doc0', 'doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6'],
|
|
183
|
+
newTombstoneHistoryStartsAtClock: 8, // clock of doc7, the oldest survivor
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('[TP2] one over MAX_TOMBSTONES deletes the buffer plus one with default constants', () => {
|
|
188
|
+
const result = computeTombstonePruning({
|
|
189
|
+
tombstones: makeTombstones(MAX_TOMBSTONES + 1),
|
|
190
|
+
documentClock: 100000,
|
|
191
|
+
})
|
|
192
|
+
expect(result!.idsToDelete.length).toBe(TOMBSTONE_PRUNE_BUFFER_SIZE + 1)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('[TP2] works with a zero buffer size', () => {
|
|
196
|
+
const result = computeTombstonePruning({
|
|
197
|
+
tombstones: makeTombstones(101),
|
|
198
|
+
documentClock: 1000,
|
|
199
|
+
maxTombstones: 100,
|
|
200
|
+
pruneBufferSize: 0,
|
|
201
|
+
})
|
|
202
|
+
expect(result!.idsToDelete).toEqual(['doc0'])
|
|
203
|
+
expect(result!.newTombstoneHistoryStartsAtClock).toBe(2)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('[TP2] extends the cutoff rather than splitting a clock value', () => {
|
|
207
|
+
const tombstones = [
|
|
208
|
+
{ id: 'a', clock: 1 },
|
|
209
|
+
{ id: 'b1', clock: 2 },
|
|
210
|
+
{ id: 'b2', clock: 2 },
|
|
211
|
+
{ id: 'b3', clock: 2 },
|
|
212
|
+
{ id: 'b4', clock: 2 },
|
|
213
|
+
{ id: 'c', clock: 3 },
|
|
214
|
+
]
|
|
215
|
+
// max 4, buffer 1 => initial cutoff 3, which would split the clock-2 group
|
|
216
|
+
const result = computeTombstonePruning({
|
|
217
|
+
tombstones,
|
|
218
|
+
documentClock: 100,
|
|
219
|
+
maxTombstones: 4,
|
|
220
|
+
pruneBufferSize: 1,
|
|
221
|
+
})
|
|
222
|
+
expect(result!.idsToDelete).toEqual(['a', 'b1', 'b2', 'b3', 'b4'])
|
|
223
|
+
expect(result!.newTombstoneHistoryStartsAtClock).toBe(3)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('[TP2] leaves the cutoff alone when it falls on a clock boundary', () => {
|
|
227
|
+
const tombstones = [
|
|
228
|
+
{ id: 'a1', clock: 1 },
|
|
229
|
+
{ id: 'a2', clock: 1 },
|
|
230
|
+
{ id: 'a3', clock: 1 },
|
|
231
|
+
{ id: 'b1', clock: 2 },
|
|
232
|
+
{ id: 'b2', clock: 2 },
|
|
233
|
+
{ id: 'b3', clock: 2 },
|
|
234
|
+
{ id: 'c1', clock: 3 },
|
|
235
|
+
{ id: 'c2', clock: 3 },
|
|
236
|
+
{ id: 'c3', clock: 3 },
|
|
237
|
+
{ id: 'd1', clock: 4 },
|
|
238
|
+
]
|
|
239
|
+
// max 5, buffer 1 => cutoff 6, which lands exactly between clocks 2 and 3
|
|
240
|
+
const result = computeTombstonePruning({
|
|
241
|
+
tombstones,
|
|
242
|
+
documentClock: 100,
|
|
243
|
+
maxTombstones: 5,
|
|
244
|
+
pruneBufferSize: 1,
|
|
245
|
+
})
|
|
246
|
+
expect(result!.idsToDelete).toEqual(['a1', 'a2', 'a3', 'b1', 'b2', 'b3'])
|
|
247
|
+
expect(result!.newTombstoneHistoryStartsAtClock).toBe(3)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('[TP2] handles non-contiguous clock values', () => {
|
|
251
|
+
const tombstones = [
|
|
252
|
+
{ id: 'a', clock: 1 },
|
|
253
|
+
{ id: 'b', clock: 5 },
|
|
254
|
+
{ id: 'c', clock: 10 },
|
|
255
|
+
{ id: 'd', clock: 50 },
|
|
256
|
+
{ id: 'e', clock: 100 },
|
|
257
|
+
]
|
|
258
|
+
const result = computeTombstonePruning({
|
|
259
|
+
tombstones,
|
|
260
|
+
documentClock: 200,
|
|
261
|
+
maxTombstones: 3,
|
|
262
|
+
pruneBufferSize: 1,
|
|
263
|
+
})
|
|
264
|
+
expect(result!.idsToDelete).toEqual(['a', 'b', 'c'])
|
|
265
|
+
expect(result!.newTombstoneHistoryStartsAtClock).toBe(50)
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
describe('history clock fallback', () => {
|
|
270
|
+
it('[TP3] uses documentClock when every tombstone is deleted', () => {
|
|
271
|
+
// max 5, buffer 10 => cutoff 15, but only 10 items exist
|
|
272
|
+
const result = computeTombstonePruning({
|
|
273
|
+
tombstones: makeTombstones(10),
|
|
274
|
+
documentClock: 999,
|
|
275
|
+
maxTombstones: 5,
|
|
276
|
+
pruneBufferSize: 10,
|
|
277
|
+
})
|
|
278
|
+
expect(result!.idsToDelete.length).toBe(10)
|
|
279
|
+
expect(result!.newTombstoneHistoryStartsAtClock).toBe(999)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('[TP3] uses documentClock when a shared clock value forces deleting everything', () => {
|
|
283
|
+
const result = computeTombstonePruning({
|
|
284
|
+
tombstones: makeTombstones(100, () => 5),
|
|
285
|
+
documentClock: 1000,
|
|
286
|
+
maxTombstones: 50,
|
|
287
|
+
pruneBufferSize: 10,
|
|
288
|
+
})
|
|
289
|
+
expect(result!.idsToDelete.length).toBe(100)
|
|
290
|
+
expect(result!.newTombstoneHistoryStartsAtClock).toBe(1000)
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
})
|