@powersync/service-core 1.21.0 → 1.23.0
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 +48 -0
- package/dist/api/RouteAPI.d.ts +2 -2
- package/dist/api/diagnostics.d.ts +12 -1
- package/dist/api/diagnostics.js +34 -22
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.js +3 -2
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/entry/commands/compact-action.js +1 -1
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +6 -2
- package/dist/modules/AbstractModule.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +2 -2
- package/dist/replication/AbstractReplicator.js +24 -23
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/RelationCache.d.ts +9 -2
- package/dist/replication/RelationCache.js +21 -2
- package/dist/replication/RelationCache.js.map +1 -1
- package/dist/replication/ReplicationModule.d.ts +3 -3
- package/dist/replication/ReplicationModule.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +12 -12
- package/dist/routes/configure-fastify.js +3 -1
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/admin.d.ts +24 -24
- package/dist/routes/endpoints/admin.js +49 -35
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.js +1 -1
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +1 -1
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +22 -13
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +1 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.d.ts +2 -0
- package/dist/routes/route-register.js +65 -3
- package/dist/routes/route-register.js.map +1 -1
- package/dist/runner/teardown.js +8 -8
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +29 -6
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +26 -27
- package/dist/storage/BucketStorageFactory.js +5 -19
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/ParsedSyncConfigSet.d.ts +23 -0
- package/dist/storage/ParsedSyncConfigSet.js +2 -0
- package/dist/storage/ParsedSyncConfigSet.js.map +1 -0
- package/dist/storage/PersistedReplicationStream.d.ts +32 -0
- package/dist/storage/PersistedReplicationStream.js +26 -0
- package/dist/storage/PersistedReplicationStream.js.map +1 -0
- package/dist/storage/PersistedSyncConfigContent.d.ts +60 -0
- package/dist/storage/PersistedSyncConfigContent.js +123 -0
- package/dist/storage/PersistedSyncConfigContent.js.map +1 -0
- package/dist/storage/PersistedSyncConfigStatus.d.ts +13 -0
- package/dist/storage/PersistedSyncConfigStatus.js +2 -0
- package/dist/storage/PersistedSyncConfigStatus.js.map +1 -0
- package/dist/storage/SourceEntity.d.ts +8 -1
- package/dist/storage/SourceTable.d.ts +37 -10
- package/dist/storage/SourceTable.js +50 -14
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +80 -24
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/implementation/BucketDefinitionMapping.d.ts +117 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js +277 -0
- package/dist/storage/implementation/BucketDefinitionMapping.js.map +1 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.d.ts +6 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +31 -0
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js.map +1 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.d.ts +17 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js +62 -0
- package/dist/storage/implementation/IncrementalReprocessingSyncConfigLog.js.map +1 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.d.ts +15 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.js +20 -0
- package/dist/storage/implementation/ReplicationStreamStorageIds.js.map +1 -0
- package/dist/storage/storage-index.d.ts +8 -1
- package/dist/storage/storage-index.js +8 -1
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +4 -4
- package/dist/sync/BucketChecksumState.js +1 -1
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -2
- package/dist/sync/sync.js +5 -0
- package/dist/sync/sync.js.map +1 -1
- package/dist/tracing/PerformanceTracer.d.ts +17 -1
- package/dist/tracing/PerformanceTracer.js +3 -0
- package/dist/tracing/PerformanceTracer.js.map +1 -1
- package/dist/util/checkpointing.js +1 -1
- package/dist/util/checkpointing.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -1
- package/dist/util/lsn.d.ts +4 -0
- package/dist/util/lsn.js +10 -0
- package/dist/util/lsn.js.map +1 -1
- package/dist/util/util-index.d.ts +1 -0
- package/dist/util/util-index.js +1 -0
- package/dist/util/util-index.js.map +1 -1
- package/dist/util/utils.d.ts +5 -0
- package/dist/util/utils.js +7 -0
- package/dist/util/utils.js.map +1 -1
- package/package.json +6 -7
- package/src/api/RouteAPI.ts +2 -2
- package/src/api/diagnostics.ts +45 -28
- package/src/auth/RemoteJWKSCollector.ts +3 -1
- package/src/entry/commands/compact-action.ts +1 -1
- package/src/modules/AbstractModule.ts +6 -2
- package/src/replication/AbstractReplicator.ts +27 -26
- package/src/replication/RelationCache.ts +23 -4
- package/src/replication/ReplicationModule.ts +2 -3
- package/src/routes/configure-fastify.ts +8 -1
- package/src/routes/endpoints/admin.ts +75 -49
- package/src/routes/endpoints/checkpointing.ts +1 -1
- package/src/routes/endpoints/socket-route.ts +1 -1
- package/src/routes/endpoints/sync-rules.ts +23 -13
- package/src/routes/endpoints/sync-stream.ts +1 -1
- package/src/routes/route-register.ts +73 -4
- package/src/runner/teardown.ts +8 -8
- package/src/storage/BucketStorageBatch.ts +32 -7
- package/src/storage/BucketStorageFactory.ts +36 -40
- package/src/storage/ParsedSyncConfigSet.ts +25 -0
- package/src/storage/PersistedReplicationStream.ts +55 -0
- package/src/storage/PersistedSyncConfigContent.ts +188 -0
- package/src/storage/PersistedSyncConfigStatus.ts +14 -0
- package/src/storage/SourceEntity.ts +9 -1
- package/src/storage/SourceTable.ts +72 -21
- package/src/storage/SyncRulesBucketStorage.ts +87 -26
- package/src/storage/implementation/BucketDefinitionMapping.ts +436 -0
- package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +41 -0
- package/src/storage/implementation/IncrementalReprocessingSyncConfigLog.ts +109 -0
- package/src/storage/implementation/ReplicationStreamStorageIds.ts +21 -0
- package/src/storage/storage-index.ts +9 -1
- package/src/sync/BucketChecksumState.ts +5 -5
- package/src/sync/sync.ts +8 -3
- package/src/tracing/PerformanceTracer.ts +24 -1
- package/src/util/checkpointing.ts +1 -1
- package/src/util/config/types.ts +1 -1
- package/src/util/lsn.ts +9 -0
- package/src/util/util-index.ts +1 -0
- package/src/util/utils.ts +8 -0
- package/test/src/auth.test.ts +11 -0
- package/test/src/diagnostics.test.ts +64 -33
- package/test/src/module-loader.test.ts +33 -1
- package/test/src/routes/admin.test.ts +180 -18
- package/test/src/routes/error-handler.integration.test.ts +275 -0
- package/test/src/routes/mocks.ts +6 -2
- package/test/src/routes/stream.test.ts +15 -4
- package/test/src/storage/SourceTable.test.ts +89 -0
- package/test/src/sync/BucketChecksumState.test.ts +25 -17
- package/tsconfig.json +1 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +0 -59
- package/dist/storage/PersistedSyncRulesContent.js +0 -100
- package/dist/storage/PersistedSyncRulesContent.js.map +0 -1
- package/src/storage/PersistedSyncRulesContent.ts +0 -174
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { Logger, ObserverClient } from '@powersync/lib-services-framework';
|
|
2
2
|
import {
|
|
3
3
|
BucketDataSource,
|
|
4
|
-
|
|
4
|
+
HydratedSyncConfig,
|
|
5
5
|
ParameterLookupRows,
|
|
6
6
|
ScopedParameterLookup
|
|
7
7
|
} from '@powersync/service-sync-rules';
|
|
8
|
+
import * as bson from 'bson';
|
|
8
9
|
import { PerformanceTracer } from '../tracing/PerformanceTracer.js';
|
|
9
10
|
import * as util from '../util/util-index.js';
|
|
10
11
|
import { BucketStorageBatch, FlushedResult, SaveUpdate } from './BucketStorageBatch.js';
|
|
11
12
|
import { BucketStorageFactory } from './BucketStorageFactory.js';
|
|
12
|
-
import {
|
|
13
|
+
import { ParsedSyncConfigSet } from './ParsedSyncConfigSet.js';
|
|
14
|
+
import { ParseSyncConfigOptions } from './PersistedSyncConfigContent.js';
|
|
13
15
|
import { SourceEntityDescriptor } from './SourceEntity.js';
|
|
14
16
|
import { SourceTable } from './SourceTable.js';
|
|
17
|
+
import { StorageVersionConfig } from './StorageVersionConfig.js';
|
|
15
18
|
import { SyncStorageWriteCheckpointAPI } from './WriteCheckpointAPI.js';
|
|
16
19
|
|
|
17
20
|
/**
|
|
@@ -20,17 +23,13 @@ import { SyncStorageWriteCheckpointAPI } from './WriteCheckpointAPI.js';
|
|
|
20
23
|
export interface SyncRulesBucketStorage
|
|
21
24
|
extends ObserverClient<SyncRulesBucketStorageListener>,
|
|
22
25
|
SyncStorageWriteCheckpointAPI {
|
|
23
|
-
readonly
|
|
24
|
-
readonly
|
|
26
|
+
readonly replicationStreamId: number;
|
|
27
|
+
readonly replicationStreamName: string;
|
|
28
|
+
readonly storageConfig: StorageVersionConfig;
|
|
25
29
|
|
|
26
30
|
readonly factory: BucketStorageFactory;
|
|
27
31
|
readonly logger: Logger;
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
* Resolve a table, keeping track of it internally.
|
|
31
|
-
*/
|
|
32
|
-
resolveTable(options: ResolveTableOptions): Promise<ResolveTableResult>;
|
|
33
|
-
|
|
34
33
|
/**
|
|
35
34
|
* Create a new writer.
|
|
36
35
|
*
|
|
@@ -46,7 +45,20 @@ export interface SyncRulesBucketStorage
|
|
|
46
45
|
callback: (batch: BucketStorageBatch) => Promise<void>
|
|
47
46
|
): Promise<FlushedResult | null>;
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Get the canonical parsed sync config set for the given parse options.
|
|
50
|
+
*
|
|
51
|
+
* Repeated calls with the same options return the same instance for the lifetime of
|
|
52
|
+
* this storage instance. This is the identity boundary for parsed sync config state:
|
|
53
|
+
* all operations against this storage instance must use parsed objects from sets
|
|
54
|
+
* returned here, never from an independent parse of the same content.
|
|
55
|
+
*/
|
|
56
|
+
getParsedSyncConfigSet(options: ParseSyncConfigOptions): ParsedSyncConfigSet;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Shortcut for `getParsedSyncConfigSet(options).hydratedSyncConfig`.
|
|
60
|
+
*/
|
|
61
|
+
getParsedSyncRules(options: ParseSyncConfigOptions): HydratedSyncConfig;
|
|
50
62
|
|
|
51
63
|
/**
|
|
52
64
|
* Terminate the replication stream.
|
|
@@ -57,7 +69,7 @@ export interface SyncRulesBucketStorage
|
|
|
57
69
|
*/
|
|
58
70
|
terminate(options?: TerminateOptions): Promise<void>;
|
|
59
71
|
|
|
60
|
-
getStatus(): Promise<
|
|
72
|
+
getStatus(): Promise<ReplicationStreamStatus>;
|
|
61
73
|
|
|
62
74
|
/**
|
|
63
75
|
* Clear the storage, without changing state.
|
|
@@ -135,6 +147,12 @@ export interface SyncRulesBucketStorage
|
|
|
135
147
|
* Clear checksum cache. Primarily intended for tests.
|
|
136
148
|
*/
|
|
137
149
|
clearChecksumCache(): void;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Optional storage-provider cleanup for sync configs that have been stopped
|
|
153
|
+
* inside this replication stream.
|
|
154
|
+
*/
|
|
155
|
+
cleanupStoppedSyncConfigs?(options: CleanupStoppedSyncConfigsOptions): Promise<CleanupStoppedSyncConfigsResult>;
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
export interface SyncRulesBucketStorageListener {
|
|
@@ -151,27 +169,35 @@ export interface BucketChecksumRequest {
|
|
|
151
169
|
source: BucketDataSource;
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
export interface
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
172
|
+
export interface ReplicationStreamStatus {
|
|
173
|
+
/**
|
|
174
|
+
* Source position to resume replication from.
|
|
175
|
+
*/
|
|
176
|
+
resumeLsn: string | null;
|
|
177
|
+
/**
|
|
178
|
+
* True if _every_ active/processing sync config has a client-visible checkpoint.
|
|
179
|
+
*/
|
|
180
|
+
snapshotDone: boolean;
|
|
159
181
|
}
|
|
160
|
-
export interface
|
|
161
|
-
group_id: number;
|
|
182
|
+
export interface ResolveTablesOptions {
|
|
162
183
|
connection_id: number;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
184
|
+
source: SourceEntityDescriptor;
|
|
185
|
+
/**
|
|
186
|
+
* For tests only - custom id generator for stable ids.
|
|
187
|
+
*/
|
|
188
|
+
idGenerator?: () => string | bson.ObjectId;
|
|
189
|
+
/**
|
|
190
|
+
* For tests only - override the parsed sync config set used.
|
|
191
|
+
*/
|
|
192
|
+
parsedSyncConfig?: ParsedSyncConfigSet;
|
|
167
193
|
}
|
|
168
194
|
|
|
169
|
-
export interface
|
|
170
|
-
|
|
195
|
+
export interface ResolveTablesResult {
|
|
196
|
+
tables: SourceTable[];
|
|
171
197
|
dropTables: SourceTable[];
|
|
172
198
|
}
|
|
173
199
|
|
|
174
|
-
export interface CreateWriterOptions extends
|
|
200
|
+
export interface CreateWriterOptions extends ParseSyncConfigOptions {
|
|
175
201
|
zeroLSN: string;
|
|
176
202
|
/**
|
|
177
203
|
* Whether or not to store a copy of the current data.
|
|
@@ -198,11 +224,18 @@ export interface CreateWriterOptions extends ParseSyncRulesOptions {
|
|
|
198
224
|
*/
|
|
199
225
|
markRecordUnavailable?: BucketStorageMarkRecordUnavailable;
|
|
200
226
|
|
|
227
|
+
hooks?: StorageHooks;
|
|
228
|
+
|
|
201
229
|
tracer?: PerformanceTracer<'storage' | 'evaluate'>;
|
|
202
230
|
|
|
203
231
|
logger?: Logger;
|
|
204
232
|
}
|
|
205
233
|
|
|
234
|
+
export interface StorageHooks {
|
|
235
|
+
beforeBatchFlush?: (batch: BucketStorageBatch) => Promise<void>;
|
|
236
|
+
afterBatchFlush?: (batch: BucketStorageBatch) => Promise<void>;
|
|
237
|
+
}
|
|
238
|
+
|
|
206
239
|
/**
|
|
207
240
|
* @deprecated Use `CreateWriterOptions`.
|
|
208
241
|
*/
|
|
@@ -249,6 +282,9 @@ export interface CompactOptions {
|
|
|
249
282
|
/** Minimum of 1 */
|
|
250
283
|
moveBatchQueryLimit?: number;
|
|
251
284
|
|
|
285
|
+
/** Byte cap per read batch for streaming compaction. Default 16MB. */
|
|
286
|
+
moveBatchByteLimit?: number;
|
|
287
|
+
|
|
252
288
|
/**
|
|
253
289
|
* Minimum number new operations in a bucket to trigger compaction of that bucket.
|
|
254
290
|
*
|
|
@@ -274,7 +310,14 @@ export interface CompactOptions {
|
|
|
274
310
|
}
|
|
275
311
|
|
|
276
312
|
export interface PopulateChecksumCacheOptions {
|
|
277
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Compute checksums up to this op id.
|
|
315
|
+
*
|
|
316
|
+
* Defaults to the highest persisted op id for the replication stream, which covers
|
|
317
|
+
* the common case of populating the cache right after initial replication, before
|
|
318
|
+
* the first checkpoint exists.
|
|
319
|
+
*/
|
|
320
|
+
maxOpId?: util.InternalOpId;
|
|
278
321
|
minBucketChanges?: number;
|
|
279
322
|
signal?: AbortSignal;
|
|
280
323
|
}
|
|
@@ -290,6 +333,24 @@ export interface ClearStorageOptions {
|
|
|
290
333
|
signal?: AbortSignal;
|
|
291
334
|
}
|
|
292
335
|
|
|
336
|
+
export interface CleanupStoppedSyncConfigsOptions {
|
|
337
|
+
signal?: AbortSignal;
|
|
338
|
+
logger?: Logger;
|
|
339
|
+
defaultSchema: string;
|
|
340
|
+
sourceConnectionTag: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface CleanupStoppedSyncConfigsResult {
|
|
344
|
+
stoppedSyncConfigsRemoved: number;
|
|
345
|
+
bucketDataCollectionsDropped: number;
|
|
346
|
+
parameterIndexCollectionsDropped: number;
|
|
347
|
+
/** Best-effort number, not accurate if the operation takes longer than the timeout. */
|
|
348
|
+
bucketStateDocumentsDeleted: number;
|
|
349
|
+
sourceRecordCollectionsDropped: number;
|
|
350
|
+
sourceTablesUpdated: number;
|
|
351
|
+
sourceTablesDeleted: number;
|
|
352
|
+
}
|
|
353
|
+
|
|
293
354
|
export interface TerminateOptions extends ClearStorageOptions {
|
|
294
355
|
/**
|
|
295
356
|
* If true, also clear the storage before terminating.
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { ServiceAssertionError } from '@powersync/lib-services-framework';
|
|
2
|
+
import {
|
|
3
|
+
BucketDataSource,
|
|
4
|
+
BucketDefinitionId,
|
|
5
|
+
HashMap,
|
|
6
|
+
ParameterIndexId,
|
|
7
|
+
ParameterIndexLookupCreator,
|
|
8
|
+
ParameterLookupDefinitionId,
|
|
9
|
+
SerializedBucketDataSourceWithDataSources,
|
|
10
|
+
SerializedParameterIndexLookupCreator,
|
|
11
|
+
serializedStreamBucketDataSourceEquality,
|
|
12
|
+
serializedStreamParameterIndexLookupCreatorEquality,
|
|
13
|
+
SerializedSyncPlanV1,
|
|
14
|
+
SourceTableRef,
|
|
15
|
+
SyncConfigWithErrors
|
|
16
|
+
} from '@powersync/service-sync-rules';
|
|
17
|
+
|
|
18
|
+
export interface SerializedSyncConfigWithMapping {
|
|
19
|
+
plan: SerializedSyncPlanV1;
|
|
20
|
+
mapping: SingleSyncConfigBucketDefinitionMapping;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type IncrementalMappingDefinitionType = 'bucket_data' | 'parameter_lookup';
|
|
24
|
+
|
|
25
|
+
export interface IncrementalMappingDefinitionChange {
|
|
26
|
+
type: IncrementalMappingDefinitionType;
|
|
27
|
+
name: string;
|
|
28
|
+
id: BucketDefinitionId | ParameterIndexId;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IncrementalMappingChanges {
|
|
32
|
+
reusedDefinitions: IncrementalMappingDefinitionChange[];
|
|
33
|
+
addedDefinitions: IncrementalMappingDefinitionChange[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IncrementalMappingResult {
|
|
37
|
+
mapping: SingleSyncConfigBucketDefinitionMapping;
|
|
38
|
+
changes: IncrementalMappingChanges;
|
|
39
|
+
}
|
|
40
|
+
export interface SyncConfigWithMapping {
|
|
41
|
+
syncConfigId?: string;
|
|
42
|
+
syncConfig: SyncConfigWithErrors;
|
|
43
|
+
mapping: SingleSyncConfigBucketDefinitionMapping | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SyncConfigWithRequiredMapping {
|
|
47
|
+
syncConfigId: string;
|
|
48
|
+
syncConfig: SyncConfigWithErrors;
|
|
49
|
+
mapping: SingleSyncConfigBucketDefinitionMapping;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PersistedDefinitionMapping {
|
|
53
|
+
/**
|
|
54
|
+
* Map of uniqueName -> id, unique per replication stream.
|
|
55
|
+
*/
|
|
56
|
+
definitions: Record<string, string>;
|
|
57
|
+
/**
|
|
58
|
+
* Map of (lookupName, queryId) -> id, unique per replication stream.
|
|
59
|
+
*/
|
|
60
|
+
parameter_indexes: Record<string, string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Represents a mapping from bucket data sources and parameter lookup sources to stable IDs used for bucket definition and parameter index persistence.
|
|
65
|
+
*
|
|
66
|
+
* Usage is deliberately restricted: writer/batch paths resolve through their own parsed
|
|
67
|
+
* sync config set's mapping, and read paths resolve through the persisted
|
|
68
|
+
* {@link SingleSyncConfigBucketDefinitionMapping} of the single sync config they serve.
|
|
69
|
+
* Do not construct ad-hoc resolution paths from parsed sources.
|
|
70
|
+
*/
|
|
71
|
+
export interface BucketDefinitionMapping {
|
|
72
|
+
/**
|
|
73
|
+
* Given a BucketDataSource within the associated SyncConfig(s), return the BucketDefinitionId, or throw if not found.
|
|
74
|
+
*/
|
|
75
|
+
bucketSourceId(source: BucketDataSource): BucketDefinitionId;
|
|
76
|
+
|
|
77
|
+
parameterLookupId(source: ParameterIndexLookupCreator): ParameterIndexId;
|
|
78
|
+
|
|
79
|
+
allBucketDefinitionIds(): BucketDefinitionId[];
|
|
80
|
+
|
|
81
|
+
allParameterIndexIds(): ParameterIndexId[];
|
|
82
|
+
|
|
83
|
+
syncConfigIdsForSourceTable(
|
|
84
|
+
selectedSyncConfigIds: string[],
|
|
85
|
+
table: SourceTableRef,
|
|
86
|
+
bucketDataSourceIds: BucketDefinitionId[],
|
|
87
|
+
parameterLookupSourceIds: ParameterIndexId[]
|
|
88
|
+
): string[];
|
|
89
|
+
|
|
90
|
+
snapshotBlockingSourceTablesFilter(syncConfigId: string): Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A BucketDefinitionMapping associated with a single SyncConfig.
|
|
95
|
+
*
|
|
96
|
+
* Within a single SyncConfig, unique names are the persistence key for bucket definitions,
|
|
97
|
+
* so lookups here are name-based by design. MongoHydrationState and
|
|
98
|
+
* MultiSyncConfigBucketDefinitionMapping handle the mapping across multiple SyncConfigs in
|
|
99
|
+
* the same replication stream, where names are not unique.
|
|
100
|
+
*/
|
|
101
|
+
export class SingleSyncConfigBucketDefinitionMapping implements BucketDefinitionMapping {
|
|
102
|
+
static fromPersistedMapping(mapping: PersistedDefinitionMapping): SingleSyncConfigBucketDefinitionMapping {
|
|
103
|
+
return new SingleSyncConfigBucketDefinitionMapping(mapping.definitions ?? {}, mapping.parameter_indexes ?? {});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static fromParsedSyncConfig(syncConfig: SyncConfigWithErrors): SingleSyncConfigBucketDefinitionMapping {
|
|
107
|
+
const definitionNames = syncConfig.config.bucketDataSources.map((source) => source.uniqueName).sort();
|
|
108
|
+
const parameterKeys = syncConfig.config.bucketParameterLookupSources
|
|
109
|
+
.map((source) => `${source.sourceId.lookupName}#${source.sourceId.queryId}`)
|
|
110
|
+
.sort();
|
|
111
|
+
|
|
112
|
+
const definitions: Record<string, BucketDefinitionId> = {};
|
|
113
|
+
const parameterLookups: Record<string, ParameterIndexId> = {};
|
|
114
|
+
|
|
115
|
+
for (const [index, uniqueName] of definitionNames.entries()) {
|
|
116
|
+
definitions[uniqueName] = (index + 1).toString(16);
|
|
117
|
+
}
|
|
118
|
+
for (const [index, key] of parameterKeys.entries()) {
|
|
119
|
+
parameterLookups[key] = (index + 1).toString(16);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return new SingleSyncConfigBucketDefinitionMapping(definitions, parameterLookups);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
*
|
|
127
|
+
* @param compatibleConfigs SyncConfigs with definitions that may be re-used
|
|
128
|
+
* @param newPlan
|
|
129
|
+
* @param reservedMappings _all_ mappings used currently and historically for the replication stream; used to construct new ids
|
|
130
|
+
* @returns
|
|
131
|
+
*/
|
|
132
|
+
static constructIncrementalMappingFromSerializedPlans(
|
|
133
|
+
compatibleConfigs: SerializedSyncConfigWithMapping[],
|
|
134
|
+
newPlan: SerializedSyncPlanV1,
|
|
135
|
+
reservedMappings: SingleSyncConfigBucketDefinitionMapping[]
|
|
136
|
+
): SingleSyncConfigBucketDefinitionMapping {
|
|
137
|
+
return this.constructIncrementalMappingWithChanges(compatibleConfigs, newPlan, reservedMappings).mapping;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static constructIncrementalMappingWithChanges(
|
|
141
|
+
compatibleConfigs: SerializedSyncConfigWithMapping[],
|
|
142
|
+
newPlan: SerializedSyncPlanV1,
|
|
143
|
+
reservedMappings: SingleSyncConfigBucketDefinitionMapping[]
|
|
144
|
+
): IncrementalMappingResult {
|
|
145
|
+
let nextBucketDefinitionId =
|
|
146
|
+
reservedMappings
|
|
147
|
+
.map((mapping) => mapping.allBucketDefinitionIds())
|
|
148
|
+
.flat()
|
|
149
|
+
.reduce((maxId, id) => Math.max(maxId, parseInt(id, 16)), 0) + 1;
|
|
150
|
+
function generateNewBucketDefinitionId(): BucketDefinitionId {
|
|
151
|
+
const id = nextBucketDefinitionId.toString(16);
|
|
152
|
+
nextBucketDefinitionId++;
|
|
153
|
+
return id;
|
|
154
|
+
}
|
|
155
|
+
let nextParameterIndexId =
|
|
156
|
+
reservedMappings
|
|
157
|
+
.map((mapping) => mapping.allParameterIndexIds())
|
|
158
|
+
.flat()
|
|
159
|
+
.reduce((maxId, id) => Math.max(maxId, parseInt(id, 16)), 0) + 1;
|
|
160
|
+
function generateNewParameterIndexId(): ParameterIndexId {
|
|
161
|
+
const id = nextParameterIndexId.toString(16);
|
|
162
|
+
nextParameterIndexId++;
|
|
163
|
+
return id;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const definitions: Record<string, BucketDefinitionId> = {};
|
|
167
|
+
const parameterLookups: Record<string, ParameterIndexId> = {};
|
|
168
|
+
const changes: IncrementalMappingChanges = {
|
|
169
|
+
reusedDefinitions: [],
|
|
170
|
+
addedDefinitions: []
|
|
171
|
+
};
|
|
172
|
+
const compatibleBuckets = new HashMap<SerializedBucketDataSourceWithDataSources, BucketDefinitionId>(
|
|
173
|
+
serializedStreamBucketDataSourceEquality
|
|
174
|
+
);
|
|
175
|
+
const compatibleParameterLookups = new HashMap<SerializedParameterIndexLookupCreator, ParameterIndexId>(
|
|
176
|
+
serializedStreamParameterIndexLookupCreatorEquality
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
for (const config of compatibleConfigs) {
|
|
180
|
+
for (const bucket of config.plan.buckets) {
|
|
181
|
+
compatibleBuckets.putIfAbsent({ bucket, dataSources: config.plan.dataSources }, () =>
|
|
182
|
+
config.mapping.bucketSourceIdByName(bucket.uniqueName)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const parameterLookup of config.plan.parameterIndexes) {
|
|
187
|
+
compatibleParameterLookups.putIfAbsent(parameterLookup, () =>
|
|
188
|
+
config.mapping.parameterLookupIdByKey(parameterLookupKey(parameterLookup.lookupScope))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const bucket of newPlan.buckets) {
|
|
194
|
+
const compatibleId = compatibleBuckets.get({ bucket, dataSources: newPlan.dataSources });
|
|
195
|
+
const id = compatibleId ?? generateNewBucketDefinitionId();
|
|
196
|
+
definitions[bucket.uniqueName] = id;
|
|
197
|
+
const change: IncrementalMappingDefinitionChange = {
|
|
198
|
+
type: 'bucket_data',
|
|
199
|
+
name: bucket.uniqueName,
|
|
200
|
+
id
|
|
201
|
+
};
|
|
202
|
+
if (compatibleId == null) {
|
|
203
|
+
changes.addedDefinitions.push(change);
|
|
204
|
+
} else {
|
|
205
|
+
changes.reusedDefinitions.push(change);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const parameterLookup of newPlan.parameterIndexes) {
|
|
210
|
+
const compatibleId = compatibleParameterLookups.get(parameterLookup);
|
|
211
|
+
const id = compatibleId ?? generateNewParameterIndexId();
|
|
212
|
+
const key = parameterLookupKey(parameterLookup.lookupScope);
|
|
213
|
+
parameterLookups[key] = id;
|
|
214
|
+
const change: IncrementalMappingDefinitionChange = {
|
|
215
|
+
type: 'parameter_lookup',
|
|
216
|
+
name: key,
|
|
217
|
+
id
|
|
218
|
+
};
|
|
219
|
+
if (compatibleId == null) {
|
|
220
|
+
changes.addedDefinitions.push(change);
|
|
221
|
+
} else {
|
|
222
|
+
changes.reusedDefinitions.push(change);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
mapping: new SingleSyncConfigBucketDefinitionMapping(definitions, parameterLookups),
|
|
228
|
+
changes
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
constructor(
|
|
233
|
+
private definitions: Record<string, BucketDefinitionId> = {},
|
|
234
|
+
private parameterLookupMapping: Record<string, ParameterIndexId> = {}
|
|
235
|
+
) {}
|
|
236
|
+
|
|
237
|
+
bucketSourceId(source: BucketDataSource): BucketDefinitionId {
|
|
238
|
+
return this.bucketSourceIdByName(source.uniqueName);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
bucketSourceIdByName(uniqueName: string): BucketDefinitionId {
|
|
242
|
+
const defId = this.definitions[uniqueName];
|
|
243
|
+
if (defId == null) {
|
|
244
|
+
throw new ServiceAssertionError(`No mapping found for bucket source ${uniqueName}`);
|
|
245
|
+
}
|
|
246
|
+
return defId;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
allBucketDefinitionIds(): BucketDefinitionId[] {
|
|
250
|
+
return Object.values(this.definitions);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
bucketDefinitionEntries(): IncrementalMappingDefinitionChange[] {
|
|
254
|
+
return Object.entries(this.definitions).map(([name, id]) => ({
|
|
255
|
+
type: 'bucket_data',
|
|
256
|
+
name,
|
|
257
|
+
id
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
allParameterIndexIds(): ParameterIndexId[] {
|
|
262
|
+
return Object.values(this.parameterLookupMapping);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
parameterIndexEntries(): IncrementalMappingDefinitionChange[] {
|
|
266
|
+
return Object.entries(this.parameterLookupMapping).map(([name, id]) => ({
|
|
267
|
+
type: 'parameter_lookup',
|
|
268
|
+
name,
|
|
269
|
+
id
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
allDefinitionEntries(): IncrementalMappingDefinitionChange[] {
|
|
274
|
+
return [...this.bucketDefinitionEntries(), ...this.parameterIndexEntries()];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
parameterLookupId(source: ParameterIndexLookupCreator): ParameterIndexId {
|
|
278
|
+
return this.parameterLookupIdByKey(parameterLookupKey(source.sourceId));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
parameterLookupIdByKey(key: string): ParameterIndexId {
|
|
282
|
+
const defId = this.parameterLookupMapping[key];
|
|
283
|
+
if (defId == null) {
|
|
284
|
+
throw new ServiceAssertionError(`No mapping found for parameter lookup source ${key}`);
|
|
285
|
+
}
|
|
286
|
+
return defId;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
serialize(): PersistedDefinitionMapping {
|
|
290
|
+
return {
|
|
291
|
+
definitions: { ...this.definitions },
|
|
292
|
+
parameter_indexes: { ...this.parameterLookupMapping }
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
syncConfigIdsForSourceTable(
|
|
297
|
+
selectedSyncConfigIds: string[],
|
|
298
|
+
_table: SourceTableRef,
|
|
299
|
+
bucketDataSourceIds: BucketDefinitionId[],
|
|
300
|
+
parameterLookupSourceIds: ParameterIndexId[]
|
|
301
|
+
): string[] {
|
|
302
|
+
return bucketDataSourceIds.length > 0 || parameterLookupSourceIds.length > 0 ? selectedSyncConfigIds : [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
snapshotBlockingSourceTablesFilter(_syncConfigId: string): Record<string, unknown> {
|
|
306
|
+
const clauses: Record<string, unknown>[] = [];
|
|
307
|
+
const bucketDataSourceIds = this.allBucketDefinitionIds();
|
|
308
|
+
if (bucketDataSourceIds.length > 0) {
|
|
309
|
+
clauses.push({ bucket_data_source_ids: { $in: bucketDataSourceIds } });
|
|
310
|
+
}
|
|
311
|
+
const parameterLookupSourceIds = this.allParameterIndexIds();
|
|
312
|
+
if (parameterLookupSourceIds.length > 0) {
|
|
313
|
+
clauses.push({ parameter_lookup_source_ids: { $in: parameterLookupSourceIds } });
|
|
314
|
+
}
|
|
315
|
+
if (clauses.length == 0) {
|
|
316
|
+
return {
|
|
317
|
+
snapshot_done: false,
|
|
318
|
+
_id: { $exists: false }
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
snapshot_done: false,
|
|
323
|
+
$or: clauses
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* A BucketDefinitionMapping across all SyncConfigs of one parse of a replication stream.
|
|
330
|
+
*
|
|
331
|
+
* Source lookups are strictly identity-based: a source that did not come from the parse
|
|
332
|
+
* this mapping was constructed with is an error. Unique names are not unique across sync
|
|
333
|
+
* configs in a replication stream, so there is deliberately no name-based fallback.
|
|
334
|
+
*/
|
|
335
|
+
export class MultiSyncConfigBucketDefinitionMapping implements BucketDefinitionMapping {
|
|
336
|
+
private bucketDataSourceMappings = new WeakMap<BucketDataSource, SingleSyncConfigBucketDefinitionMapping>();
|
|
337
|
+
private bucketDataSourceSyncConfigIdsById = new Map<BucketDefinitionId, Set<string>>();
|
|
338
|
+
private parameterLookupMappings = new WeakMap<ParameterIndexLookupCreator, SingleSyncConfigBucketDefinitionMapping>();
|
|
339
|
+
private parameterLookupSyncConfigIdsById = new Map<ParameterIndexId, Set<string>>();
|
|
340
|
+
private syncConfigsById = new Map<string, SyncConfigWithRequiredMapping>();
|
|
341
|
+
private mappings: SingleSyncConfigBucketDefinitionMapping[];
|
|
342
|
+
|
|
343
|
+
constructor(syncConfigs: SyncConfigWithRequiredMapping[]) {
|
|
344
|
+
this.mappings = syncConfigs.map((config) => config.mapping);
|
|
345
|
+
|
|
346
|
+
for (const config of syncConfigs) {
|
|
347
|
+
this.syncConfigsById.set(config.syncConfigId, config);
|
|
348
|
+
for (const source of config.syncConfig.config.bucketDataSources) {
|
|
349
|
+
this.bucketDataSourceMappings.set(source, config.mapping);
|
|
350
|
+
addSetEntry(this.bucketDataSourceSyncConfigIdsById, config.mapping.bucketSourceId(source), config.syncConfigId);
|
|
351
|
+
}
|
|
352
|
+
for (const source of config.syncConfig.config.bucketParameterLookupSources) {
|
|
353
|
+
this.parameterLookupMappings.set(source, config.mapping);
|
|
354
|
+
addSetEntry(
|
|
355
|
+
this.parameterLookupSyncConfigIdsById,
|
|
356
|
+
config.mapping.parameterLookupId(source),
|
|
357
|
+
config.syncConfigId
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
bucketSourceId(source: BucketDataSource): BucketDefinitionId {
|
|
364
|
+
const mapping = this.bucketDataSourceMappings.get(source);
|
|
365
|
+
if (mapping == null) {
|
|
366
|
+
throw new ServiceAssertionError(
|
|
367
|
+
`Bucket source ${source.uniqueName} is not associated with this parse of the sync configs`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return mapping.bucketSourceId(source);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
allBucketDefinitionIds(): BucketDefinitionId[] {
|
|
374
|
+
return [...new Set(this.mappings.flatMap((mapping) => mapping.allBucketDefinitionIds()))];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
parameterLookupId(source: ParameterIndexLookupCreator): ParameterIndexId {
|
|
378
|
+
const mapping = this.parameterLookupMappings.get(source);
|
|
379
|
+
if (mapping == null) {
|
|
380
|
+
throw new ServiceAssertionError(
|
|
381
|
+
`Parameter lookup source ${source.sourceId.lookupName}#${source.sourceId.queryId} is not associated with this parse of the sync configs`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return mapping.parameterLookupId(source);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
allParameterIndexIds(): ParameterIndexId[] {
|
|
388
|
+
return [...new Set(this.mappings.flatMap((mapping) => mapping.allParameterIndexIds()))];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
syncConfigIdsForSourceTable(
|
|
392
|
+
_selectedSyncConfigIds: string[],
|
|
393
|
+
table: SourceTableRef,
|
|
394
|
+
bucketDataSourceIds: BucketDefinitionId[],
|
|
395
|
+
parameterLookupSourceIds: ParameterIndexId[]
|
|
396
|
+
): string[] {
|
|
397
|
+
const ids = new Set<string>();
|
|
398
|
+
for (const sourceId of bucketDataSourceIds) {
|
|
399
|
+
addAll(ids, this.bucketDataSourceSyncConfigIdsById.get(sourceId));
|
|
400
|
+
}
|
|
401
|
+
for (const sourceId of parameterLookupSourceIds) {
|
|
402
|
+
addAll(ids, this.parameterLookupSyncConfigIdsById.get(sourceId));
|
|
403
|
+
}
|
|
404
|
+
for (const [syncConfigId, config] of this.syncConfigsById) {
|
|
405
|
+
if (config.syncConfig.config.tableTriggersEvent(table)) {
|
|
406
|
+
ids.add(syncConfigId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return [...ids];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
snapshotBlockingSourceTablesFilter(syncConfigId: string): Record<string, unknown> {
|
|
413
|
+
const config = this.syncConfigsById.get(syncConfigId);
|
|
414
|
+
if (config == null) {
|
|
415
|
+
throw new ServiceAssertionError(`No mapping found for sync config ${syncConfigId}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return config.mapping.snapshotBlockingSourceTablesFilter(syncConfigId);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function parameterLookupKey(id: ParameterLookupDefinitionId) {
|
|
423
|
+
return `${id.lookupName}#${id.queryId}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function addSetEntry<K, V>(map: Map<K, Set<V>>, key: K, value: V) {
|
|
427
|
+
const existing = map.get(key) ?? new Set<V>();
|
|
428
|
+
existing.add(value);
|
|
429
|
+
map.set(key, existing);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function addAll<T>(target: Set<T>, values: Iterable<T> | undefined) {
|
|
433
|
+
for (const value of values ?? []) {
|
|
434
|
+
target.add(value);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CompatibilityContext } from '@powersync/service-sync-rules';
|
|
2
|
+
import { Logger } from 'winston';
|
|
3
|
+
import { SerializedSyncPlan, UpdateSyncRulesOptions } from '../BucketStorageFactory.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if a sync config update can use incremental reprocessing.
|
|
7
|
+
*/
|
|
8
|
+
export function isCompatible(
|
|
9
|
+
existingPlans: (SerializedSyncPlan | null)[],
|
|
10
|
+
updateConfig: UpdateSyncRulesOptions['config'],
|
|
11
|
+
logger: Logger
|
|
12
|
+
): boolean {
|
|
13
|
+
if (updateConfig.plan == null) {
|
|
14
|
+
// Only support sync streams with serialized plans
|
|
15
|
+
logger.info(`Not using current sync streams - incremental reprocessing not supported`);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// We don't check the storage version here - that is checked upstream.
|
|
20
|
+
if (existingPlans.length == 0) {
|
|
21
|
+
// Could technically be compatible, but there is no reason to re-use this stream.
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (existingPlans.some((plan) => plan == null)) {
|
|
26
|
+
// Only support sync streams with serialized plans
|
|
27
|
+
logger.info(`Existing replication stream not using current sync streams - incremental reprocessing not supported`);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Technically we can compare the serialized compatibility versions? But this does not add much overhead.
|
|
32
|
+
const first = existingPlans[0];
|
|
33
|
+
const streamCompatibility = CompatibilityContext.deserialize(first!.compatibility);
|
|
34
|
+
if (!streamCompatibility.equals(updateConfig.parsed.config.compatibility)) {
|
|
35
|
+
// Compatibility options must match
|
|
36
|
+
logger.info(`Compatibility options changed - incremental reprocessing not supported`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|