@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,5 +1,13 @@
|
|
|
1
|
+
import * as sqlite from 'node:sqlite';
|
|
2
|
+
|
|
1
3
|
import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
HydratedSyncConfig,
|
|
6
|
+
nodeSqlite,
|
|
7
|
+
SourceSchema,
|
|
8
|
+
SqlSyncRules,
|
|
9
|
+
StaticSchema
|
|
10
|
+
} from '@powersync/service-sync-rules';
|
|
3
11
|
import { internal_routes } from '@powersync/service-types';
|
|
4
12
|
|
|
5
13
|
import { DEFAULT_HYDRATION_STATE } from '@powersync/service-sync-rules';
|
|
@@ -63,20 +71,35 @@ export const diagnostics = routeDefinition({
|
|
|
63
71
|
const {
|
|
64
72
|
storageEngine: { activeBucketStorage }
|
|
65
73
|
} = service_context;
|
|
66
|
-
const active = await activeBucketStorage.
|
|
67
|
-
const
|
|
74
|
+
const active = await activeBucketStorage.getActiveSyncConfig();
|
|
75
|
+
const deploying = await activeBucketStorage.getDeployingSyncConfig();
|
|
68
76
|
|
|
69
|
-
const active_status = await api.getSyncRulesStatus(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
const active_status = await api.getSyncRulesStatus(
|
|
78
|
+
apiHandler,
|
|
79
|
+
active?.content ?? null,
|
|
80
|
+
{
|
|
81
|
+
include_content,
|
|
82
|
+
check_connection: status.connected,
|
|
83
|
+
live_status: true,
|
|
84
|
+
active: true
|
|
85
|
+
},
|
|
86
|
+
active?.storage
|
|
87
|
+
);
|
|
74
88
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
const deploying_status =
|
|
90
|
+
deploying == null
|
|
91
|
+
? undefined
|
|
92
|
+
: await api.getSyncRulesStatus(
|
|
93
|
+
apiHandler,
|
|
94
|
+
deploying.content,
|
|
95
|
+
{
|
|
96
|
+
include_content,
|
|
97
|
+
check_connection: status.connected,
|
|
98
|
+
live_status: true,
|
|
99
|
+
active: false
|
|
100
|
+
},
|
|
101
|
+
deploying.storage
|
|
102
|
+
);
|
|
80
103
|
|
|
81
104
|
return internal_routes.DiagnosticsResponse.encode({
|
|
82
105
|
connections: [
|
|
@@ -87,7 +110,7 @@ export const diagnostics = routeDefinition({
|
|
|
87
110
|
}
|
|
88
111
|
],
|
|
89
112
|
active_sync_rules: active_status,
|
|
90
|
-
deploying_sync_rules:
|
|
113
|
+
deploying_sync_rules: deploying_status
|
|
91
114
|
});
|
|
92
115
|
}
|
|
93
116
|
});
|
|
@@ -117,12 +140,16 @@ export const reprocess = routeDefinition({
|
|
|
117
140
|
storageEngine: { activeBucketStorage }
|
|
118
141
|
} = service_context;
|
|
119
142
|
const apiHandler = service_context.routerEngine.getAPI();
|
|
120
|
-
const next = await activeBucketStorage.
|
|
143
|
+
const next = await activeBucketStorage.getDeployingSyncConfig();
|
|
121
144
|
if (next != null) {
|
|
122
|
-
throw new
|
|
145
|
+
throw new errors.ServiceError({
|
|
146
|
+
status: 409,
|
|
147
|
+
code: ErrorCode.PSYNC_S4106,
|
|
148
|
+
description: 'Busy processing sync config - cannot reprocess'
|
|
149
|
+
});
|
|
123
150
|
}
|
|
124
151
|
|
|
125
|
-
const active = await activeBucketStorage.
|
|
152
|
+
const active = await activeBucketStorage.getActiveSyncConfig();
|
|
126
153
|
if (active == null) {
|
|
127
154
|
throw new errors.ServiceError({
|
|
128
155
|
status: 422,
|
|
@@ -130,13 +157,12 @@ export const reprocess = routeDefinition({
|
|
|
130
157
|
description: 'No active sync config'
|
|
131
158
|
});
|
|
132
159
|
}
|
|
133
|
-
|
|
134
160
|
// There are some differences between this and using asUpdateOptions():
|
|
135
161
|
// 1. This always re-parses the source YAML. If there are changes to the sync stream compiler, that can affect the sync plan.
|
|
136
162
|
// 2. If the source does not set the storage version, this will update it do the current version.
|
|
137
163
|
// We can consider tweaking this behavior in the future.
|
|
138
164
|
const new_rules = await activeBucketStorage.updateSyncRules(
|
|
139
|
-
storage.updateSyncRulesFromYaml(active.
|
|
165
|
+
storage.updateSyncRulesFromYaml(active.content.sync_rules_content, {
|
|
140
166
|
// This sync config already passed validation. But if the config is not valid anymore due
|
|
141
167
|
// to a service change, we do want to report the error here.
|
|
142
168
|
validate: true
|
|
@@ -151,41 +177,47 @@ export const reprocess = routeDefinition({
|
|
|
151
177
|
// Previously the connection was asserted with `!`
|
|
152
178
|
tag: baseConfig.tag,
|
|
153
179
|
id: baseConfig.id,
|
|
154
|
-
slot_name: new_rules.
|
|
180
|
+
slot_name: new_rules.replicationStreamName
|
|
155
181
|
}
|
|
156
182
|
]
|
|
157
183
|
});
|
|
158
184
|
}
|
|
159
185
|
});
|
|
160
186
|
|
|
161
|
-
class FakeSyncRulesContentForValidation extends storage.
|
|
187
|
+
class FakeSyncRulesContentForValidation extends storage.PersistedSyncConfigContent {
|
|
162
188
|
constructor(
|
|
163
189
|
private readonly apiHandler: api.RouteAPI,
|
|
164
190
|
private readonly schema: SourceSchema,
|
|
165
|
-
data: storage.
|
|
191
|
+
data: storage.PersistedSyncConfigContentData
|
|
166
192
|
) {
|
|
167
193
|
super(data);
|
|
168
194
|
}
|
|
169
195
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
196
|
+
parsed(options: storage.ParseSyncConfigOptions): storage.ParsedSyncConfigSet {
|
|
197
|
+
const syncConfig = SqlSyncRules.fromYaml(this.sync_rules_content, {
|
|
198
|
+
...this.apiHandler.getParseSyncRulesOptions(),
|
|
199
|
+
schema: this.schema
|
|
200
|
+
});
|
|
175
201
|
|
|
176
|
-
|
|
202
|
+
let hydrated: HydratedSyncConfig | undefined;
|
|
177
203
|
return {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
schema: this.schema
|
|
182
|
-
}),
|
|
204
|
+
replicationStreamId: this.replicationStreamId,
|
|
205
|
+
replicationStreamName: this.replicationStreamName,
|
|
206
|
+
syncConfigs: [syncConfig],
|
|
183
207
|
hydrationState: DEFAULT_HYDRATION_STATE,
|
|
184
|
-
|
|
185
|
-
|
|
208
|
+
get hydratedSyncConfig(): HydratedSyncConfig {
|
|
209
|
+
hydrated ??= syncConfig.config.hydrate({
|
|
210
|
+
hydrationState: DEFAULT_HYDRATION_STATE,
|
|
211
|
+
sqlite: nodeSqlite(sqlite)
|
|
212
|
+
});
|
|
213
|
+
return hydrated;
|
|
186
214
|
}
|
|
187
215
|
};
|
|
188
216
|
}
|
|
217
|
+
|
|
218
|
+
async getSyncConfigStatus(): Promise<storage.PersistedSyncConfigStatus | null> {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
189
221
|
}
|
|
190
222
|
|
|
191
223
|
export const validate = routeDefinition({
|
|
@@ -205,10 +237,8 @@ export const validate = routeDefinition({
|
|
|
205
237
|
|
|
206
238
|
const sync_rules = new FakeSyncRulesContentForValidation(apiHandler, schema, {
|
|
207
239
|
// Dummy values
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
active: false,
|
|
211
|
-
last_checkpoint_lsn: '',
|
|
240
|
+
replicationStreamId: 0,
|
|
241
|
+
replicationStreamName: '',
|
|
212
242
|
storageVersion: storage.LEGACY_STORAGE_VERSION,
|
|
213
243
|
sync_rules_content: content,
|
|
214
244
|
compiled_plan: null
|
|
@@ -222,16 +252,12 @@ export const validate = routeDefinition({
|
|
|
222
252
|
});
|
|
223
253
|
}
|
|
224
254
|
|
|
225
|
-
const status = (await api.getSyncRulesStatus(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
check_connection: connectionStatus.connected,
|
|
232
|
-
live_status: false
|
|
233
|
-
}
|
|
234
|
-
))!;
|
|
255
|
+
const status = (await api.getSyncRulesStatus(apiHandler, sync_rules, {
|
|
256
|
+
include_content: false,
|
|
257
|
+
check_connection: connectionStatus.connected,
|
|
258
|
+
live_status: false,
|
|
259
|
+
active: false
|
|
260
|
+
}))!;
|
|
235
261
|
|
|
236
262
|
if (connectionStatus == null) {
|
|
237
263
|
status.errors.push({ level: 'fatal', message: 'No connection configured', ts: new Date().toISOString() });
|
|
@@ -30,7 +30,7 @@ export const writeCheckpoint = routeDefinition({
|
|
|
30
30
|
|
|
31
31
|
logger.info(`Waiting for LSN checkpoint: ${head}`);
|
|
32
32
|
while (Date.now() - start < timeout) {
|
|
33
|
-
const bucketStorage = await service_context.storageEngine.activeBucketStorage.
|
|
33
|
+
const bucketStorage = (await service_context.storageEngine.activeBucketStorage.getActiveSyncConfig())?.storage;
|
|
34
34
|
const cp = await bucketStorage?.getCheckpoint();
|
|
35
35
|
if (cp == null) {
|
|
36
36
|
throw new Error('No sync config available');
|
|
@@ -71,7 +71,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
71
71
|
storageEngine: { activeBucketStorage }
|
|
72
72
|
} = service_context;
|
|
73
73
|
|
|
74
|
-
const bucketStorage = await activeBucketStorage.
|
|
74
|
+
const bucketStorage = (await activeBucketStorage.getActiveSyncConfig())?.storage;
|
|
75
75
|
if (bucketStorage == null) {
|
|
76
76
|
responder.onError(
|
|
77
77
|
new errors.ServiceError({
|
|
@@ -73,7 +73,7 @@ export const deploySyncRules = routeDefinition({
|
|
|
73
73
|
const sync_rules = await storageEngine.activeBucketStorage.updateSyncRules(updateSyncRulesFromConfig(syncConfig));
|
|
74
74
|
|
|
75
75
|
return {
|
|
76
|
-
slot_name: sync_rules.
|
|
76
|
+
slot_name: sync_rules.replicationStreamName
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
});
|
|
@@ -110,8 +110,8 @@ export const currentSyncRules = routeDefinition({
|
|
|
110
110
|
storageEngine: { activeBucketStorage }
|
|
111
111
|
} = service_context;
|
|
112
112
|
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
113
|
+
const active = await activeBucketStorage.getActiveSyncConfig();
|
|
114
|
+
if (!active) {
|
|
115
115
|
throw new errors.ServiceError({
|
|
116
116
|
status: 422,
|
|
117
117
|
code: ErrorCode.PSYNC_S4104,
|
|
@@ -119,15 +119,16 @@ export const currentSyncRules = routeDefinition({
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
const sync_rules = active.content;
|
|
122
123
|
const apiHandler = service_context.routerEngine.getAPI();
|
|
123
124
|
const info = await debugSyncRules(apiHandler, sync_rules.sync_rules_content);
|
|
124
|
-
const next = await activeBucketStorage.
|
|
125
|
+
const next = await activeBucketStorage.getDeployingSyncConfig();
|
|
125
126
|
|
|
126
|
-
const next_info = next ? await debugSyncRules(apiHandler, next.sync_rules_content) : null;
|
|
127
|
+
const next_info = next ? await debugSyncRules(apiHandler, next.content.sync_rules_content) : null;
|
|
127
128
|
|
|
128
129
|
const response = {
|
|
129
130
|
current: {
|
|
130
|
-
slot_name: sync_rules.
|
|
131
|
+
slot_name: sync_rules.replicationStreamName,
|
|
131
132
|
content: sync_rules.sync_rules_content,
|
|
132
133
|
...info
|
|
133
134
|
},
|
|
@@ -135,8 +136,8 @@ export const currentSyncRules = routeDefinition({
|
|
|
135
136
|
next == null
|
|
136
137
|
? null
|
|
137
138
|
: {
|
|
138
|
-
slot_name: next.
|
|
139
|
-
content: next.sync_rules_content,
|
|
139
|
+
slot_name: next.content.replicationStreamName,
|
|
140
|
+
content: next.content.sync_rules_content,
|
|
140
141
|
...next_info
|
|
141
142
|
}
|
|
142
143
|
};
|
|
@@ -156,9 +157,17 @@ export const reprocessSyncRules = routeDefinition({
|
|
|
156
157
|
const {
|
|
157
158
|
storageEngine: { activeBucketStorage }
|
|
158
159
|
} = payload.context.service_context;
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const next = await activeBucketStorage.getDeployingSyncConfig();
|
|
161
|
+
if (next != null) {
|
|
162
|
+
throw new errors.ServiceError({
|
|
163
|
+
status: 409,
|
|
164
|
+
code: ErrorCode.PSYNC_S4106,
|
|
165
|
+
description: 'Busy processing sync config - cannot reprocess'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const active = await activeBucketStorage.getActiveSyncConfig();
|
|
170
|
+
if (active == null) {
|
|
162
171
|
throw new errors.ServiceError({
|
|
163
172
|
status: 422,
|
|
164
173
|
code: ErrorCode.PSYNC_S4104,
|
|
@@ -166,15 +175,16 @@ export const reprocessSyncRules = routeDefinition({
|
|
|
166
175
|
});
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
const sync_rules = active.content;
|
|
169
179
|
const new_rules = await activeBucketStorage.updateSyncRules(
|
|
170
|
-
updateSyncRulesFromYaml(sync_rules.
|
|
180
|
+
updateSyncRulesFromYaml(sync_rules.sync_rules_content, {
|
|
171
181
|
// This sync config already passed validation. But if the rules are not valid anymore due
|
|
172
182
|
// to a service change, we do want to report the error here.
|
|
173
183
|
validate: true
|
|
174
184
|
})
|
|
175
185
|
);
|
|
176
186
|
return {
|
|
177
|
-
slot_name: new_rules.
|
|
187
|
+
slot_name: new_rules.replicationStreamName
|
|
178
188
|
};
|
|
179
189
|
}
|
|
180
190
|
});
|
|
@@ -62,7 +62,7 @@ export const syncStreamed = routeDefinition({
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const bucketStorage = await storageEngine.activeBucketStorage.
|
|
65
|
+
const bucketStorage = (await storageEngine.activeBucketStorage.getActiveSyncConfig())?.storage;
|
|
66
66
|
|
|
67
67
|
if (bucketStorage == null) {
|
|
68
68
|
throw new errors.ServiceError({
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type fastify from 'fastify';
|
|
2
|
+
import * as zlib from 'node:zlib';
|
|
2
3
|
import * as uuid from 'uuid';
|
|
3
4
|
|
|
4
|
-
import { errors, HTTPMethod, logger, RouteNotFound, router
|
|
5
|
+
import { errors, HTTPMethod, logger, RouteNotFound, router } from '@powersync/lib-services-framework';
|
|
5
6
|
import { FastifyReply } from 'fastify';
|
|
6
7
|
import { Context, ContextProvider, RequestEndpoint, RequestEndpointHandlerPayload } from './router.js';
|
|
7
8
|
|
|
@@ -105,18 +106,86 @@ export function registerFastifyNotFoundHandler(app: fastify.FastifyInstance) {
|
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
/** Registers a custom error handler that emits service-error JSON honouring any negotiated `Content-Encoding`. */
|
|
110
|
+
export function registerFastifyErrorHandler(app: fastify.FastifyInstance) {
|
|
111
|
+
app.setErrorHandler<Error>(async (error, _request, reply) => {
|
|
112
|
+
if (reply.raw.headersSent) {
|
|
113
|
+
// Headers already flushed - reply.code/header would throw ERR_HTTP_HEADERS_SENT.
|
|
114
|
+
reply.raw.destroy(error);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { status, data } = fastifyErrorToResponse(error);
|
|
119
|
+
if (status >= 500) {
|
|
120
|
+
logger.error('Request failed', error);
|
|
121
|
+
} else {
|
|
122
|
+
logger.warn(`Request failed: ${error.message}`, {
|
|
123
|
+
status,
|
|
124
|
+
code: data.error.code
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const json = JSON.stringify(data);
|
|
129
|
+
|
|
130
|
+
// Fastify's default handler leaves `content-encoding` intact when it rewrites the body.
|
|
131
|
+
const encoding = reply.getHeader('content-encoding');
|
|
132
|
+
let body: string | Buffer = json;
|
|
133
|
+
if (encoding === 'gzip') {
|
|
134
|
+
body = zlib.gzipSync(json);
|
|
135
|
+
} else if (encoding === 'zstd') {
|
|
136
|
+
body = zlib.zstdCompressSync(json);
|
|
137
|
+
} else {
|
|
138
|
+
reply.removeHeader('content-encoding');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
reply
|
|
142
|
+
.code(status)
|
|
143
|
+
.header('content-type', 'application/json; charset=utf-8')
|
|
144
|
+
.header('content-length', Buffer.byteLength(body))
|
|
145
|
+
.send(body);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type ErrorResponseData = { error: errors.ErrorData };
|
|
150
|
+
|
|
151
|
+
function serviceErrorToResponse(serviceError: errors.ServiceError): router.RouterResponse<ErrorResponseData> {
|
|
109
152
|
return new router.RouterResponse({
|
|
110
|
-
status:
|
|
153
|
+
status: serviceError.errorData.status || 500,
|
|
111
154
|
headers: {
|
|
112
155
|
'Content-Type': 'application/json'
|
|
113
156
|
},
|
|
114
157
|
data: {
|
|
115
|
-
error:
|
|
158
|
+
error: serviceError.errorData
|
|
116
159
|
}
|
|
117
160
|
});
|
|
118
161
|
}
|
|
119
162
|
|
|
163
|
+
function fastifyErrorToResponse(error: any): router.RouterResponse<ErrorResponseData> {
|
|
164
|
+
// Preserve 4xx status from Fastify built-ins (validation, invalid JSON body, etc.) instead of collapsing to 500.
|
|
165
|
+
if (
|
|
166
|
+
typeof error?.statusCode === 'number' &&
|
|
167
|
+
error.statusCode >= 400 &&
|
|
168
|
+
error.statusCode < 500 &&
|
|
169
|
+
typeof error.code === 'string'
|
|
170
|
+
) {
|
|
171
|
+
return new router.RouterResponse({
|
|
172
|
+
status: error.statusCode,
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/json'
|
|
175
|
+
},
|
|
176
|
+
data: {
|
|
177
|
+
error: {
|
|
178
|
+
name: error.name,
|
|
179
|
+
code: error.code,
|
|
180
|
+
status: error.statusCode,
|
|
181
|
+
description: error.message
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return serviceErrorToResponse(errors.asServiceError(error));
|
|
187
|
+
}
|
|
188
|
+
|
|
120
189
|
async function respond(reply: FastifyReply, response: router.RouterResponse) {
|
|
121
190
|
Object.keys(response.headers).forEach((key) => {
|
|
122
191
|
reply.header(key, response.headers[key]);
|
package/src/runner/teardown.ts
CHANGED
|
@@ -40,22 +40,22 @@ async function terminateSyncRules(storageFactory: storage.BucketStorageFactory,
|
|
|
40
40
|
const locks: storage.ReplicationLock[] = [];
|
|
41
41
|
while (Date.now() - start < 120_000) {
|
|
42
42
|
let retry = false;
|
|
43
|
-
const
|
|
43
|
+
const replicatingStreams = await storageFactory.getReplicatingReplicationStreams();
|
|
44
44
|
// Lock all the replicating replication streams
|
|
45
|
-
for (const
|
|
46
|
-
const lock = await
|
|
45
|
+
for (const replicatingStream of replicatingStreams) {
|
|
46
|
+
const lock = await replicatingStream.lock();
|
|
47
47
|
locks.push(lock);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const
|
|
51
|
-
const
|
|
50
|
+
const stoppedStreams = await storageFactory.getStoppedReplicationStreams();
|
|
51
|
+
const combinedStreams = [...replicatingStreams, ...stoppedStreams];
|
|
52
52
|
try {
|
|
53
53
|
// Clean up any module specific configuration for the replication stream
|
|
54
|
-
await moduleManager.tearDown({ syncRules:
|
|
54
|
+
await moduleManager.tearDown({ replicationStreams: combinedStreams, syncRules: combinedStreams });
|
|
55
55
|
|
|
56
56
|
// Mark the replication stream as terminated
|
|
57
|
-
for (let
|
|
58
|
-
const syncRulesStorage = storageFactory.getInstance(
|
|
57
|
+
for (let replicationStream of combinedStreams) {
|
|
58
|
+
const syncRulesStorage = storageFactory.getInstance(replicationStream);
|
|
59
59
|
// The storage will be dropped at the end of the teardown, so we don't need to clear it here
|
|
60
60
|
await syncRulesStorage.terminate({ clearStorage: false });
|
|
61
61
|
}
|
|
@@ -5,6 +5,7 @@ import { InternalOpId } from '../util/utils.js';
|
|
|
5
5
|
import { ReplicationEventPayload } from './ReplicationEventPayload.js';
|
|
6
6
|
import { SourceTable, TableSnapshotStatus } from './SourceTable.js';
|
|
7
7
|
import { BatchedCustomWriteCheckpointOptions } from './storage-index.js';
|
|
8
|
+
import { ResolveTablesOptions, ResolveTablesResult } from './SyncRulesBucketStorage.js';
|
|
8
9
|
|
|
9
10
|
export const DEFAULT_BUCKET_BATCH_COMMIT_OPTIONS: ResolvedBucketBatchCommitOptions = {
|
|
10
11
|
createEmptyCheckpoints: true,
|
|
@@ -22,6 +23,11 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
22
23
|
*/
|
|
23
24
|
last_flushed_op: InternalOpId | null;
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* True for snapshot batches that should skip rows already present in current_data.
|
|
28
|
+
*/
|
|
29
|
+
readonly skipExistingRows: boolean;
|
|
30
|
+
|
|
25
31
|
/**
|
|
26
32
|
* Save an op, and potentially flush.
|
|
27
33
|
*
|
|
@@ -77,11 +83,6 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
77
83
|
*/
|
|
78
84
|
setResumeLsn(lsn: string): Promise<void>;
|
|
79
85
|
|
|
80
|
-
/**
|
|
81
|
-
* Get the last checkpoint LSN, from either commit or keepalive.
|
|
82
|
-
*/
|
|
83
|
-
lastCheckpointLsn: string | null;
|
|
84
|
-
|
|
85
86
|
/**
|
|
86
87
|
* LSN to resume from.
|
|
87
88
|
*
|
|
@@ -91,10 +92,36 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
91
92
|
|
|
92
93
|
markTableSnapshotDone(tables: SourceTable[], no_checkpoint_before_lsn?: string): Promise<SourceTable[]>;
|
|
93
94
|
markTableSnapshotRequired(table: SourceTable): Promise<void>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Mark the full replication snapshot as done without validating individual source table snapshot state.
|
|
98
|
+
*
|
|
99
|
+
* This is primarily intended for storage tests and setup helpers that manually construct storage state.
|
|
100
|
+
* Replicators should use `markSnapshotDone()` instead, because that validates that all known source tables
|
|
101
|
+
* have completed snapshotting before allowing checkpoints to be unblocked.
|
|
102
|
+
*/
|
|
94
103
|
markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void>;
|
|
95
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Mark the full replication snapshot as done after validating that all known source tables have completed snapshotting.
|
|
107
|
+
*
|
|
108
|
+
* Replicators should use this method when completing an initial snapshot. The validation prevents races where
|
|
109
|
+
* new source tables are marked as requiring a snapshot while global snapshot finalization is running.
|
|
110
|
+
*
|
|
111
|
+
* If `throwOnConflict` is false, this will instead return early without throwing if there are source tables that still require snapshotting.
|
|
112
|
+
* Use that only in cases where concurrency is expected, and can automatically retry/continue.
|
|
113
|
+
*/
|
|
114
|
+
markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void>;
|
|
115
|
+
|
|
96
116
|
updateTableProgress(table: SourceTable, progress: Partial<TableSnapshotStatus>): Promise<SourceTable>;
|
|
97
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Get the current status for an existing source table without creating or resolving a replacement table.
|
|
120
|
+
*/
|
|
121
|
+
getSourceTableStatus(table: SourceTable): Promise<SourceTable | null>;
|
|
122
|
+
|
|
123
|
+
resolveTables(options: ResolveTablesOptions): Promise<ResolveTablesResult>;
|
|
124
|
+
|
|
98
125
|
/**
|
|
99
126
|
* Queues the creation of a custom Write Checkpoint. This will be persisted after operations are flushed.
|
|
100
127
|
*/
|
|
@@ -184,8 +211,6 @@ export interface CheckpointResult {
|
|
|
184
211
|
/**
|
|
185
212
|
* True if a checkpoint was actually created by this operation. This can be false even if checkpointBlocked is false,
|
|
186
213
|
* if the checkpoint was empty.
|
|
187
|
-
*
|
|
188
|
-
* This is primarily used for testing.
|
|
189
214
|
*/
|
|
190
215
|
checkpointCreated: boolean;
|
|
191
216
|
}
|