@powersync/service-core 0.0.0-dev-20250214100224 → 0.0.0-dev-20250227082606

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 (120) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/dist/api/RouteAPI.d.ts +1 -1
  3. package/dist/api/diagnostics.js +107 -169
  4. package/dist/api/diagnostics.js.map +1 -1
  5. package/dist/entry/commands/compact-action.js +10 -73
  6. package/dist/entry/commands/compact-action.js.map +1 -1
  7. package/dist/modules/AbstractModule.d.ts +1 -1
  8. package/dist/replication/AbstractReplicator.js +8 -76
  9. package/dist/replication/AbstractReplicator.js.map +1 -1
  10. package/dist/routes/RouterEngine.js.map +1 -1
  11. package/dist/routes/endpoints/checkpointing.js +3 -2
  12. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  13. package/dist/routes/endpoints/socket-route.js +7 -6
  14. package/dist/routes/endpoints/socket-route.js.map +1 -1
  15. package/dist/routes/endpoints/sync-stream.js +7 -6
  16. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  17. package/dist/runner/teardown.js +3 -65
  18. package/dist/runner/teardown.js.map +1 -1
  19. package/dist/storage/BucketStorage.d.ts +8 -441
  20. package/dist/storage/BucketStorage.js +9 -10
  21. package/dist/storage/BucketStorage.js.map +1 -1
  22. package/dist/storage/BucketStorageBatch.d.ts +130 -0
  23. package/dist/storage/BucketStorageBatch.js +10 -0
  24. package/dist/storage/BucketStorageBatch.js.map +1 -0
  25. package/dist/storage/BucketStorageFactory.d.ts +145 -0
  26. package/dist/storage/BucketStorageFactory.js +2 -0
  27. package/dist/storage/BucketStorageFactory.js.map +1 -0
  28. package/dist/storage/ChecksumCache.js.map +1 -1
  29. package/dist/storage/PersistedSyncRulesContent.d.ts +20 -0
  30. package/dist/storage/PersistedSyncRulesContent.js +2 -0
  31. package/dist/storage/PersistedSyncRulesContent.js.map +1 -0
  32. package/dist/storage/ReplicationEventPayload.d.ts +1 -1
  33. package/dist/storage/ReplicationLock.d.ts +4 -0
  34. package/dist/storage/ReplicationLock.js +2 -0
  35. package/dist/storage/ReplicationLock.js.map +1 -0
  36. package/dist/storage/SourceEntity.d.ts +6 -2
  37. package/dist/storage/SourceTable.d.ts +2 -2
  38. package/dist/storage/SourceTable.js.map +1 -1
  39. package/dist/storage/StorageEngine.d.ts +4 -4
  40. package/dist/storage/StorageEngine.js +2 -2
  41. package/dist/storage/StorageEngine.js.map +1 -1
  42. package/dist/storage/StorageProvider.d.ts +4 -1
  43. package/dist/storage/SyncRulesBucketStorage.d.ts +207 -0
  44. package/dist/storage/SyncRulesBucketStorage.js +7 -0
  45. package/dist/storage/SyncRulesBucketStorage.js.map +1 -0
  46. package/dist/storage/bson.d.ts +14 -3
  47. package/dist/storage/bson.js +18 -2
  48. package/dist/storage/bson.js.map +1 -1
  49. package/dist/storage/storage-index.d.ts +5 -0
  50. package/dist/storage/storage-index.js +5 -0
  51. package/dist/storage/storage-index.js.map +1 -1
  52. package/dist/sync/BucketChecksumState.d.ts +95 -0
  53. package/dist/sync/BucketChecksumState.js +321 -0
  54. package/dist/sync/BucketChecksumState.js.map +1 -0
  55. package/dist/sync/SyncContext.d.ts +17 -0
  56. package/dist/sync/SyncContext.js +23 -0
  57. package/dist/sync/SyncContext.js.map +1 -0
  58. package/dist/sync/sync-index.d.ts +2 -0
  59. package/dist/sync/sync-index.js +2 -0
  60. package/dist/sync/sync-index.js.map +1 -1
  61. package/dist/sync/sync.d.ts +10 -4
  62. package/dist/sync/sync.js +142 -148
  63. package/dist/sync/sync.js.map +1 -1
  64. package/dist/sync/util.d.ts +9 -0
  65. package/dist/sync/util.js +44 -0
  66. package/dist/sync/util.js.map +1 -1
  67. package/dist/system/ServiceContext.d.ts +3 -0
  68. package/dist/system/ServiceContext.js +7 -0
  69. package/dist/system/ServiceContext.js.map +1 -1
  70. package/dist/util/checkpointing.d.ts +1 -1
  71. package/dist/util/checkpointing.js +15 -78
  72. package/dist/util/checkpointing.js.map +1 -1
  73. package/dist/util/config/compound-config-collector.js +13 -1
  74. package/dist/util/config/compound-config-collector.js.map +1 -1
  75. package/dist/util/config/defaults.d.ts +5 -0
  76. package/dist/util/config/defaults.js +6 -0
  77. package/dist/util/config/defaults.js.map +1 -0
  78. package/dist/util/config/types.d.ts +7 -2
  79. package/dist/util/config/types.js.map +1 -1
  80. package/dist/util/protocol-types.d.ts +13 -4
  81. package/package.json +6 -6
  82. package/src/api/RouteAPI.ts +1 -1
  83. package/src/api/diagnostics.ts +1 -1
  84. package/src/entry/commands/compact-action.ts +2 -3
  85. package/src/modules/AbstractModule.ts +1 -1
  86. package/src/replication/AbstractReplicator.ts +7 -12
  87. package/src/routes/RouterEngine.ts +1 -0
  88. package/src/routes/endpoints/checkpointing.ts +3 -3
  89. package/src/routes/endpoints/socket-route.ts +9 -6
  90. package/src/routes/endpoints/sync-stream.ts +10 -6
  91. package/src/runner/teardown.ts +1 -1
  92. package/src/storage/BucketStorage.ts +8 -550
  93. package/src/storage/BucketStorageBatch.ts +158 -0
  94. package/src/storage/BucketStorageFactory.ts +166 -0
  95. package/src/storage/ChecksumCache.ts +1 -0
  96. package/src/storage/PersistedSyncRulesContent.ts +26 -0
  97. package/src/storage/ReplicationEventPayload.ts +1 -1
  98. package/src/storage/ReplicationLock.ts +5 -0
  99. package/src/storage/SourceEntity.ts +6 -2
  100. package/src/storage/SourceTable.ts +1 -1
  101. package/src/storage/StorageEngine.ts +4 -4
  102. package/src/storage/StorageProvider.ts +4 -1
  103. package/src/storage/SyncRulesBucketStorage.ts +265 -0
  104. package/src/storage/bson.ts +22 -4
  105. package/src/storage/storage-index.ts +5 -0
  106. package/src/sync/BucketChecksumState.ts +418 -0
  107. package/src/sync/SyncContext.ts +36 -0
  108. package/src/sync/sync-index.ts +2 -0
  109. package/src/sync/sync.ts +199 -177
  110. package/src/sync/util.ts +54 -0
  111. package/src/system/ServiceContext.ts +9 -0
  112. package/src/util/checkpointing.ts +4 -6
  113. package/src/util/config/compound-config-collector.ts +24 -1
  114. package/src/util/config/defaults.ts +5 -0
  115. package/src/util/config/types.ts +8 -2
  116. package/src/util/protocol-types.ts +16 -4
  117. package/test/src/auth.test.ts +5 -5
  118. package/test/src/sync/BucketChecksumState.test.ts +580 -0
  119. package/test/src/sync/util.test.ts +34 -0
  120. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,418 @@
1
+ import { BucketDescription, RequestParameters, SqlSyncRules } from '@powersync/service-sync-rules';
2
+
3
+ import * as storage from '../storage/storage-index.js';
4
+ import * as util from '../util/util-index.js';
5
+
6
+ import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
7
+ import { BucketParameterQuerier } from '@powersync/service-sync-rules/src/BucketParameterQuerier.js';
8
+ import { BucketSyncState } from './sync.js';
9
+ import { SyncContext } from './SyncContext.js';
10
+
11
+ export interface BucketChecksumStateOptions {
12
+ syncContext: SyncContext;
13
+ bucketStorage: BucketChecksumStateStorage;
14
+ syncRules: SqlSyncRules;
15
+ syncParams: RequestParameters;
16
+ initialBucketPositions?: { name: string; after: string }[];
17
+ }
18
+
19
+ /**
20
+ * Represents the state of the checksums and data for a specific connection.
21
+ *
22
+ * Handles incrementally re-computing checkpoints.
23
+ */
24
+ export class BucketChecksumState {
25
+ private readonly context: SyncContext;
26
+ private readonly bucketStorage: BucketChecksumStateStorage;
27
+
28
+ /**
29
+ * Bucket state of bucket id -> op_id.
30
+ * This starts with the state from the client. May contain buckets that the user do not have access to (anymore).
31
+ */
32
+ public bucketDataPositions = new Map<string, BucketSyncState>();
33
+
34
+ /**
35
+ * Last checksums sent to the client. We keep this to calculate checkpoint diffs.
36
+ */
37
+ private lastChecksums: util.ChecksumMap | null = null;
38
+ private lastWriteCheckpoint: bigint | null = null;
39
+
40
+ private readonly parameterState: BucketParameterState;
41
+
42
+ /**
43
+ * Keep track of buckets that need to be downloaded. This is specifically relevant when
44
+ * partial checkpoints are sent.
45
+ */
46
+ private pendingBucketDownloads = new Set<string>();
47
+
48
+ constructor(options: BucketChecksumStateOptions) {
49
+ this.context = options.syncContext;
50
+ this.bucketStorage = options.bucketStorage;
51
+ this.parameterState = new BucketParameterState(
52
+ options.syncContext,
53
+ options.bucketStorage,
54
+ options.syncRules,
55
+ options.syncParams
56
+ );
57
+ this.bucketDataPositions = new Map();
58
+
59
+ for (let { name, after: start } of options.initialBucketPositions ?? []) {
60
+ this.bucketDataPositions.set(name, { start_op_id: start });
61
+ }
62
+ }
63
+
64
+ async buildNextCheckpointLine(next: storage.StorageCheckpointUpdate): Promise<CheckpointLine | null> {
65
+ const { writeCheckpoint, base } = next;
66
+ const user_id = this.parameterState.syncParams.user_id;
67
+
68
+ const storage = this.bucketStorage;
69
+
70
+ const update = await this.parameterState.getCheckpointUpdate(next);
71
+ if (update == null) {
72
+ return null;
73
+ }
74
+
75
+ const { buckets: allBuckets, updatedBuckets } = update;
76
+
77
+ let dataBucketsNew = new Map<string, BucketSyncState>();
78
+ for (let bucket of allBuckets) {
79
+ dataBucketsNew.set(bucket.bucket, {
80
+ description: bucket,
81
+ start_op_id: this.bucketDataPositions.get(bucket.bucket)?.start_op_id ?? '0'
82
+ });
83
+ }
84
+ this.bucketDataPositions = dataBucketsNew;
85
+ if (dataBucketsNew.size > this.context.maxBuckets) {
86
+ throw new ServiceError(
87
+ ErrorCode.PSYNC_S2305,
88
+ `Too many buckets: ${dataBucketsNew.size} (limit of ${this.context.maxBuckets})`
89
+ );
90
+ }
91
+
92
+ let checksumMap: util.ChecksumMap;
93
+ if (updatedBuckets != null) {
94
+ if (this.lastChecksums == null) {
95
+ throw new ServiceAssertionError(`Bucket diff received without existing checksums`);
96
+ }
97
+
98
+ // Re-check updated buckets only
99
+ let checksumLookups: string[] = [];
100
+
101
+ let newChecksums = new Map<string, util.BucketChecksum>();
102
+ for (let bucket of dataBucketsNew.keys()) {
103
+ if (!updatedBuckets.has(bucket)) {
104
+ const existing = this.lastChecksums.get(bucket);
105
+ if (existing == null) {
106
+ // If this happens, it means updatedBuckets did not correctly include all new buckets
107
+ throw new ServiceAssertionError(`Existing checksum not found for bucket ${bucket}`);
108
+ }
109
+ // Bucket is not specifically updated, and we have a previous checksum
110
+ newChecksums.set(bucket, existing);
111
+ } else {
112
+ checksumLookups.push(bucket);
113
+ }
114
+ }
115
+
116
+ let updatedChecksums = await storage.getChecksums(base.checkpoint, checksumLookups);
117
+ for (let [bucket, value] of updatedChecksums.entries()) {
118
+ newChecksums.set(bucket, value);
119
+ }
120
+ checksumMap = newChecksums;
121
+ } else {
122
+ // Re-check all buckets
123
+ const bucketList = [...dataBucketsNew.keys()];
124
+ checksumMap = await storage.getChecksums(base.checkpoint, bucketList);
125
+ }
126
+ // Subset of buckets for which there may be new data in this batch.
127
+ let bucketsToFetch: BucketDescription[];
128
+
129
+ let checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
130
+
131
+ if (this.lastChecksums) {
132
+ // TODO: If updatedBuckets is present, we can use that to more efficiently calculate a diff,
133
+ // and avoid any unnecessary loops through the entire list of buckets.
134
+ const diff = util.checksumsDiff(this.lastChecksums, checksumMap);
135
+
136
+ if (
137
+ this.lastWriteCheckpoint == writeCheckpoint &&
138
+ diff.removedBuckets.length == 0 &&
139
+ diff.updatedBuckets.length == 0
140
+ ) {
141
+ // No changes - don't send anything to the client
142
+ return null;
143
+ }
144
+
145
+ let generateBucketsToFetch = new Set<string>();
146
+ for (let bucket of diff.updatedBuckets) {
147
+ generateBucketsToFetch.add(bucket.bucket);
148
+ }
149
+ for (let bucket of this.pendingBucketDownloads) {
150
+ // Bucket from a previous checkpoint that hasn't been downloaded yet.
151
+ // If we still have this bucket, include it in the list of buckets to fetch.
152
+ if (checksumMap.has(bucket)) {
153
+ generateBucketsToFetch.add(bucket);
154
+ }
155
+ }
156
+
157
+ const updatedBucketDescriptions = diff.updatedBuckets.map((e) => ({
158
+ ...e,
159
+ priority: this.bucketDataPositions.get(e.bucket)!.description!.priority
160
+ }));
161
+ bucketsToFetch = [...generateBucketsToFetch].map((b) => {
162
+ return {
163
+ bucket: b,
164
+ priority: this.bucketDataPositions.get(b)!.description!.priority
165
+ };
166
+ });
167
+
168
+ let message = `Updated checkpoint: ${base.checkpoint} | `;
169
+ message += `write: ${writeCheckpoint} | `;
170
+ message += `buckets: ${allBuckets.length} | `;
171
+ message += `updated: ${limitedBuckets(diff.updatedBuckets, 20)} | `;
172
+ message += `removed: ${limitedBuckets(diff.removedBuckets, 20)}`;
173
+ logger.info(message, {
174
+ checkpoint: base.checkpoint,
175
+ user_id: user_id,
176
+ buckets: allBuckets.length,
177
+ updated: diff.updatedBuckets.length,
178
+ removed: diff.removedBuckets.length
179
+ });
180
+
181
+ checkpointLine = {
182
+ checkpoint_diff: {
183
+ last_op_id: base.checkpoint,
184
+ write_checkpoint: writeCheckpoint ? String(writeCheckpoint) : undefined,
185
+ removed_buckets: diff.removedBuckets,
186
+ updated_buckets: updatedBucketDescriptions
187
+ }
188
+ } satisfies util.StreamingSyncCheckpointDiff;
189
+ } else {
190
+ let message = `New checkpoint: ${base.checkpoint} | write: ${writeCheckpoint} | `;
191
+ message += `buckets: ${allBuckets.length} ${limitedBuckets(allBuckets, 20)}`;
192
+ logger.info(message, { checkpoint: base.checkpoint, user_id: user_id, buckets: allBuckets.length });
193
+ bucketsToFetch = allBuckets;
194
+ checkpointLine = {
195
+ checkpoint: {
196
+ last_op_id: base.checkpoint,
197
+ write_checkpoint: writeCheckpoint ? String(writeCheckpoint) : undefined,
198
+ buckets: [...checksumMap.values()].map((e) => ({
199
+ ...e,
200
+ priority: this.bucketDataPositions.get(e.bucket)!.description!.priority
201
+ }))
202
+ }
203
+ } satisfies util.StreamingSyncCheckpoint;
204
+ }
205
+
206
+ this.lastChecksums = checksumMap;
207
+ this.lastWriteCheckpoint = writeCheckpoint;
208
+ this.pendingBucketDownloads = new Set(bucketsToFetch.map((b) => b.bucket));
209
+
210
+ return {
211
+ checkpointLine,
212
+ bucketsToFetch
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Get bucket positions to sync, given the list of buckets.
218
+ *
219
+ * @param bucketsToFetch List of buckets to fetch, typically from buildNextCheckpointLine, or a subset of that
220
+ * @returns
221
+ */
222
+ getFilteredBucketPositions(bucketsToFetch: BucketDescription[]): Map<string, string> {
223
+ const filtered = new Map<string, string>();
224
+ for (let bucket of bucketsToFetch) {
225
+ const state = this.bucketDataPositions.get(bucket.bucket);
226
+ if (state) {
227
+ filtered.set(bucket.bucket, state.start_op_id);
228
+ }
229
+ }
230
+ return filtered;
231
+ }
232
+
233
+ /**
234
+ * Update the position of bucket data the client has.
235
+ *
236
+ * @param bucket the bucket name
237
+ * @param nextAfter sync operations >= this value in the next batch
238
+ */
239
+ updateBucketPosition(options: { bucket: string; nextAfter: string; hasMore: boolean }) {
240
+ const state = this.bucketDataPositions.get(options.bucket);
241
+ if (state) {
242
+ state.start_op_id = options.nextAfter;
243
+ }
244
+ if (!options.hasMore) {
245
+ this.pendingBucketDownloads.delete(options.bucket);
246
+ }
247
+ }
248
+ }
249
+
250
+ export interface CheckpointUpdate {
251
+ /**
252
+ * All buckets forming part of the checkpoint.
253
+ */
254
+ buckets: BucketDescription[];
255
+
256
+ /**
257
+ * If present, a set of buckets that have been updated since the last checkpoint.
258
+ *
259
+ * If null, assume that any bucket in `buckets` may have been updated.
260
+ */
261
+ updatedBuckets: Set<string> | null;
262
+ }
263
+
264
+ export class BucketParameterState {
265
+ private readonly context: SyncContext;
266
+ public readonly bucketStorage: BucketChecksumStateStorage;
267
+ public readonly syncRules: SqlSyncRules;
268
+ public readonly syncParams: RequestParameters;
269
+ private readonly querier: BucketParameterQuerier;
270
+ private readonly staticBuckets: Map<string, BucketDescription>;
271
+
272
+ constructor(
273
+ context: SyncContext,
274
+ bucketStorage: BucketChecksumStateStorage,
275
+ syncRules: SqlSyncRules,
276
+ syncParams: RequestParameters
277
+ ) {
278
+ this.context = context;
279
+ this.bucketStorage = bucketStorage;
280
+ this.syncRules = syncRules;
281
+ this.syncParams = syncParams;
282
+
283
+ this.querier = syncRules.getBucketParameterQuerier(this.syncParams);
284
+ this.staticBuckets = new Map<string, BucketDescription>(this.querier.staticBuckets.map((b) => [b.bucket, b]));
285
+ }
286
+
287
+ async getCheckpointUpdate(checkpoint: storage.StorageCheckpointUpdate): Promise<CheckpointUpdate | null> {
288
+ const querier = this.querier;
289
+ let update: CheckpointUpdate | null;
290
+ if (querier.hasDynamicBuckets) {
291
+ update = await this.getCheckpointUpdateDynamic(checkpoint);
292
+ } else {
293
+ update = await this.getCheckpointUpdateStatic(checkpoint);
294
+ }
295
+
296
+ if (update == null) {
297
+ return null;
298
+ }
299
+
300
+ if (update.buckets.length > this.context.maxParameterQueryResults) {
301
+ // TODO: Limit number of results even before we get to this point
302
+ // This limit applies _before_ we get the unique set
303
+ const error = new ServiceError(
304
+ ErrorCode.PSYNC_S2305,
305
+ `Too many parameter query results: ${update.buckets.length} (limit of ${this.context.maxParameterQueryResults})`
306
+ );
307
+ logger.error(error.message, {
308
+ checkpoint: checkpoint,
309
+ user_id: this.syncParams.user_id,
310
+ buckets: update.buckets.length
311
+ });
312
+
313
+ throw error;
314
+ }
315
+ return update;
316
+ }
317
+
318
+ /**
319
+ * For static buckets, we can keep track of which buckets have been updated.
320
+ */
321
+ private async getCheckpointUpdateStatic(
322
+ checkpoint: storage.StorageCheckpointUpdate
323
+ ): Promise<CheckpointUpdate | null> {
324
+ const querier = this.querier;
325
+ const update = checkpoint.update;
326
+
327
+ if (update.invalidateDataBuckets) {
328
+ return {
329
+ buckets: querier.staticBuckets,
330
+ updatedBuckets: null
331
+ };
332
+ }
333
+
334
+ let updatedBuckets = new Set<string>();
335
+
336
+ for (let bucket of update.updatedDataBuckets ?? []) {
337
+ if (this.staticBuckets.has(bucket)) {
338
+ updatedBuckets.add(bucket);
339
+ }
340
+ }
341
+
342
+ if (updatedBuckets.size == 0) {
343
+ // No change - skip this checkpoint
344
+ return null;
345
+ }
346
+
347
+ return {
348
+ buckets: querier.staticBuckets,
349
+ updatedBuckets
350
+ };
351
+ }
352
+
353
+ /**
354
+ * For dynamic buckets, we need to re-query the list of buckets every time.
355
+ */
356
+ private async getCheckpointUpdateDynamic(
357
+ checkpoint: storage.StorageCheckpointUpdate
358
+ ): Promise<CheckpointUpdate | null> {
359
+ const querier = this.querier;
360
+ const storage = this.bucketStorage;
361
+ const staticBuckets = querier.staticBuckets;
362
+ const update = checkpoint.update;
363
+
364
+ let hasChange = false;
365
+ if (update.invalidateDataBuckets || update.updatedDataBuckets?.length > 0) {
366
+ hasChange = true;
367
+ } else if (update.invalidateParameterBuckets) {
368
+ hasChange = true;
369
+ } else {
370
+ for (let bucket of update.updatedParameterBucketDefinitions ?? []) {
371
+ if (querier.dynamicBucketDefinitions.has(bucket)) {
372
+ hasChange = true;
373
+ break;
374
+ }
375
+ }
376
+ }
377
+
378
+ if (!hasChange) {
379
+ return null;
380
+ }
381
+
382
+ const dynamicBuckets = await querier.queryDynamicBucketDescriptions({
383
+ getParameterSets(lookups) {
384
+ return storage.getParameterSets(checkpoint.base.checkpoint, lookups);
385
+ }
386
+ });
387
+ const allBuckets = [...staticBuckets, ...dynamicBuckets];
388
+
389
+ return {
390
+ buckets: allBuckets,
391
+ // We cannot track individual bucket updates for dynamic lookups yet
392
+ updatedBuckets: null
393
+ };
394
+ }
395
+ }
396
+
397
+ export interface CheckpointLine {
398
+ checkpointLine: util.StreamingSyncCheckpointDiff | util.StreamingSyncCheckpoint;
399
+ bucketsToFetch: BucketDescription[];
400
+ }
401
+
402
+ // Use a more specific type to simplify testing
403
+ export type BucketChecksumStateStorage = Pick<storage.SyncRulesBucketStorage, 'getChecksums' | 'getParameterSets'>;
404
+
405
+ function limitedBuckets(buckets: string[] | { bucket: string }[], limit: number) {
406
+ buckets = buckets.map((b) => {
407
+ if (typeof b != 'string') {
408
+ return b.bucket;
409
+ } else {
410
+ return b;
411
+ }
412
+ });
413
+ if (buckets.length <= limit) {
414
+ return JSON.stringify(buckets);
415
+ }
416
+ const limited = buckets.slice(0, limit);
417
+ return `${JSON.stringify(limited)}...`;
418
+ }
@@ -0,0 +1,36 @@
1
+ import { Semaphore, SemaphoreInterface, withTimeout } from 'async-mutex';
2
+
3
+ export interface SyncContextOptions {
4
+ maxBuckets: number;
5
+ maxParameterQueryResults: number;
6
+ maxDataFetchConcurrency: number;
7
+ }
8
+
9
+ /**
10
+ * Maximum duration to wait for the mutex to become available.
11
+ *
12
+ * This gives an explicit error if there are mutex issues, rather than just hanging.
13
+ */
14
+ const MUTEX_ACQUIRE_TIMEOUT = 30_000;
15
+
16
+ /**
17
+ * Represents the context in which sync happens.
18
+ *
19
+ * This is global to all sync requests, not per request.
20
+ */
21
+ export class SyncContext {
22
+ readonly maxBuckets: number;
23
+ readonly maxParameterQueryResults: number;
24
+
25
+ readonly syncSemaphore: SemaphoreInterface;
26
+
27
+ constructor(options: SyncContextOptions) {
28
+ this.maxBuckets = options.maxBuckets;
29
+ this.maxParameterQueryResults = options.maxParameterQueryResults;
30
+ this.syncSemaphore = withTimeout(
31
+ new Semaphore(options.maxDataFetchConcurrency),
32
+ MUTEX_ACQUIRE_TIMEOUT,
33
+ new Error(`Timeout while waiting for data`)
34
+ );
35
+ }
36
+ }
@@ -5,3 +5,5 @@ export * from './RequestTracker.js';
5
5
  export * from './safeRace.js';
6
6
  export * from './sync.js';
7
7
  export * from './util.js';
8
+ export * from './BucketChecksumState.js';
9
+ export * from './SyncContext.js';