@powersync/service-core 1.23.2 → 1.24.0
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 +39 -0
- package/dist/api/RouteAPI.d.ts +17 -3
- package/dist/entry/commands/compact-action.js +4 -0
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +9 -4
- package/dist/replication/AbstractReplicator.js +35 -11
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +31 -0
- package/dist/routes/endpoints/checkpointing.d.ts +62 -0
- package/dist/routes/endpoints/checkpointing.js +63 -3
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +2 -1
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +4 -1
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +2 -2
- package/dist/storage/CheckpointChecksumInvalidatedError.d.ts +12 -0
- package/dist/storage/CheckpointChecksumInvalidatedError.js +17 -0
- package/dist/storage/CheckpointChecksumInvalidatedError.js.map +1 -0
- package/dist/storage/SyncRulesBucketStorage.d.ts +26 -1
- package/dist/storage/SyncRulesBucketStorage.js +3 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +60 -3
- package/dist/storage/WriteCheckpointAPI.js +33 -0
- package/dist/storage/WriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +4 -4
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -1
- package/dist/storage/storage-index.d.ts +1 -0
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +7 -0
- package/dist/sync/BucketChecksumState.js +33 -13
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.js +34 -6
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.d.ts +5 -0
- package/dist/sync/util.js +9 -0
- package/dist/sync/util.js.map +1 -1
- package/dist/util/checkpointing.d.ts +1 -0
- package/dist/util/checkpointing.js +1 -1
- package/dist/util/checkpointing.js.map +1 -1
- package/dist/util/config/compound-config-collector.js +9 -1
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/defaults.d.ts +1 -0
- package/dist/util/config/defaults.js +1 -0
- package/dist/util/config/defaults.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -0
- package/dist/util/write-checkpoint-batcher.d.ts +1 -1
- package/dist/util/write-checkpoint-batcher.js +18 -8
- package/dist/util/write-checkpoint-batcher.js.map +1 -1
- package/package.json +5 -5
- package/src/api/RouteAPI.ts +18 -3
- package/src/entry/commands/compact-action.ts +6 -0
- package/src/replication/AbstractReplicator.ts +42 -12
- package/src/routes/endpoints/checkpointing.ts +77 -3
- package/src/routes/endpoints/socket-route.ts +2 -1
- package/src/storage/BucketStorageBatch.ts +4 -1
- package/src/storage/BucketStorageFactory.ts +2 -2
- package/src/storage/CheckpointChecksumInvalidatedError.ts +17 -0
- package/src/storage/SyncRulesBucketStorage.ts +32 -1
- package/src/storage/WriteCheckpointAPI.ts +108 -3
- package/src/storage/implementation/BucketDefinitionMapping.ts +4 -4
- package/src/storage/storage-index.ts +1 -0
- package/src/sync/BucketChecksumState.ts +38 -13
- package/src/sync/sync.ts +33 -7
- package/src/sync/util.ts +12 -0
- package/src/util/checkpointing.ts +2 -1
- package/src/util/config/compound-config-collector.ts +12 -0
- package/src/util/config/defaults.ts +1 -0
- package/src/util/config/types.ts +1 -0
- package/src/util/write-checkpoint-batcher.ts +30 -10
- package/test/src/AbstractReplicator.test.ts +147 -0
- package/test/src/config.test.ts +60 -0
- package/test/src/routes/checkpointing.test.ts +54 -0
- package/test/src/sync/BucketChecksumState.test.ts +57 -11
- package/test/src/sync/util.test.ts +13 -1
- package/test/src/util/checkpointing.test.ts +99 -20
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createWriteCheckpoint, WriteCheckpointBatcher } from '@/index.js';
|
|
1
|
+
import { createWriteCheckpoint, storage, WriteCheckpointBatcher } from '@/index.js';
|
|
2
2
|
import { describe, expect, test, vi } from 'vitest';
|
|
3
3
|
|
|
4
4
|
function deferred<T = void>() {
|
|
@@ -10,11 +10,14 @@ async function waitForAsyncWork() {
|
|
|
10
10
|
await new Promise((resolve) => setImmediate(resolve));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function createStorage() {
|
|
13
|
+
function createStorage(options?: { shouldAdvance?: (checkpoint: { user_id: string }) => boolean }) {
|
|
14
14
|
let nextCheckpoint = 1n;
|
|
15
15
|
const bucketStorage = {
|
|
16
16
|
createManagedWriteCheckpoints: vi.fn(async (checkpoints: { user_id: string }[]) => {
|
|
17
|
-
return
|
|
17
|
+
return {
|
|
18
|
+
writeCheckpoints: new Map(checkpoints.map((checkpoint) => [checkpoint.user_id, nextCheckpoint++])),
|
|
19
|
+
shouldAdvance: checkpoints.some((checkpoint) => options?.shouldAdvance?.(checkpoint) ?? true)
|
|
20
|
+
};
|
|
18
21
|
})
|
|
19
22
|
};
|
|
20
23
|
|
|
@@ -25,6 +28,31 @@ function createStorage() {
|
|
|
25
28
|
return { bucketStorage, storage };
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Mock RouteAPI.createReplicationHead. The callback persists the write-checkpoint
|
|
33
|
+
* mapping and returns { response, shouldAdvance }; the source marker (tracked by
|
|
34
|
+
* `advanceMarker`) is only forced when shouldAdvance is true. An optional gate per
|
|
35
|
+
* batch keeps the call in flight so concurrency can be exercised.
|
|
36
|
+
*/
|
|
37
|
+
function createApi(options?: { gates?: PromiseWithResolvers<void>[] }) {
|
|
38
|
+
const advanceMarker = vi.fn(async (_head: string) => undefined);
|
|
39
|
+
const createReplicationHead = vi.fn(
|
|
40
|
+
async (callback: (head: string) => Promise<{ response: unknown; shouldAdvance: boolean }>) => {
|
|
41
|
+
const batch = createReplicationHead.mock.calls.length;
|
|
42
|
+
const head = `head-${batch}`;
|
|
43
|
+
const { response, shouldAdvance } = await callback(head);
|
|
44
|
+
if (shouldAdvance) {
|
|
45
|
+
await advanceMarker(head);
|
|
46
|
+
}
|
|
47
|
+
if (options?.gates) {
|
|
48
|
+
await options.gates[batch - 1].promise;
|
|
49
|
+
}
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
return { createReplicationHead, advanceMarker };
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
function createBatcher(api: any, storage: any) {
|
|
29
57
|
return new WriteCheckpointBatcher(
|
|
30
58
|
() => api,
|
|
@@ -33,17 +61,25 @@ function createBatcher(api: any, storage: any) {
|
|
|
33
61
|
}
|
|
34
62
|
|
|
35
63
|
describe('write checkpoint batching', () => {
|
|
64
|
+
test('deduplicates managed checkpoints by greatest supplied request id per user', () => {
|
|
65
|
+
expect(
|
|
66
|
+
storage.uniqueManagedWriteCheckpoints([
|
|
67
|
+
{ user_id: 'user-a', heads: { '1': 'generated' } },
|
|
68
|
+
{ user_id: 'user-a', heads: { '1': 'stale' }, checkpoint_request_id: 41n },
|
|
69
|
+
{ user_id: 'user-a', heads: { '1': 'winner' }, checkpoint_request_id: 43n },
|
|
70
|
+
{ user_id: 'user-a', heads: { '1': 'lower' }, checkpoint_request_id: 42n },
|
|
71
|
+
{ user_id: 'user-b', heads: { '1': 'generated-b' } }
|
|
72
|
+
])
|
|
73
|
+
).toEqual([
|
|
74
|
+
{ user_id: 'user-a', heads: { '1': 'winner' }, checkpoint_request_id: 43n },
|
|
75
|
+
{ user_id: 'user-b', heads: { '1': 'generated-b' } }
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
36
79
|
test('coalesces same-turn requests and dispatches queued requests as capacity becomes available', async () => {
|
|
37
80
|
const gates = [deferred(), deferred()];
|
|
38
81
|
const { bucketStorage, storage } = createStorage();
|
|
39
|
-
const api = {
|
|
40
|
-
createReplicationHead: vi.fn(async (callback: (head: string) => Promise<unknown>) => {
|
|
41
|
-
const batch = api.createReplicationHead.mock.calls.length;
|
|
42
|
-
const result = await callback(`head-${batch}`);
|
|
43
|
-
await gates[batch - 1].promise;
|
|
44
|
-
return result;
|
|
45
|
-
})
|
|
46
|
-
};
|
|
82
|
+
const api = createApi({ gates });
|
|
47
83
|
const batcher = createBatcher(api, storage);
|
|
48
84
|
|
|
49
85
|
const firstBatch = [
|
|
@@ -92,17 +128,58 @@ describe('write checkpoint batching', () => {
|
|
|
92
128
|
]);
|
|
93
129
|
});
|
|
94
130
|
|
|
131
|
+
test('passes supplied checkpoint request ids into the storage batch', async () => {
|
|
132
|
+
const { bucketStorage, storage } = createStorage();
|
|
133
|
+
const api = createApi();
|
|
134
|
+
const batcher = createBatcher(api, storage);
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
createWriteCheckpoint({
|
|
138
|
+
userId: 'user-a',
|
|
139
|
+
clientId: 'client-1',
|
|
140
|
+
checkpointRequestId: 42n,
|
|
141
|
+
batcher
|
|
142
|
+
})
|
|
143
|
+
).resolves.toEqual({ writeCheckpoint: '1', replicationHead: 'head-1' });
|
|
144
|
+
|
|
145
|
+
expect(bucketStorage.createManagedWriteCheckpoints).toHaveBeenCalledWith([
|
|
146
|
+
{
|
|
147
|
+
user_id: 'user-a/client-1',
|
|
148
|
+
heads: { '1': 'head-1' },
|
|
149
|
+
checkpoint_request_id: 42n
|
|
150
|
+
}
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('does not advance the source marker when storage reports no checkpoint updates', async () => {
|
|
155
|
+
const { storage } = createStorage({ shouldAdvance: () => false });
|
|
156
|
+
const api = createApi();
|
|
157
|
+
const batcher = createBatcher(api, storage);
|
|
158
|
+
|
|
159
|
+
await expect(createWriteCheckpoint({ userId: 'user-a', clientId: undefined, batcher })).resolves.toEqual({
|
|
160
|
+
writeCheckpoint: '1',
|
|
161
|
+
replicationHead: 'head-1'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(api.createReplicationHead).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(api.advanceMarker).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('advances the source marker when storage reports a checkpoint update', async () => {
|
|
169
|
+
const { storage } = createStorage();
|
|
170
|
+
const api = createApi();
|
|
171
|
+
const batcher = createBatcher(api, storage);
|
|
172
|
+
|
|
173
|
+
await createWriteCheckpoint({ userId: 'user-a', clientId: undefined, batcher });
|
|
174
|
+
|
|
175
|
+
expect(api.advanceMarker).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(api.advanceMarker).toHaveBeenCalledWith('head-1');
|
|
177
|
+
});
|
|
178
|
+
|
|
95
179
|
test('allows three executing batches and queues later requests until one completes', async () => {
|
|
96
180
|
const gates = [deferred(), deferred(), deferred(), deferred()];
|
|
97
181
|
const { storage } = createStorage();
|
|
98
|
-
const api = {
|
|
99
|
-
createReplicationHead: vi.fn(async (callback: (head: string) => Promise<unknown>) => {
|
|
100
|
-
const batch = api.createReplicationHead.mock.calls.length;
|
|
101
|
-
const result = await callback(`head-${batch}`);
|
|
102
|
-
await gates[batch - 1].promise;
|
|
103
|
-
return result;
|
|
104
|
-
})
|
|
105
|
-
};
|
|
182
|
+
const api = createApi({ gates });
|
|
106
183
|
const batcher = createBatcher(api, storage);
|
|
107
184
|
|
|
108
185
|
const first = createWriteCheckpoint({
|
|
@@ -160,11 +237,13 @@ describe('write checkpoint batching', () => {
|
|
|
160
237
|
const api = {
|
|
161
238
|
createReplicationHead: vi.fn(async () => {
|
|
162
239
|
throw error;
|
|
163
|
-
})
|
|
240
|
+
}),
|
|
241
|
+
advanceMarker: vi.fn(async () => undefined)
|
|
164
242
|
};
|
|
165
243
|
const batcher = createBatcher(api, storage);
|
|
166
244
|
|
|
167
245
|
await expect(createWriteCheckpoint({ userId: 'user-a', clientId: undefined, batcher })).rejects.toBe(error);
|
|
168
246
|
expect(api.createReplicationHead).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(api.advanceMarker).not.toHaveBeenCalled();
|
|
169
248
|
});
|
|
170
249
|
});
|