@powersync/service-core 0.18.1 → 1.7.1
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/CHANGELOG.md +29 -0
- package/dist/api/RouteAPI.d.ts +1 -1
- package/dist/api/diagnostics.js +107 -169
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +10 -73
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +1 -1
- package/dist/replication/AbstractReplicator.js +8 -76
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.js +3 -2
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +5 -5
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +5 -5
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/runner/teardown.js +3 -65
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +8 -441
- package/dist/storage/BucketStorage.js +9 -10
- package/dist/storage/BucketStorage.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +130 -0
- package/dist/storage/BucketStorageBatch.js +10 -0
- package/dist/storage/BucketStorageBatch.js.map +1 -0
- package/dist/storage/BucketStorageFactory.d.ts +145 -0
- package/dist/storage/BucketStorageFactory.js +2 -0
- package/dist/storage/BucketStorageFactory.js.map +1 -0
- package/dist/storage/ChecksumCache.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +20 -0
- package/dist/storage/PersistedSyncRulesContent.js +2 -0
- package/dist/storage/PersistedSyncRulesContent.js.map +1 -0
- package/dist/storage/ReplicationEventPayload.d.ts +1 -1
- package/dist/storage/ReplicationLock.d.ts +4 -0
- package/dist/storage/ReplicationLock.js +2 -0
- package/dist/storage/ReplicationLock.js.map +1 -0
- package/dist/storage/SourceEntity.d.ts +6 -2
- package/dist/storage/SourceTable.d.ts +2 -2
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/StorageEngine.d.ts +4 -4
- package/dist/storage/StorageEngine.js +2 -2
- package/dist/storage/StorageEngine.js.map +1 -1
- package/dist/storage/StorageProvider.d.ts +4 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +207 -0
- package/dist/storage/SyncRulesBucketStorage.js +7 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -0
- package/dist/storage/bson.d.ts +14 -3
- package/dist/storage/bson.js +18 -2
- package/dist/storage/bson.js.map +1 -1
- package/dist/storage/storage-index.d.ts +5 -0
- package/dist/storage/storage-index.js +5 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +91 -0
- package/dist/sync/BucketChecksumState.js +313 -0
- package/dist/sync/BucketChecksumState.js.map +1 -0
- package/dist/sync/sync-index.d.ts +1 -0
- package/dist/sync/sync-index.js +1 -0
- package/dist/sync/sync-index.js.map +1 -1
- package/dist/sync/sync.d.ts +7 -3
- package/dist/sync/sync.js +139 -135
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.d.ts +9 -0
- package/dist/sync/util.js +44 -0
- package/dist/sync/util.js.map +1 -1
- package/dist/util/checkpointing.d.ts +1 -1
- package/dist/util/checkpointing.js +15 -78
- package/dist/util/checkpointing.js.map +1 -1
- package/dist/util/protocol-types.d.ts +13 -4
- package/package.json +5 -5
- package/src/api/RouteAPI.ts +1 -1
- package/src/api/diagnostics.ts +1 -1
- package/src/entry/commands/compact-action.ts +2 -3
- package/src/modules/AbstractModule.ts +1 -1
- package/src/replication/AbstractReplicator.ts +7 -12
- package/src/routes/endpoints/checkpointing.ts +3 -3
- package/src/routes/endpoints/socket-route.ts +7 -5
- package/src/routes/endpoints/sync-stream.ts +8 -5
- package/src/runner/teardown.ts +1 -1
- package/src/storage/BucketStorage.ts +8 -550
- package/src/storage/BucketStorageBatch.ts +158 -0
- package/src/storage/BucketStorageFactory.ts +166 -0
- package/src/storage/ChecksumCache.ts +1 -0
- package/src/storage/PersistedSyncRulesContent.ts +26 -0
- package/src/storage/ReplicationEventPayload.ts +1 -1
- package/src/storage/ReplicationLock.ts +5 -0
- package/src/storage/SourceEntity.ts +6 -2
- package/src/storage/SourceTable.ts +1 -1
- package/src/storage/StorageEngine.ts +4 -4
- package/src/storage/StorageProvider.ts +4 -1
- package/src/storage/SyncRulesBucketStorage.ts +265 -0
- package/src/storage/bson.ts +22 -4
- package/src/storage/storage-index.ts +5 -0
- package/src/sync/BucketChecksumState.ts +392 -0
- package/src/sync/sync-index.ts +1 -0
- package/src/sync/sync.ts +182 -157
- package/src/sync/util.ts +54 -0
- package/src/util/checkpointing.ts +4 -6
- package/src/util/protocol-types.ts +16 -4
- package/test/src/auth.test.ts +5 -5
- package/test/src/sync/BucketChecksumState.test.ts +565 -0
- package/test/src/sync/util.test.ts +34 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BucketChecksum,
|
|
3
|
+
BucketChecksumState,
|
|
4
|
+
BucketChecksumStateStorage,
|
|
5
|
+
CHECKPOINT_INVALIDATE_ALL,
|
|
6
|
+
ChecksumMap,
|
|
7
|
+
OpId,
|
|
8
|
+
WatchFilterEvent
|
|
9
|
+
} from '@/index.js';
|
|
10
|
+
import { RequestParameters, SqliteJsonRow, SqliteJsonValue, SqlSyncRules } from '@powersync/service-sync-rules';
|
|
11
|
+
import { describe, expect, test } from 'vitest';
|
|
12
|
+
|
|
13
|
+
describe('BucketChecksumState', () => {
|
|
14
|
+
// Single global[] bucket.
|
|
15
|
+
// We don't care about data in these tests
|
|
16
|
+
const SYNC_RULES_GLOBAL = SqlSyncRules.fromYaml(
|
|
17
|
+
`
|
|
18
|
+
bucket_definitions:
|
|
19
|
+
global:
|
|
20
|
+
data: []
|
|
21
|
+
`,
|
|
22
|
+
{ defaultSchema: 'public' }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// global[1] and global[2]
|
|
26
|
+
const SYNC_RULES_GLOBAL_TWO = SqlSyncRules.fromYaml(
|
|
27
|
+
`
|
|
28
|
+
bucket_definitions:
|
|
29
|
+
global:
|
|
30
|
+
parameters:
|
|
31
|
+
- select 1 as id
|
|
32
|
+
- select 2 as id
|
|
33
|
+
data: []
|
|
34
|
+
`,
|
|
35
|
+
{ defaultSchema: 'public' }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// by_project[n]
|
|
39
|
+
const SYNC_RULES_DYNAMIC = SqlSyncRules.fromYaml(
|
|
40
|
+
`
|
|
41
|
+
bucket_definitions:
|
|
42
|
+
by_project:
|
|
43
|
+
parameters: select id from projects where user_id = request.user_id()
|
|
44
|
+
data: []
|
|
45
|
+
`,
|
|
46
|
+
{ defaultSchema: 'public' }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
test('global bucket with update', async () => {
|
|
50
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
51
|
+
// Set intial state
|
|
52
|
+
storage.updateTestChecksum({ bucket: 'global[]', checksum: 1, count: 1 });
|
|
53
|
+
|
|
54
|
+
const state = new BucketChecksumState({
|
|
55
|
+
syncParams: new RequestParameters({ sub: '' }, {}),
|
|
56
|
+
syncRules: SYNC_RULES_GLOBAL,
|
|
57
|
+
bucketStorage: storage
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const line = (await state.buildNextCheckpointLine({
|
|
61
|
+
base: { checkpoint: '1', lsn: '1' },
|
|
62
|
+
writeCheckpoint: null,
|
|
63
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
64
|
+
}))!;
|
|
65
|
+
expect(line.checkpointLine).toEqual({
|
|
66
|
+
checkpoint: {
|
|
67
|
+
buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3 }],
|
|
68
|
+
last_op_id: '1',
|
|
69
|
+
write_checkpoint: undefined
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
expect(line.bucketsToFetch).toEqual([
|
|
73
|
+
{
|
|
74
|
+
bucket: 'global[]',
|
|
75
|
+
priority: 3
|
|
76
|
+
}
|
|
77
|
+
]);
|
|
78
|
+
// This is the bucket data to be fetched
|
|
79
|
+
expect(state.getFilteredBucketPositions(line.bucketsToFetch)).toEqual(new Map([['global[]', '0']]));
|
|
80
|
+
|
|
81
|
+
// This similuates the bucket data being sent
|
|
82
|
+
state.updateBucketPosition({ bucket: 'global[]', nextAfter: '1', hasMore: false });
|
|
83
|
+
|
|
84
|
+
// Update bucket storage state
|
|
85
|
+
storage.updateTestChecksum({ bucket: 'global[]', checksum: 2, count: 2 });
|
|
86
|
+
|
|
87
|
+
// Now we get a new line
|
|
88
|
+
const line2 = (await state.buildNextCheckpointLine({
|
|
89
|
+
base: { checkpoint: '2', lsn: '2' },
|
|
90
|
+
writeCheckpoint: null,
|
|
91
|
+
update: {
|
|
92
|
+
updatedDataBuckets: ['global[]'],
|
|
93
|
+
invalidateDataBuckets: false,
|
|
94
|
+
updatedParameterBucketDefinitions: [],
|
|
95
|
+
invalidateParameterBuckets: false
|
|
96
|
+
}
|
|
97
|
+
}))!;
|
|
98
|
+
expect(line2.checkpointLine).toEqual({
|
|
99
|
+
checkpoint_diff: {
|
|
100
|
+
removed_buckets: [],
|
|
101
|
+
updated_buckets: [{ bucket: 'global[]', checksum: 2, count: 2, priority: 3 }],
|
|
102
|
+
last_op_id: '2',
|
|
103
|
+
write_checkpoint: undefined
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
expect(state.getFilteredBucketPositions(line2.bucketsToFetch)).toEqual(new Map([['global[]', '1']]));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('global bucket with initial state', async () => {
|
|
110
|
+
// This tests the client sending an initial state
|
|
111
|
+
// This does not affect the checkpoint, but does affect the data to be fetched
|
|
112
|
+
/// (getFilteredBucketStates)
|
|
113
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
114
|
+
// Set intial state
|
|
115
|
+
storage.updateTestChecksum({ bucket: 'global[]', checksum: 1, count: 1 });
|
|
116
|
+
|
|
117
|
+
const state = new BucketChecksumState({
|
|
118
|
+
// Client sets the initial state here
|
|
119
|
+
initialBucketPositions: [{ name: 'global[]', after: '1' }],
|
|
120
|
+
syncParams: new RequestParameters({ sub: '' }, {}),
|
|
121
|
+
syncRules: SYNC_RULES_GLOBAL,
|
|
122
|
+
bucketStorage: storage
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const line = (await state.buildNextCheckpointLine({
|
|
126
|
+
base: { checkpoint: '1', lsn: '1' },
|
|
127
|
+
writeCheckpoint: null,
|
|
128
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
129
|
+
}))!;
|
|
130
|
+
expect(line.checkpointLine).toEqual({
|
|
131
|
+
checkpoint: {
|
|
132
|
+
buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3 }],
|
|
133
|
+
last_op_id: '1',
|
|
134
|
+
write_checkpoint: undefined
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
expect(line.bucketsToFetch).toEqual([
|
|
138
|
+
{
|
|
139
|
+
bucket: 'global[]',
|
|
140
|
+
priority: 3
|
|
141
|
+
}
|
|
142
|
+
]);
|
|
143
|
+
// This is the main difference between this and the previous test
|
|
144
|
+
expect(state.getFilteredBucketPositions(line.bucketsToFetch)).toEqual(new Map([['global[]', '1']]));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('multiple static buckets', async () => {
|
|
148
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
149
|
+
// Set intial state
|
|
150
|
+
storage.updateTestChecksum({ bucket: 'global[1]', checksum: 1, count: 1 });
|
|
151
|
+
storage.updateTestChecksum({ bucket: 'global[2]', checksum: 1, count: 1 });
|
|
152
|
+
|
|
153
|
+
const state = new BucketChecksumState({
|
|
154
|
+
syncParams: new RequestParameters({ sub: '' }, {}),
|
|
155
|
+
syncRules: SYNC_RULES_GLOBAL_TWO,
|
|
156
|
+
bucketStorage: storage
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const line = (await state.buildNextCheckpointLine({
|
|
160
|
+
base: { checkpoint: '1', lsn: '1' },
|
|
161
|
+
writeCheckpoint: null,
|
|
162
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
163
|
+
}))!;
|
|
164
|
+
expect(line.checkpointLine).toEqual({
|
|
165
|
+
checkpoint: {
|
|
166
|
+
buckets: [
|
|
167
|
+
{ bucket: 'global[1]', checksum: 1, count: 1, priority: 3 },
|
|
168
|
+
{ bucket: 'global[2]', checksum: 1, count: 1, priority: 3 }
|
|
169
|
+
],
|
|
170
|
+
last_op_id: '1',
|
|
171
|
+
write_checkpoint: undefined
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
expect(line.bucketsToFetch).toEqual([
|
|
175
|
+
{
|
|
176
|
+
bucket: 'global[1]',
|
|
177
|
+
priority: 3
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
bucket: 'global[2]',
|
|
181
|
+
priority: 3
|
|
182
|
+
}
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
storage.updateTestChecksum({ bucket: 'global[1]', checksum: 2, count: 2 });
|
|
186
|
+
storage.updateTestChecksum({ bucket: 'global[2]', checksum: 2, count: 2 });
|
|
187
|
+
|
|
188
|
+
const line2 = (await state.buildNextCheckpointLine({
|
|
189
|
+
base: { checkpoint: '2', lsn: '2' },
|
|
190
|
+
writeCheckpoint: null,
|
|
191
|
+
update: {
|
|
192
|
+
...CHECKPOINT_INVALIDATE_ALL,
|
|
193
|
+
updatedDataBuckets: ['global[1]', 'global[2]'],
|
|
194
|
+
invalidateDataBuckets: false
|
|
195
|
+
}
|
|
196
|
+
}))!;
|
|
197
|
+
expect(line2.checkpointLine).toEqual({
|
|
198
|
+
checkpoint_diff: {
|
|
199
|
+
removed_buckets: [],
|
|
200
|
+
updated_buckets: [
|
|
201
|
+
{ bucket: 'global[1]', checksum: 2, count: 2, priority: 3 },
|
|
202
|
+
{ bucket: 'global[2]', checksum: 2, count: 2, priority: 3 }
|
|
203
|
+
],
|
|
204
|
+
last_op_id: '2',
|
|
205
|
+
write_checkpoint: undefined
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('removing a static bucket', async () => {
|
|
211
|
+
// This tests the client sending an initial state, with a bucket that we don't have.
|
|
212
|
+
// This makes effectively no difference to the output. By not including the bucket
|
|
213
|
+
// in the output, the client will remove the bucket.
|
|
214
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
215
|
+
|
|
216
|
+
const state = new BucketChecksumState({
|
|
217
|
+
// Client sets the initial state here
|
|
218
|
+
initialBucketPositions: [{ name: 'something_here[]', after: '1' }],
|
|
219
|
+
syncParams: new RequestParameters({ sub: '' }, {}),
|
|
220
|
+
syncRules: SYNC_RULES_GLOBAL,
|
|
221
|
+
bucketStorage: storage
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
storage.updateTestChecksum({ bucket: 'global[]', checksum: 1, count: 1 });
|
|
225
|
+
|
|
226
|
+
const line = (await state.buildNextCheckpointLine({
|
|
227
|
+
base: { checkpoint: '1', lsn: '1' },
|
|
228
|
+
writeCheckpoint: null,
|
|
229
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
230
|
+
}))!;
|
|
231
|
+
expect(line.checkpointLine).toEqual({
|
|
232
|
+
checkpoint: {
|
|
233
|
+
buckets: [{ bucket: 'global[]', checksum: 1, count: 1, priority: 3 }],
|
|
234
|
+
last_op_id: '1',
|
|
235
|
+
write_checkpoint: undefined
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
expect(line.bucketsToFetch).toEqual([
|
|
239
|
+
{
|
|
240
|
+
bucket: 'global[]',
|
|
241
|
+
priority: 3
|
|
242
|
+
}
|
|
243
|
+
]);
|
|
244
|
+
expect(state.getFilteredBucketPositions(line.bucketsToFetch)).toEqual(new Map([['global[]', '0']]));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('invalidating individual bucket', async () => {
|
|
248
|
+
// We manually control the filter events here.
|
|
249
|
+
|
|
250
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
251
|
+
// Set initial state
|
|
252
|
+
storage.updateTestChecksum({ bucket: 'global[1]', checksum: 1, count: 1 });
|
|
253
|
+
storage.updateTestChecksum({ bucket: 'global[2]', checksum: 1, count: 1 });
|
|
254
|
+
|
|
255
|
+
const state = new BucketChecksumState({
|
|
256
|
+
syncParams: new RequestParameters({ sub: '' }, {}),
|
|
257
|
+
syncRules: SYNC_RULES_GLOBAL_TWO,
|
|
258
|
+
bucketStorage: storage
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// We specifically do not set this here, so that we have manual control over the events.
|
|
262
|
+
// storage.filter = state.checkpointFilter;
|
|
263
|
+
|
|
264
|
+
await state.buildNextCheckpointLine({
|
|
265
|
+
base: { checkpoint: '1', lsn: '1' },
|
|
266
|
+
writeCheckpoint: null,
|
|
267
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
state.updateBucketPosition({ bucket: 'global[1]', nextAfter: '1', hasMore: false });
|
|
271
|
+
state.updateBucketPosition({ bucket: 'global[2]', nextAfter: '1', hasMore: false });
|
|
272
|
+
|
|
273
|
+
storage.updateTestChecksum({ bucket: 'global[1]', checksum: 2, count: 2 });
|
|
274
|
+
storage.updateTestChecksum({ bucket: 'global[2]', checksum: 2, count: 2 });
|
|
275
|
+
|
|
276
|
+
const line2 = (await state.buildNextCheckpointLine({
|
|
277
|
+
base: { checkpoint: '2', lsn: '2' },
|
|
278
|
+
writeCheckpoint: null,
|
|
279
|
+
update: {
|
|
280
|
+
...CHECKPOINT_INVALIDATE_ALL,
|
|
281
|
+
// Invalidate the state for global[1] - will only re-check the single bucket.
|
|
282
|
+
// This is essentially inconsistent state, but is the simplest way to test that
|
|
283
|
+
// the filter is working.
|
|
284
|
+
updatedDataBuckets: ['global[1]'],
|
|
285
|
+
invalidateDataBuckets: false
|
|
286
|
+
}
|
|
287
|
+
}))!;
|
|
288
|
+
expect(line2.checkpointLine).toEqual({
|
|
289
|
+
checkpoint_diff: {
|
|
290
|
+
removed_buckets: [],
|
|
291
|
+
updated_buckets: [
|
|
292
|
+
// This does not include global[2], since it was not invalidated.
|
|
293
|
+
{ bucket: 'global[1]', checksum: 2, count: 2, priority: 3 }
|
|
294
|
+
],
|
|
295
|
+
last_op_id: '2',
|
|
296
|
+
write_checkpoint: undefined
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
expect(line2.bucketsToFetch).toEqual([{ bucket: 'global[1]', priority: 3 }]);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('invalidating all buckets', async () => {
|
|
303
|
+
// We manually control the filter events here.
|
|
304
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
305
|
+
|
|
306
|
+
const state = new BucketChecksumState({
|
|
307
|
+
syncParams: new RequestParameters({ sub: '' }, {}),
|
|
308
|
+
syncRules: SYNC_RULES_GLOBAL_TWO,
|
|
309
|
+
bucketStorage: storage
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// We specifically do not set this here, so that we have manual control over the events.
|
|
313
|
+
// storage.filter = state.checkpointFilter;
|
|
314
|
+
|
|
315
|
+
// Set initial state
|
|
316
|
+
storage.updateTestChecksum({ bucket: 'global[1]', checksum: 1, count: 1 });
|
|
317
|
+
storage.updateTestChecksum({ bucket: 'global[2]', checksum: 1, count: 1 });
|
|
318
|
+
|
|
319
|
+
await state.buildNextCheckpointLine({
|
|
320
|
+
base: { checkpoint: '1', lsn: '1' },
|
|
321
|
+
writeCheckpoint: null,
|
|
322
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
storage.updateTestChecksum({ bucket: 'global[1]', checksum: 2, count: 2 });
|
|
326
|
+
storage.updateTestChecksum({ bucket: 'global[2]', checksum: 2, count: 2 });
|
|
327
|
+
|
|
328
|
+
const line2 = (await state.buildNextCheckpointLine({
|
|
329
|
+
base: { checkpoint: '2', lsn: '2' },
|
|
330
|
+
writeCheckpoint: null,
|
|
331
|
+
// Invalidate the state - will re-check all buckets
|
|
332
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
333
|
+
}))!;
|
|
334
|
+
expect(line2.checkpointLine).toEqual({
|
|
335
|
+
checkpoint_diff: {
|
|
336
|
+
removed_buckets: [],
|
|
337
|
+
updated_buckets: [
|
|
338
|
+
{ bucket: 'global[1]', checksum: 2, count: 2, priority: 3 },
|
|
339
|
+
{ bucket: 'global[2]', checksum: 2, count: 2, priority: 3 }
|
|
340
|
+
],
|
|
341
|
+
last_op_id: '2',
|
|
342
|
+
write_checkpoint: undefined
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
expect(line2.bucketsToFetch).toEqual([
|
|
346
|
+
{ bucket: 'global[1]', priority: 3 },
|
|
347
|
+
{ bucket: 'global[2]', priority: 3 }
|
|
348
|
+
]);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('interrupt and resume static buckets checkpoint', async () => {
|
|
352
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
353
|
+
// Set intial state
|
|
354
|
+
storage.updateTestChecksum({ bucket: 'global[1]', checksum: 3, count: 3 });
|
|
355
|
+
storage.updateTestChecksum({ bucket: 'global[2]', checksum: 3, count: 3 });
|
|
356
|
+
|
|
357
|
+
const state = new BucketChecksumState({
|
|
358
|
+
syncParams: new RequestParameters({ sub: '' }, {}),
|
|
359
|
+
syncRules: SYNC_RULES_GLOBAL_TWO,
|
|
360
|
+
bucketStorage: storage
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const line = (await state.buildNextCheckpointLine({
|
|
364
|
+
base: { checkpoint: '3', lsn: '3' },
|
|
365
|
+
writeCheckpoint: null,
|
|
366
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
367
|
+
}))!;
|
|
368
|
+
expect(line.checkpointLine).toEqual({
|
|
369
|
+
checkpoint: {
|
|
370
|
+
buckets: [
|
|
371
|
+
{ bucket: 'global[1]', checksum: 3, count: 3, priority: 3 },
|
|
372
|
+
{ bucket: 'global[2]', checksum: 3, count: 3, priority: 3 }
|
|
373
|
+
],
|
|
374
|
+
last_op_id: '3',
|
|
375
|
+
write_checkpoint: undefined
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
expect(line.bucketsToFetch).toEqual([
|
|
379
|
+
{
|
|
380
|
+
bucket: 'global[1]',
|
|
381
|
+
priority: 3
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
bucket: 'global[2]',
|
|
385
|
+
priority: 3
|
|
386
|
+
}
|
|
387
|
+
]);
|
|
388
|
+
|
|
389
|
+
// This is the bucket data to be fetched
|
|
390
|
+
expect(state.getFilteredBucketPositions(line.bucketsToFetch)).toEqual(
|
|
391
|
+
new Map([
|
|
392
|
+
['global[1]', '0'],
|
|
393
|
+
['global[2]', '0']
|
|
394
|
+
])
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// No data changes here.
|
|
398
|
+
// We simulate partial data sent, before a checkpoint is interrupted.
|
|
399
|
+
state.updateBucketPosition({ bucket: 'global[1]', nextAfter: '3', hasMore: false });
|
|
400
|
+
state.updateBucketPosition({ bucket: 'global[2]', nextAfter: '1', hasMore: true });
|
|
401
|
+
storage.updateTestChecksum({ bucket: 'global[1]', checksum: 4, count: 4 });
|
|
402
|
+
|
|
403
|
+
const line2 = (await state.buildNextCheckpointLine({
|
|
404
|
+
base: { checkpoint: '4', lsn: '4' },
|
|
405
|
+
writeCheckpoint: null,
|
|
406
|
+
update: {
|
|
407
|
+
...CHECKPOINT_INVALIDATE_ALL,
|
|
408
|
+
invalidateDataBuckets: false,
|
|
409
|
+
updatedDataBuckets: ['global[1]']
|
|
410
|
+
}
|
|
411
|
+
}))!;
|
|
412
|
+
expect(line2.checkpointLine).toEqual({
|
|
413
|
+
checkpoint_diff: {
|
|
414
|
+
removed_buckets: [],
|
|
415
|
+
updated_buckets: [
|
|
416
|
+
{
|
|
417
|
+
bucket: 'global[1]',
|
|
418
|
+
checksum: 4,
|
|
419
|
+
count: 4,
|
|
420
|
+
priority: 3
|
|
421
|
+
}
|
|
422
|
+
],
|
|
423
|
+
last_op_id: '4',
|
|
424
|
+
write_checkpoint: undefined
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
// This should contain both buckets, even though only one changed.
|
|
428
|
+
expect(line2.bucketsToFetch).toEqual([
|
|
429
|
+
{
|
|
430
|
+
bucket: 'global[1]',
|
|
431
|
+
priority: 3
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
bucket: 'global[2]',
|
|
435
|
+
priority: 3
|
|
436
|
+
}
|
|
437
|
+
]);
|
|
438
|
+
|
|
439
|
+
expect(state.getFilteredBucketPositions(line2.bucketsToFetch)).toEqual(
|
|
440
|
+
new Map([
|
|
441
|
+
['global[1]', '3'],
|
|
442
|
+
['global[2]', '1']
|
|
443
|
+
])
|
|
444
|
+
);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('dynamic buckets with updates', async () => {
|
|
448
|
+
const storage = new MockBucketChecksumStateStorage();
|
|
449
|
+
// Set intial state
|
|
450
|
+
storage.updateTestChecksum({ bucket: 'by_project[1]', checksum: 1, count: 1 });
|
|
451
|
+
storage.updateTestChecksum({ bucket: 'by_project[2]', checksum: 1, count: 1 });
|
|
452
|
+
storage.updateTestChecksum({ bucket: 'by_project[3]', checksum: 1, count: 1 });
|
|
453
|
+
|
|
454
|
+
const state = new BucketChecksumState({
|
|
455
|
+
syncParams: new RequestParameters({ sub: 'u1' }, {}),
|
|
456
|
+
syncRules: SYNC_RULES_DYNAMIC,
|
|
457
|
+
bucketStorage: storage
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
storage.getParameterSets = async (checkpoint: OpId, lookups: SqliteJsonValue[][]): Promise<SqliteJsonRow[]> => {
|
|
461
|
+
expect(checkpoint).toEqual('1');
|
|
462
|
+
expect(lookups).toEqual([['by_project', '1', 'u1']]);
|
|
463
|
+
return [{ id: 1 }, { id: 2 }];
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const line = (await state.buildNextCheckpointLine({
|
|
467
|
+
base: { checkpoint: '1', lsn: '1' },
|
|
468
|
+
writeCheckpoint: null,
|
|
469
|
+
update: CHECKPOINT_INVALIDATE_ALL
|
|
470
|
+
}))!;
|
|
471
|
+
expect(line.checkpointLine).toEqual({
|
|
472
|
+
checkpoint: {
|
|
473
|
+
buckets: [
|
|
474
|
+
{ bucket: 'by_project[1]', checksum: 1, count: 1, priority: 3 },
|
|
475
|
+
{ bucket: 'by_project[2]', checksum: 1, count: 1, priority: 3 }
|
|
476
|
+
],
|
|
477
|
+
last_op_id: '1',
|
|
478
|
+
write_checkpoint: undefined
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
expect(line.bucketsToFetch).toEqual([
|
|
482
|
+
{
|
|
483
|
+
bucket: 'by_project[1]',
|
|
484
|
+
priority: 3
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
bucket: 'by_project[2]',
|
|
488
|
+
priority: 3
|
|
489
|
+
}
|
|
490
|
+
]);
|
|
491
|
+
// This is the bucket data to be fetched
|
|
492
|
+
expect(state.getFilteredBucketPositions(line.bucketsToFetch)).toEqual(
|
|
493
|
+
new Map([
|
|
494
|
+
['by_project[1]', '0'],
|
|
495
|
+
['by_project[2]', '0']
|
|
496
|
+
])
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
state.updateBucketPosition({ bucket: 'by_project[1]', nextAfter: '1', hasMore: false });
|
|
500
|
+
state.updateBucketPosition({ bucket: 'by_project[2]', nextAfter: '1', hasMore: false });
|
|
501
|
+
|
|
502
|
+
storage.getParameterSets = async (checkpoint: OpId, lookups: SqliteJsonValue[][]): Promise<SqliteJsonRow[]> => {
|
|
503
|
+
expect(checkpoint).toEqual('2');
|
|
504
|
+
expect(lookups).toEqual([['by_project', '1', 'u1']]);
|
|
505
|
+
return [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Now we get a new line
|
|
509
|
+
const line2 = (await state.buildNextCheckpointLine({
|
|
510
|
+
base: { checkpoint: '2', lsn: '2' },
|
|
511
|
+
writeCheckpoint: null,
|
|
512
|
+
update: {
|
|
513
|
+
invalidateDataBuckets: false,
|
|
514
|
+
updatedDataBuckets: [],
|
|
515
|
+
updatedParameterBucketDefinitions: ['by_project'],
|
|
516
|
+
invalidateParameterBuckets: false
|
|
517
|
+
}
|
|
518
|
+
}))!;
|
|
519
|
+
expect(line2.checkpointLine).toEqual({
|
|
520
|
+
checkpoint_diff: {
|
|
521
|
+
removed_buckets: [],
|
|
522
|
+
updated_buckets: [{ bucket: 'by_project[3]', checksum: 1, count: 1, priority: 3 }],
|
|
523
|
+
last_op_id: '2',
|
|
524
|
+
write_checkpoint: undefined
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
expect(state.getFilteredBucketPositions(line2.bucketsToFetch)).toEqual(new Map([['by_project[3]', '0']]));
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
class MockBucketChecksumStateStorage implements BucketChecksumStateStorage {
|
|
532
|
+
private state: ChecksumMap = new Map();
|
|
533
|
+
public filter?: (event: WatchFilterEvent) => boolean;
|
|
534
|
+
|
|
535
|
+
constructor() {}
|
|
536
|
+
|
|
537
|
+
updateTestChecksum(checksum: BucketChecksum): void {
|
|
538
|
+
this.state.set(checksum.bucket, checksum);
|
|
539
|
+
this.filter?.({ changedDataBucket: checksum.bucket });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
invalidate() {
|
|
543
|
+
this.filter?.({ invalidate: true });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async getChecksums(checkpoint: OpId, buckets: string[]): Promise<ChecksumMap> {
|
|
547
|
+
return new Map<string, BucketChecksum>(
|
|
548
|
+
buckets.map((bucket) => {
|
|
549
|
+
const checksum = this.state.get(bucket);
|
|
550
|
+
return [
|
|
551
|
+
bucket,
|
|
552
|
+
{
|
|
553
|
+
bucket: bucket,
|
|
554
|
+
checksum: checksum?.checksum ?? 0,
|
|
555
|
+
count: checksum?.count ?? 0
|
|
556
|
+
}
|
|
557
|
+
];
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async getParameterSets(checkpoint: OpId, lookups: SqliteJsonValue[][]): Promise<SqliteJsonRow[]> {
|
|
563
|
+
throw new Error('Method not implemented.');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { acquireSemaphoreAbortable } from '@/index.js';
|
|
2
|
+
import { Semaphore, SemaphoreInterface } from 'async-mutex';
|
|
3
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('acquireSemaphoreAbortable', () => {
|
|
6
|
+
test('can acquire', async () => {
|
|
7
|
+
const semaphore = new Semaphore(1);
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
|
|
10
|
+
expect(await acquireSemaphoreAbortable(semaphore, controller.signal)).not.toBe('aborted');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('can cancel', async () => {
|
|
14
|
+
const semaphore = new Semaphore(1);
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
|
|
17
|
+
const resolve = vi.fn();
|
|
18
|
+
const reject = vi.fn();
|
|
19
|
+
|
|
20
|
+
// First invocation: Lock the semaphore
|
|
21
|
+
const result = await acquireSemaphoreAbortable(semaphore, controller.signal);
|
|
22
|
+
expect(result).not.toBe('aborted');
|
|
23
|
+
const [count, release] = result as [number, SemaphoreInterface.Releaser];
|
|
24
|
+
|
|
25
|
+
acquireSemaphoreAbortable(semaphore, controller.signal).then(resolve, reject);
|
|
26
|
+
controller.abort();
|
|
27
|
+
await Promise.resolve();
|
|
28
|
+
expect(reject).not.toHaveBeenCalled();
|
|
29
|
+
expect(resolve).toHaveBeenCalledWith('aborted');
|
|
30
|
+
|
|
31
|
+
// Releasing the semaphore should not invoke resolve again
|
|
32
|
+
release();
|
|
33
|
+
});
|
|
34
|
+
});
|