@powersync/service-core 0.0.0-dev-20260225093637 → 0.0.0-dev-20260313100403

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 (60) hide show
  1. package/CHANGELOG.md +42 -7
  2. package/dist/auth/utils.d.ts +2 -1
  3. package/dist/auth/utils.js +1 -1
  4. package/dist/auth/utils.js.map +1 -1
  5. package/dist/entry/commands/config-command.js +1 -1
  6. package/dist/entry/commands/config-command.js.map +1 -1
  7. package/dist/routes/endpoints/admin.js +3 -1
  8. package/dist/routes/endpoints/admin.js.map +1 -1
  9. package/dist/routes/endpoints/sync-stream.js +6 -1
  10. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  11. package/dist/storage/BucketStorageBatch.d.ts +29 -8
  12. package/dist/storage/BucketStorageBatch.js.map +1 -1
  13. package/dist/storage/BucketStorageFactory.d.ts +27 -8
  14. package/dist/storage/BucketStorageFactory.js +14 -2
  15. package/dist/storage/BucketStorageFactory.js.map +1 -1
  16. package/dist/storage/ChecksumCache.d.ts +5 -2
  17. package/dist/storage/ChecksumCache.js +8 -4
  18. package/dist/storage/ChecksumCache.js.map +1 -1
  19. package/dist/storage/PersistedSyncRulesContent.d.ts +8 -2
  20. package/dist/storage/PersistedSyncRulesContent.js +31 -7
  21. package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
  22. package/dist/storage/ReportStorage.d.ts +3 -3
  23. package/dist/storage/SourceTable.d.ts +7 -2
  24. package/dist/storage/SourceTable.js.map +1 -1
  25. package/dist/storage/StorageVersionConfig.d.ts +33 -0
  26. package/dist/storage/StorageVersionConfig.js +39 -6
  27. package/dist/storage/StorageVersionConfig.js.map +1 -1
  28. package/dist/storage/SyncRulesBucketStorage.d.ts +26 -6
  29. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  30. package/dist/sync/BucketChecksumState.d.ts +3 -3
  31. package/dist/sync/BucketChecksumState.js +12 -42
  32. package/dist/sync/BucketChecksumState.js.map +1 -1
  33. package/dist/sync/sync.js.map +1 -1
  34. package/dist/sync/util.d.ts +1 -0
  35. package/dist/sync/util.js +10 -0
  36. package/dist/sync/util.js.map +1 -1
  37. package/dist/util/env.js +1 -1
  38. package/package.json +5 -5
  39. package/src/auth/utils.ts +5 -2
  40. package/src/entry/commands/config-command.ts +1 -1
  41. package/src/routes/endpoints/admin.ts +3 -1
  42. package/src/routes/endpoints/sync-stream.ts +6 -1
  43. package/src/storage/BucketStorageBatch.ts +33 -9
  44. package/src/storage/BucketStorageFactory.ts +53 -4
  45. package/src/storage/ChecksumCache.ts +14 -6
  46. package/src/storage/PersistedSyncRulesContent.ts +45 -7
  47. package/src/storage/ReportStorage.ts +3 -3
  48. package/src/storage/SourceTable.ts +7 -1
  49. package/src/storage/StorageVersionConfig.ts +54 -6
  50. package/src/storage/SyncRulesBucketStorage.ts +33 -6
  51. package/src/sync/BucketChecksumState.ts +18 -49
  52. package/src/sync/sync.ts +9 -3
  53. package/src/sync/util.ts +10 -0
  54. package/src/util/env.ts +1 -1
  55. package/test/src/auth.test.ts +20 -1
  56. package/test/src/checksum_cache.test.ts +102 -57
  57. package/test/src/config.test.ts +1 -0
  58. package/test/src/sync/BucketChecksumState.test.ts +53 -21
  59. package/test/src/utils.ts +9 -0
  60. package/tsconfig.tsbuildinfo +1 -1
@@ -1,5 +1,10 @@
1
1
  import { Logger, ObserverClient } from '@powersync/lib-services-framework';
2
- import { HydratedSyncRules, ScopedParameterLookup, SqliteJsonRow } from '@powersync/service-sync-rules';
2
+ import {
3
+ BucketDataSource,
4
+ HydratedSyncRules,
5
+ ScopedParameterLookup,
6
+ SqliteJsonRow
7
+ } from '@powersync/service-sync-rules';
3
8
  import * as util from '../util/util-index.js';
4
9
  import { BucketStorageBatch, FlushedResult, SaveUpdate } from './BucketStorageBatch.js';
5
10
  import { BucketStorageFactory } from './BucketStorageFactory.js';
@@ -25,10 +30,17 @@ export interface SyncRulesBucketStorage
25
30
  resolveTable(options: ResolveTableOptions): Promise<ResolveTableResult>;
26
31
 
27
32
  /**
28
- * Use this to get access to update storage data.
33
+ * Create a new writer.
34
+ *
35
+ * The writer must be flushed and disposed when done.
36
+ */
37
+ createWriter(options: CreateWriterOptions): Promise<BucketStorageBatch>;
38
+
39
+ /**
40
+ * @deprecated Use `createWriter()` with `await using` instead.
29
41
  */
30
42
  startBatch(
31
- options: StartBatchOptions,
43
+ options: CreateWriterOptions,
32
44
  callback: (batch: BucketStorageBatch) => Promise<void>
33
45
  ): Promise<FlushedResult | null>;
34
46
 
@@ -103,7 +115,7 @@ export interface SyncRulesBucketStorage
103
115
  */
104
116
  getBucketDataBatch(
105
117
  checkpoint: util.InternalOpId,
106
- dataBuckets: Map<string, util.InternalOpId>,
118
+ dataBuckets: BucketDataRequest[],
107
119
  options?: BucketDataBatchOptions
108
120
  ): AsyncIterable<SyncBucketDataChunk>;
109
121
 
@@ -115,7 +127,7 @@ export interface SyncRulesBucketStorage
115
127
  * This may be slow, depending on the size of the buckets.
116
128
  * The checksums are cached internally to compensate for this, but does not cover all cases.
117
129
  */
118
- getChecksums(checkpoint: util.InternalOpId, buckets: string[]): Promise<util.ChecksumMap>;
130
+ getChecksums(checkpoint: util.InternalOpId, buckets: BucketChecksumRequest[]): Promise<util.ChecksumMap>;
119
131
 
120
132
  /**
121
133
  * Clear checksum cache. Primarily intended for tests.
@@ -127,6 +139,16 @@ export interface SyncRulesBucketStorageListener {
127
139
  batchStarted: (batch: BucketStorageBatch) => void;
128
140
  }
129
141
 
142
+ export interface BucketDataRequest {
143
+ bucket: string;
144
+ start: util.InternalOpId;
145
+ source: BucketDataSource;
146
+ }
147
+ export interface BucketChecksumRequest {
148
+ bucket: string;
149
+ source: BucketDataSource;
150
+ }
151
+
130
152
  export interface SyncRuleStatus {
131
153
  checkpoint_lsn: string | null;
132
154
  active: boolean;
@@ -147,7 +169,7 @@ export interface ResolveTableResult {
147
169
  dropTables: SourceTable[];
148
170
  }
149
171
 
150
- export interface StartBatchOptions extends ParseSyncRulesOptions {
172
+ export interface CreateWriterOptions extends ParseSyncRulesOptions {
151
173
  zeroLSN: string;
152
174
  /**
153
175
  * Whether or not to store a copy of the current data.
@@ -177,6 +199,11 @@ export interface StartBatchOptions extends ParseSyncRulesOptions {
177
199
  logger?: Logger;
178
200
  }
179
201
 
202
+ /**
203
+ * @deprecated Use `CreateWriterOptions`.
204
+ */
205
+ export interface StartBatchOptions extends CreateWriterOptions {}
206
+
180
207
  export interface CompactOptions {
181
208
  /**
182
209
  * Heap memory limit for the compact process.
@@ -7,7 +7,8 @@ import {
7
7
  QuerierError,
8
8
  RequestedStream,
9
9
  RequestParameters,
10
- ResolvedBucket
10
+ ResolvedBucket,
11
+ mergeBuckets
11
12
  } from '@powersync/service-sync-rules';
12
13
 
13
14
  import * as storage from '../storage/storage-index.js';
@@ -137,20 +138,20 @@ export class BucketChecksumState {
137
138
  }
138
139
 
139
140
  // Re-check updated buckets only
140
- let checksumLookups: string[] = [];
141
+ let checksumLookups: storage.BucketChecksumRequest[] = [];
141
142
 
142
143
  let newChecksums = new Map<string, util.BucketChecksum>();
143
- for (let bucket of bucketDescriptionMap.keys()) {
144
- if (!updatedBuckets.has(bucket)) {
145
- 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);
146
147
  if (existing == null) {
147
148
  // If this happens, it means updatedBuckets did not correctly include all new buckets
148
- throw new ServiceAssertionError(`Existing checksum not found for bucket ${bucket}`);
149
+ throw new ServiceAssertionError(`Existing checksum not found for bucket ${desc.bucket}`);
149
150
  }
150
151
  // Bucket is not specifically updated, and we have a previous checksum
151
- newChecksums.set(bucket, existing);
152
+ newChecksums.set(desc.bucket, existing);
152
153
  } else {
153
- checksumLookups.push(bucket);
154
+ checksumLookups.push({ bucket: desc.bucket, source: desc.source });
154
155
  }
155
156
  }
156
157
 
@@ -163,12 +164,12 @@ export class BucketChecksumState {
163
164
  checksumMap = newChecksums;
164
165
  } else {
165
166
  // Re-check all buckets
166
- const bucketList = [...bucketDescriptionMap.keys()];
167
+ const bucketList = [...bucketDescriptionMap.values()].map((b) => ({ bucket: b.bucket, source: b.source }));
167
168
  checksumMap = await storage.getChecksums(base.checkpoint, bucketList);
168
169
  }
169
170
 
170
171
  // Subset of buckets for which there may be new data in this batch.
171
- let bucketsToFetch: BucketDescription[];
172
+ let bucketsToFetch: ResolvedBucket[];
172
173
 
173
174
  let checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
174
175
 
@@ -207,10 +208,7 @@ export class BucketChecksumState {
207
208
  ...this.parameterState.translateResolvedBucket(bucketDescriptionMap.get(e.bucket)!, streamNameToIndex)
208
209
  }));
209
210
  bucketsToFetch = [...generateBucketsToFetch].map((b) => {
210
- return {
211
- priority: bucketDescriptionMap.get(b)!.priority,
212
- bucket: b
213
- };
211
+ return bucketDescriptionMap.get(b)!;
214
212
  });
215
213
 
216
214
  deferredLog = () => {
@@ -265,7 +263,7 @@ export class BucketChecksumState {
265
263
  totalParamResults
266
264
  );
267
265
  };
268
- bucketsToFetch = allBuckets.map((b) => ({ bucket: b.bucket, priority: b.priority }));
266
+ bucketsToFetch = allBuckets;
269
267
 
270
268
  const subscriptions: util.StreamDescription[] = [];
271
269
  const streamNameToIndex = new Map<string, number>();
@@ -342,17 +340,17 @@ export class BucketChecksumState {
342
340
  deferredLog();
343
341
  },
344
342
 
345
- getFilteredBucketPositions: (buckets?: BucketDescription[]): Map<string, util.InternalOpId> => {
343
+ getFilteredBucketPositions: (buckets?: ResolvedBucket[]): storage.BucketDataRequest[] => {
346
344
  if (!hasAdvanced) {
347
345
  throw new ServiceAssertionError('Call line.advance() before getFilteredBucketPositions()');
348
346
  }
349
347
  buckets ??= bucketsToFetch;
350
- const filtered = new Map<string, util.InternalOpId>();
348
+ const filtered: storage.BucketDataRequest[] = [];
351
349
 
352
350
  for (let bucket of buckets) {
353
351
  const state = this.bucketDataPositions.get(bucket.bucket);
354
352
  if (state) {
355
- filtered.set(bucket.bucket, state.start_op_id);
353
+ filtered.push({ bucket: bucket.bucket, start: state.start_op_id, source: bucket.source });
356
354
  }
357
355
  }
358
356
  return filtered;
@@ -660,7 +658,7 @@ export class BucketParameterState {
660
658
 
661
659
  export interface CheckpointLine {
662
660
  checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
663
- bucketsToFetch: BucketDescription[];
661
+ bucketsToFetch: ResolvedBucket[];
664
662
 
665
663
  /**
666
664
  * Call when a checkpoint line is being sent to a client, to update the internal state.
@@ -672,7 +670,7 @@ export interface CheckpointLine {
672
670
  *
673
671
  * @param bucketsToFetch List of buckets to fetch - either this.bucketsToFetch, or a subset of it. Defaults to this.bucketsToFetch.
674
672
  */
675
- getFilteredBucketPositions(bucketsToFetch?: BucketDescription[]): Map<string, util.InternalOpId>;
673
+ getFilteredBucketPositions(bucketsToFetch?: ResolvedBucket[]): storage.BucketDataRequest[];
676
674
 
677
675
  /**
678
676
  * Update the position of bucket data the client has, after it was sent to the client.
@@ -762,32 +760,3 @@ function limitedBuckets(buckets: string[] | { bucket: string }[], limit: number)
762
760
  const limited = buckets.slice(0, limit);
763
761
  return `${JSON.stringify(limited)}...`;
764
762
  }
765
-
766
- /**
767
- * Resolves duplicate buckets in the given array, merging the inclusion reasons for duplicate.
768
- *
769
- * It's possible for duplicates to occur when a stream has multiple subscriptions, consider e.g.
770
- *
771
- * ```
772
- * sync_streams:
773
- * assets_by_category:
774
- * query: select * from assets where category in (request.parameters() -> 'categories')
775
- * ```
776
- *
777
- * Here, a client might subscribe once with `{"categories": [1]}` and once with `{"categories": [1, 2]}`. Since each
778
- * subscription is evaluated independently, this would lead to three buckets, with a duplicate `assets_by_category[1]`
779
- * bucket.
780
- */
781
- function mergeBuckets(buckets: ResolvedBucket[]): ResolvedBucket[] {
782
- const byBucketId: Record<string, ResolvedBucket> = {};
783
-
784
- for (const bucket of buckets) {
785
- if (Object.hasOwn(byBucketId, bucket.bucket)) {
786
- byBucketId[bucket.bucket].inclusion_reasons.push(...bucket.inclusion_reasons);
787
- } else {
788
- byBucketId[bucket.bucket] = structuredClone(bucket);
789
- }
790
- }
791
-
792
- return Object.values(byBucketId);
793
- }
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
  /**
package/src/util/env.ts CHANGED
@@ -17,7 +17,7 @@ export const env = utils.collectEnvironmentVariables({
17
17
  */
18
18
  POWERSYNC_SYNC_RULES_B64: utils.type.string.optional(),
19
19
  /**
20
- * Base64 encoded contents of sync rules YAML
20
+ * Base64 encoded contents of sync config YAML
21
21
  */
22
22
  POWERSYNC_SYNC_CONFIG_B64: utils.type.string.optional(),
23
23
  /**
@@ -7,7 +7,7 @@ import { RemoteJWKSCollector } from '../../src/auth/RemoteJWKSCollector.js';
7
7
  import { KeyResult } from '../../src/auth/KeyCollector.js';
8
8
  import { CachedKeyCollector } from '../../src/auth/CachedKeyCollector.js';
9
9
  import { JwtPayload, StaticSupabaseKeyCollector } from '@/index.js';
10
- import { debugKeyNotFound } from '../../src/auth/utils.js';
10
+ import { debugKeyNotFound, getSupabaseJwksUrl } from '../../src/auth/utils.js';
11
11
 
12
12
  const publicKeyRSA: jose.JWK = {
13
13
  use: 'sig',
@@ -496,6 +496,25 @@ describe('JWT Auth', () => {
496
496
  expect(verified.parsedPayload.claim).toEqual('test-claim-2');
497
497
  });
498
498
 
499
+ test('Supabase connection parsing', () => {
500
+ const details = getSupabaseJwksUrl({
501
+ type: 'postgresql',
502
+ uri: 'postgresql://db.abc123.supabase.co:5432/postgres'
503
+ });
504
+ expect(details).toEqual({
505
+ hostname: 'db.abc123.supabase.co',
506
+ projectId: 'abc123',
507
+ url: 'https://abc123.supabase.co/auth/v1/.well-known/jwks.json'
508
+ });
509
+
510
+ // supabase.com is not a supabase URI
511
+ const other = getSupabaseJwksUrl({
512
+ type: 'postgresql',
513
+ uri: 'postgresql://db.abc123.supabase.com:5432/postgres'
514
+ });
515
+ expect(other).toEqual(null);
516
+ });
517
+
499
518
  describe('debugKeyNotFound', () => {
500
519
  test('Supabase token with legacy auth not configured', async () => {
501
520
  const keys = await StaticSupabaseKeyCollector.importKeys([]);