@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.
Files changed (94) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/api/diagnostics.js +11 -4
  3. package/dist/api/diagnostics.js.map +1 -1
  4. package/dist/entry/commands/compact-action.js +13 -2
  5. package/dist/entry/commands/compact-action.js.map +1 -1
  6. package/dist/entry/commands/config-command.js +2 -2
  7. package/dist/entry/commands/config-command.js.map +1 -1
  8. package/dist/replication/AbstractReplicator.js +2 -5
  9. package/dist/replication/AbstractReplicator.js.map +1 -1
  10. package/dist/routes/configure-fastify.d.ts +84 -0
  11. package/dist/routes/endpoints/admin.d.ts +168 -0
  12. package/dist/routes/endpoints/admin.js +34 -20
  13. package/dist/routes/endpoints/admin.js.map +1 -1
  14. package/dist/routes/endpoints/sync-rules.js +6 -9
  15. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  16. package/dist/routes/endpoints/sync-stream.js +6 -1
  17. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  18. package/dist/storage/BucketStorageBatch.d.ts +21 -8
  19. package/dist/storage/BucketStorageBatch.js.map +1 -1
  20. package/dist/storage/BucketStorageFactory.d.ts +48 -15
  21. package/dist/storage/BucketStorageFactory.js +70 -1
  22. package/dist/storage/BucketStorageFactory.js.map +1 -1
  23. package/dist/storage/ChecksumCache.d.ts +5 -2
  24. package/dist/storage/ChecksumCache.js +8 -4
  25. package/dist/storage/ChecksumCache.js.map +1 -1
  26. package/dist/storage/PersistedSyncRulesContent.d.ts +33 -3
  27. package/dist/storage/PersistedSyncRulesContent.js +80 -1
  28. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  29. package/dist/storage/SourceTable.d.ts +7 -2
  30. package/dist/storage/SourceTable.js.map +1 -1
  31. package/dist/storage/StorageVersionConfig.d.ts +53 -0
  32. package/dist/storage/StorageVersionConfig.js +53 -0
  33. package/dist/storage/StorageVersionConfig.js.map +1 -0
  34. package/dist/storage/SyncRulesBucketStorage.d.ts +14 -4
  35. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  36. package/dist/storage/storage-index.d.ts +1 -0
  37. package/dist/storage/storage-index.js +1 -0
  38. package/dist/storage/storage-index.js.map +1 -1
  39. package/dist/sync/BucketChecksumState.d.ts +8 -4
  40. package/dist/sync/BucketChecksumState.js +97 -52
  41. package/dist/sync/BucketChecksumState.js.map +1 -1
  42. package/dist/sync/sync.js.map +1 -1
  43. package/dist/sync/util.d.ts +1 -0
  44. package/dist/sync/util.js +10 -0
  45. package/dist/sync/util.js.map +1 -1
  46. package/dist/util/config/collectors/config-collector.js +13 -0
  47. package/dist/util/config/collectors/config-collector.js.map +1 -1
  48. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.d.ts +1 -1
  49. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +4 -4
  50. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
  51. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.d.ts +1 -1
  52. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +2 -2
  53. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  54. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.d.ts +1 -1
  55. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js +3 -3
  56. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js.map +1 -1
  57. package/dist/util/config/types.d.ts +1 -1
  58. package/dist/util/config/types.js.map +1 -1
  59. package/dist/util/env.d.ts +1 -0
  60. package/dist/util/env.js +5 -0
  61. package/dist/util/env.js.map +1 -1
  62. package/package.json +6 -6
  63. package/src/api/diagnostics.ts +12 -4
  64. package/src/entry/commands/compact-action.ts +15 -2
  65. package/src/entry/commands/config-command.ts +3 -3
  66. package/src/replication/AbstractReplicator.ts +3 -5
  67. package/src/routes/endpoints/admin.ts +43 -25
  68. package/src/routes/endpoints/sync-rules.ts +14 -13
  69. package/src/routes/endpoints/sync-stream.ts +6 -1
  70. package/src/storage/BucketStorageBatch.ts +23 -9
  71. package/src/storage/BucketStorageFactory.ts +116 -19
  72. package/src/storage/ChecksumCache.ts +14 -6
  73. package/src/storage/PersistedSyncRulesContent.ts +119 -4
  74. package/src/storage/SourceTable.ts +7 -1
  75. package/src/storage/StorageVersionConfig.ts +78 -0
  76. package/src/storage/SyncRulesBucketStorage.ts +20 -4
  77. package/src/storage/storage-index.ts +1 -0
  78. package/src/sync/BucketChecksumState.ts +147 -65
  79. package/src/sync/sync.ts +9 -3
  80. package/src/sync/util.ts +10 -0
  81. package/src/util/config/collectors/config-collector.ts +16 -0
  82. package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +5 -5
  83. package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +3 -3
  84. package/src/util/config/sync-rules/impl/inline-sync-rules-collector.ts +4 -4
  85. package/src/util/config/types.ts +1 -2
  86. package/src/util/env.ts +5 -0
  87. package/test/src/checksum_cache.test.ts +102 -57
  88. package/test/src/config.test.ts +115 -0
  89. package/test/src/routes/admin.test.ts +48 -0
  90. package/test/src/routes/mocks.ts +22 -1
  91. package/test/src/routes/stream.test.ts +3 -2
  92. package/test/src/sync/BucketChecksumState.test.ts +332 -93
  93. package/test/src/utils.ts +9 -0
  94. 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: string[] = [];
141
+ let checksumLookups: storage.BucketChecksumRequest[] = [];
140
142
 
141
143
  let newChecksums = new Map<string, util.BucketChecksum>();
142
- for (let bucket of bucketDescriptionMap.keys()) {
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.keys()];
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: BucketDescription[];
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
- this.logger.info(message, {
222
- checkpoint: base.checkpoint,
223
- user_id: userIdForLogs,
224
- buckets: allBuckets.length,
225
- updated: diff.updatedBuckets.length,
226
- removed: diff.removedBuckets.length
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} ${limitedBuckets(allBuckets, 20)}`;
242
- this.logger.info(message, { checkpoint: base.checkpoint, user_id: userIdForLogs, buckets: allBuckets.length });
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.map((b) => ({ bucket: b.bucket, priority: b.priority }));
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?: BucketDescription[]): Map<string, util.InternalOpId> => {
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 = new Map<string, util.InternalOpId>();
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.set(bucket.bucket, state.start_op_id);
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
- this.logger.error(error.message, {
533
+
534
+ let errorMessage = error.message;
535
+ const logData: any = {
506
536
  checkpoint: checkpoint,
507
537
  user_id: this.syncParams.userId,
508
- buckets: update.buckets.length
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: BucketDescription[];
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?: BucketDescription[]): Map<string, util.InternalOpId>;
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 { BucketDescription, BucketPriority, HydratedSyncRules, SqliteJsonValue } from '@powersync/service-sync-rules';
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, BucketDescription[]][] = bucketsByPriority;
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: BucketDescription[];
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 { sync_rules_base64 } = runnerConfig;
12
- if (!sync_rules_base64) {
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.sync_rules?.exit_on_error ?? true,
19
- content: Buffer.from(sync_rules_base64, 'base64').toString()
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.sync_rules?.path;
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.sync_rules?.exit_on_error ?? true,
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.sync_rules?.content;
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.sync_rules
18
+ exit_on_error: baseConfig.sync_config?.exit_on_error ?? true,
19
+ ...baseConfig.sync_config
20
20
  };
21
21
  }
22
22
  }
@@ -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
- sync_rules_base64?: string;
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
  */