@powersync/service-core 1.19.2 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/api/diagnostics.js +11 -4
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/entry/commands/compact-action.js +13 -2
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/entry/commands/config-command.js +2 -2
- package/dist/entry/commands/config-command.js.map +1 -1
- package/dist/replication/AbstractReplicator.js +2 -5
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +84 -0
- package/dist/routes/endpoints/admin.d.ts +168 -0
- package/dist/routes/endpoints/admin.js +34 -20
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +6 -9
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +6 -1
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +21 -8
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +48 -15
- package/dist/storage/BucketStorageFactory.js +70 -1
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/ChecksumCache.d.ts +5 -2
- package/dist/storage/ChecksumCache.js +8 -4
- package/dist/storage/ChecksumCache.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +33 -3
- package/dist/storage/PersistedSyncRulesContent.js +80 -1
- package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/SourceTable.d.ts +7 -2
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/StorageVersionConfig.d.ts +53 -0
- package/dist/storage/StorageVersionConfig.js +53 -0
- package/dist/storage/StorageVersionConfig.js.map +1 -0
- package/dist/storage/SyncRulesBucketStorage.d.ts +14 -4
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/storage-index.d.ts +1 -0
- package/dist/storage/storage-index.js +1 -0
- package/dist/storage/storage-index.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +8 -4
- package/dist/sync/BucketChecksumState.js +97 -52
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.d.ts +1 -0
- package/dist/sync/util.js +10 -0
- package/dist/sync/util.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +13 -0
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js +4 -4
- package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +2 -2
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.d.ts +1 -1
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js +3 -3
- package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +1 -1
- package/dist/util/config/types.js.map +1 -1
- package/dist/util/env.d.ts +1 -0
- package/dist/util/env.js +5 -0
- package/dist/util/env.js.map +1 -1
- package/package.json +6 -6
- package/src/api/diagnostics.ts +12 -4
- package/src/entry/commands/compact-action.ts +15 -2
- package/src/entry/commands/config-command.ts +3 -3
- package/src/replication/AbstractReplicator.ts +3 -5
- package/src/routes/endpoints/admin.ts +43 -25
- package/src/routes/endpoints/sync-rules.ts +14 -13
- package/src/routes/endpoints/sync-stream.ts +6 -1
- package/src/storage/BucketStorageBatch.ts +23 -9
- package/src/storage/BucketStorageFactory.ts +116 -19
- package/src/storage/ChecksumCache.ts +14 -6
- package/src/storage/PersistedSyncRulesContent.ts +119 -4
- package/src/storage/SourceTable.ts +7 -1
- package/src/storage/StorageVersionConfig.ts +78 -0
- package/src/storage/SyncRulesBucketStorage.ts +20 -4
- package/src/storage/storage-index.ts +1 -0
- package/src/sync/BucketChecksumState.ts +147 -65
- package/src/sync/sync.ts +9 -3
- package/src/sync/util.ts +10 -0
- package/src/util/config/collectors/config-collector.ts +16 -0
- package/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts +5 -5
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +3 -3
- package/src/util/config/sync-rules/impl/inline-sync-rules-collector.ts +4 -4
- package/src/util/config/types.ts +1 -2
- package/src/util/env.ts +5 -0
- package/test/src/checksum_cache.test.ts +102 -57
- package/test/src/config.test.ts +115 -0
- package/test/src/routes/admin.test.ts +48 -0
- package/test/src/routes/mocks.ts +22 -1
- package/test/src/routes/stream.test.ts +3 -2
- package/test/src/sync/BucketChecksumState.test.ts +332 -93
- package/test/src/utils.ts +9 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseObserver, logger } from '@powersync/lib-services-framework';
|
|
2
2
|
import { ParseSyncRulesOptions, PersistedSyncRules, PersistedSyncRulesContent } from './PersistedSyncRulesContent.js';
|
|
3
3
|
import { ReplicationEventPayload } from './ReplicationEventPayload.js';
|
|
4
4
|
import { ReplicationLock } from './ReplicationLock.js';
|
|
5
5
|
import { SyncRulesBucketStorage } from './SyncRulesBucketStorage.js';
|
|
6
6
|
import { ReportStorage } from './ReportStorage.js';
|
|
7
|
+
import {
|
|
8
|
+
PrecompiledSyncConfig,
|
|
9
|
+
SerializedCompatibilityContext,
|
|
10
|
+
serializeSyncPlan,
|
|
11
|
+
SqlSyncRules,
|
|
12
|
+
SyncConfig
|
|
13
|
+
} from '@powersync/service-sync-rules';
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
16
|
* Represents a configured storage provider.
|
|
@@ -13,23 +20,41 @@ import { ReportStorage } from './ReportStorage.js';
|
|
|
13
20
|
*
|
|
14
21
|
* Storage APIs for a specific copy of sync rules are provided by the `SyncRulesBucketStorage` instances.
|
|
15
22
|
*/
|
|
16
|
-
export
|
|
23
|
+
export abstract class BucketStorageFactory
|
|
24
|
+
extends BaseObserver<BucketStorageFactoryListener>
|
|
25
|
+
implements AsyncDisposable
|
|
26
|
+
{
|
|
17
27
|
/**
|
|
18
28
|
* Update sync rules from configuration, if changed.
|
|
19
29
|
*/
|
|
20
|
-
configureSyncRules(
|
|
30
|
+
async configureSyncRules(
|
|
21
31
|
options: UpdateSyncRulesOptions
|
|
22
|
-
): Promise<{ updated: boolean; persisted_sync_rules?: PersistedSyncRulesContent; lock?: ReplicationLock }
|
|
32
|
+
): Promise<{ updated: boolean; persisted_sync_rules?: PersistedSyncRulesContent; lock?: ReplicationLock }> {
|
|
33
|
+
const next = await this.getNextSyncRulesContent();
|
|
34
|
+
const active = await this.getActiveSyncRulesContent();
|
|
35
|
+
|
|
36
|
+
if (next?.sync_rules_content == options.config.yaml) {
|
|
37
|
+
logger.info('Sync rules from configuration unchanged');
|
|
38
|
+
return { updated: false };
|
|
39
|
+
} else if (next == null && active?.sync_rules_content == options.config.yaml) {
|
|
40
|
+
logger.info('Sync rules from configuration unchanged');
|
|
41
|
+
return { updated: false };
|
|
42
|
+
} else {
|
|
43
|
+
logger.info('Sync rules updated from configuration');
|
|
44
|
+
const persisted_sync_rules = await this.updateSyncRules(options);
|
|
45
|
+
return { updated: true, persisted_sync_rules, lock: persisted_sync_rules.current_lock ?? undefined };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
23
48
|
|
|
24
49
|
/**
|
|
25
50
|
* Get a storage instance to query sync data for specific sync rules.
|
|
26
51
|
*/
|
|
27
|
-
getInstance(syncRules: PersistedSyncRulesContent, options?: GetIntanceOptions): SyncRulesBucketStorage;
|
|
52
|
+
abstract getInstance(syncRules: PersistedSyncRulesContent, options?: GetIntanceOptions): SyncRulesBucketStorage;
|
|
28
53
|
|
|
29
54
|
/**
|
|
30
55
|
* Deploy new sync rules.
|
|
31
56
|
*/
|
|
32
|
-
updateSyncRules(options: UpdateSyncRulesOptions): Promise<PersistedSyncRulesContent>;
|
|
57
|
+
abstract updateSyncRules(options: UpdateSyncRulesOptions): Promise<PersistedSyncRulesContent>;
|
|
33
58
|
|
|
34
59
|
/**
|
|
35
60
|
* Indicate that a slot was removed, and we should re-sync by creating
|
|
@@ -41,57 +66,65 @@ export interface BucketStorageFactory extends ObserverClient<BucketStorageFactor
|
|
|
41
66
|
*
|
|
42
67
|
* Replication should be restarted after this.
|
|
43
68
|
*/
|
|
44
|
-
restartReplication(sync_rules_group_id: number): Promise<void>;
|
|
69
|
+
abstract restartReplication(sync_rules_group_id: number): Promise<void>;
|
|
45
70
|
|
|
46
71
|
/**
|
|
47
72
|
* Get the sync rules used for querying.
|
|
48
73
|
*/
|
|
49
|
-
getActiveSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null
|
|
74
|
+
async getActiveSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null> {
|
|
75
|
+
const content = await this.getActiveSyncRulesContent();
|
|
76
|
+
return content?.parsed(options) ?? null;
|
|
77
|
+
}
|
|
50
78
|
|
|
51
79
|
/**
|
|
52
80
|
* Get the sync rules used for querying.
|
|
53
81
|
*/
|
|
54
|
-
getActiveSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
|
|
82
|
+
abstract getActiveSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
|
|
55
83
|
|
|
56
84
|
/**
|
|
57
85
|
* Get the sync rules that will be active next once done with initial replicatino.
|
|
58
86
|
*/
|
|
59
|
-
getNextSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null
|
|
87
|
+
async getNextSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null> {
|
|
88
|
+
const content = await this.getNextSyncRulesContent();
|
|
89
|
+
return content?.parsed(options) ?? null;
|
|
90
|
+
}
|
|
60
91
|
|
|
61
92
|
/**
|
|
62
93
|
* Get the sync rules that will be active next once done with initial replicatino.
|
|
63
94
|
*/
|
|
64
|
-
getNextSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
|
|
95
|
+
abstract getNextSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
|
|
65
96
|
|
|
66
97
|
/**
|
|
67
98
|
* Get all sync rules currently replicating. Typically this is the "active" and "next" sync rules.
|
|
68
99
|
*/
|
|
69
|
-
getReplicatingSyncRules(): Promise<PersistedSyncRulesContent[]>;
|
|
100
|
+
abstract getReplicatingSyncRules(): Promise<PersistedSyncRulesContent[]>;
|
|
70
101
|
|
|
71
102
|
/**
|
|
72
103
|
* Get all sync rules stopped but not terminated yet.
|
|
73
104
|
*/
|
|
74
|
-
getStoppedSyncRules(): Promise<PersistedSyncRulesContent[]>;
|
|
105
|
+
abstract getStoppedSyncRules(): Promise<PersistedSyncRulesContent[]>;
|
|
75
106
|
|
|
76
107
|
/**
|
|
77
108
|
* Get the active storage instance.
|
|
78
109
|
*/
|
|
79
|
-
getActiveStorage(): Promise<SyncRulesBucketStorage | null>;
|
|
110
|
+
abstract getActiveStorage(): Promise<SyncRulesBucketStorage | null>;
|
|
80
111
|
|
|
81
112
|
/**
|
|
82
113
|
* Get storage size of active sync rules.
|
|
83
114
|
*/
|
|
84
|
-
getStorageMetrics(): Promise<StorageMetrics>;
|
|
115
|
+
abstract getStorageMetrics(): Promise<StorageMetrics>;
|
|
85
116
|
|
|
86
117
|
/**
|
|
87
118
|
* Get the unique identifier for this instance of Powersync
|
|
88
119
|
*/
|
|
89
|
-
getPowerSyncInstanceId(): Promise<string>;
|
|
120
|
+
abstract getPowerSyncInstanceId(): Promise<string>;
|
|
90
121
|
|
|
91
122
|
/**
|
|
92
123
|
* Get a unique identifier for the system used for storage.
|
|
93
124
|
*/
|
|
94
|
-
getSystemIdentifier(): Promise<BucketStorageSystemIdentifier>;
|
|
125
|
+
abstract getSystemIdentifier(): Promise<BucketStorageSystemIdentifier>;
|
|
126
|
+
|
|
127
|
+
abstract [Symbol.asyncDispose](): PromiseLike<void>;
|
|
95
128
|
}
|
|
96
129
|
|
|
97
130
|
export interface BucketStorageFactoryListener {
|
|
@@ -119,9 +152,67 @@ export interface StorageMetrics {
|
|
|
119
152
|
}
|
|
120
153
|
|
|
121
154
|
export interface UpdateSyncRulesOptions {
|
|
122
|
-
|
|
155
|
+
config: {
|
|
156
|
+
yaml: string;
|
|
157
|
+
/**
|
|
158
|
+
* The serialized sync plan for the sync configuration, or `null` for configurations not using the sync stream
|
|
159
|
+
* compiler.
|
|
160
|
+
*/
|
|
161
|
+
plan: SerializedSyncPlan | null;
|
|
162
|
+
};
|
|
123
163
|
lock?: boolean;
|
|
124
|
-
|
|
164
|
+
storageVersion?: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface SerializedSyncPlan {
|
|
168
|
+
/**
|
|
169
|
+
* The serialized plan, from {@link serializeSyncPlan}.
|
|
170
|
+
*/
|
|
171
|
+
plan: unknown;
|
|
172
|
+
compatibility: SerializedCompatibilityContext;
|
|
173
|
+
/**
|
|
174
|
+
* Event descriptors are not currently represented in the sync plan because they don't use the sync streams compiler
|
|
175
|
+
* yet.
|
|
176
|
+
*
|
|
177
|
+
* We might revisit that in the future, but for now we store SQL text of their definitions here to be able to restore
|
|
178
|
+
* them.
|
|
179
|
+
*/
|
|
180
|
+
eventDescriptors: Record<string, string[]>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function updateSyncRulesFromYaml(
|
|
184
|
+
content: string,
|
|
185
|
+
options?: Omit<UpdateSyncRulesOptions, 'config'> & { validate?: boolean }
|
|
186
|
+
): UpdateSyncRulesOptions {
|
|
187
|
+
const { config } = SqlSyncRules.fromYaml(content, {
|
|
188
|
+
// No schema-based validation at this point
|
|
189
|
+
schema: undefined,
|
|
190
|
+
defaultSchema: 'not_applicable', // Not needed for validation
|
|
191
|
+
throwOnError: options?.validate ?? false
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return updateSyncRulesFromConfig(config, options);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function updateSyncRulesFromConfig(
|
|
198
|
+
parsed: SyncConfig,
|
|
199
|
+
options?: Omit<UpdateSyncRulesOptions, 'config'>
|
|
200
|
+
): UpdateSyncRulesOptions {
|
|
201
|
+
let plan: SerializedSyncPlan | null = null;
|
|
202
|
+
if (parsed instanceof PrecompiledSyncConfig) {
|
|
203
|
+
const eventDescriptors: Record<string, string[]> = {};
|
|
204
|
+
for (const event of parsed.eventDescriptors) {
|
|
205
|
+
eventDescriptors[event.name] = event.sourceQueries.map((q) => q.sql);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
plan = {
|
|
209
|
+
compatibility: parsed.compatibility.serialize(),
|
|
210
|
+
plan: serializeSyncPlan(parsed.plan),
|
|
211
|
+
eventDescriptors
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { config: { yaml: parsed.content, plan }, ...options };
|
|
125
216
|
}
|
|
126
217
|
|
|
127
218
|
export interface GetIntanceOptions {
|
|
@@ -166,3 +257,9 @@ export interface TestStorageOptions {
|
|
|
166
257
|
}
|
|
167
258
|
export type TestStorageFactory = (options?: TestStorageOptions) => Promise<BucketStorageFactory>;
|
|
168
259
|
export type TestReportStorageFactory = (options?: TestStorageOptions) => Promise<ReportStorage>;
|
|
260
|
+
|
|
261
|
+
export interface TestStorageConfig {
|
|
262
|
+
factory: TestStorageFactory;
|
|
263
|
+
tableIdStrings: boolean;
|
|
264
|
+
storageVersion?: number;
|
|
265
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { OrderedSet } from '@js-sdsl/ordered-set';
|
|
2
2
|
import { LRUCache } from 'lru-cache/min';
|
|
3
|
+
import { BucketDataSource } from '@powersync/service-sync-rules';
|
|
3
4
|
import { BucketChecksum } from '../util/protocol-types.js';
|
|
4
5
|
import { addBucketChecksums, ChecksumMap, InternalOpId, PartialChecksum } from '../util/utils.js';
|
|
6
|
+
import { BucketChecksumRequest } from './SyncRulesBucketStorage.js';
|
|
5
7
|
|
|
6
8
|
interface ChecksumFetchContext {
|
|
7
9
|
fetch(bucket: string): Promise<BucketChecksum>;
|
|
@@ -10,6 +12,7 @@ interface ChecksumFetchContext {
|
|
|
10
12
|
|
|
11
13
|
export interface FetchPartialBucketChecksum {
|
|
12
14
|
bucket: string;
|
|
15
|
+
source: BucketDataSource;
|
|
13
16
|
start?: InternalOpId;
|
|
14
17
|
end: InternalOpId;
|
|
15
18
|
}
|
|
@@ -113,10 +116,10 @@ export class ChecksumCache {
|
|
|
113
116
|
this.bucketCheckpoints.clear();
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
async getChecksums(checkpoint: InternalOpId, buckets:
|
|
119
|
+
async getChecksums(checkpoint: InternalOpId, buckets: BucketChecksumRequest[]): Promise<BucketChecksum[]> {
|
|
117
120
|
const checksums = await this.getChecksumMap(checkpoint, buckets);
|
|
118
121
|
// Return results in the same order as the request
|
|
119
|
-
return buckets.map((bucket) => checksums.get(bucket)!);
|
|
122
|
+
return buckets.map((bucket) => checksums.get(bucket.bucket)!);
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
/**
|
|
@@ -126,7 +129,7 @@ export class ChecksumCache {
|
|
|
126
129
|
*
|
|
127
130
|
* @returns a Map with exactly one entry for each bucket requested
|
|
128
131
|
*/
|
|
129
|
-
async getChecksumMap(checkpoint: InternalOpId, buckets:
|
|
132
|
+
async getChecksumMap(checkpoint: InternalOpId, buckets: BucketChecksumRequest[]): Promise<ChecksumMap> {
|
|
130
133
|
// Buckets that don't have a cached checksum for this checkpoint yet
|
|
131
134
|
let toFetch = new Set<string>();
|
|
132
135
|
|
|
@@ -164,19 +167,21 @@ export class ChecksumCache {
|
|
|
164
167
|
// One promise to await to ensure all fetch requests completed.
|
|
165
168
|
let settledPromise: Promise<PromiseSettledResult<void>[]> | null = null;
|
|
166
169
|
|
|
170
|
+
const sourceMap = new Map<string, BucketDataSource>();
|
|
171
|
+
|
|
167
172
|
try {
|
|
168
173
|
// Individual cache fetch promises
|
|
169
174
|
let cacheFetchPromises: Promise<void>[] = [];
|
|
170
175
|
|
|
171
176
|
for (let bucket of buckets) {
|
|
172
|
-
const cacheKey = makeCacheKey(checkpoint, bucket);
|
|
177
|
+
const cacheKey = makeCacheKey(checkpoint, bucket.bucket);
|
|
173
178
|
let status: LRUCache.Status<BucketChecksum> = {};
|
|
174
179
|
const p = this.cache.fetch(cacheKey, { context: context, status: status }).then((checksums) => {
|
|
175
180
|
if (checksums == null) {
|
|
176
181
|
// Should never happen
|
|
177
182
|
throw new Error(`Failed to get checksums for ${cacheKey}`);
|
|
178
183
|
}
|
|
179
|
-
finalResults.set(bucket, checksums);
|
|
184
|
+
finalResults.set(bucket.bucket, checksums);
|
|
180
185
|
});
|
|
181
186
|
cacheFetchPromises.push(p);
|
|
182
187
|
if (status.fetch == 'hit' || status.fetch == 'inflight') {
|
|
@@ -185,7 +190,8 @@ export class ChecksumCache {
|
|
|
185
190
|
// In either case, we don't need to fetch a new checksum.
|
|
186
191
|
} else {
|
|
187
192
|
// We need a new request for this checksum.
|
|
188
|
-
toFetch.add(bucket);
|
|
193
|
+
toFetch.add(bucket.bucket);
|
|
194
|
+
sourceMap.set(bucket.bucket, bucket.source);
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
// We do this directly after creating the promises, otherwise
|
|
@@ -220,6 +226,7 @@ export class ChecksumCache {
|
|
|
220
226
|
// Partial checksum found - make a partial checksum request
|
|
221
227
|
bucketRequest = {
|
|
222
228
|
bucket,
|
|
229
|
+
source: sourceMap.get(bucket)!,
|
|
223
230
|
start: cp,
|
|
224
231
|
end: checkpoint
|
|
225
232
|
};
|
|
@@ -240,6 +247,7 @@ export class ChecksumCache {
|
|
|
240
247
|
// No partial checksum found - make a new full checksum request
|
|
241
248
|
bucketRequest = {
|
|
242
249
|
bucket,
|
|
250
|
+
source: sourceMap.get(bucket)!,
|
|
243
251
|
end: checkpoint
|
|
244
252
|
};
|
|
245
253
|
add.set(bucket, {
|
|
@@ -1,18 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
2
|
+
import {
|
|
3
|
+
CompatibilityContext,
|
|
4
|
+
CompatibilityOption,
|
|
5
|
+
DEFAULT_HYDRATION_STATE,
|
|
6
|
+
deserializeSyncPlan,
|
|
7
|
+
HydratedSyncRules,
|
|
8
|
+
HydrationState,
|
|
9
|
+
javaScriptExpressionEngine,
|
|
10
|
+
PrecompiledSyncConfig,
|
|
11
|
+
SqlEventDescriptor,
|
|
12
|
+
SqlSyncRules,
|
|
13
|
+
SyncConfigWithErrors,
|
|
14
|
+
versionedHydrationState
|
|
15
|
+
} from '@powersync/service-sync-rules';
|
|
16
|
+
import { SerializedSyncPlan, UpdateSyncRulesOptions } from './BucketStorageFactory.js';
|
|
2
17
|
import { ReplicationLock } from './ReplicationLock.js';
|
|
18
|
+
import { STORAGE_VERSION_CONFIG, StorageVersionConfig } from './StorageVersionConfig.js';
|
|
3
19
|
|
|
4
20
|
export interface ParseSyncRulesOptions {
|
|
5
21
|
defaultSchema: string;
|
|
6
22
|
}
|
|
7
23
|
|
|
8
|
-
export interface
|
|
24
|
+
export interface PersistedSyncRulesContentData {
|
|
9
25
|
readonly id: number;
|
|
10
26
|
readonly sync_rules_content: string;
|
|
27
|
+
readonly compiled_plan: SerializedSyncPlan | null;
|
|
11
28
|
readonly slot_name: string;
|
|
12
29
|
/**
|
|
13
30
|
* True if this is the "active" copy of the sync rules.
|
|
14
31
|
*/
|
|
15
32
|
readonly active: boolean;
|
|
33
|
+
readonly storageVersion: number;
|
|
16
34
|
|
|
17
35
|
readonly last_checkpoint_lsn: string | null;
|
|
18
36
|
|
|
@@ -20,16 +38,113 @@ export interface PersistedSyncRulesContent {
|
|
|
20
38
|
readonly last_fatal_error_ts?: Date | null;
|
|
21
39
|
readonly last_keepalive_ts?: Date | null;
|
|
22
40
|
readonly last_checkpoint_ts?: Date | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export abstract class PersistedSyncRulesContent implements PersistedSyncRulesContentData {
|
|
44
|
+
readonly id!: number;
|
|
45
|
+
readonly sync_rules_content!: string;
|
|
46
|
+
readonly compiled_plan!: SerializedSyncPlan | null;
|
|
47
|
+
readonly slot_name!: string;
|
|
48
|
+
readonly active!: boolean;
|
|
49
|
+
readonly storageVersion!: number;
|
|
50
|
+
|
|
51
|
+
readonly last_checkpoint_lsn!: string | null;
|
|
52
|
+
|
|
53
|
+
readonly last_fatal_error?: string | null;
|
|
54
|
+
readonly last_fatal_error_ts?: Date | null;
|
|
55
|
+
readonly last_keepalive_ts?: Date | null;
|
|
56
|
+
readonly last_checkpoint_ts?: Date | null;
|
|
57
|
+
|
|
58
|
+
abstract readonly current_lock: ReplicationLock | null;
|
|
59
|
+
|
|
60
|
+
constructor(data: PersistedSyncRulesContentData) {
|
|
61
|
+
Object.assign(this, data);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load the storage config.
|
|
66
|
+
*
|
|
67
|
+
* This may throw if the persisted storage version is not supported.
|
|
68
|
+
*/
|
|
69
|
+
getStorageConfig(): StorageVersionConfig {
|
|
70
|
+
const storageConfig = STORAGE_VERSION_CONFIG[this.storageVersion];
|
|
71
|
+
if (storageConfig == null) {
|
|
72
|
+
throw new ServiceError(
|
|
73
|
+
ErrorCode.PSYNC_S1005,
|
|
74
|
+
`Unsupported storage version ${this.storageVersion} for sync rules ${this.id}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return storageConfig;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
parsed(options: ParseSyncRulesOptions): PersistedSyncRules {
|
|
81
|
+
let hydrationState: HydrationState;
|
|
82
|
+
|
|
83
|
+
// Do we have a compiled sync plan? If so, restore from there instead of parsing everything again.
|
|
84
|
+
let config: SyncConfigWithErrors;
|
|
85
|
+
if (this.compiled_plan != null) {
|
|
86
|
+
const plan = deserializeSyncPlan(this.compiled_plan.plan);
|
|
87
|
+
const compatibility = CompatibilityContext.deserialize(this.compiled_plan.compatibility);
|
|
88
|
+
const eventDefinitions: SqlEventDescriptor[] = [];
|
|
89
|
+
for (const [name, queries] of Object.entries(this.compiled_plan.eventDescriptors)) {
|
|
90
|
+
const descriptor = new SqlEventDescriptor(name, compatibility);
|
|
91
|
+
for (const query of queries) {
|
|
92
|
+
descriptor.addSourceQuery(query, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
eventDefinitions.push(descriptor);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const precompiled = new PrecompiledSyncConfig(plan, compatibility, eventDefinitions, {
|
|
99
|
+
defaultSchema: options.defaultSchema,
|
|
100
|
+
engine: javaScriptExpressionEngine(compatibility),
|
|
101
|
+
sourceText: this.sync_rules_content
|
|
102
|
+
});
|
|
23
103
|
|
|
24
|
-
|
|
104
|
+
config = { config: precompiled, errors: [] };
|
|
105
|
+
} else {
|
|
106
|
+
config = SqlSyncRules.fromYaml(this.sync_rules_content, options);
|
|
107
|
+
}
|
|
25
108
|
|
|
26
|
-
|
|
109
|
+
const storageConfig = this.getStorageConfig();
|
|
110
|
+
if (
|
|
111
|
+
storageConfig.versionedBuckets ||
|
|
112
|
+
config.config.compatibility.isEnabled(CompatibilityOption.versionedBucketIds)
|
|
113
|
+
) {
|
|
114
|
+
hydrationState = versionedHydrationState(this.id);
|
|
115
|
+
} else {
|
|
116
|
+
hydrationState = DEFAULT_HYDRATION_STATE;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: this.id,
|
|
121
|
+
slot_name: this.slot_name,
|
|
122
|
+
sync_rules: config,
|
|
123
|
+
hydrationState,
|
|
124
|
+
hydratedSyncRules: () => {
|
|
125
|
+
return config.config.hydrate({ hydrationState });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
asUpdateOptions(options?: Omit<UpdateSyncRulesOptions, 'config'>): UpdateSyncRulesOptions {
|
|
131
|
+
return {
|
|
132
|
+
config: { yaml: this.sync_rules_content, plan: this.compiled_plan },
|
|
133
|
+
...options
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
abstract lock(): Promise<ReplicationLock>;
|
|
27
138
|
}
|
|
28
139
|
|
|
29
140
|
export interface PersistedSyncRules {
|
|
30
141
|
readonly id: number;
|
|
31
142
|
readonly sync_rules: SyncConfigWithErrors;
|
|
32
143
|
readonly slot_name: string;
|
|
144
|
+
/**
|
|
145
|
+
* For testing only.
|
|
146
|
+
*/
|
|
147
|
+
readonly hydrationState: HydrationState;
|
|
33
148
|
|
|
34
149
|
hydratedSyncRules(): HydratedSyncRules;
|
|
35
150
|
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { DEFAULT_TAG } from '@powersync/service-sync-rules';
|
|
2
2
|
import * as util from '../util/util-index.js';
|
|
3
3
|
import { ColumnDescriptor, SourceEntityDescriptor } from './SourceEntity.js';
|
|
4
|
+
import { bson } from '../index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format of the id depends on the bucket storage module. It should be consistent within the module.
|
|
8
|
+
*/
|
|
9
|
+
export type SourceTableId = string | bson.ObjectId;
|
|
4
10
|
|
|
5
11
|
export interface SourceTableOptions {
|
|
6
|
-
id:
|
|
12
|
+
id: SourceTableId;
|
|
7
13
|
connectionTag: string;
|
|
8
14
|
objectId: number | string | undefined;
|
|
9
15
|
schema: string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface StorageVersionConfig {
|
|
2
|
+
version: number;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether versioned bucket names are automatically enabled.
|
|
6
|
+
*
|
|
7
|
+
* If this is false, bucket names may still be versioned depending on the sync config.
|
|
8
|
+
*
|
|
9
|
+
* Introduced in STORAGE_VERSION_2.
|
|
10
|
+
*/
|
|
11
|
+
versionedBuckets: boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether to use soft deletes for current_data, improving replication concurrency support.
|
|
15
|
+
*
|
|
16
|
+
* Introduced in STORAGE_VERSION_3.
|
|
17
|
+
*/
|
|
18
|
+
softDeleteCurrentData: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Corresponds to the storage version initially used, before we started explicitly versioning storage.
|
|
23
|
+
*/
|
|
24
|
+
export const STORAGE_VERSION_1 = 1;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* First new storage version.
|
|
28
|
+
*
|
|
29
|
+
* Uses versioned bucket names.
|
|
30
|
+
*
|
|
31
|
+
* On MongoDB storage, this always uses Long for checksums.
|
|
32
|
+
*/
|
|
33
|
+
export const STORAGE_VERSION_2 = 2;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* This version is currently unstable, and not enabled by default yet.
|
|
37
|
+
*
|
|
38
|
+
* This is used to build towards incremental reprocessing.
|
|
39
|
+
*/
|
|
40
|
+
export const STORAGE_VERSION_3 = 3;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Oldest supported storage version.
|
|
44
|
+
*/
|
|
45
|
+
export const LEGACY_STORAGE_VERSION = STORAGE_VERSION_1;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Default storage version for newly persisted sync rules.
|
|
49
|
+
*/
|
|
50
|
+
export const CURRENT_STORAGE_VERSION = STORAGE_VERSION_2;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* All versions that can be loaded.
|
|
54
|
+
*
|
|
55
|
+
* This includes unstable versions.
|
|
56
|
+
*/
|
|
57
|
+
export const SUPPORTED_STORAGE_VERSIONS = [STORAGE_VERSION_1, STORAGE_VERSION_2, STORAGE_VERSION_3];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Shared storage-version behavior across storage implementations.
|
|
61
|
+
*/
|
|
62
|
+
export const STORAGE_VERSION_CONFIG: Record<number, StorageVersionConfig | undefined> = {
|
|
63
|
+
[STORAGE_VERSION_1]: {
|
|
64
|
+
version: STORAGE_VERSION_1,
|
|
65
|
+
versionedBuckets: false,
|
|
66
|
+
softDeleteCurrentData: false
|
|
67
|
+
},
|
|
68
|
+
[STORAGE_VERSION_2]: {
|
|
69
|
+
version: STORAGE_VERSION_2,
|
|
70
|
+
versionedBuckets: true,
|
|
71
|
+
softDeleteCurrentData: false
|
|
72
|
+
},
|
|
73
|
+
[STORAGE_VERSION_3]: {
|
|
74
|
+
version: STORAGE_VERSION_3,
|
|
75
|
+
versionedBuckets: true,
|
|
76
|
+
softDeleteCurrentData: true
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Logger, ObserverClient } from '@powersync/lib-services-framework';
|
|
2
|
-
import {
|
|
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';
|
|
@@ -103,7 +108,7 @@ export interface SyncRulesBucketStorage
|
|
|
103
108
|
*/
|
|
104
109
|
getBucketDataBatch(
|
|
105
110
|
checkpoint: util.InternalOpId,
|
|
106
|
-
dataBuckets:
|
|
111
|
+
dataBuckets: BucketDataRequest[],
|
|
107
112
|
options?: BucketDataBatchOptions
|
|
108
113
|
): AsyncIterable<SyncBucketDataChunk>;
|
|
109
114
|
|
|
@@ -115,7 +120,7 @@ export interface SyncRulesBucketStorage
|
|
|
115
120
|
* This may be slow, depending on the size of the buckets.
|
|
116
121
|
* The checksums are cached internally to compensate for this, but does not cover all cases.
|
|
117
122
|
*/
|
|
118
|
-
getChecksums(checkpoint: util.InternalOpId, buckets:
|
|
123
|
+
getChecksums(checkpoint: util.InternalOpId, buckets: BucketChecksumRequest[]): Promise<util.ChecksumMap>;
|
|
119
124
|
|
|
120
125
|
/**
|
|
121
126
|
* Clear checksum cache. Primarily intended for tests.
|
|
@@ -127,6 +132,16 @@ export interface SyncRulesBucketStorageListener {
|
|
|
127
132
|
batchStarted: (batch: BucketStorageBatch) => void;
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
export interface BucketDataRequest {
|
|
136
|
+
bucket: string;
|
|
137
|
+
start: util.InternalOpId;
|
|
138
|
+
source: BucketDataSource;
|
|
139
|
+
}
|
|
140
|
+
export interface BucketChecksumRequest {
|
|
141
|
+
bucket: string;
|
|
142
|
+
source: BucketDataSource;
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
export interface SyncRuleStatus {
|
|
131
146
|
checkpoint_lsn: string | null;
|
|
132
147
|
active: boolean;
|
|
@@ -202,7 +217,8 @@ export interface CompactOptions {
|
|
|
202
217
|
*
|
|
203
218
|
* If not specified, compacts all buckets.
|
|
204
219
|
*
|
|
205
|
-
* These
|
|
220
|
+
* These must be full bucket names (e.g., "global[]", "mybucket[\"user1\"]").
|
|
221
|
+
* Bucket definition names (e.g., "global") are not supported.
|
|
206
222
|
*/
|
|
207
223
|
compactBuckets?: string[];
|
|
208
224
|
|