@powersync/service-core 1.19.2 → 1.20.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 +50 -0
- package/dist/api/diagnostics.js +11 -4
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +13 -2
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/entry/commands/config-command.js +2 -2
- package/dist/entry/commands/config-command.js.map +1 -1
- package/dist/replication/AbstractReplicator.js +2 -5
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +84 -0
- package/dist/routes/endpoints/admin.d.ts +168 -0
- package/dist/routes/endpoints/admin.js +34 -20
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +6 -9
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +6 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +21 -8
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +48 -15
- package/dist/storage/BucketStorageFactory.js +70 -1
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/ChecksumCache.d.ts +5 -2
- package/dist/storage/ChecksumCache.js +8 -4
- package/dist/storage/ChecksumCache.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +33 -3
- package/dist/storage/PersistedSyncRulesContent.js +80 -1
- package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/SourceTable.d.ts +7 -2
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/StorageVersionConfig.d.ts +53 -0
- package/dist/storage/StorageVersionConfig.js +53 -0
- package/dist/storage/StorageVersionConfig.js.map +1 -0
- package/dist/storage/SyncRulesBucketStorage.d.ts +14 -4
- package/dist/storage/SyncRulesBucketStorage.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 +8 -4
- package/dist/sync/BucketChecksumState.js +97 -52
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.d.ts +1 -0
- package/dist/sync/util.js +10 -0
- package/dist/sync/util.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +13 -0
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +4 -4
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +2 -2
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js +3 -3
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -1
- package/dist/util/config/types.js.map +1 -1
- package/dist/util/env.d.ts +1 -0
- package/dist/util/env.js +5 -0
- package/dist/util/env.js.map +1 -1
- package/package.json +6 -6
- package/src/api/diagnostics.ts +12 -4
- package/src/entry/commands/compact-action.ts +15 -2
- package/src/entry/commands/config-command.ts +3 -3
- package/src/replication/AbstractReplicator.ts +3 -5
- package/src/routes/endpoints/admin.ts +43 -25
- package/src/routes/endpoints/sync-rules.ts +14 -13
- package/src/routes/endpoints/sync-stream.ts +6 -1
- package/src/storage/BucketStorageBatch.ts +23 -9
- package/src/storage/BucketStorageFactory.ts +116 -19
- package/src/storage/ChecksumCache.ts +14 -6
- package/src/storage/PersistedSyncRulesContent.ts +119 -4
- package/src/storage/SourceTable.ts +7 -1
- package/src/storage/StorageVersionConfig.ts +78 -0
- package/src/storage/SyncRulesBucketStorage.ts +20 -4
- package/src/storage/storage-index.ts +1 -0
- package/src/sync/BucketChecksumState.ts +147 -65
- package/src/sync/sync.ts +9 -3
- package/src/sync/util.ts +10 -0
- package/src/util/config/collectors/config-collector.ts +16 -0
- package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +5 -5
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +3 -3
- package/src/util/config/sync-rules/impl/inline-sync-rules-collector.ts +4 -4
- package/src/util/config/types.ts +1 -2
- package/src/util/env.ts +5 -0
- package/test/src/checksum_cache.test.ts +102 -57
- package/test/src/config.test.ts +115 -0
- package/test/src/routes/admin.test.ts +48 -0
- package/test/src/routes/mocks.ts +22 -1
- package/test/src/routes/stream.test.ts +3 -2
- package/test/src/sync/BucketChecksumState.test.ts +332 -93
- package/test/src/utils.ts +9 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BucketDescription,
|
|
3
|
+
BucketParameterQuerier,
|
|
3
4
|
BucketPriority,
|
|
4
5
|
BucketSource,
|
|
5
6
|
HydratedSyncRules,
|
|
7
|
+
QuerierError,
|
|
6
8
|
RequestedStream,
|
|
7
9
|
RequestParameters,
|
|
8
|
-
ResolvedBucket
|
|
10
|
+
ResolvedBucket,
|
|
11
|
+
mergeBuckets
|
|
9
12
|
} from '@powersync/service-sync-rules';
|
|
10
13
|
|
|
11
14
|
import * as storage from '../storage/storage-index.js';
|
|
@@ -18,7 +21,6 @@ import {
|
|
|
18
21
|
ServiceAssertionError,
|
|
19
22
|
ServiceError
|
|
20
23
|
} from '@powersync/lib-services-framework';
|
|
21
|
-
import { BucketParameterQuerier, QuerierError } from '@powersync/service-sync-rules/src/BucketParameterQuerier.js';
|
|
22
24
|
import { JwtPayload } from '../auth/JwtPayload.js';
|
|
23
25
|
import { SyncContext } from './SyncContext.js';
|
|
24
26
|
import { getIntersection, hasIntersection } from './util.js';
|
|
@@ -117,7 +119,7 @@ export class BucketChecksumState {
|
|
|
117
119
|
const storage = this.bucketStorage;
|
|
118
120
|
|
|
119
121
|
const update = await this.parameterState.getCheckpointUpdate(next);
|
|
120
|
-
const { buckets: allBuckets, updatedBuckets } = update;
|
|
122
|
+
const { buckets: allBuckets, updatedBuckets, parameterQueryResultsByDefinition } = update;
|
|
121
123
|
|
|
122
124
|
/** Set of all buckets in this checkpoint. */
|
|
123
125
|
const bucketDescriptionMap = new Map(allBuckets.map((b) => [b.bucket, b]));
|
|
@@ -136,20 +138,20 @@ export class BucketChecksumState {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
// Re-check updated buckets only
|
|
139
|
-
let checksumLookups:
|
|
141
|
+
let checksumLookups: storage.BucketChecksumRequest[] = [];
|
|
140
142
|
|
|
141
143
|
let newChecksums = new Map<string, util.BucketChecksum>();
|
|
142
|
-
for (let
|
|
143
|
-
if (!updatedBuckets.has(bucket)) {
|
|
144
|
-
const existing = this.lastChecksums.get(bucket);
|
|
144
|
+
for (let desc of bucketDescriptionMap.values()) {
|
|
145
|
+
if (!updatedBuckets.has(desc.bucket)) {
|
|
146
|
+
const existing = this.lastChecksums.get(desc.bucket);
|
|
145
147
|
if (existing == null) {
|
|
146
148
|
// If this happens, it means updatedBuckets did not correctly include all new buckets
|
|
147
|
-
throw new ServiceAssertionError(`Existing checksum not found for bucket ${bucket}`);
|
|
149
|
+
throw new ServiceAssertionError(`Existing checksum not found for bucket ${desc.bucket}`);
|
|
148
150
|
}
|
|
149
151
|
// Bucket is not specifically updated, and we have a previous checksum
|
|
150
|
-
newChecksums.set(bucket, existing);
|
|
152
|
+
newChecksums.set(desc.bucket, existing);
|
|
151
153
|
} else {
|
|
152
|
-
checksumLookups.push(bucket);
|
|
154
|
+
checksumLookups.push({ bucket: desc.bucket, source: desc.source });
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
157
|
|
|
@@ -162,12 +164,12 @@ export class BucketChecksumState {
|
|
|
162
164
|
checksumMap = newChecksums;
|
|
163
165
|
} else {
|
|
164
166
|
// Re-check all buckets
|
|
165
|
-
const bucketList = [...bucketDescriptionMap.
|
|
167
|
+
const bucketList = [...bucketDescriptionMap.values()].map((b) => ({ bucket: b.bucket, source: b.source }));
|
|
166
168
|
checksumMap = await storage.getChecksums(base.checkpoint, bucketList);
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
// Subset of buckets for which there may be new data in this batch.
|
|
170
|
-
let bucketsToFetch:
|
|
172
|
+
let bucketsToFetch: ResolvedBucket[];
|
|
171
173
|
|
|
172
174
|
let checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
|
|
173
175
|
|
|
@@ -206,25 +208,31 @@ export class BucketChecksumState {
|
|
|
206
208
|
...this.parameterState.translateResolvedBucket(bucketDescriptionMap.get(e.bucket)!, streamNameToIndex)
|
|
207
209
|
}));
|
|
208
210
|
bucketsToFetch = [...generateBucketsToFetch].map((b) => {
|
|
209
|
-
return
|
|
210
|
-
priority: bucketDescriptionMap.get(b)!.priority,
|
|
211
|
-
bucket: b
|
|
212
|
-
};
|
|
211
|
+
return bucketDescriptionMap.get(b)!;
|
|
213
212
|
});
|
|
214
213
|
|
|
215
214
|
deferredLog = () => {
|
|
215
|
+
const totalParamResults = computeTotalParamResults(parameterQueryResultsByDefinition);
|
|
216
216
|
let message = `Updated checkpoint: ${base.checkpoint} | `;
|
|
217
217
|
message += `write: ${writeCheckpoint} | `;
|
|
218
218
|
message += `buckets: ${allBuckets.length} | `;
|
|
219
|
+
if (totalParamResults !== undefined) {
|
|
220
|
+
message += `param_results: ${totalParamResults} | `;
|
|
221
|
+
}
|
|
219
222
|
message += `updated: ${limitedBuckets(diff.updatedBuckets, 20)} | `;
|
|
220
223
|
message += `removed: ${limitedBuckets(diff.removedBuckets, 20)}`;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
logCheckpoint(
|
|
225
|
+
this.logger,
|
|
226
|
+
message,
|
|
227
|
+
{
|
|
228
|
+
checkpoint: base.checkpoint,
|
|
229
|
+
user_id: userIdForLogs,
|
|
230
|
+
buckets: allBuckets.length,
|
|
231
|
+
updated: diff.updatedBuckets.length,
|
|
232
|
+
removed: diff.removedBuckets.length
|
|
233
|
+
},
|
|
234
|
+
totalParamResults
|
|
235
|
+
);
|
|
228
236
|
};
|
|
229
237
|
|
|
230
238
|
checkpointLine = {
|
|
@@ -237,11 +245,25 @@ export class BucketChecksumState {
|
|
|
237
245
|
} satisfies util.StreamingSyncCheckpointDiff;
|
|
238
246
|
} else {
|
|
239
247
|
deferredLog = () => {
|
|
248
|
+
const totalParamResults = computeTotalParamResults(parameterQueryResultsByDefinition);
|
|
240
249
|
let message = `New checkpoint: ${base.checkpoint} | write: ${writeCheckpoint} | `;
|
|
241
|
-
message += `buckets: ${allBuckets.length}
|
|
242
|
-
|
|
250
|
+
message += `buckets: ${allBuckets.length}`;
|
|
251
|
+
if (totalParamResults !== undefined) {
|
|
252
|
+
message += ` | param_results: ${totalParamResults}`;
|
|
253
|
+
}
|
|
254
|
+
message += ` ${limitedBuckets(allBuckets, 20)}`;
|
|
255
|
+
logCheckpoint(
|
|
256
|
+
this.logger,
|
|
257
|
+
message,
|
|
258
|
+
{
|
|
259
|
+
checkpoint: base.checkpoint,
|
|
260
|
+
user_id: userIdForLogs,
|
|
261
|
+
buckets: allBuckets.length
|
|
262
|
+
},
|
|
263
|
+
totalParamResults
|
|
264
|
+
);
|
|
243
265
|
};
|
|
244
|
-
bucketsToFetch = allBuckets
|
|
266
|
+
bucketsToFetch = allBuckets;
|
|
245
267
|
|
|
246
268
|
const subscriptions: util.StreamDescription[] = [];
|
|
247
269
|
const streamNameToIndex = new Map<string, number>();
|
|
@@ -318,17 +340,17 @@ export class BucketChecksumState {
|
|
|
318
340
|
deferredLog();
|
|
319
341
|
},
|
|
320
342
|
|
|
321
|
-
getFilteredBucketPositions: (buckets?:
|
|
343
|
+
getFilteredBucketPositions: (buckets?: ResolvedBucket[]): storage.BucketDataRequest[] => {
|
|
322
344
|
if (!hasAdvanced) {
|
|
323
345
|
throw new ServiceAssertionError('Call line.advance() before getFilteredBucketPositions()');
|
|
324
346
|
}
|
|
325
347
|
buckets ??= bucketsToFetch;
|
|
326
|
-
const filtered =
|
|
348
|
+
const filtered: storage.BucketDataRequest[] = [];
|
|
327
349
|
|
|
328
350
|
for (let bucket of buckets) {
|
|
329
351
|
const state = this.bucketDataPositions.get(bucket.bucket);
|
|
330
352
|
if (state) {
|
|
331
|
-
filtered.
|
|
353
|
+
filtered.push({ bucket: bucket.bucket, start: state.start_op_id, source: bucket.source });
|
|
332
354
|
}
|
|
333
355
|
}
|
|
334
356
|
return filtered;
|
|
@@ -370,6 +392,12 @@ export interface CheckpointUpdate {
|
|
|
370
392
|
* If null, assume that any bucket in `buckets` may have been updated.
|
|
371
393
|
*/
|
|
372
394
|
updatedBuckets: Set<string> | typeof INVALIDATE_ALL_BUCKETS;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Number of parameter query results per sync stream definition (before deduplication).
|
|
398
|
+
* Map from definition name to count.
|
|
399
|
+
*/
|
|
400
|
+
parameterQueryResultsByDefinition?: Map<string, number>;
|
|
373
401
|
}
|
|
374
402
|
|
|
375
403
|
export class BucketParameterState {
|
|
@@ -502,11 +530,21 @@ export class BucketParameterState {
|
|
|
502
530
|
ErrorCode.PSYNC_S2305,
|
|
503
531
|
`Too many parameter query results: ${update.buckets.length} (limit of ${this.context.maxParameterQueryResults})`
|
|
504
532
|
);
|
|
505
|
-
|
|
533
|
+
|
|
534
|
+
let errorMessage = error.message;
|
|
535
|
+
const logData: any = {
|
|
506
536
|
checkpoint: checkpoint,
|
|
507
537
|
user_id: this.syncParams.userId,
|
|
508
|
-
|
|
509
|
-
}
|
|
538
|
+
parameter_query_results: update.buckets.length
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
if (update.parameterQueryResultsByDefinition && update.parameterQueryResultsByDefinition.size > 0) {
|
|
542
|
+
const breakdown = formatParameterQueryBreakdown(update.parameterQueryResultsByDefinition);
|
|
543
|
+
errorMessage += breakdown.message;
|
|
544
|
+
logData.parameter_query_results_by_definition = breakdown.countsByDefinition;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this.logger.error(errorMessage, logData);
|
|
510
548
|
|
|
511
549
|
throw error;
|
|
512
550
|
}
|
|
@@ -562,6 +600,7 @@ export class BucketParameterState {
|
|
|
562
600
|
}
|
|
563
601
|
|
|
564
602
|
let dynamicBuckets: ResolvedBucket[];
|
|
603
|
+
let parameterQueryResultsByDefinition: Map<string, number> | undefined;
|
|
565
604
|
if (hasParameterChange || this.cachedDynamicBuckets == null || this.cachedDynamicBucketSet == null) {
|
|
566
605
|
const recordedLookups = new Set<string>();
|
|
567
606
|
|
|
@@ -574,6 +613,14 @@ export class BucketParameterState {
|
|
|
574
613
|
return checkpoint.base.getParameterSets(lookups);
|
|
575
614
|
}
|
|
576
615
|
});
|
|
616
|
+
|
|
617
|
+
// Count parameter query results per definition (before deduplication)
|
|
618
|
+
parameterQueryResultsByDefinition = new Map<string, number>();
|
|
619
|
+
for (const bucket of dynamicBuckets) {
|
|
620
|
+
const count = parameterQueryResultsByDefinition.get(bucket.definition) ?? 0;
|
|
621
|
+
parameterQueryResultsByDefinition.set(bucket.definition, count + 1);
|
|
622
|
+
}
|
|
623
|
+
|
|
577
624
|
this.cachedDynamicBuckets = dynamicBuckets;
|
|
578
625
|
this.cachedDynamicBucketSet = new Set<string>(dynamicBuckets.map((b) => b.bucket));
|
|
579
626
|
this.lookupsFromPreviousCheckpoint = recordedLookups;
|
|
@@ -596,12 +643,14 @@ export class BucketParameterState {
|
|
|
596
643
|
return {
|
|
597
644
|
buckets: allBuckets,
|
|
598
645
|
// We cannot track individual bucket updates for dynamic lookups yet
|
|
599
|
-
updatedBuckets: INVALIDATE_ALL_BUCKETS
|
|
646
|
+
updatedBuckets: INVALIDATE_ALL_BUCKETS,
|
|
647
|
+
parameterQueryResultsByDefinition
|
|
600
648
|
};
|
|
601
649
|
} else {
|
|
602
650
|
return {
|
|
603
651
|
buckets: allBuckets,
|
|
604
|
-
updatedBuckets: updatedBuckets
|
|
652
|
+
updatedBuckets: updatedBuckets,
|
|
653
|
+
parameterQueryResultsByDefinition
|
|
605
654
|
};
|
|
606
655
|
}
|
|
607
656
|
}
|
|
@@ -609,7 +658,7 @@ export class BucketParameterState {
|
|
|
609
658
|
|
|
610
659
|
export interface CheckpointLine {
|
|
611
660
|
checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
|
|
612
|
-
bucketsToFetch:
|
|
661
|
+
bucketsToFetch: ResolvedBucket[];
|
|
613
662
|
|
|
614
663
|
/**
|
|
615
664
|
* Call when a checkpoint line is being sent to a client, to update the internal state.
|
|
@@ -621,7 +670,7 @@ export interface CheckpointLine {
|
|
|
621
670
|
*
|
|
622
671
|
* @param bucketsToFetch List of buckets to fetch - either this.bucketsToFetch, or a subset of it. Defaults to this.bucketsToFetch.
|
|
623
672
|
*/
|
|
624
|
-
getFilteredBucketPositions(bucketsToFetch?:
|
|
673
|
+
getFilteredBucketPositions(bucketsToFetch?: ResolvedBucket[]): storage.BucketDataRequest[];
|
|
625
674
|
|
|
626
675
|
/**
|
|
627
676
|
* Update the position of bucket data the client has, after it was sent to the client.
|
|
@@ -635,6 +684,68 @@ export interface CheckpointLine {
|
|
|
635
684
|
// Use a more specific type to simplify testing
|
|
636
685
|
export type BucketChecksumStateStorage = Pick<storage.SyncRulesBucketStorage, 'getChecksums'>;
|
|
637
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Compute the total number of parameter query results across all definitions.
|
|
689
|
+
*/
|
|
690
|
+
function computeTotalParamResults(
|
|
691
|
+
parameterQueryResultsByDefinition: Map<string, number> | undefined
|
|
692
|
+
): number | undefined {
|
|
693
|
+
if (!parameterQueryResultsByDefinition) {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
return Array.from(parameterQueryResultsByDefinition.values()).reduce((sum, count) => sum + count, 0);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Log a checkpoint message, enriching it with parameter query result counts if available.
|
|
701
|
+
*
|
|
702
|
+
* @param logger The logger instance to use
|
|
703
|
+
* @param message The base message string (param_results will NOT be appended — caller includes it if needed)
|
|
704
|
+
* @param logData The base log data object
|
|
705
|
+
* @param totalParamResults The total parameter query results count, or undefined if not applicable
|
|
706
|
+
*/
|
|
707
|
+
function logCheckpoint(
|
|
708
|
+
logger: Logger,
|
|
709
|
+
message: string,
|
|
710
|
+
logData: Record<string, any>,
|
|
711
|
+
totalParamResults: number | undefined
|
|
712
|
+
): void {
|
|
713
|
+
if (totalParamResults !== undefined) {
|
|
714
|
+
logData.parameter_query_results = totalParamResults;
|
|
715
|
+
}
|
|
716
|
+
logger.info(message, logData);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Format a breakdown of parameter query results by sync rule definition.
|
|
721
|
+
*
|
|
722
|
+
* Sorts definitions by count (descending), includes the top 10, and returns both the
|
|
723
|
+
* formatted message string and the counts record suitable for structured log data.
|
|
724
|
+
*/
|
|
725
|
+
function formatParameterQueryBreakdown(parameterQueryResultsByDefinition: Map<string, number>): {
|
|
726
|
+
message: string;
|
|
727
|
+
countsByDefinition: Record<string, number>;
|
|
728
|
+
} {
|
|
729
|
+
// Sort definitions by count (descending) and take top 10
|
|
730
|
+
const allSorted = Array.from(parameterQueryResultsByDefinition.entries()).sort((a, b) => b[1] - a[1]);
|
|
731
|
+
const sortedDefinitions = allSorted.slice(0, 10);
|
|
732
|
+
|
|
733
|
+
let message = '\nParameter query results by definition:';
|
|
734
|
+
const countsByDefinition: Record<string, number> = {};
|
|
735
|
+
for (const [definition, count] of sortedDefinitions) {
|
|
736
|
+
message += `\n ${definition}: ${count}`;
|
|
737
|
+
countsByDefinition[definition] = count;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (allSorted.length > 10) {
|
|
741
|
+
const remainingResults = allSorted.slice(10).reduce((sum, [, count]) => sum + count, 0);
|
|
742
|
+
const remainingDefinitions = allSorted.length - 10;
|
|
743
|
+
message += `\n ... and ${remainingResults} more results from ${remainingDefinitions} definitions`;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return { message, countsByDefinition };
|
|
747
|
+
}
|
|
748
|
+
|
|
638
749
|
function limitedBuckets(buckets: string[] | { bucket: string }[], limit: number) {
|
|
639
750
|
buckets = buckets.map((b) => {
|
|
640
751
|
if (typeof b != 'string') {
|
|
@@ -649,32 +760,3 @@ function limitedBuckets(buckets: string[] | { bucket: string }[], limit: number)
|
|
|
649
760
|
const limited = buckets.slice(0, limit);
|
|
650
761
|
return `${JSON.stringify(limited)}...`;
|
|
651
762
|
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Resolves duplicate buckets in the given array, merging the inclusion reasons for duplicate.
|
|
655
|
-
*
|
|
656
|
-
* It's possible for duplicates to occur when a stream has multiple subscriptions, consider e.g.
|
|
657
|
-
*
|
|
658
|
-
* ```
|
|
659
|
-
* sync_streams:
|
|
660
|
-
* assets_by_category:
|
|
661
|
-
* query: select * from assets where category in (request.parameters() -> 'categories')
|
|
662
|
-
* ```
|
|
663
|
-
*
|
|
664
|
-
* Here, a client might subscribe once with `{"categories": [1]}` and once with `{"categories": [1, 2]}`. Since each
|
|
665
|
-
* subscription is evaluated independently, this would lead to three buckets, with a duplicate `assets_by_category[1]`
|
|
666
|
-
* bucket.
|
|
667
|
-
*/
|
|
668
|
-
function mergeBuckets(buckets: ResolvedBucket[]): ResolvedBucket[] {
|
|
669
|
-
const byBucketId: Record<string, ResolvedBucket> = {};
|
|
670
|
-
|
|
671
|
-
for (const bucket of buckets) {
|
|
672
|
-
if (Object.hasOwn(byBucketId, bucket.bucket)) {
|
|
673
|
-
byBucketId[bucket.bucket].inclusion_reasons.push(...bucket.inclusion_reasons);
|
|
674
|
-
} else {
|
|
675
|
-
byBucketId[bucket.bucket] = structuredClone(bucket);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return Object.values(byBucketId);
|
|
680
|
-
}
|
package/src/sync/sync.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { JSONBig, JsonContainer } from '@powersync/service-jsonbig';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BucketDescription,
|
|
4
|
+
BucketPriority,
|
|
5
|
+
HydratedSyncRules,
|
|
6
|
+
ResolvedBucket,
|
|
7
|
+
SqliteJsonValue
|
|
8
|
+
} from '@powersync/service-sync-rules';
|
|
3
9
|
|
|
4
10
|
import { AbortError } from 'ix/aborterror.js';
|
|
5
11
|
|
|
@@ -179,7 +185,7 @@ async function* streamResponseInner(
|
|
|
179
185
|
// receive a sync complete message after the synchronization is done (which happens in the last
|
|
180
186
|
// bucketDataInBatches iteration). Without any batch, the line is missing and clients might not complete their
|
|
181
187
|
// sync properly.
|
|
182
|
-
const priorityBatches: [BucketPriority | null,
|
|
188
|
+
const priorityBatches: [BucketPriority | null, ResolvedBucket[]][] = bucketsByPriority;
|
|
183
189
|
if (priorityBatches.length == 0) {
|
|
184
190
|
priorityBatches.push([null, []]);
|
|
185
191
|
}
|
|
@@ -257,7 +263,7 @@ interface BucketDataRequest {
|
|
|
257
263
|
/** Contains current bucket state. Modified by the request as data is sent. */
|
|
258
264
|
checkpointLine: CheckpointLine;
|
|
259
265
|
/** Subset of checkpointLine.bucketsToFetch, filtered by priority. */
|
|
260
|
-
bucketsToFetch:
|
|
266
|
+
bucketsToFetch: ResolvedBucket[];
|
|
261
267
|
/** Whether data lines should be encoded in a legacy format where {@link util.OplogEntry.data} is a nested object. */
|
|
262
268
|
legacyDataLines: boolean;
|
|
263
269
|
/** Signals that the connection was aborted and that streaming should stop ASAP. */
|
package/src/sync/util.ts
CHANGED
|
@@ -183,6 +183,16 @@ export function settledPromise<T>(promise: Promise<T>): Promise<PromiseSettledRe
|
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
export function unsettledPromise<T>(settled: Promise<PromiseSettledResult<T>>): Promise<T> {
|
|
187
|
+
return settled.then((result) => {
|
|
188
|
+
if (result.status === 'fulfilled') {
|
|
189
|
+
return Promise.resolve(result.value);
|
|
190
|
+
} else {
|
|
191
|
+
return Promise.reject(result.reason);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
186
196
|
export type MapOrSet<T> = Map<T, any> | Set<T>;
|
|
187
197
|
|
|
188
198
|
/**
|
|
@@ -40,6 +40,16 @@ export abstract class ConfigCollector {
|
|
|
40
40
|
*/
|
|
41
41
|
const decoded = this.decode(serialized);
|
|
42
42
|
this.validate(decoded);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* For internal convenience, we duplicate sync_rules and sync_config. Making them interchangeable.
|
|
46
|
+
* Note, we only do this after validation (which only allows one option to be present)
|
|
47
|
+
*/
|
|
48
|
+
if (decoded.sync_config) {
|
|
49
|
+
decoded.sync_rules = decoded.sync_config;
|
|
50
|
+
} else if (decoded.sync_rules) {
|
|
51
|
+
decoded.sync_config = decoded.sync_rules;
|
|
52
|
+
}
|
|
43
53
|
return decoded;
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -52,6 +62,12 @@ export abstract class ConfigCollector {
|
|
|
52
62
|
if (!valid.valid) {
|
|
53
63
|
throw new Error(`Failed to validate PowerSync config: ${valid.errors.join(', ')}`);
|
|
54
64
|
}
|
|
65
|
+
|
|
66
|
+
if (config.sync_config && config.sync_rules) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'Both `sync_config` and `sync_rules` are present in the service configuration. Please consolidate into one sync_config.'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
decode(encoded: configFile.SerializedPowerSyncConfig): configFile.PowerSyncConfig {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { configFile } from '@powersync/service-types';
|
|
1
2
|
import { RunnerConfig, SyncRulesConfig } from '../../types.js';
|
|
2
3
|
import { SyncRulesCollector } from '../sync-collector.js';
|
|
3
|
-
import { configFile } from '@powersync/service-types';
|
|
4
4
|
|
|
5
5
|
export class Base64SyncRulesCollector extends SyncRulesCollector {
|
|
6
6
|
get name(): string {
|
|
@@ -8,15 +8,15 @@ export class Base64SyncRulesCollector extends SyncRulesCollector {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
async collect(baseConfig: configFile.PowerSyncConfig, runnerConfig: RunnerConfig): Promise<SyncRulesConfig | null> {
|
|
11
|
-
const {
|
|
12
|
-
if (!
|
|
11
|
+
const { sync_config_base64 } = runnerConfig;
|
|
12
|
+
if (!sync_config_base64) {
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
return {
|
|
17
17
|
present: true,
|
|
18
|
-
exit_on_error: baseConfig.
|
|
19
|
-
content: Buffer.from(
|
|
18
|
+
exit_on_error: baseConfig.sync_config?.exit_on_error ?? true,
|
|
19
|
+
content: Buffer.from(sync_config_base64, 'base64').toString()
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { configFile } from '@powersync/service-types';
|
|
1
2
|
import * as path from 'path';
|
|
2
3
|
import { RunnerConfig, SyncRulesConfig } from '../../types.js';
|
|
3
4
|
import { SyncRulesCollector } from '../sync-collector.js';
|
|
4
|
-
import { configFile } from '@powersync/service-types';
|
|
5
5
|
|
|
6
6
|
export class FileSystemSyncRulesCollector extends SyncRulesCollector {
|
|
7
7
|
get name(): string {
|
|
@@ -9,7 +9,7 @@ export class FileSystemSyncRulesCollector extends SyncRulesCollector {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
async collect(baseConfig: configFile.PowerSyncConfig, runnerConfig: RunnerConfig): Promise<SyncRulesConfig | null> {
|
|
12
|
-
const sync_path = baseConfig.
|
|
12
|
+
const sync_path = baseConfig.sync_config?.path;
|
|
13
13
|
if (!sync_path) {
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
@@ -20,7 +20,7 @@ export class FileSystemSyncRulesCollector extends SyncRulesCollector {
|
|
|
20
20
|
// Only persist the path here, and load on demand using `loadSyncRules()`.
|
|
21
21
|
return {
|
|
22
22
|
present: true,
|
|
23
|
-
exit_on_error: baseConfig.
|
|
23
|
+
exit_on_error: baseConfig.sync_config?.exit_on_error ?? true,
|
|
24
24
|
path: config_path ? path.resolve(path.dirname(config_path), sync_path) : sync_path
|
|
25
25
|
};
|
|
26
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { configFile } from '@powersync/service-types';
|
|
1
2
|
import { SyncRulesConfig } from '../../types.js';
|
|
2
3
|
import { SyncRulesCollector } from '../sync-collector.js';
|
|
3
|
-
import { configFile } from '@powersync/service-types';
|
|
4
4
|
|
|
5
5
|
export class InlineSyncRulesCollector extends SyncRulesCollector {
|
|
6
6
|
get name(): string {
|
|
@@ -8,15 +8,15 @@ export class InlineSyncRulesCollector extends SyncRulesCollector {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
async collect(baseConfig: configFile.PowerSyncConfig): Promise<SyncRulesConfig | null> {
|
|
11
|
-
const content = baseConfig
|
|
11
|
+
const content = baseConfig?.sync_config?.content;
|
|
12
12
|
if (!content) {
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
return {
|
|
17
17
|
present: true,
|
|
18
|
-
exit_on_error: true,
|
|
19
|
-
...baseConfig.
|
|
18
|
+
exit_on_error: baseConfig.sync_config?.exit_on_error ?? true,
|
|
19
|
+
...baseConfig.sync_config
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
}
|
package/src/util/config/types.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { configFile } from '@powersync/service-types';
|
|
2
2
|
import { CompoundKeyCollector } from '../../auth/CompoundKeyCollector.js';
|
|
3
|
-
import { KeySpec } from '../../auth/KeySpec.js';
|
|
4
3
|
import { KeyStore } from '../../auth/KeyStore.js';
|
|
5
4
|
|
|
6
5
|
export enum ServiceRunner {
|
|
@@ -12,7 +11,7 @@ export enum ServiceRunner {
|
|
|
12
11
|
export type RunnerConfig = {
|
|
13
12
|
config_path?: string;
|
|
14
13
|
config_base64?: string;
|
|
15
|
-
|
|
14
|
+
sync_config_base64?: string;
|
|
16
15
|
};
|
|
17
16
|
|
|
18
17
|
export type MigrationContext = {
|
package/src/util/env.ts
CHANGED
|
@@ -12,9 +12,14 @@ export const env = utils.collectEnvironmentVariables({
|
|
|
12
12
|
*/
|
|
13
13
|
POWERSYNC_CONFIG_B64: utils.type.string.optional(),
|
|
14
14
|
/**
|
|
15
|
+
* @deprecated use POWERSYNC_SYNC_CONFIG_B64 instead.
|
|
15
16
|
* Base64 encoded contents of sync rules YAML
|
|
16
17
|
*/
|
|
17
18
|
POWERSYNC_SYNC_RULES_B64: utils.type.string.optional(),
|
|
19
|
+
/**
|
|
20
|
+
* Base64 encoded contents of sync config YAML
|
|
21
|
+
*/
|
|
22
|
+
POWERSYNC_SYNC_CONFIG_B64: utils.type.string.optional(),
|
|
18
23
|
/**
|
|
19
24
|
* Runner to be started in this process
|
|
20
25
|
*/
|