@powersync/service-core 1.11.3 → 1.12.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 +56 -0
- package/dist/api/RouteAPI.d.ts +0 -4
- package/dist/auth/CachedKeyCollector.js +2 -7
- package/dist/auth/CachedKeyCollector.js.map +1 -1
- package/dist/auth/CompoundKeyCollector.js.map +1 -1
- package/dist/auth/KeyCollector.d.ts +2 -2
- package/dist/auth/KeyStore.js +32 -14
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.d.ts +1 -0
- package/dist/auth/RemoteJWKSCollector.js +39 -16
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/auth/auth-index.d.ts +1 -0
- package/dist/auth/auth-index.js +1 -0
- package/dist/auth/auth-index.js.map +1 -1
- package/dist/auth/utils.d.ts +6 -0
- package/dist/auth/utils.js +97 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/entry/commands/compact-action.js +4 -1
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/entry/commands/migrate-action.js +4 -1
- package/dist/entry/commands/migrate-action.js.map +1 -1
- package/dist/entry/commands/test-connection-action.js +4 -1
- package/dist/entry/commands/test-connection-action.js.map +1 -1
- package/dist/routes/RouterEngine.d.ts +2 -0
- package/dist/routes/RouterEngine.js +15 -10
- package/dist/routes/RouterEngine.js.map +1 -1
- package/dist/routes/auth.d.ts +5 -16
- package/dist/routes/auth.js +6 -4
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +3 -21
- package/dist/routes/configure-fastify.js +3 -6
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/configure-rsocket.js +28 -14
- package/dist/routes/configure-rsocket.js.map +1 -1
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.d.ts +4 -28
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/route-endpoints-index.d.ts +1 -0
- package/dist/routes/endpoints/route-endpoints-index.js +1 -0
- package/dist/routes/endpoints/route-endpoints-index.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +22 -8
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +6 -6
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.d.ts +2 -14
- package/dist/routes/endpoints/sync-stream.js +28 -9
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.js +10 -6
- package/dist/routes/route-register.js.map +1 -1
- package/dist/routes/router.d.ts +8 -7
- package/dist/routes/router.js.map +1 -1
- package/dist/runner/teardown.js +4 -1
- package/dist/runner/teardown.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +40 -18
- package/dist/sync/BucketChecksumState.js +122 -74
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +22 -1
- package/dist/sync/RequestTracker.js +51 -2
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/sync.d.ts +3 -5
- package/dist/sync/sync.js +49 -34
- package/dist/sync/sync.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +19 -4
- package/dist/system/ServiceContext.js +20 -8
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +4 -33
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config/collectors/impl/yaml-env.d.ts +7 -0
- package/dist/util/config/collectors/impl/yaml-env.js +59 -0
- package/dist/util/config/collectors/impl/yaml-env.js.map +1 -0
- package/dist/util/config/compound-config-collector.js +18 -1
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +11 -0
- package/dist/util/protocol-types.d.ts +9 -9
- package/dist/util/protocol-types.js.map +1 -1
- package/dist/util/utils.d.ts +1 -1
- package/package.json +6 -7
- package/src/api/RouteAPI.ts +0 -5
- package/src/auth/CachedKeyCollector.ts +4 -6
- package/src/auth/CompoundKeyCollector.ts +2 -1
- package/src/auth/KeyCollector.ts +2 -2
- package/src/auth/KeyStore.ts +45 -20
- package/src/auth/RemoteJWKSCollector.ts +39 -16
- package/src/auth/auth-index.ts +1 -0
- package/src/auth/utils.ts +102 -0
- package/src/entry/commands/compact-action.ts +4 -1
- package/src/entry/commands/migrate-action.ts +4 -1
- package/src/entry/commands/test-connection-action.ts +4 -1
- package/src/routes/RouterEngine.ts +21 -11
- package/src/routes/auth.ts +7 -6
- package/src/routes/configure-fastify.ts +6 -8
- package/src/routes/configure-rsocket.ts +33 -18
- package/src/routes/endpoints/admin.ts +5 -5
- package/src/routes/endpoints/checkpointing.ts +2 -2
- package/src/routes/endpoints/route-endpoints-index.ts +1 -0
- package/src/routes/endpoints/socket-route.ts +27 -11
- package/src/routes/endpoints/sync-rules.ts +10 -10
- package/src/routes/endpoints/sync-stream.ts +34 -11
- package/src/routes/route-register.ts +10 -7
- package/src/routes/router.ts +11 -4
- package/src/runner/teardown.ts +5 -1
- package/src/sync/BucketChecksumState.ts +162 -77
- package/src/sync/RequestTracker.ts +70 -3
- package/src/sync/sync.ts +72 -49
- package/src/system/ServiceContext.ts +31 -12
- package/src/util/config/collectors/config-collector.ts +4 -40
- package/src/util/config/collectors/impl/yaml-env.ts +67 -0
- package/src/util/config/compound-config-collector.ts +22 -5
- package/src/util/config/types.ts +13 -0
- package/src/util/protocol-types.ts +15 -10
- package/test/src/auth.test.ts +29 -11
- package/test/src/config.test.ts +72 -0
- package/test/src/sync/BucketChecksumState.test.ts +32 -18
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -3,10 +3,15 @@ import { BucketDescription, RequestParameters, SqlSyncRules } from '@powersync/s
|
|
|
3
3
|
import * as storage from '../storage/storage-index.js';
|
|
4
4
|
import * as util from '../util/util-index.js';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ErrorCode,
|
|
8
|
+
Logger,
|
|
9
|
+
ServiceAssertionError,
|
|
10
|
+
ServiceError,
|
|
11
|
+
logger as defaultLogger
|
|
12
|
+
} from '@powersync/lib-services-framework';
|
|
7
13
|
import { JSONBig } from '@powersync/service-jsonbig';
|
|
8
14
|
import { BucketParameterQuerier } from '@powersync/service-sync-rules/src/BucketParameterQuerier.js';
|
|
9
|
-
import { BucketSyncState } from './sync.js';
|
|
10
15
|
import { SyncContext } from './SyncContext.js';
|
|
11
16
|
import { getIntersection, hasIntersection } from './util.js';
|
|
12
17
|
|
|
@@ -15,9 +20,14 @@ export interface BucketChecksumStateOptions {
|
|
|
15
20
|
bucketStorage: BucketChecksumStateStorage;
|
|
16
21
|
syncRules: SqlSyncRules;
|
|
17
22
|
syncParams: RequestParameters;
|
|
23
|
+
logger?: Logger;
|
|
18
24
|
initialBucketPositions?: { name: string; after: util.InternalOpId }[];
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
type BucketSyncState = {
|
|
28
|
+
start_op_id: util.InternalOpId;
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* Represents the state of the checksums and data for a specific connection.
|
|
23
33
|
*
|
|
@@ -30,8 +40,10 @@ export class BucketChecksumState {
|
|
|
30
40
|
/**
|
|
31
41
|
* Bucket state of bucket id -> op_id.
|
|
32
42
|
* This starts with the state from the client. May contain buckets that the user do not have access to (anymore).
|
|
43
|
+
*
|
|
44
|
+
* This is always updated in-place.
|
|
33
45
|
*/
|
|
34
|
-
public bucketDataPositions = new Map<string, BucketSyncState>();
|
|
46
|
+
public readonly bucketDataPositions = new Map<string, BucketSyncState>();
|
|
35
47
|
|
|
36
48
|
/**
|
|
37
49
|
* Last checksums sent to the client. We keep this to calculate checkpoint diffs.
|
|
@@ -47,14 +59,18 @@ export class BucketChecksumState {
|
|
|
47
59
|
*/
|
|
48
60
|
private pendingBucketDownloads = new Set<string>();
|
|
49
61
|
|
|
62
|
+
private readonly logger: Logger;
|
|
63
|
+
|
|
50
64
|
constructor(options: BucketChecksumStateOptions) {
|
|
51
65
|
this.context = options.syncContext;
|
|
52
66
|
this.bucketStorage = options.bucketStorage;
|
|
67
|
+
this.logger = options.logger ?? defaultLogger;
|
|
53
68
|
this.parameterState = new BucketParameterState(
|
|
54
69
|
options.syncContext,
|
|
55
70
|
options.bucketStorage,
|
|
56
71
|
options.syncRules,
|
|
57
|
-
options.syncParams
|
|
72
|
+
options.syncParams,
|
|
73
|
+
this.logger
|
|
58
74
|
);
|
|
59
75
|
this.bucketDataPositions = new Map();
|
|
60
76
|
|
|
@@ -63,30 +79,33 @@ export class BucketChecksumState {
|
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Build a new checkpoint line for an underlying storage checkpoint update if any buckets have changed.
|
|
84
|
+
*
|
|
85
|
+
* This call is idempotent - no internal state is updated directly when this is called.
|
|
86
|
+
*
|
|
87
|
+
* When the checkpoint line is sent to the client, call `CheckpointLine.advance()` to update the internal state.
|
|
88
|
+
* A line may be safely discarded (not sent to the client) if `advance()` is not called.
|
|
89
|
+
*
|
|
90
|
+
* @param next The updated checkpoint
|
|
91
|
+
* @returns A {@link CheckpointLine} if any of the buckets watched by this connected was updated, or otherwise `null`.
|
|
92
|
+
*/
|
|
66
93
|
async buildNextCheckpointLine(next: storage.StorageCheckpointUpdate): Promise<CheckpointLine | null> {
|
|
67
94
|
const { writeCheckpoint, base } = next;
|
|
68
|
-
const user_id = this.parameterState.syncParams.
|
|
95
|
+
const user_id = this.parameterState.syncParams.userId;
|
|
69
96
|
|
|
70
97
|
const storage = this.bucketStorage;
|
|
71
98
|
|
|
72
99
|
const update = await this.parameterState.getCheckpointUpdate(next);
|
|
73
|
-
if (update == null && this.lastWriteCheckpoint == writeCheckpoint) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
100
|
const { buckets: allBuckets, updatedBuckets } = update;
|
|
77
101
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
start_op_id: this.bucketDataPositions.get(bucket.bucket)?.start_op_id ?? 0n
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
this.bucketDataPositions = dataBucketsNew;
|
|
86
|
-
if (dataBucketsNew.size > this.context.maxBuckets) {
|
|
102
|
+
/** Set of all buckets in this checkpoint. */
|
|
103
|
+
const bucketDescriptionMap = new Map(allBuckets.map((b) => [b.bucket, b]));
|
|
104
|
+
|
|
105
|
+
if (bucketDescriptionMap.size > this.context.maxBuckets) {
|
|
87
106
|
throw new ServiceError(
|
|
88
107
|
ErrorCode.PSYNC_S2305,
|
|
89
|
-
`Too many buckets: ${
|
|
108
|
+
`Too many buckets: ${bucketDescriptionMap.size} (limit of ${this.context.maxBuckets})`
|
|
90
109
|
);
|
|
91
110
|
}
|
|
92
111
|
|
|
@@ -100,7 +119,7 @@ export class BucketChecksumState {
|
|
|
100
119
|
let checksumLookups: string[] = [];
|
|
101
120
|
|
|
102
121
|
let newChecksums = new Map<string, util.BucketChecksum>();
|
|
103
|
-
for (let bucket of
|
|
122
|
+
for (let bucket of bucketDescriptionMap.keys()) {
|
|
104
123
|
if (!updatedBuckets.has(bucket)) {
|
|
105
124
|
const existing = this.lastChecksums.get(bucket);
|
|
106
125
|
if (existing == null) {
|
|
@@ -123,7 +142,7 @@ export class BucketChecksumState {
|
|
|
123
142
|
checksumMap = newChecksums;
|
|
124
143
|
} else {
|
|
125
144
|
// Re-check all buckets
|
|
126
|
-
const bucketList = [...
|
|
145
|
+
const bucketList = [...bucketDescriptionMap.keys()];
|
|
127
146
|
checksumMap = await storage.getChecksums(base.checkpoint, bucketList);
|
|
128
147
|
}
|
|
129
148
|
|
|
@@ -132,6 +151,9 @@ export class BucketChecksumState {
|
|
|
132
151
|
|
|
133
152
|
let checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
|
|
134
153
|
|
|
154
|
+
// Log function that is deferred until the checkpoint line is sent to the client.
|
|
155
|
+
let deferredLog: () => void;
|
|
156
|
+
|
|
135
157
|
if (this.lastChecksums) {
|
|
136
158
|
// TODO: If updatedBuckets is present, we can use that to more efficiently calculate a diff,
|
|
137
159
|
// and avoid any unnecessary loops through the entire list of buckets.
|
|
@@ -160,27 +182,29 @@ export class BucketChecksumState {
|
|
|
160
182
|
|
|
161
183
|
const updatedBucketDescriptions = diff.updatedBuckets.map((e) => ({
|
|
162
184
|
...e,
|
|
163
|
-
priority:
|
|
185
|
+
priority: bucketDescriptionMap.get(e.bucket)!.priority
|
|
164
186
|
}));
|
|
165
187
|
bucketsToFetch = [...generateBucketsToFetch].map((b) => {
|
|
166
188
|
return {
|
|
167
189
|
bucket: b,
|
|
168
|
-
priority:
|
|
190
|
+
priority: bucketDescriptionMap.get(b)!.priority
|
|
169
191
|
};
|
|
170
192
|
});
|
|
171
193
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
194
|
+
deferredLog = () => {
|
|
195
|
+
let message = `Updated checkpoint: ${base.checkpoint} | `;
|
|
196
|
+
message += `write: ${writeCheckpoint} | `;
|
|
197
|
+
message += `buckets: ${allBuckets.length} | `;
|
|
198
|
+
message += `updated: ${limitedBuckets(diff.updatedBuckets, 20)} | `;
|
|
199
|
+
message += `removed: ${limitedBuckets(diff.removedBuckets, 20)}`;
|
|
200
|
+
this.logger.info(message, {
|
|
201
|
+
checkpoint: base.checkpoint,
|
|
202
|
+
user_id: user_id,
|
|
203
|
+
buckets: allBuckets.length,
|
|
204
|
+
updated: diff.updatedBuckets.length,
|
|
205
|
+
removed: diff.removedBuckets.length
|
|
206
|
+
});
|
|
207
|
+
};
|
|
184
208
|
|
|
185
209
|
checkpointLine = {
|
|
186
210
|
checkpoint_diff: {
|
|
@@ -191,9 +215,11 @@ export class BucketChecksumState {
|
|
|
191
215
|
}
|
|
192
216
|
} satisfies util.StreamingSyncCheckpointDiff;
|
|
193
217
|
} else {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
218
|
+
deferredLog = () => {
|
|
219
|
+
let message = `New checkpoint: ${base.checkpoint} | write: ${writeCheckpoint} | `;
|
|
220
|
+
message += `buckets: ${allBuckets.length} ${limitedBuckets(allBuckets, 20)}`;
|
|
221
|
+
this.logger.info(message, { checkpoint: base.checkpoint, user_id: user_id, buckets: allBuckets.length });
|
|
222
|
+
};
|
|
197
223
|
bucketsToFetch = allBuckets;
|
|
198
224
|
checkpointLine = {
|
|
199
225
|
checkpoint: {
|
|
@@ -201,53 +227,89 @@ export class BucketChecksumState {
|
|
|
201
227
|
write_checkpoint: writeCheckpoint ? String(writeCheckpoint) : undefined,
|
|
202
228
|
buckets: [...checksumMap.values()].map((e) => ({
|
|
203
229
|
...e,
|
|
204
|
-
priority:
|
|
230
|
+
priority: bucketDescriptionMap.get(e.bucket)!.priority
|
|
205
231
|
}))
|
|
206
232
|
}
|
|
207
233
|
} satisfies util.StreamingSyncCheckpoint;
|
|
208
234
|
}
|
|
209
235
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
236
|
+
const pendingBucketDownloads = new Set(bucketsToFetch.map((b) => b.bucket));
|
|
237
|
+
|
|
238
|
+
let hasAdvanced = false;
|
|
213
239
|
|
|
214
240
|
return {
|
|
215
241
|
checkpointLine,
|
|
216
|
-
bucketsToFetch
|
|
217
|
-
|
|
218
|
-
|
|
242
|
+
bucketsToFetch,
|
|
243
|
+
advance: () => {
|
|
244
|
+
hasAdvanced = true;
|
|
245
|
+
// bucketDataPositions must be updated in-place - it represents the current state of
|
|
246
|
+
// the connection, not of the checkpoint line.
|
|
247
|
+
// The following could happen:
|
|
248
|
+
// 1. A = buildCheckpointLine()
|
|
249
|
+
// 2. A.advance()
|
|
250
|
+
// 3. B = buildCheckpointLine()
|
|
251
|
+
// 4. A.updateBucketPosition()
|
|
252
|
+
// 5. B.advance()
|
|
253
|
+
// In that case, it is important that the updated bucket position for A takes effect
|
|
254
|
+
// for checkpoint B.
|
|
255
|
+
let bucketsToRemove: string[] = [];
|
|
256
|
+
for (let bucket of this.bucketDataPositions.keys()) {
|
|
257
|
+
if (!bucketDescriptionMap.has(bucket)) {
|
|
258
|
+
bucketsToRemove.push(bucket);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
for (let bucket of bucketsToRemove) {
|
|
262
|
+
this.bucketDataPositions.delete(bucket);
|
|
263
|
+
}
|
|
264
|
+
for (let bucket of allBuckets) {
|
|
265
|
+
if (!this.bucketDataPositions.has(bucket.bucket)) {
|
|
266
|
+
// Bucket the client hasn't seen before - initialize with 0.
|
|
267
|
+
this.bucketDataPositions.set(bucket.bucket, { start_op_id: 0n });
|
|
268
|
+
}
|
|
269
|
+
// If the bucket position is already present, we keep the current position.
|
|
270
|
+
}
|
|
219
271
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
*/
|
|
226
|
-
getFilteredBucketPositions(bucketsToFetch: BucketDescription[]): Map<string, util.InternalOpId> {
|
|
227
|
-
const filtered = new Map<string, util.InternalOpId>();
|
|
228
|
-
for (let bucket of bucketsToFetch) {
|
|
229
|
-
const state = this.bucketDataPositions.get(bucket.bucket);
|
|
230
|
-
if (state) {
|
|
231
|
-
filtered.set(bucket.bucket, state.start_op_id);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return filtered;
|
|
235
|
-
}
|
|
272
|
+
this.lastChecksums = checksumMap;
|
|
273
|
+
this.lastWriteCheckpoint = writeCheckpoint;
|
|
274
|
+
this.pendingBucketDownloads = pendingBucketDownloads;
|
|
275
|
+
deferredLog();
|
|
276
|
+
},
|
|
236
277
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
278
|
+
getFilteredBucketPositions: (buckets?: BucketDescription[]): Map<string, util.InternalOpId> => {
|
|
279
|
+
if (!hasAdvanced) {
|
|
280
|
+
throw new ServiceAssertionError('Call line.advance() before getFilteredBucketPositions()');
|
|
281
|
+
}
|
|
282
|
+
buckets ??= bucketsToFetch;
|
|
283
|
+
const filtered = new Map<string, util.InternalOpId>();
|
|
284
|
+
|
|
285
|
+
for (let bucket of buckets) {
|
|
286
|
+
const state = this.bucketDataPositions.get(bucket.bucket);
|
|
287
|
+
if (state) {
|
|
288
|
+
filtered.set(bucket.bucket, state.start_op_id);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return filtered;
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
updateBucketPosition: (options: { bucket: string; nextAfter: util.InternalOpId; hasMore: boolean }) => {
|
|
295
|
+
if (!hasAdvanced) {
|
|
296
|
+
throw new ServiceAssertionError('Call line.advance() before updateBucketPosition()');
|
|
297
|
+
}
|
|
298
|
+
const state = this.bucketDataPositions.get(options.bucket);
|
|
299
|
+
if (state) {
|
|
300
|
+
state.start_op_id = options.nextAfter;
|
|
301
|
+
} else {
|
|
302
|
+
// If we hit this, another checkpoint has removed the bucket in the meantime, meaning
|
|
303
|
+
// line.advance() has been called on it. In that case we don't need the bucket state anymore.
|
|
304
|
+
// It is generally not expected to happen, but we still cover the case.
|
|
305
|
+
}
|
|
306
|
+
if (!options.hasMore) {
|
|
307
|
+
// This specifically updates the per-checkpoint line. Completing a download for one line,
|
|
308
|
+
// does not remove it from the next line, since it could have new updates there.
|
|
309
|
+
pendingBucketDownloads.delete(options.bucket);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
251
313
|
}
|
|
252
314
|
}
|
|
253
315
|
|
|
@@ -274,6 +336,7 @@ export class BucketParameterState {
|
|
|
274
336
|
public readonly syncParams: RequestParameters;
|
|
275
337
|
private readonly querier: BucketParameterQuerier;
|
|
276
338
|
private readonly staticBuckets: Map<string, BucketDescription>;
|
|
339
|
+
private readonly logger: Logger;
|
|
277
340
|
private cachedDynamicBuckets: BucketDescription[] | null = null;
|
|
278
341
|
private cachedDynamicBucketSet: Set<string> | null = null;
|
|
279
342
|
|
|
@@ -283,12 +346,14 @@ export class BucketParameterState {
|
|
|
283
346
|
context: SyncContext,
|
|
284
347
|
bucketStorage: BucketChecksumStateStorage,
|
|
285
348
|
syncRules: SqlSyncRules,
|
|
286
|
-
syncParams: RequestParameters
|
|
349
|
+
syncParams: RequestParameters,
|
|
350
|
+
logger: Logger
|
|
287
351
|
) {
|
|
288
352
|
this.context = context;
|
|
289
353
|
this.bucketStorage = bucketStorage;
|
|
290
354
|
this.syncRules = syncRules;
|
|
291
355
|
this.syncParams = syncParams;
|
|
356
|
+
this.logger = logger;
|
|
292
357
|
|
|
293
358
|
this.querier = syncRules.getBucketParameterQuerier(this.syncParams);
|
|
294
359
|
this.staticBuckets = new Map<string, BucketDescription>(this.querier.staticBuckets.map((b) => [b.bucket, b]));
|
|
@@ -311,9 +376,9 @@ export class BucketParameterState {
|
|
|
311
376
|
ErrorCode.PSYNC_S2305,
|
|
312
377
|
`Too many parameter query results: ${update.buckets.length} (limit of ${this.context.maxParameterQueryResults})`
|
|
313
378
|
);
|
|
314
|
-
logger.error(error.message, {
|
|
379
|
+
this.logger.error(error.message, {
|
|
315
380
|
checkpoint: checkpoint,
|
|
316
|
-
user_id: this.syncParams.
|
|
381
|
+
user_id: this.syncParams.userId,
|
|
317
382
|
buckets: update.buckets.length
|
|
318
383
|
});
|
|
319
384
|
|
|
@@ -413,6 +478,26 @@ export class BucketParameterState {
|
|
|
413
478
|
export interface CheckpointLine {
|
|
414
479
|
checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
|
|
415
480
|
bucketsToFetch: BucketDescription[];
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Call when a checkpoint line is being sent to a client, to update the internal state.
|
|
484
|
+
*/
|
|
485
|
+
advance: () => void;
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get bucket positions to sync, given the list of buckets.
|
|
489
|
+
*
|
|
490
|
+
* @param bucketsToFetch List of buckets to fetch - either this.bucketsToFetch, or a subset of it. Defaults to this.bucketsToFetch.
|
|
491
|
+
*/
|
|
492
|
+
getFilteredBucketPositions(bucketsToFetch?: BucketDescription[]): Map<string, util.InternalOpId>;
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Update the position of bucket data the client has, after it was sent to the client.
|
|
496
|
+
*
|
|
497
|
+
* @param bucket the bucket name
|
|
498
|
+
* @param nextAfter sync operations >= this value in the next batch
|
|
499
|
+
*/
|
|
500
|
+
updateBucketPosition(options: { bucket: string; nextAfter: util.InternalOpId; hasMore: boolean }): void;
|
|
416
501
|
}
|
|
417
502
|
|
|
418
503
|
// Use a more specific type to simplify testing
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MetricsEngine } from '../metrics/MetricsEngine.js';
|
|
2
2
|
|
|
3
3
|
import { APIMetric } from '@powersync/service-types';
|
|
4
|
+
import { SyncBucketData } from '../util/protocol-types.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Record sync stats per request stream.
|
|
@@ -8,15 +9,24 @@ import { APIMetric } from '@powersync/service-types';
|
|
|
8
9
|
export class RequestTracker {
|
|
9
10
|
operationsSynced = 0;
|
|
10
11
|
dataSyncedBytes = 0;
|
|
12
|
+
operationCounts: OperationCounts = { put: 0, remove: 0, move: 0, clear: 0 };
|
|
13
|
+
largeBuckets: Record<string, number> = {};
|
|
11
14
|
|
|
12
15
|
constructor(private metrics: MetricsEngine) {
|
|
13
16
|
this.metrics = metrics;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
addOperationsSynced(operations:
|
|
17
|
-
this.operationsSynced += operations;
|
|
19
|
+
addOperationsSynced(operations: OperationsSentStats) {
|
|
20
|
+
this.operationsSynced += operations.total;
|
|
21
|
+
this.operationCounts.put += operations.operations.put;
|
|
22
|
+
this.operationCounts.remove += operations.operations.remove;
|
|
23
|
+
this.operationCounts.move += operations.operations.move;
|
|
24
|
+
this.operationCounts.clear += operations.operations.clear;
|
|
25
|
+
if (operations.total > 100 || operations.bucket in this.largeBuckets) {
|
|
26
|
+
this.largeBuckets[operations.bucket] = (this.largeBuckets[operations.bucket] ?? 0) + operations.total;
|
|
27
|
+
}
|
|
18
28
|
|
|
19
|
-
this.metrics.getCounter(APIMetric.OPERATIONS_SYNCED).add(operations);
|
|
29
|
+
this.metrics.getCounter(APIMetric.OPERATIONS_SYNCED).add(operations.total);
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
addDataSynced(bytes: number) {
|
|
@@ -24,4 +34,61 @@ export class RequestTracker {
|
|
|
24
34
|
|
|
25
35
|
this.metrics.getCounter(APIMetric.DATA_SYNCED_BYTES).add(bytes);
|
|
26
36
|
}
|
|
37
|
+
|
|
38
|
+
getLogMeta() {
|
|
39
|
+
return {
|
|
40
|
+
operations_synced: this.operationsSynced,
|
|
41
|
+
data_synced_bytes: this.dataSyncedBytes,
|
|
42
|
+
operation_counts: this.operationCounts,
|
|
43
|
+
large_buckets: this.largeBuckets
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface OperationCounts {
|
|
49
|
+
put: number;
|
|
50
|
+
remove: number;
|
|
51
|
+
move: number;
|
|
52
|
+
clear: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OperationsSentStats {
|
|
56
|
+
bucket: string;
|
|
57
|
+
operations: OperationCounts;
|
|
58
|
+
total: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function statsForBatch(batch: SyncBucketData): OperationsSentStats {
|
|
62
|
+
let put = 0;
|
|
63
|
+
let remove = 0;
|
|
64
|
+
let move = 0;
|
|
65
|
+
let clear = 0;
|
|
66
|
+
|
|
67
|
+
for (const entry of batch.data) {
|
|
68
|
+
switch (entry.op) {
|
|
69
|
+
case 'PUT':
|
|
70
|
+
put++;
|
|
71
|
+
break;
|
|
72
|
+
case 'REMOVE':
|
|
73
|
+
remove++;
|
|
74
|
+
break;
|
|
75
|
+
case 'MOVE':
|
|
76
|
+
move++;
|
|
77
|
+
break;
|
|
78
|
+
case 'CLEAR':
|
|
79
|
+
clear++;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
bucket: batch.bucket,
|
|
86
|
+
operations: {
|
|
87
|
+
put,
|
|
88
|
+
remove,
|
|
89
|
+
move,
|
|
90
|
+
clear
|
|
91
|
+
},
|
|
92
|
+
total: put + remove + move + clear
|
|
93
|
+
};
|
|
27
94
|
}
|