@powersync/service-core 1.23.1 → 1.23.3

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 (81) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/api/RouteAPI.d.ts +25 -11
  3. package/dist/replication/AbstractReplicator.d.ts +8 -0
  4. package/dist/replication/AbstractReplicator.js.map +1 -1
  5. package/dist/routes/RouterEngine.d.ts +1 -0
  6. package/dist/routes/RouterEngine.js +6 -1
  7. package/dist/routes/RouterEngine.js.map +1 -1
  8. package/dist/routes/configure-fastify.d.ts +52 -44
  9. package/dist/routes/configure-fastify.js +15 -2
  10. package/dist/routes/configure-fastify.js.map +1 -1
  11. package/dist/routes/endpoints/checkpointing.js +1 -3
  12. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  13. package/dist/routes/endpoints/socket-route.js +2 -1
  14. package/dist/routes/endpoints/socket-route.js.map +1 -1
  15. package/dist/storage/BucketStorageBatch.d.ts +60 -11
  16. package/dist/storage/BucketStorageBatch.js.map +1 -1
  17. package/dist/storage/ChecksumCache.d.ts +11 -6
  18. package/dist/storage/ChecksumCache.js +5 -4
  19. package/dist/storage/ChecksumCache.js.map +1 -1
  20. package/dist/storage/SyncRulesBucketStorage.d.ts +35 -5
  21. package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
  22. package/dist/storage/WriteCheckpointAPI.d.ts +1 -1
  23. package/dist/sync/BucketChecksumState.d.ts +5 -2
  24. package/dist/sync/BucketChecksumState.js +36 -8
  25. package/dist/sync/BucketChecksumState.js.map +1 -1
  26. package/dist/sync/RequestTracker.d.ts +7 -0
  27. package/dist/sync/RequestTracker.js +12 -0
  28. package/dist/sync/RequestTracker.js.map +1 -1
  29. package/dist/sync/sync.js +118 -38
  30. package/dist/sync/sync.js.map +1 -1
  31. package/dist/system/ServiceContext.d.ts +2 -0
  32. package/dist/system/ServiceContext.js +2 -0
  33. package/dist/system/ServiceContext.js.map +1 -1
  34. package/dist/tracing/PerformanceTracer.d.ts +7 -1
  35. package/dist/tracing/PerformanceTracer.js +8 -0
  36. package/dist/tracing/PerformanceTracer.js.map +1 -1
  37. package/dist/util/checkpointing.d.ts +3 -8
  38. package/dist/util/checkpointing.js +1 -16
  39. package/dist/util/checkpointing.js.map +1 -1
  40. package/dist/util/config/compound-config-collector.js +10 -2
  41. package/dist/util/config/compound-config-collector.js.map +1 -1
  42. package/dist/util/config/defaults.d.ts +1 -0
  43. package/dist/util/config/defaults.js +1 -0
  44. package/dist/util/config/defaults.js.map +1 -1
  45. package/dist/util/config/types.d.ts +1 -0
  46. package/dist/util/util-index.d.ts +1 -0
  47. package/dist/util/util-index.js +1 -0
  48. package/dist/util/util-index.js.map +1 -1
  49. package/dist/util/utils.d.ts +1 -1
  50. package/dist/util/utils.js +1 -1
  51. package/dist/util/write-checkpoint-batcher.d.ts +19 -0
  52. package/dist/util/write-checkpoint-batcher.js +90 -0
  53. package/dist/util/write-checkpoint-batcher.js.map +1 -0
  54. package/package.json +5 -5
  55. package/src/api/RouteAPI.ts +25 -11
  56. package/src/replication/AbstractReplicator.ts +8 -0
  57. package/src/routes/RouterEngine.ts +7 -1
  58. package/src/routes/configure-fastify.ts +25 -2
  59. package/src/routes/endpoints/checkpointing.ts +1 -4
  60. package/src/routes/endpoints/socket-route.ts +2 -1
  61. package/src/storage/BucketStorageBatch.ts +61 -11
  62. package/src/storage/ChecksumCache.ts +27 -14
  63. package/src/storage/SyncRulesBucketStorage.ts +42 -5
  64. package/src/storage/WriteCheckpointAPI.ts +1 -1
  65. package/src/sync/BucketChecksumState.ts +65 -8
  66. package/src/sync/RequestTracker.ts +20 -0
  67. package/src/sync/sync.ts +148 -44
  68. package/src/system/ServiceContext.ts +7 -0
  69. package/src/tracing/PerformanceTracer.ts +18 -1
  70. package/src/util/checkpointing.ts +3 -22
  71. package/src/util/config/compound-config-collector.ts +13 -1
  72. package/src/util/config/defaults.ts +1 -0
  73. package/src/util/config/types.ts +1 -0
  74. package/src/util/util-index.ts +1 -0
  75. package/src/util/utils.ts +1 -1
  76. package/src/util/write-checkpoint-batcher.ts +122 -0
  77. package/test/src/checksum_cache.test.ts +22 -0
  78. package/test/src/config.test.ts +42 -0
  79. package/test/src/sync/BucketChecksumState.test.ts +41 -1
  80. package/test/src/util/checkpointing.test.ts +170 -0
  81. package/tsconfig.tsbuildinfo +1 -1
@@ -19,6 +19,11 @@ import { SyncStorageWriteCheckpointAPI } from './WriteCheckpointAPI.js';
19
19
 
20
20
  /**
21
21
  * Storage for a specific replication stream.
22
+ *
23
+ * Replication writes source changes, source table metadata, snapshot progress,
24
+ * resume positions, and checkpoint boundaries through this interface. The sync
25
+ * API reads the same stream state as checkpoints, checksums, bucket data,
26
+ * parameter lookup rows, and write-checkpoint acknowledgements.
22
27
  */
23
28
  export interface SyncRulesBucketStorage
24
29
  extends ObserverClient<SyncRulesBucketStorageListener>,
@@ -31,13 +36,18 @@ export interface SyncRulesBucketStorage
31
36
  readonly logger: Logger;
32
37
 
33
38
  /**
34
- * Create a new writer.
39
+ * Create a writer for a source-specific unit of replication work.
35
40
  *
36
- * The writer must be flushed and disposed when done.
41
+ * The writer may be used for a long-running streaming attempt, a table
42
+ * snapshot, a page of snapshot work, or a short setup step. Pending work
43
+ * that should be persisted must be flushed, committed, or kept alive at the
44
+ * appropriate source boundary before the writer is disposed.
37
45
  */
38
46
  createWriter(options: CreateWriterOptions): Promise<BucketStorageBatch>;
39
47
 
40
48
  /**
49
+ * Callback wrapper around `createWriter()`.
50
+ *
41
51
  * @deprecated Use `createWriter()` with `await using` instead.
42
52
  */
43
53
  startBatch(
@@ -103,7 +113,7 @@ export interface SyncRulesBucketStorage
103
113
  *
104
114
  * This is a best-effort optimization:
105
115
  * 1. This may include more changes than what actually occurred.
106
- * 2. This may return invalidateDataBuckets or invalidateParameterBuckets instead of of returning
116
+ * 2. This may return invalidateDataBuckets or invalidateParameterBuckets instead of returning
107
117
  * specific changes.
108
118
  * @param options
109
119
  */
@@ -128,7 +138,7 @@ export interface SyncRulesBucketStorage
128
138
  * @param options batch size options
129
139
  */
130
140
  getBucketDataBatch(
131
- checkpoint: util.InternalOpId,
141
+ checkpoint: ReplicationCheckpoint,
132
142
  dataBuckets: BucketDataRequest[],
133
143
  options?: BucketDataBatchOptions
134
144
  ): AsyncIterable<SyncBucketDataChunk>;
@@ -141,7 +151,11 @@ export interface SyncRulesBucketStorage
141
151
  * This may be slow, depending on the size of the buckets.
142
152
  * The checksums are cached internally to compensate for this, but does not cover all cases.
143
153
  */
144
- getChecksums(checkpoint: util.InternalOpId, buckets: BucketChecksumRequest[]): Promise<util.ChecksumMap>;
154
+ getChecksums(
155
+ checkpoint: ReplicationCheckpoint,
156
+ buckets: BucketChecksumRequest[],
157
+ options?: BucketChecksumOptions
158
+ ): Promise<util.ChecksumMap>;
145
159
 
146
160
  /**
147
161
  * Clear checksum cache. Primarily intended for tests.
@@ -169,6 +183,12 @@ export interface BucketChecksumRequest {
169
183
  source: BucketDataSource;
170
184
  }
171
185
 
186
+ export interface BucketChecksumOptions {
187
+ requestHint?: BucketRequestHint;
188
+ }
189
+
190
+ export type BucketRequestHint = 'bulk' | 'incremental';
191
+
172
192
  export interface ReplicationStreamStatus {
173
193
  /**
174
194
  * Source position to resume replication from.
@@ -181,6 +201,9 @@ export interface ReplicationStreamStatus {
181
201
  }
182
202
  export interface ResolveTablesOptions {
183
203
  connection_id: number;
204
+ /**
205
+ * Source table or collection metadata discovered during snapshot or streaming.
206
+ */
184
207
  source: SourceEntityDescriptor;
185
208
  /**
186
209
  * For tests only - custom id generator for stable ids.
@@ -193,11 +216,23 @@ export interface ResolveTablesOptions {
193
216
  }
194
217
 
195
218
  export interface ResolveTablesResult {
219
+ /**
220
+ * Current source table mappings that should receive replicated data.
221
+ */
196
222
  tables: SourceTable[];
223
+ /**
224
+ * Outdated source table mappings that should be removed by the connector.
225
+ */
197
226
  dropTables: SourceTable[];
198
227
  }
199
228
 
229
+ /**
230
+ * Options for creating a storage writer.
231
+ */
200
232
  export interface CreateWriterOptions extends ParseSyncConfigOptions {
233
+ /**
234
+ * Source-specific zero or empty replication position.
235
+ */
201
236
  zeroLSN: string;
202
237
  /**
203
238
  * Whether or not to store a copy of the current data.
@@ -359,6 +394,8 @@ export interface TerminateOptions extends ClearStorageOptions {
359
394
  }
360
395
 
361
396
  export interface BucketDataBatchOptions {
397
+ requestHint?: BucketRequestHint;
398
+
362
399
  /** Limit number of documents returned. Defaults to 1000. */
363
400
  limit?: number;
364
401
 
@@ -58,7 +58,7 @@ export type LastWriteCheckpointFilters = CustomWriteCheckpointFilters | ManagedW
58
58
  export interface BaseWriteCheckpointAPI {
59
59
  readonly writeCheckpointMode: WriteCheckpointMode;
60
60
  setWriteCheckpointMode(mode: WriteCheckpointMode): void;
61
- createManagedWriteCheckpoint(checkpoint: ManagedWriteCheckpointOptions): Promise<bigint>;
61
+ createManagedWriteCheckpoints(checkpoints: ManagedWriteCheckpointOptions[]): Promise<Map<string, bigint>>;
62
62
  }
63
63
 
64
64
  /**
@@ -22,9 +22,24 @@ import {
22
22
  } from '@powersync/lib-services-framework';
23
23
  import { JwtPayload } from '../auth/JwtPayload.js';
24
24
  import { ParameterSetLimitExceededError } from '../storage/storage-index.js';
25
+ import { PerformanceTracer } from '../tracing/PerformanceTracer.js';
25
26
  import { SyncContext } from './SyncContext.js';
26
27
  import { getIntersection, hasIntersection } from './util.js';
27
28
 
29
+ export type SyncCheckpointTraceCategory =
30
+ // buildNextCheckpointLine, excluding parameter computation and checksum lookups
31
+ | 'checkpoint'
32
+ // Parameter lookups
33
+ | 'parameters'
34
+ // Checksum calculations
35
+ | 'checksum'
36
+ // Waiting for a data lock to become available
37
+ | 'acquiring_lock'
38
+ // Data fetches. Holds one data lock for this period.
39
+ | 'bucket_data'
40
+ // Sending data to client, may include serialization overhead. Holds one data lock for this period in most cases.
41
+ | 'sending';
42
+
28
43
  export interface BucketChecksumStateOptions {
29
44
  syncContext: SyncContext;
30
45
  bucketStorage: BucketChecksumStateStorage;
@@ -112,13 +127,18 @@ export class BucketChecksumState {
112
127
  * @param next The updated checkpoint
113
128
  * @returns A {@link CheckpointLine} if any of the buckets watched by this connected was updated, or otherwise `null`.
114
129
  */
115
- async buildNextCheckpointLine(next: storage.StorageCheckpointUpdate): Promise<CheckpointLine | null> {
130
+ async buildNextCheckpointLine(
131
+ next: storage.StorageCheckpointUpdate,
132
+ tracer?: PerformanceTracer<SyncCheckpointTraceCategory>
133
+ ): Promise<CheckpointLine | null> {
134
+ tracer ??= new PerformanceTracer<SyncCheckpointTraceCategory>(`sync checkpoint ${next.base.checkpoint}`);
135
+
116
136
  const { writeCheckpoint, base } = next;
117
137
  const userIdForLogs = this.parameterState.syncParams.userId;
118
138
 
119
139
  const storage = this.bucketStorage;
120
140
 
121
- const update = await this.parameterState.getCheckpointUpdate(next);
141
+ const update = await this.parameterState.getCheckpointUpdate(next, tracer);
122
142
  const { buckets: allBuckets, updatedBuckets, usedParameterResults } = update;
123
143
 
124
144
  /** Set of all buckets in this checkpoint. */
@@ -161,6 +181,7 @@ export class BucketChecksumState {
161
181
 
162
182
  // Re-check updated buckets only
163
183
  let checksumLookups: storage.BucketChecksumRequest[] = [];
184
+ let hasNewBucket = false;
164
185
 
165
186
  let newChecksums = new Map<string, util.BucketChecksum>();
166
187
  for (let desc of bucketDescriptionMap.values()) {
@@ -174,11 +195,17 @@ export class BucketChecksumState {
174
195
  newChecksums.set(desc.bucket, existing);
175
196
  } else {
176
197
  checksumLookups.push({ bucket: desc.bucket, source: desc.source });
198
+ hasNewBucket ||= !this.lastChecksums.has(desc.bucket);
177
199
  }
178
200
  }
179
201
 
180
202
  if (checksumLookups.length > 0) {
181
- let updatedChecksums = await storage.getChecksums(base.checkpoint, checksumLookups);
203
+ const requestHint: storage.BucketRequestHint = hasNewBucket ? 'bulk' : 'incremental';
204
+ let updatedChecksums: util.ChecksumMap;
205
+
206
+ using _ = tracer.span('checksum');
207
+ updatedChecksums = await storage.getChecksums(base, checksumLookups, { requestHint });
208
+
182
209
  for (let [bucket, value] of updatedChecksums.entries()) {
183
210
  newChecksums.set(bucket, value);
184
211
  }
@@ -186,12 +213,23 @@ export class BucketChecksumState {
186
213
  checksumMap = newChecksums;
187
214
  } else {
188
215
  // Re-check all buckets
189
- const bucketList = [...bucketDescriptionMap.values()].map((b) => ({ bucket: b.bucket, source: b.source }));
190
- checksumMap = await storage.getChecksums(base.checkpoint, bucketList);
216
+ const hasNewBucket =
217
+ this.lastChecksums == null ||
218
+ [...bucketDescriptionMap.values()].some((b) => !this.lastChecksums!.has(b.bucket));
219
+ const requestHint: storage.BucketRequestHint = hasNewBucket ? 'bulk' : 'incremental';
220
+ const bucketList = [...bucketDescriptionMap.values()].map((b) => ({
221
+ bucket: b.bucket,
222
+ source: b.source
223
+ }));
224
+
225
+ using _ = tracer.span('checksum');
226
+ checksumMap = await storage.getChecksums(base, bucketList, { requestHint });
191
227
  }
192
228
 
193
229
  // Subset of buckets for which there may be new data in this batch.
194
230
  let bucketsToFetch: ResolvedBucket[];
231
+ let bucketDataRequestHint: storage.BucketRequestHint;
232
+ const newBucketDownloads = new Set<string>();
195
233
 
196
234
  let checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
197
235
 
@@ -232,6 +270,12 @@ export class BucketChecksumState {
232
270
  bucketsToFetch = [...generateBucketsToFetch].map((b) => {
233
271
  return bucketDescriptionMap.get(b)!;
234
272
  });
273
+ for (const bucket of diff.updatedBuckets) {
274
+ if (!this.lastChecksums!.has(bucket.bucket)) {
275
+ newBucketDownloads.add(bucket.bucket);
276
+ }
277
+ }
278
+ bucketDataRequestHint = newBucketDownloads.size > 0 ? 'bulk' : 'incremental';
235
279
 
236
280
  deferredLog = () => {
237
281
  let message = `Updated checkpoint: ${base.checkpoint} | `;
@@ -280,6 +324,10 @@ export class BucketChecksumState {
280
324
  );
281
325
  };
282
326
  bucketsToFetch = allBuckets;
327
+ for (const bucket of bucketsToFetch) {
328
+ newBucketDownloads.add(bucket.bucket);
329
+ }
330
+ bucketDataRequestHint = 'bulk';
283
331
 
284
332
  const subscriptions: util.StreamDescription[] = [];
285
333
  const streamNameToIndex = new Map<string, number>();
@@ -321,6 +369,7 @@ export class BucketChecksumState {
321
369
  return {
322
370
  checkpointLine,
323
371
  bucketsToFetch,
372
+ bucketDataRequestHint,
324
373
  advance: () => {
325
374
  hasAdvanced = true;
326
375
  // bucketDataPositions must be updated in-place - it represents the current state of
@@ -512,12 +561,15 @@ export class BucketParameterState {
512
561
  return (desc.subscribedToByDefault && this.includeDefaultStreams) || this.subscribedStreamNames.has(desc.name);
513
562
  }
514
563
 
515
- async getCheckpointUpdate(checkpoint: storage.StorageCheckpointUpdate): Promise<CheckpointUpdate> {
564
+ async getCheckpointUpdate(
565
+ checkpoint: storage.StorageCheckpointUpdate,
566
+ tracer: PerformanceTracer<SyncCheckpointTraceCategory>
567
+ ): Promise<CheckpointUpdate> {
516
568
  const querier = this.querier;
517
569
  let update: CheckpointUpdate;
518
570
  if (querier.hasDynamicBuckets) {
519
571
  try {
520
- update = await this.getCheckpointUpdateDynamic(checkpoint);
572
+ update = await this.getCheckpointUpdateDynamic(checkpoint, tracer);
521
573
  } catch (e: unknown) {
522
574
  if (e instanceof ParameterSetLimitExceededError) {
523
575
  // Too many parameter results, create a breakdown of which streams are responsible for the most queries and
@@ -580,7 +632,10 @@ export class BucketParameterState {
580
632
  /**
581
633
  * For dynamic buckets, we need to re-query the list of buckets every time.
582
634
  */
583
- private async getCheckpointUpdateDynamic(checkpoint: storage.StorageCheckpointUpdate): Promise<CheckpointUpdate> {
635
+ private async getCheckpointUpdateDynamic(
636
+ checkpoint: storage.StorageCheckpointUpdate,
637
+ tracer: PerformanceTracer<SyncCheckpointTraceCategory>
638
+ ): Promise<CheckpointUpdate> {
584
639
  const querier = this.querier;
585
640
  const staticBuckets = this.staticBuckets.values();
586
641
  const update = checkpoint.update;
@@ -636,6 +691,7 @@ export class BucketParameterState {
636
691
  }
637
692
 
638
693
  try {
694
+ using _ = tracer.span('parameters', definition);
639
695
  const results = await checkpoint.base.getParameterSets(lookups, remainingBudget);
640
696
  const numRows = results.reduce((a, b) => a + b.rows.length, 0);
641
697
 
@@ -692,6 +748,7 @@ export class BucketParameterState {
692
748
  export interface CheckpointLine {
693
749
  checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
694
750
  bucketsToFetch: ResolvedBucket[];
751
+ bucketDataRequestHint: storage.BucketRequestHint;
695
752
 
696
753
  /**
697
754
  * Call when a checkpoint line is being sent to a client, to update the internal state.
@@ -15,6 +15,8 @@ export class RequestTracker {
15
15
  largeBuckets: Record<string, number> = {};
16
16
 
17
17
  private encoding: string | undefined = undefined;
18
+ private lastCheckpointLogOperationsSynced = 0;
19
+ private lastCheckpointLogDataSyncedBytes = 0;
18
20
 
19
21
  constructor(private metrics: MetricsEngine) {
20
22
  this.metrics = metrics;
@@ -68,6 +70,19 @@ export class RequestTracker {
68
70
  encoding: this.encoding
69
71
  };
70
72
  }
73
+
74
+ getIncrementalCheckpointStats(): CheckpointLogStats {
75
+ const operationsSynced = this.operationsSynced - this.lastCheckpointLogOperationsSynced;
76
+ const dataSyncedBytes = this.dataSyncedBytes - this.lastCheckpointLogDataSyncedBytes;
77
+
78
+ this.lastCheckpointLogOperationsSynced = this.operationsSynced;
79
+ this.lastCheckpointLogDataSyncedBytes = this.dataSyncedBytes;
80
+
81
+ return {
82
+ operations_synced: operationsSynced,
83
+ data_synced_bytes: dataSyncedBytes
84
+ };
85
+ }
71
86
  }
72
87
 
73
88
  export interface OperationCounts {
@@ -83,6 +98,11 @@ export interface OperationsSentStats {
83
98
  total: number;
84
99
  }
85
100
 
101
+ export interface CheckpointLogStats {
102
+ operations_synced: number;
103
+ data_synced_bytes: number;
104
+ }
105
+
86
106
  export function statsForBatch(batch: SyncBucketData): OperationsSentStats {
87
107
  let put = 0;
88
108
  let remove = 0;