@powersync/service-core 0.0.0-dev-20260909133214 → 0.0.0-dev-20260929095716
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 +45 -5
- package/dist/api/diagnostics.js +5 -2
- package/dist/api/diagnostics.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/entry/commands/config-command.js +1 -1
- package/dist/entry/commands/config-command.js.map +1 -1
- package/dist/migrations/PowerSyncMigrationManager.js +2 -1
- package/dist/migrations/PowerSyncMigrationManager.js.map +1 -1
- package/dist/replication/AbstractReplicator.js +8 -4
- package/dist/replication/AbstractReplicator.js.map +1 -1
- package/dist/replication/ReplicationModule.d.ts +0 -7
- package/dist/replication/ReplicationModule.js +0 -1
- package/dist/replication/ReplicationModule.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +3 -3
- package/dist/routes/endpoints/admin.d.ts +6 -6
- package/dist/routes/endpoints/admin.js +15 -11
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +7 -5
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +16 -10
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +21 -2
- package/dist/storage/BucketStorageFactory.js +17 -4
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/PersistedReplicationStream.d.ts +2 -1
- package/dist/storage/PersistedReplicationStream.js.map +1 -1
- package/dist/storage/PersistedSyncConfigContent.d.ts +4 -4
- package/dist/storage/PersistedSyncConfigContent.js +12 -5
- package/dist/storage/PersistedSyncConfigContent.js.map +1 -1
- package/dist/storage/StorageEngine.d.ts +4 -0
- package/dist/storage/StorageEngine.js +7 -1
- package/dist/storage/StorageEngine.js.map +1 -1
- package/dist/storage/StorageProvider.d.ts +8 -0
- package/dist/storage/SyncConfigParser.d.ts +44 -0
- package/dist/storage/SyncConfigParser.js +70 -0
- package/dist/storage/SyncConfigParser.js.map +1 -0
- package/dist/storage/SyncRulesBucketStorage.d.ts +3 -1
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.js +9 -5
- package/dist/storage/implementation/IncrementalReprocessingCompatibility.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 +1 -1
- package/dist/sync/BucketChecksumState.js +5 -4
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/util.d.ts +1 -1
- package/dist/sync/util.js +3 -0
- package/dist/sync/util.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +20 -0
- package/dist/system/ServiceContext.js +29 -1
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/util/param-logging.d.ts +5 -2
- package/dist/util/param-logging.js +17 -9
- package/dist/util/param-logging.js.map +1 -1
- package/dist/util/protocol-types.d.ts +7 -7
- package/dist/util/protocol-types.js +3 -3
- package/package.json +4 -4
- package/src/api/diagnostics.ts +6 -2
- package/src/entry/commands/compact-action.ts +1 -1
- package/src/entry/commands/config-command.ts +1 -1
- package/src/migrations/PowerSyncMigrationManager.ts +2 -1
- package/src/replication/AbstractReplicator.ts +8 -4
- package/src/replication/ReplicationModule.ts +0 -10
- package/src/routes/endpoints/admin.ts +15 -17
- package/src/routes/endpoints/socket-route.ts +9 -6
- package/src/routes/endpoints/sync-rules.ts +19 -12
- package/src/storage/BucketStorageFactory.ts +33 -6
- package/src/storage/PersistedReplicationStream.ts +2 -1
- package/src/storage/PersistedSyncConfigContent.ts +16 -8
- package/src/storage/StorageEngine.ts +10 -1
- package/src/storage/StorageProvider.ts +8 -0
- package/src/storage/SyncConfigParser.ts +121 -0
- package/src/storage/SyncRulesBucketStorage.ts +3 -1
- package/src/storage/implementation/IncrementalReprocessingCompatibility.ts +14 -5
- package/src/storage/storage-index.ts +1 -0
- package/src/sync/BucketChecksumState.ts +5 -4
- package/src/sync/util.ts +4 -1
- package/src/system/ServiceContext.ts +47 -1
- package/src/util/param-logging.ts +21 -14
- package/src/util/protocol-types.ts +5 -5
- package/test/src/PersistedSyncConfigContent.test.ts +3 -1
- package/test/src/SyncConfigParser.test.ts +330 -0
- package/test/src/broadcast_iterable.test.ts +26 -8
- package/test/src/diagnostics.test.ts +23 -0
- package/test/src/routes/admin.test.ts +22 -0
- package/test/src/routes/mocks.ts +2 -0
- package/test/src/sync/BucketChecksumState.test.ts +195 -6
- package/test/src/sync/util.test.ts +19 -0
- package/test/src/util/param-logging.test.ts +59 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as framework from '@powersync/lib-services-framework';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
4
5
|
import * as system from '../system/system-index.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -28,7 +29,7 @@ export abstract class AbstractPowerSyncMigrationAgent extends framework.Abstract
|
|
|
28
29
|
|
|
29
30
|
return await Promise.all(
|
|
30
31
|
migrations.map(async (migration) => {
|
|
31
|
-
const module = await import(path.resolve(migrationsDir, migration));
|
|
32
|
+
const module = await import(pathToFileURL(path.resolve(migrationsDir, migration)).href);
|
|
32
33
|
return {
|
|
33
34
|
name: path.basename(migration).replace(path.extname(migration), ''),
|
|
34
35
|
up: module.up,
|
|
@@ -165,10 +165,14 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
165
165
|
// versions before that check was added, so we keep the lock for now - for where te service version and sync config is updated at
|
|
166
166
|
// the same time.
|
|
167
167
|
|
|
168
|
+
const parsed = this.options.storageEngine.activeBucketStorage.syncConfigParser.parseContent(loadedSyncConfig, {
|
|
169
|
+
schema: undefined,
|
|
170
|
+
defaultSchema: 'not_applicable',
|
|
171
|
+
throwOnError: this.syncRuleProvider.exitOnError
|
|
172
|
+
});
|
|
168
173
|
const { lock } = await this.storage.configureSyncRules(
|
|
169
|
-
storage.
|
|
174
|
+
storage.updateSyncRulesFromConfig(parsed, {
|
|
170
175
|
lock: true,
|
|
171
|
-
validate: this.syncRuleProvider.exitOnError,
|
|
172
176
|
version_label: versionLabel
|
|
173
177
|
})
|
|
174
178
|
);
|
|
@@ -182,7 +186,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
182
186
|
throw e;
|
|
183
187
|
}
|
|
184
188
|
} else {
|
|
185
|
-
this.logger.info('No sync
|
|
189
|
+
this.logger.info('No sync config configured - configure via API');
|
|
186
190
|
}
|
|
187
191
|
let useFastRefresh = true;
|
|
188
192
|
const fastRefreshDeadline = Date.now() + FAST_REFRESH_TIMEOUT_MS;
|
|
@@ -290,7 +294,7 @@ export abstract class AbstractReplicator<T extends AbstractReplicationJob = Abst
|
|
|
290
294
|
} else {
|
|
291
295
|
lock = await replicationStream.lock();
|
|
292
296
|
}
|
|
293
|
-
const syncRuleStorage = this.storage.getInstance(replicationStream);
|
|
297
|
+
const syncRuleStorage = this.storage.getInstance(replicationStream, { replicationLock: lock });
|
|
294
298
|
const newJob = this.createJob({
|
|
295
299
|
lock: lock,
|
|
296
300
|
storage: syncRuleStorage
|
|
@@ -63,14 +63,6 @@ export abstract class ReplicationModule<TConfig extends types.configFile.DataSou
|
|
|
63
63
|
*/
|
|
64
64
|
protected abstract createReplicator(context: system.ServiceContext): AbstractReplicator;
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* Any additional initialization specific to the module should be added here. Will be called if necessary after the
|
|
68
|
-
* main initialization has been completed
|
|
69
|
-
* @param context
|
|
70
|
-
* @protected
|
|
71
|
-
*/
|
|
72
|
-
protected abstract onInitialized(context: system.ServiceContext): Promise<void>;
|
|
73
|
-
|
|
74
66
|
public abstract testConnection(config: TConfig): Promise<ConnectionTestResult>;
|
|
75
67
|
|
|
76
68
|
/**
|
|
@@ -100,8 +92,6 @@ export abstract class ReplicationModule<TConfig extends types.configFile.DataSou
|
|
|
100
92
|
|
|
101
93
|
context.replicationEngine?.register(this.createReplicator(context));
|
|
102
94
|
context.routerEngine?.registerAPI(this.createRouteAPIAdapter());
|
|
103
|
-
|
|
104
|
-
await this.onInitialized(context);
|
|
105
95
|
}
|
|
106
96
|
|
|
107
97
|
protected decodeConfig(config: TConfig): void {
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import * as sqlite from 'node:sqlite';
|
|
2
2
|
|
|
3
3
|
import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
|
|
4
|
-
import {
|
|
5
|
-
HydratedSyncConfig,
|
|
6
|
-
nodeSqlite,
|
|
7
|
-
SourceSchema,
|
|
8
|
-
SqlSyncRules,
|
|
9
|
-
StaticSchema
|
|
10
|
-
} from '@powersync/service-sync-rules';
|
|
4
|
+
import { HydratedSyncConfig, nodeSqlite, SourceSchema, StaticSchema } from '@powersync/service-sync-rules';
|
|
11
5
|
import { internal_routes } from '@powersync/service-types';
|
|
12
6
|
|
|
13
7
|
import { DEFAULT_HYDRATION_STATE } from '@powersync/service-sync-rules';
|
|
@@ -158,14 +152,18 @@ export const reprocess = routeDefinition({
|
|
|
158
152
|
});
|
|
159
153
|
}
|
|
160
154
|
// There are some differences between this and using asUpdateOptions():
|
|
161
|
-
// 1. This always re-parses the source YAML. If there are changes to the
|
|
155
|
+
// 1. This always re-parses the source YAML. If there are changes to the Sync Streams compiler, that can affect the sync plan.
|
|
162
156
|
// 2. If the source does not set the storage version, this will update it do the current version.
|
|
163
157
|
// We can consider tweaking this behavior in the future.
|
|
158
|
+
const parsed = service_context.syncConfigParser.parseContent(active.content.sync_rules_content, {
|
|
159
|
+
...apiHandler.getParseSyncRulesOptions(),
|
|
160
|
+
schema: undefined,
|
|
161
|
+
// This sync config already passed validation. But if the config is not valid anymore due
|
|
162
|
+
// to a service change, we do want to report the error here.
|
|
163
|
+
throwOnError: true
|
|
164
|
+
});
|
|
164
165
|
const new_rules = await activeBucketStorage.updateSyncRules(
|
|
165
|
-
storage.
|
|
166
|
-
// This sync config already passed validation. But if the config is not valid anymore due
|
|
167
|
-
// to a service change, we do want to report the error here.
|
|
168
|
-
validate: true,
|
|
166
|
+
storage.updateSyncRulesFromConfig(parsed, {
|
|
169
167
|
version_label: active.content.version_label,
|
|
170
168
|
forceNewReplicationStream: true
|
|
171
169
|
})
|
|
@@ -188,16 +186,16 @@ export const reprocess = routeDefinition({
|
|
|
188
186
|
|
|
189
187
|
class FakeSyncRulesContentForValidation extends storage.PersistedSyncConfigContent {
|
|
190
188
|
constructor(
|
|
191
|
-
private readonly
|
|
189
|
+
private readonly validationParser: storage.SyncConfigParser,
|
|
192
190
|
private readonly schema: SourceSchema,
|
|
193
191
|
data: storage.PersistedSyncConfigContentData
|
|
194
192
|
) {
|
|
195
|
-
super(data);
|
|
193
|
+
super(data, validationParser);
|
|
196
194
|
}
|
|
197
195
|
|
|
198
196
|
parsed(options: storage.ParseSyncConfigOptions): storage.ParsedSyncConfigSet {
|
|
199
|
-
const syncConfig =
|
|
200
|
-
...
|
|
197
|
+
const syncConfig = this.validationParser.parseContent(this.sync_rules_content, {
|
|
198
|
+
...options,
|
|
201
199
|
schema: this.schema
|
|
202
200
|
});
|
|
203
201
|
|
|
@@ -237,7 +235,7 @@ export const validate = routeDefinition({
|
|
|
237
235
|
const schemaData = await api.getConnectionsSchema(apiHandler);
|
|
238
236
|
const schema = new StaticSchema(schemaData.connections);
|
|
239
237
|
|
|
240
|
-
const sync_rules = new FakeSyncRulesContentForValidation(
|
|
238
|
+
const sync_rules = new FakeSyncRulesContentForValidation(service_context.syncConfigParser, schema, {
|
|
241
239
|
// Dummy values
|
|
242
240
|
replicationStreamId: 0,
|
|
243
241
|
replicationStreamName: '',
|
|
@@ -16,6 +16,10 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
16
16
|
const { routerEngine, metricsEngine, syncContext } = service_context;
|
|
17
17
|
const streamStart = Date.now();
|
|
18
18
|
|
|
19
|
+
// Format client-controlled values before registering connection resources.
|
|
20
|
+
const formattedAppMetadata = params.app_metadata ? limitParamsForLogging(params.app_metadata) : undefined;
|
|
21
|
+
const formattedClientParams = params.parameters ? limitParamsForLogging(params.parameters) : undefined;
|
|
22
|
+
|
|
19
23
|
logger.defaultMeta = {
|
|
20
24
|
...logger.defaultMeta,
|
|
21
25
|
user_id: context.token_payload!.userIdJson,
|
|
@@ -99,13 +103,12 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
99
103
|
tracker.setCompressed(connection.tracker.encoding);
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
const formattedAppMetadata = params.app_metadata ? limitParamsForLogging(params.app_metadata) : undefined;
|
|
103
|
-
logger.info('Sync stream started', {
|
|
104
|
-
app_metadata: formattedAppMetadata,
|
|
105
|
-
client_params: params.parameters ? limitParamsForLogging(params.parameters) : undefined
|
|
106
|
-
});
|
|
107
|
-
|
|
108
106
|
try {
|
|
107
|
+
logger.info('Sync stream started', {
|
|
108
|
+
app_metadata: formattedAppMetadata,
|
|
109
|
+
client_params: formattedClientParams
|
|
110
|
+
});
|
|
111
|
+
|
|
109
112
|
for await (const data of sync.streamResponse({
|
|
110
113
|
syncContext: syncContext,
|
|
111
114
|
bucketStorage: bucketStorage,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
|
|
2
|
-
import {
|
|
2
|
+
import { SyncConfigWithErrors, SyncRulesErrors } from '@powersync/service-sync-rules';
|
|
3
3
|
import type { FastifyPluginAsync } from 'fastify';
|
|
4
4
|
import * as t from 'ts-codec';
|
|
5
5
|
|
|
6
6
|
import { RouteAPI } from '../../api/RouteAPI.js';
|
|
7
|
-
import { updateSyncRulesFromConfig
|
|
7
|
+
import { updateSyncRulesFromConfig } from '../../storage/BucketStorageFactory.js';
|
|
8
|
+
import { SyncConfigParser } from '../../storage/SyncConfigParser.js';
|
|
8
9
|
import { authApi } from '../auth.js';
|
|
9
10
|
import { routeDefinition } from '../router.js';
|
|
10
11
|
|
|
@@ -56,7 +57,7 @@ export const deploySyncRules = routeDefinition({
|
|
|
56
57
|
|
|
57
58
|
try {
|
|
58
59
|
const apiHandler = service_context.routerEngine.getAPI();
|
|
59
|
-
syncConfig =
|
|
60
|
+
syncConfig = service_context.syncConfigParser.parseContent(content, {
|
|
60
61
|
...apiHandler.getParseSyncRulesOptions(),
|
|
61
62
|
// We don't do any schema-level validation at this point
|
|
62
63
|
schema: undefined
|
|
@@ -94,7 +95,7 @@ export const validateSyncRules = routeDefinition({
|
|
|
94
95
|
const { service_context } = payload.context;
|
|
95
96
|
const apiHandler = service_context.routerEngine.getAPI();
|
|
96
97
|
|
|
97
|
-
const info = await debugSyncRules(apiHandler, content);
|
|
98
|
+
const info = await debugSyncRules(apiHandler, content, service_context.syncConfigParser);
|
|
98
99
|
|
|
99
100
|
return replyPrettyJson(info);
|
|
100
101
|
}
|
|
@@ -121,10 +122,12 @@ export const currentSyncRules = routeDefinition({
|
|
|
121
122
|
|
|
122
123
|
const sync_rules = active.content;
|
|
123
124
|
const apiHandler = service_context.routerEngine.getAPI();
|
|
124
|
-
const info = await debugSyncRules(apiHandler, sync_rules.sync_rules_content);
|
|
125
|
+
const info = await debugSyncRules(apiHandler, sync_rules.sync_rules_content, service_context.syncConfigParser);
|
|
125
126
|
const next = await activeBucketStorage.getDeployingSyncConfig();
|
|
126
127
|
|
|
127
|
-
const next_info = next
|
|
128
|
+
const next_info = next
|
|
129
|
+
? await debugSyncRules(apiHandler, next.content.sync_rules_content, service_context.syncConfigParser)
|
|
130
|
+
: null;
|
|
128
131
|
|
|
129
132
|
const response = {
|
|
130
133
|
current: {
|
|
@@ -176,11 +179,15 @@ export const reprocessSyncRules = routeDefinition({
|
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
const sync_rules = active.content;
|
|
182
|
+
const parsed = payload.context.service_context.syncConfigParser.parseContent(sync_rules.sync_rules_content, {
|
|
183
|
+
...payload.context.service_context.routerEngine.getAPI().getParseSyncRulesOptions(),
|
|
184
|
+
schema: undefined,
|
|
185
|
+
// This sync config already passed validation. But if the rules are not valid anymore due
|
|
186
|
+
// to a service change, we do want to report the error here.
|
|
187
|
+
throwOnError: true
|
|
188
|
+
});
|
|
179
189
|
const new_rules = await activeBucketStorage.updateSyncRules(
|
|
180
|
-
|
|
181
|
-
// This sync config already passed validation. But if the rules are not valid anymore due
|
|
182
|
-
// to a service change, we do want to report the error here.
|
|
183
|
-
validate: true,
|
|
190
|
+
updateSyncRulesFromConfig(parsed, {
|
|
184
191
|
version_label: sync_rules.version_label,
|
|
185
192
|
forceNewReplicationStream: true
|
|
186
193
|
})
|
|
@@ -201,9 +208,9 @@ function replyPrettyJson(payload: any) {
|
|
|
201
208
|
});
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
async function debugSyncRules(apiHandler: RouteAPI, sync_rules: string) {
|
|
211
|
+
async function debugSyncRules(apiHandler: RouteAPI, sync_rules: string, syncConfigParser: SyncConfigParser) {
|
|
205
212
|
try {
|
|
206
|
-
const rules =
|
|
213
|
+
const rules = syncConfigParser.parseContent(sync_rules, {
|
|
207
214
|
...apiHandler.getParseSyncRulesOptions(),
|
|
208
215
|
// No schema-based validation at this point
|
|
209
216
|
schema: undefined
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
SerializedSyncPlan as RawSerializedSyncPlan,
|
|
5
5
|
SerializedCompatibilityContext,
|
|
6
6
|
serializeSyncPlan,
|
|
7
|
-
SqlSyncRules,
|
|
8
7
|
SyncConfigWithErrors
|
|
9
8
|
} from '@powersync/service-sync-rules';
|
|
10
9
|
import { ReplicationError } from '@powersync/service-types';
|
|
@@ -14,8 +13,16 @@ import { PersistedSyncConfigContent } from './PersistedSyncConfigContent.js';
|
|
|
14
13
|
import { ReplicationEventPayload } from './ReplicationEventPayload.js';
|
|
15
14
|
import { ReplicationLock } from './ReplicationLock.js';
|
|
16
15
|
import { ReportStorage } from './ReportStorage.js';
|
|
16
|
+
import { SqlSyncConfigParser, SyncConfigParser } from './SyncConfigParser.js';
|
|
17
17
|
import { SyncRulesBucketStorage } from './SyncRulesBucketStorage.js';
|
|
18
18
|
|
|
19
|
+
export interface BucketStorageFactoryOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Service-wide parser assembled before storage startup.
|
|
22
|
+
*/
|
|
23
|
+
syncConfigParser: SyncConfigParser;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
/**
|
|
20
27
|
* Represents a configured storage provider.
|
|
21
28
|
*
|
|
@@ -28,6 +35,16 @@ export abstract class BucketStorageFactory
|
|
|
28
35
|
extends BaseObserver<BucketStorageFactoryListener>
|
|
29
36
|
implements AsyncDisposable
|
|
30
37
|
{
|
|
38
|
+
readonly syncConfigParser: SyncConfigParser;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a storage factory using the service-wide parser assembled before storage startup.
|
|
42
|
+
*/
|
|
43
|
+
constructor(options: BucketStorageFactoryOptions) {
|
|
44
|
+
super();
|
|
45
|
+
this.syncConfigParser = options.syncConfigParser;
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
/**
|
|
32
49
|
* Update sync config from configuration, if changed.
|
|
33
50
|
*/
|
|
@@ -117,10 +134,14 @@ export abstract class BucketStorageFactory
|
|
|
117
134
|
*/
|
|
118
135
|
abstract getSystemIdentifier(): Promise<BucketStorageSystemIdentifier>;
|
|
119
136
|
|
|
120
|
-
|
|
137
|
+
async [Symbol.asyncDispose](): Promise<void> {
|
|
138
|
+
await this.iterateAsyncListeners(async (listener) => listener.beforeDispose?.());
|
|
139
|
+
}
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
export interface BucketStorageFactoryListener {
|
|
143
|
+
/** Release owned resources before the factory closes its database connections. */
|
|
144
|
+
beforeDispose: () => Promise<void>;
|
|
124
145
|
syncStorageCreated: (storage: SyncRulesBucketStorage) => void;
|
|
125
146
|
replicationEvent: (event: ReplicationEventPayload) => void;
|
|
126
147
|
}
|
|
@@ -177,7 +198,7 @@ export interface UpdateSyncRulesOptions {
|
|
|
177
198
|
config: {
|
|
178
199
|
yaml: string;
|
|
179
200
|
/**
|
|
180
|
-
* The serialized sync plan for the sync configuration, or `null` for configurations not using the
|
|
201
|
+
* The serialized sync plan for the sync configuration, or `null` for configurations not using the Sync Streams
|
|
181
202
|
* compiler.
|
|
182
203
|
*/
|
|
183
204
|
plan: SerializedSyncPlan | null;
|
|
@@ -223,14 +244,15 @@ export function updateSyncRulesFromYaml(
|
|
|
223
244
|
content: string,
|
|
224
245
|
options?: Omit<UpdateSyncRulesOptions, 'config'> & { validate?: boolean }
|
|
225
246
|
): UpdateSyncRulesOptions {
|
|
226
|
-
const
|
|
247
|
+
const { validate, ...updateOptions } = options ?? {};
|
|
248
|
+
const config = new SqlSyncConfigParser().parseContent(content, {
|
|
227
249
|
// No schema-based validation at this point
|
|
228
250
|
schema: undefined,
|
|
229
251
|
defaultSchema: options?.defaultSchema ?? 'not_applicable', // Not needed for validation
|
|
230
|
-
throwOnError:
|
|
252
|
+
throwOnError: validate ?? false
|
|
231
253
|
});
|
|
232
254
|
|
|
233
|
-
return updateSyncRulesFromConfig(config,
|
|
255
|
+
return updateSyncRulesFromConfig(config, updateOptions);
|
|
234
256
|
}
|
|
235
257
|
|
|
236
258
|
export function updateSyncRulesFromConfig(
|
|
@@ -258,6 +280,11 @@ export function updateSyncRulesFromConfig(
|
|
|
258
280
|
}
|
|
259
281
|
|
|
260
282
|
export interface GetIntanceOptions {
|
|
283
|
+
/**
|
|
284
|
+
* The job lease, including a lease acquired during initial configuration.
|
|
285
|
+
* Required for writing; optional for reading.
|
|
286
|
+
*/
|
|
287
|
+
replicationLock?: ReplicationLock;
|
|
261
288
|
/**
|
|
262
289
|
* Set to true to skip trigger any events for creating the instance.
|
|
263
290
|
*
|
|
@@ -2,9 +2,10 @@ import { logger as defaultLogger, ErrorCode, ServiceError } from '@powersync/lib
|
|
|
2
2
|
import { Logger } from 'winston';
|
|
3
3
|
import { SyncRuleState } from './BucketStorage.js';
|
|
4
4
|
import type { ParsedSyncConfigSet } from './ParsedSyncConfigSet.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { PersistedSyncConfigContent } from './PersistedSyncConfigContent.js';
|
|
6
6
|
import { ReplicationLock } from './ReplicationLock.js';
|
|
7
7
|
import { STORAGE_VERSION_CONFIG, StorageVersionConfig } from './StorageVersionConfig.js';
|
|
8
|
+
import type { ParseSyncConfigOptions } from './SyncConfigParser.js';
|
|
8
9
|
|
|
9
10
|
export abstract class PersistedReplicationStream implements PersistedReplicationStreamData {
|
|
10
11
|
readonly replicationStreamId: number;
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
HydrationState,
|
|
11
11
|
nodeSqlite,
|
|
12
12
|
PrecompiledSyncConfig,
|
|
13
|
-
SqlSyncRules,
|
|
14
13
|
SyncConfigWithErrors,
|
|
15
14
|
versionedHydrationState,
|
|
16
15
|
YamlError
|
|
@@ -22,20 +21,22 @@ import { SerializedSyncPlan, UpdateSyncRulesOptions } from './BucketStorageFacto
|
|
|
22
21
|
import { ParsedSyncConfigSet } from './ParsedSyncConfigSet.js';
|
|
23
22
|
import { PersistedSyncConfigStatus } from './PersistedSyncConfigStatus.js';
|
|
24
23
|
import { STORAGE_VERSION_CONFIG, StorageVersionConfig } from './StorageVersionConfig.js';
|
|
24
|
+
import { ParseSyncConfigOptions, SyncConfigParser } from './SyncConfigParser.js';
|
|
25
25
|
|
|
26
26
|
export interface ParsePersistedSyncConfigContentOptions {
|
|
27
27
|
content: string;
|
|
28
28
|
compiledPlan: SerializedSyncPlan | null;
|
|
29
29
|
storageVersion: number;
|
|
30
30
|
parseOptions: ParseSyncConfigOptions;
|
|
31
|
+
syncConfigParser: SyncConfigParser;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function parsePersistedSyncConfigContent(options: ParsePersistedSyncConfigContentOptions): SyncConfigWithErrors {
|
|
34
|
-
const { content, compiledPlan, storageVersion, parseOptions } = options;
|
|
35
|
+
const { content, compiledPlan, storageVersion, parseOptions, syncConfigParser } = options;
|
|
35
36
|
|
|
36
37
|
if (compiledPlan == null) {
|
|
37
38
|
// Fallback: Only parse from YAML if no compiled plan is available.
|
|
38
|
-
return
|
|
39
|
+
return syncConfigParser.parseContent(content, parseOptions);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const plan = deserializeSyncPlan(compiledPlan.plan);
|
|
@@ -69,6 +70,12 @@ export function parsePersistedSyncConfigContent(options: ParsePersistedSyncConfi
|
|
|
69
70
|
// Note: If the original content did not define a storage version, this will still set the storage version.
|
|
70
71
|
// This means asUpdateOptions will not change the storage version, even if the default changes.
|
|
71
72
|
precompiled.storageVersion = storageVersion;
|
|
73
|
+
errors.push(
|
|
74
|
+
...syncConfigParser.validatePersisted({
|
|
75
|
+
config: precompiled,
|
|
76
|
+
context: { defaultSchema: parseOptions.defaultSchema }
|
|
77
|
+
})
|
|
78
|
+
);
|
|
72
79
|
|
|
73
80
|
if (compiledPlan.errors) {
|
|
74
81
|
for (const error of compiledPlan.errors) {
|
|
@@ -105,7 +112,10 @@ export abstract class PersistedSyncConfigContent implements PersistedSyncConfigC
|
|
|
105
112
|
readonly syncConfigState: SyncRuleState;
|
|
106
113
|
readonly version_label: string | undefined;
|
|
107
114
|
|
|
108
|
-
constructor(
|
|
115
|
+
constructor(
|
|
116
|
+
data: PersistedSyncConfigContentData,
|
|
117
|
+
private readonly syncConfigParser: SyncConfigParser
|
|
118
|
+
) {
|
|
109
119
|
this.replicationStreamId = data.replicationStreamId;
|
|
110
120
|
this.sync_rules_content = data.sync_rules_content;
|
|
111
121
|
this.compiled_plan = data.compiled_plan;
|
|
@@ -144,7 +154,8 @@ export abstract class PersistedSyncConfigContent implements PersistedSyncConfigC
|
|
|
144
154
|
content: this.sync_rules_content,
|
|
145
155
|
compiledPlan: this.compiled_plan,
|
|
146
156
|
storageVersion: this.storageVersion,
|
|
147
|
-
parseOptions: options
|
|
157
|
+
parseOptions: options,
|
|
158
|
+
syncConfigParser: this.syncConfigParser
|
|
148
159
|
});
|
|
149
160
|
}
|
|
150
161
|
|
|
@@ -202,6 +213,3 @@ export interface PersistedSyncConfigContentData {
|
|
|
202
213
|
readonly version_label?: string;
|
|
203
214
|
}
|
|
204
215
|
export type PersistedSyncConfigId = string;
|
|
205
|
-
export interface ParseSyncConfigOptions {
|
|
206
|
-
defaultSchema: string;
|
|
207
|
-
}
|
|
@@ -2,9 +2,11 @@ import { BaseObserver, logger, ServiceError } from '@powersync/lib-services-fram
|
|
|
2
2
|
import { ResolvedPowerSyncConfig } from '../util/util-index.js';
|
|
3
3
|
import { BucketStorageFactory } from './BucketStorageFactory.js';
|
|
4
4
|
import { ActiveStorage, StorageProvider } from './StorageProvider.js';
|
|
5
|
+
import { SyncConfigParser } from './SyncConfigParser.js';
|
|
5
6
|
|
|
6
7
|
export type StorageEngineOptions = {
|
|
7
8
|
configuration: ResolvedPowerSyncConfig;
|
|
9
|
+
syncConfigParser: SyncConfigParser;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
export interface StorageEngineListener {
|
|
@@ -15,6 +17,7 @@ export interface StorageEngineListener {
|
|
|
15
17
|
export class StorageEngine extends BaseObserver<StorageEngineListener> {
|
|
16
18
|
// TODO: This will need to revisited when we actually support multiple storage providers.
|
|
17
19
|
private storageProviders: Map<string, StorageProvider> = new Map();
|
|
20
|
+
private startupStarted = false;
|
|
18
21
|
private currentActiveStorage: ActiveStorage | null = null;
|
|
19
22
|
|
|
20
23
|
constructor(private options: StorageEngineOptions) {
|
|
@@ -33,6 +36,10 @@ export class StorageEngine extends BaseObserver<StorageEngineListener> {
|
|
|
33
36
|
return this.currentActiveStorage;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
get started(): boolean {
|
|
40
|
+
return this.startupStarted;
|
|
41
|
+
}
|
|
42
|
+
|
|
36
43
|
/**
|
|
37
44
|
* Register a provider which generates a {@link BucketStorageFactory}
|
|
38
45
|
* given the matching config specified in the loaded {@link ResolvedPowerSyncConfig}
|
|
@@ -42,10 +49,12 @@ export class StorageEngine extends BaseObserver<StorageEngineListener> {
|
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
public async start(): Promise<void> {
|
|
52
|
+
this.startupStarted = true;
|
|
45
53
|
logger.info('Starting Storage Engine...');
|
|
46
54
|
const { configuration } = this.options;
|
|
47
55
|
this.currentActiveStorage = await this.storageProviders.get(configuration.storage.type)!.getStorage({
|
|
48
|
-
resolvedConfig: configuration
|
|
56
|
+
resolvedConfig: configuration,
|
|
57
|
+
syncConfigParser: this.options.syncConfigParser
|
|
49
58
|
});
|
|
50
59
|
this.iterateListeners((cb) => cb.storageActivated?.(this.activeBucketStorage));
|
|
51
60
|
this.currentActiveStorage.onFatalError?.((error) => {
|
|
@@ -2,6 +2,7 @@ import { ServiceError } from '@powersync/lib-services-framework';
|
|
|
2
2
|
import * as util from '../util/util-index.js';
|
|
3
3
|
import { BucketStorageFactory } from './BucketStorageFactory.js';
|
|
4
4
|
import { ReportStorage } from './ReportStorage.js';
|
|
5
|
+
import { SyncConfigParser } from './SyncConfigParser.js';
|
|
5
6
|
|
|
6
7
|
export interface ActiveStorage {
|
|
7
8
|
storage: BucketStorageFactory;
|
|
@@ -19,6 +20,10 @@ export interface ActiveStorage {
|
|
|
19
20
|
export interface GetStorageOptions {
|
|
20
21
|
// TODO: This should just be the storage config. Update once the slot name prefix coupling has been removed from the storage
|
|
21
22
|
resolvedConfig: util.ResolvedPowerSyncConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Service-wide parser containing every extension registered during module initialization.
|
|
25
|
+
*/
|
|
26
|
+
syncConfigParser: SyncConfigParser;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
@@ -31,5 +36,8 @@ export interface StorageProvider {
|
|
|
31
36
|
*/
|
|
32
37
|
type: string;
|
|
33
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Creates active storage during storage-engine startup using the fully assembled service parser.
|
|
41
|
+
*/
|
|
34
42
|
getStorage(options: GetStorageOptions): Promise<ActiveStorage>;
|
|
35
43
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AdditionalSyncConfigParser,
|
|
3
|
+
compileSyncRulesSchemaValidator,
|
|
4
|
+
createSyncRulesSchema,
|
|
5
|
+
JsonObject,
|
|
6
|
+
PrecompiledSyncConfig,
|
|
7
|
+
SqlSyncRules,
|
|
8
|
+
SyncConfig,
|
|
9
|
+
SyncConfigDiagnostic,
|
|
10
|
+
SyncConfigWithErrors,
|
|
11
|
+
SyncRulesErrors,
|
|
12
|
+
SyncRulesOptions,
|
|
13
|
+
SyncRulesSchemaValidator,
|
|
14
|
+
validateAdditionalSyncConfigParsers,
|
|
15
|
+
YamlError
|
|
16
|
+
} from '@powersync/service-sync-rules';
|
|
17
|
+
|
|
18
|
+
export type ParseSyncConfigOptions = Omit<SyncRulesOptions, 'parsers' | 'schemaValidator'>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Shared by service routes, deployment, replication and persisted storage; only initialization can register hooks.
|
|
22
|
+
*/
|
|
23
|
+
export interface SyncConfigParser {
|
|
24
|
+
/**
|
|
25
|
+
* A JSON schema which can be used to validate the format of a supplied sync config.
|
|
26
|
+
*/
|
|
27
|
+
readonly jsonSchema: JsonObject;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses string content.
|
|
31
|
+
*
|
|
32
|
+
* @returns The parsed {@link SyncConfigWithErrors}
|
|
33
|
+
*/
|
|
34
|
+
parseContent(content: string, options: ParseSyncConfigOptions): SyncConfigWithErrors;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates a persisted/parsed sync config.
|
|
38
|
+
*
|
|
39
|
+
* @returns Any errors detected.
|
|
40
|
+
*/
|
|
41
|
+
validatePersisted(options: { config: SyncConfig; context: { defaultSchema: string } }): YamlError[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class SqlSyncConfigParser implements SyncConfigParser {
|
|
45
|
+
readonly #parsers: AdditionalSyncConfigParser[];
|
|
46
|
+
#jsonSchema: JsonObject;
|
|
47
|
+
#schemaValidator: SyncRulesSchemaValidator;
|
|
48
|
+
|
|
49
|
+
constructor(parsers: readonly AdditionalSyncConfigParser[] = []) {
|
|
50
|
+
validateAdditionalSyncConfigParsers(parsers);
|
|
51
|
+
this.#parsers = [...parsers];
|
|
52
|
+
this.#jsonSchema = createSyncRulesSchema(parsers);
|
|
53
|
+
this.#schemaValidator = compileSyncRulesSchemaValidator(this.#jsonSchema);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get jsonSchema(): JsonObject {
|
|
57
|
+
// Tooling can customize its copy without changing validation for the running service.
|
|
58
|
+
return structuredClone(this.#jsonSchema);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate the candidate composition before mutating the registry.
|
|
63
|
+
*/
|
|
64
|
+
registerParser(parser: AdditionalSyncConfigParser): void {
|
|
65
|
+
const parsers = [...this.#parsers, parser];
|
|
66
|
+
validateAdditionalSyncConfigParsers(parsers);
|
|
67
|
+
const schema = createSyncRulesSchema(parsers);
|
|
68
|
+
const validator = compileSyncRulesSchemaValidator(schema);
|
|
69
|
+
this.#parsers.push(parser);
|
|
70
|
+
this.#jsonSchema = schema;
|
|
71
|
+
this.#schemaValidator = validator;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
parseContent(content: string, options: ParseSyncConfigOptions): SyncConfigWithErrors {
|
|
75
|
+
return SqlSyncRules.fromYaml(content, {
|
|
76
|
+
...options,
|
|
77
|
+
parsers: this.#parsers,
|
|
78
|
+
schemaValidator: this.#schemaValidator
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
validatePersisted({ config, context }: { config: SyncConfig; context: { defaultSchema: string } }): YamlError[] {
|
|
83
|
+
/**
|
|
84
|
+
* Ensure that all modules which created a persisted SyncConfig
|
|
85
|
+
* are currently loaded.
|
|
86
|
+
* This prevents a case for a SyncConfig with missing external modules to be loaded.
|
|
87
|
+
*/
|
|
88
|
+
const requiredIds = new Set(
|
|
89
|
+
config instanceof PrecompiledSyncConfig ? Object.keys(config.plan.moduleData ?? {}) : []
|
|
90
|
+
);
|
|
91
|
+
const registeredIds = new Set(this.#parsers.map((parser) => parser.id));
|
|
92
|
+
const missingIds = [...requiredIds].filter((id) => !registeredIds.has(id));
|
|
93
|
+
if (missingIds.length != 0) {
|
|
94
|
+
throw new Error(`Missing required sync config parsers: ${missingIds.join(', ')}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const errors: YamlError[] = [];
|
|
98
|
+
const reportDiagnostic = (diagnostic: SyncConfigDiagnostic) => {
|
|
99
|
+
const location = diagnostic.location;
|
|
100
|
+
const error = new YamlError(
|
|
101
|
+
new Error(diagnostic.message),
|
|
102
|
+
location && {
|
|
103
|
+
start: location.start_offset,
|
|
104
|
+
end: location.end_offset
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
error.type = diagnostic.level;
|
|
108
|
+
errors.push(error);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
for (const parser of this.#parsers) {
|
|
112
|
+
if (!requiredIds.has(parser.id)) continue;
|
|
113
|
+
/**
|
|
114
|
+
* This allows additional validation of module specific semantics
|
|
115
|
+
*/
|
|
116
|
+
parser.validatePersisted?.({ config, context: { ...context, reportDiagnostic } });
|
|
117
|
+
}
|
|
118
|
+
if (errors.some((error) => error.type == 'fatal')) throw new SyncRulesErrors(errors);
|
|
119
|
+
return errors;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -11,11 +11,11 @@ import * as util from '../util/util-index.js';
|
|
|
11
11
|
import { BucketStorageBatch, FlushedResult, SaveUpdate } from './BucketStorageBatch.js';
|
|
12
12
|
import { BucketStorageFactory } from './BucketStorageFactory.js';
|
|
13
13
|
import { ParsedSyncConfigSet } from './ParsedSyncConfigSet.js';
|
|
14
|
-
import { ParseSyncConfigOptions } from './PersistedSyncConfigContent.js';
|
|
15
14
|
import { SourceEntityDescriptor } from './SourceEntity.js';
|
|
16
15
|
import { SourceTable } from './SourceTable.js';
|
|
17
16
|
import { SourceTableCandidateReconciler } from './SourceTableReconciler.js';
|
|
18
17
|
import { StorageVersionConfig } from './StorageVersionConfig.js';
|
|
18
|
+
import { ParseSyncConfigOptions } from './SyncConfigParser.js';
|
|
19
19
|
import { SyncStorageWriteCheckpointAPI } from './WriteCheckpointAPI.js';
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -402,6 +402,8 @@ export interface CompactOptions {
|
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
export interface CompactInitialReplicationOptions {
|
|
405
|
+
/** Internal/testing use: bypass the new-chunk threshold in MongoDB V3/V4 storage. */
|
|
406
|
+
forceChunkCompaction?: boolean;
|
|
405
407
|
/**
|
|
406
408
|
* Compact data up to this op id.
|
|
407
409
|
*
|