@powersync/service-core 1.20.5 → 1.22.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 +66 -0
- package/dist/api/RouteAPI.d.ts +3 -3
- package/dist/api/diagnostics.d.ts +1 -1
- package/dist/api/diagnostics.js +19 -3
- 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/teardown-action.js +1 -1
- package/dist/entry/commands/teardown-action.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/AbstractModule.d.ts +1 -1
- package/dist/replication/AbstractReplicationJob.js +1 -1
- package/dist/replication/AbstractReplicationJob.js.map +1 -1
- package/dist/replication/AbstractReplicator.d.ts +6 -6
- package/dist/replication/AbstractReplicator.js +21 -21
- 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/routes/configure-fastify.js +3 -1
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/endpoints/admin.js +16 -8
- 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 +8 -8
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.js +2 -2
- 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 +4 -4
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +9 -9
- package/dist/storage/BucketStorage.js +9 -9
- package/dist/storage/BucketStorageBatch.d.ts +29 -0
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/BucketStorageFactory.d.ts +27 -18
- package/dist/storage/BucketStorageFactory.js +13 -12
- package/dist/storage/BucketStorageFactory.js.map +1 -1
- package/dist/storage/PersistedSyncRulesContent.d.ts +6 -4
- package/dist/storage/PersistedSyncRulesContent.js +15 -8
- package/dist/storage/PersistedSyncRulesContent.js.map +1 -1
- package/dist/storage/SourceEntity.d.ts +8 -1
- package/dist/storage/SourceTable.d.ts +32 -11
- package/dist/storage/SourceTable.js +41 -15
- package/dist/storage/SourceTable.js.map +1 -1
- package/dist/storage/StorageVersionConfig.d.ts +1 -1
- package/dist/storage/StorageVersionConfig.js +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +63 -18
- package/dist/storage/SyncRulesBucketStorage.js +14 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/storage/WriteCheckpointAPI.d.ts +6 -6
- package/dist/storage/WriteCheckpointAPI.js +1 -1
- package/dist/storage/bson.d.ts +0 -1
- package/dist/storage/bson.js +0 -4
- package/dist/storage/bson.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +6 -9
- package/dist/sync/BucketChecksumState.js +117 -58
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/sync.d.ts +2 -2
- package/dist/sync/sync.js.map +1 -1
- package/dist/tracing/PerformanceTracer.d.ts +60 -0
- package/dist/tracing/PerformanceTracer.js +105 -0
- package/dist/tracing/PerformanceTracer.js.map +1 -0
- package/dist/tracing/TraceWriter.d.ts +22 -0
- package/dist/tracing/TraceWriter.js +63 -0
- package/dist/tracing/TraceWriter.js.map +1 -0
- package/dist/util/checkpointing.js +1 -1
- package/dist/util/config/compound-config-collector.d.ts +1 -1
- package/dist/util/config/compound-config-collector.js +2 -2
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js +1 -1
- package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
- package/dist/util/env.js +1 -1
- package/dist/util/protocol-types.d.ts +1 -1
- package/dist/util/protocol-types.js +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 +11 -11
- package/src/api/RouteAPI.ts +3 -3
- package/src/api/diagnostics.ts +29 -6
- package/src/auth/RemoteJWKSCollector.ts +3 -1
- package/src/entry/commands/teardown-action.ts +1 -1
- package/src/index.ts +2 -0
- package/src/modules/AbstractModule.ts +1 -1
- package/src/replication/AbstractReplicationJob.ts +1 -1
- package/src/replication/AbstractReplicator.ts +23 -23
- package/src/replication/RelationCache.ts +23 -4
- package/src/routes/configure-fastify.ts +8 -1
- package/src/routes/endpoints/admin.ts +17 -8
- package/src/routes/endpoints/checkpointing.ts +1 -1
- package/src/routes/endpoints/socket-route.ts +1 -1
- package/src/routes/endpoints/sync-rules.ts +8 -8
- package/src/routes/endpoints/sync-stream.ts +2 -2
- package/src/routes/route-register.ts +73 -4
- package/src/runner/teardown.ts +4 -4
- package/src/storage/BucketStorage.ts +9 -9
- package/src/storage/BucketStorageBatch.ts +32 -0
- package/src/storage/BucketStorageFactory.ts +35 -23
- package/src/storage/PersistedSyncRulesContent.ts +20 -12
- package/src/storage/SourceEntity.ts +9 -1
- package/src/storage/SourceTable.ts +56 -22
- package/src/storage/StorageVersionConfig.ts +1 -1
- package/src/storage/SyncRulesBucketStorage.ts +74 -22
- package/src/storage/WriteCheckpointAPI.ts +6 -6
- package/src/storage/bson.ts +0 -5
- package/src/sync/BucketChecksumState.ts +142 -78
- package/src/sync/sync.ts +4 -4
- package/src/tracing/PerformanceTracer.ts +149 -0
- package/src/tracing/TraceWriter.ts +67 -0
- package/src/util/checkpointing.ts +1 -1
- package/src/util/config/compound-config-collector.ts +3 -3
- package/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts +1 -1
- package/src/util/env.ts +1 -1
- package/src/util/protocol-types.ts +1 -1
- package/src/util/util-index.ts +1 -0
- package/src/util/utils.ts +8 -0
- package/test/src/auth.test.ts +120 -1
- package/test/src/diagnostics.test.ts +155 -0
- package/test/src/routes/error-handler.integration.test.ts +275 -0
- 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 +244 -80
- package/test/tsconfig.json +0 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
@@ -35,13 +35,13 @@ export async function teardown(runnerConfig: utils.RunnerConfig) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async function terminateSyncRules(storageFactory: storage.BucketStorageFactory, moduleManager: modules.ModuleManager) {
|
|
38
|
-
logger.info(`Terminating
|
|
38
|
+
logger.info(`Terminating replication stream...`);
|
|
39
39
|
const start = Date.now();
|
|
40
40
|
const locks: storage.ReplicationLock[] = [];
|
|
41
41
|
while (Date.now() - start < 120_000) {
|
|
42
42
|
let retry = false;
|
|
43
43
|
const replicatingSyncRules = await storageFactory.getReplicatingSyncRules();
|
|
44
|
-
// Lock all the replicating
|
|
44
|
+
// Lock all the replicating replication streams
|
|
45
45
|
for (const replicatingSyncRule of replicatingSyncRules) {
|
|
46
46
|
const lock = await replicatingSyncRule.lock();
|
|
47
47
|
locks.push(lock);
|
|
@@ -50,10 +50,10 @@ async function terminateSyncRules(storageFactory: storage.BucketStorageFactory,
|
|
|
50
50
|
const stoppedSyncRules = await storageFactory.getStoppedSyncRules();
|
|
51
51
|
const combinedSyncRules = [...replicatingSyncRules, ...stoppedSyncRules];
|
|
52
52
|
try {
|
|
53
|
-
// Clean up any module specific configuration for the
|
|
53
|
+
// Clean up any module specific configuration for the replication stream
|
|
54
54
|
await moduleManager.tearDown({ syncRules: combinedSyncRules });
|
|
55
55
|
|
|
56
|
-
// Mark the
|
|
56
|
+
// Mark the replication stream as terminated
|
|
57
57
|
for (let syncRules of combinedSyncRules) {
|
|
58
58
|
const syncRulesStorage = storageFactory.getInstance(syncRules);
|
|
59
59
|
// The storage will be dropped at the end of the teardown, so we don't need to clear it here
|
|
@@ -2,36 +2,36 @@ import { ToastableSqliteRow } from '@powersync/service-sync-rules';
|
|
|
2
2
|
|
|
3
3
|
export enum SyncRuleState {
|
|
4
4
|
/**
|
|
5
|
-
* New
|
|
5
|
+
* New replication stream - needs to be processed (initial replication).
|
|
6
6
|
*
|
|
7
|
-
* While multiple
|
|
7
|
+
* While multiple replication streams _can_ be in PROCESSING,
|
|
8
8
|
* it's generally pointless, so we only keep one in that state.
|
|
9
9
|
*/
|
|
10
10
|
PROCESSING = 'PROCESSING',
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Intial processing is done, and can be used for sync.
|
|
14
14
|
*
|
|
15
|
-
* Only one
|
|
15
|
+
* Only one replication stream should be in ACTIVE or ERRORED state.
|
|
16
16
|
*/
|
|
17
17
|
ACTIVE = 'ACTIVE',
|
|
18
18
|
/**
|
|
19
|
-
* This state is used when the
|
|
19
|
+
* This state is used when the replication stream has been replaced,
|
|
20
20
|
* and replication is or should be stopped.
|
|
21
21
|
*/
|
|
22
22
|
STOP = 'STOP',
|
|
23
23
|
/**
|
|
24
|
-
* After
|
|
24
|
+
* After replication stream has been stopped, the data needs to be
|
|
25
25
|
* deleted. Once deleted, the state is TERMINATED.
|
|
26
26
|
*/
|
|
27
27
|
TERMINATED = 'TERMINATED',
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* is still the "active"
|
|
30
|
+
* Replication stream has run into a permanent replication error. It
|
|
31
|
+
* is still the "active" replication stram for syncing to users,
|
|
32
32
|
* but should not replicate anymore.
|
|
33
33
|
*
|
|
34
|
-
* It will transition to STOP when a new
|
|
34
|
+
* It will transition to STOP when a new replication stream is activated.
|
|
35
35
|
*/
|
|
36
36
|
ERRORED = 'ERRORED'
|
|
37
37
|
}
|
|
@@ -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
|
*
|
|
@@ -91,10 +97,36 @@ export interface BucketStorageBatch extends ObserverClient<BucketBatchStorageLis
|
|
|
91
97
|
|
|
92
98
|
markTableSnapshotDone(tables: SourceTable[], no_checkpoint_before_lsn?: string): Promise<SourceTable[]>;
|
|
93
99
|
markTableSnapshotRequired(table: SourceTable): Promise<void>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Mark the full replication snapshot as done without validating individual source table snapshot state.
|
|
103
|
+
*
|
|
104
|
+
* This is primarily intended for storage tests and setup helpers that manually construct storage state.
|
|
105
|
+
* Replicators should use `markSnapshotDone()` instead, because that validates that all known source tables
|
|
106
|
+
* have completed snapshotting before allowing checkpoints to be unblocked.
|
|
107
|
+
*/
|
|
94
108
|
markAllSnapshotDone(no_checkpoint_before_lsn: string): Promise<void>;
|
|
95
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Mark the full replication snapshot as done after validating that all known source tables have completed snapshotting.
|
|
112
|
+
*
|
|
113
|
+
* Replicators should use this method when completing an initial snapshot. The validation prevents races where
|
|
114
|
+
* new source tables are marked as requiring a snapshot while global snapshot finalization is running.
|
|
115
|
+
*
|
|
116
|
+
* If `throwOnConflict` is false, this will instead return early without throwing if there are source tables that still require snapshotting.
|
|
117
|
+
* Use that only in cases where concurrency is expected, and can automatically retry/continue.
|
|
118
|
+
*/
|
|
119
|
+
markSnapshotDone(no_checkpoint_before_lsn: string, options?: { throwOnConflict?: boolean }): Promise<void>;
|
|
120
|
+
|
|
96
121
|
updateTableProgress(table: SourceTable, progress: Partial<TableSnapshotStatus>): Promise<SourceTable>;
|
|
97
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Get the current status for an existing source table without creating or resolving a replacement table.
|
|
125
|
+
*/
|
|
126
|
+
getSourceTableStatus(table: SourceTable): Promise<SourceTable | null>;
|
|
127
|
+
|
|
128
|
+
resolveTables(options: ResolveTablesOptions): Promise<ResolveTablesResult>;
|
|
129
|
+
|
|
98
130
|
/**
|
|
99
131
|
* Queues the creation of a custom Write Checkpoint. This will be persisted after operations are flushed.
|
|
100
132
|
*/
|
|
@@ -17,17 +17,17 @@ import { SyncRulesBucketStorage } from './SyncRulesBucketStorage.js';
|
|
|
17
17
|
/**
|
|
18
18
|
* Represents a configured storage provider.
|
|
19
19
|
*
|
|
20
|
-
* The provider can handle multiple
|
|
21
|
-
* This is to handle replication of a new version of sync
|
|
20
|
+
* The provider can handle multiple replication streams concurrently, each with their own storage.
|
|
21
|
+
* This is to handle replication of a new version of sync config, while the old replication stream is still active.
|
|
22
22
|
*
|
|
23
|
-
* Storage APIs for a specific
|
|
23
|
+
* Storage APIs for a specific replication stream are provided by the `SyncRulesBucketStorage` instances.
|
|
24
24
|
*/
|
|
25
25
|
export abstract class BucketStorageFactory
|
|
26
26
|
extends BaseObserver<BucketStorageFactoryListener>
|
|
27
27
|
implements AsyncDisposable
|
|
28
28
|
{
|
|
29
29
|
/**
|
|
30
|
-
* Update sync
|
|
30
|
+
* Update sync config from configuration, if changed.
|
|
31
31
|
*/
|
|
32
32
|
async configureSyncRules(
|
|
33
33
|
options: UpdateSyncRulesOptions
|
|
@@ -36,42 +36,42 @@ export abstract class BucketStorageFactory
|
|
|
36
36
|
const active = await this.getActiveSyncRulesContent();
|
|
37
37
|
|
|
38
38
|
if (next?.sync_rules_content == options.config.yaml) {
|
|
39
|
-
logger.info('Sync
|
|
39
|
+
logger.info('Sync config unchanged');
|
|
40
40
|
return { updated: false };
|
|
41
41
|
} else if (next == null && active?.sync_rules_content == options.config.yaml) {
|
|
42
|
-
logger.info('Sync
|
|
42
|
+
logger.info('Sync config unchanged');
|
|
43
43
|
return { updated: false };
|
|
44
44
|
} else {
|
|
45
|
-
logger.info('Sync
|
|
45
|
+
logger.info('Sync config updated');
|
|
46
46
|
const persisted_sync_rules = await this.updateSyncRules(options);
|
|
47
47
|
return { updated: true, persisted_sync_rules, lock: persisted_sync_rules.current_lock ?? undefined };
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Get a storage instance to query sync data for specific sync
|
|
52
|
+
* Get a storage instance to query sync data for specific sync config.
|
|
53
53
|
*/
|
|
54
54
|
abstract getInstance(syncRules: PersistedSyncRulesContent, options?: GetIntanceOptions): SyncRulesBucketStorage;
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* Deploy new sync
|
|
57
|
+
* Deploy new sync config.
|
|
58
58
|
*/
|
|
59
59
|
abstract updateSyncRules(options: UpdateSyncRulesOptions): Promise<PersistedSyncRulesContent>;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Indicate that a slot was removed, and we should re-sync by creating
|
|
63
|
-
* a new
|
|
63
|
+
* a new replication stream.
|
|
64
64
|
*
|
|
65
65
|
* This is roughly the same as deploying a new version of the current sync
|
|
66
|
-
*
|
|
67
|
-
* the latest
|
|
66
|
+
* config, but also accounts for cases where the current sync config is not
|
|
67
|
+
* the latest one.
|
|
68
68
|
*
|
|
69
69
|
* Replication should be restarted after this.
|
|
70
70
|
*/
|
|
71
71
|
abstract restartReplication(sync_rules_group_id: number): Promise<void>;
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Get the sync
|
|
74
|
+
* Get the sync config used for querying.
|
|
75
75
|
*/
|
|
76
76
|
async getActiveSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null> {
|
|
77
77
|
const content = await this.getActiveSyncRulesContent();
|
|
@@ -79,12 +79,12 @@ export abstract class BucketStorageFactory
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Get the sync
|
|
82
|
+
* Get the sync config used for querying.
|
|
83
83
|
*/
|
|
84
84
|
abstract getActiveSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* Get the sync
|
|
87
|
+
* Get the sync config that will be active next once done with initial replicatino.
|
|
88
88
|
*/
|
|
89
89
|
async getNextSyncRules(options: ParseSyncRulesOptions): Promise<PersistedSyncRules | null> {
|
|
90
90
|
const content = await this.getNextSyncRulesContent();
|
|
@@ -92,17 +92,17 @@ export abstract class BucketStorageFactory
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
* Get the sync
|
|
95
|
+
* Get the sync config that will be active next once done with initial replicatino.
|
|
96
96
|
*/
|
|
97
97
|
abstract getNextSyncRulesContent(): Promise<PersistedSyncRulesContent | null>;
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* Get all sync
|
|
100
|
+
* Get all sync config currently replicating. Typically this is the "active" and "next" sync config.
|
|
101
101
|
*/
|
|
102
102
|
abstract getReplicatingSyncRules(): Promise<PersistedSyncRulesContent[]>;
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* Get all sync
|
|
105
|
+
* Get all sync config stopped but not terminated yet.
|
|
106
106
|
*/
|
|
107
107
|
abstract getStoppedSyncRules(): Promise<PersistedSyncRulesContent[]>;
|
|
108
108
|
|
|
@@ -112,12 +112,12 @@ export abstract class BucketStorageFactory
|
|
|
112
112
|
abstract getActiveStorage(): Promise<SyncRulesBucketStorage | null>;
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Get storage size of active
|
|
115
|
+
* Get storage size of active replication stream.
|
|
116
116
|
*/
|
|
117
117
|
abstract getStorageMetrics(): Promise<StorageMetrics>;
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* Get the unique identifier for this instance of Powersync
|
|
120
|
+
* Get the unique identifier for this instance of Powersync.
|
|
121
121
|
*/
|
|
122
122
|
abstract getPowerSyncInstanceId(): Promise<string>;
|
|
123
123
|
|
|
@@ -161,9 +161,20 @@ export interface UpdateSyncRulesOptions {
|
|
|
161
161
|
* compiler.
|
|
162
162
|
*/
|
|
163
163
|
plan: SerializedSyncPlan | null;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parsed sync config, primarily to generate a definition mapping.
|
|
167
|
+
* Not persisted, and the defaultSchema used for parsing is not relevant.
|
|
168
|
+
*/
|
|
169
|
+
parsed: SyncConfigWithErrors;
|
|
164
170
|
};
|
|
165
171
|
lock?: boolean;
|
|
166
172
|
storageVersion?: number;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Only relevant if the result is used. This does not affect the persisted config.
|
|
176
|
+
*/
|
|
177
|
+
defaultSchema?: string;
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
export interface SerializedSyncPlan {
|
|
@@ -190,7 +201,7 @@ export function updateSyncRulesFromYaml(
|
|
|
190
201
|
const config = SqlSyncRules.fromYaml(content, {
|
|
191
202
|
// No schema-based validation at this point
|
|
192
203
|
schema: undefined,
|
|
193
|
-
defaultSchema: 'not_applicable', // Not needed for validation
|
|
204
|
+
defaultSchema: options?.defaultSchema ?? 'not_applicable', // Not needed for validation
|
|
194
205
|
throwOnError: options?.validate ?? false
|
|
195
206
|
});
|
|
196
207
|
|
|
@@ -198,10 +209,11 @@ export function updateSyncRulesFromYaml(
|
|
|
198
209
|
}
|
|
199
210
|
|
|
200
211
|
export function updateSyncRulesFromConfig(
|
|
201
|
-
|
|
212
|
+
parsed: SyncConfigWithErrors,
|
|
202
213
|
options?: Omit<UpdateSyncRulesOptions, 'config'>
|
|
203
214
|
): UpdateSyncRulesOptions {
|
|
204
215
|
let plan: SerializedSyncPlan | null = null;
|
|
216
|
+
const { config, errors } = parsed;
|
|
205
217
|
if (config instanceof PrecompiledSyncConfig) {
|
|
206
218
|
const eventDescriptors: Record<string, string[]> = {};
|
|
207
219
|
for (const event of config.eventDescriptors) {
|
|
@@ -216,7 +228,7 @@ export function updateSyncRulesFromConfig(
|
|
|
216
228
|
};
|
|
217
229
|
}
|
|
218
230
|
|
|
219
|
-
return { config: { yaml: config.content, plan }, ...options };
|
|
231
|
+
return { config: { yaml: config.content, plan, parsed }, ...options };
|
|
220
232
|
}
|
|
221
233
|
|
|
222
234
|
export interface GetIntanceOptions {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
|
|
1
|
+
import { logger as defaultLogger, ErrorCode, Logger, ServiceError } from '@powersync/lib-services-framework';
|
|
2
2
|
import {
|
|
3
3
|
CompatibilityContext,
|
|
4
4
|
CompatibilityOption,
|
|
5
5
|
DEFAULT_HYDRATION_STATE,
|
|
6
6
|
deserializeSyncPlan,
|
|
7
7
|
ErrorLocation,
|
|
8
|
-
|
|
8
|
+
HydratedSyncConfig,
|
|
9
9
|
HydrationState,
|
|
10
|
-
|
|
10
|
+
nodeSqlite,
|
|
11
11
|
PrecompiledSyncConfig,
|
|
12
12
|
SqlEventDescriptor,
|
|
13
13
|
SqlSyncRules,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
versionedHydrationState,
|
|
16
16
|
YamlError
|
|
17
17
|
} from '@powersync/service-sync-rules';
|
|
18
|
+
import * as sqlite from 'node:sqlite';
|
|
18
19
|
import { SerializedSyncPlan, UpdateSyncRulesOptions } from './BucketStorageFactory.js';
|
|
19
20
|
import { ReplicationLock } from './ReplicationLock.js';
|
|
20
21
|
import { STORAGE_VERSION_CONFIG, StorageVersionConfig } from './StorageVersionConfig.js';
|
|
@@ -29,7 +30,7 @@ export interface PersistedSyncRulesContentData {
|
|
|
29
30
|
readonly compiled_plan: SerializedSyncPlan | null;
|
|
30
31
|
readonly slot_name: string;
|
|
31
32
|
/**
|
|
32
|
-
* True if this is the "active" copy of the sync
|
|
33
|
+
* True if this is the "active" copy of the sync config.
|
|
33
34
|
*/
|
|
34
35
|
readonly active: boolean;
|
|
35
36
|
readonly storageVersion: number;
|
|
@@ -49,6 +50,7 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
|
|
|
49
50
|
readonly slot_name!: string;
|
|
50
51
|
readonly active!: boolean;
|
|
51
52
|
readonly storageVersion!: number;
|
|
53
|
+
readonly logger: Logger;
|
|
52
54
|
|
|
53
55
|
readonly last_checkpoint_lsn!: string | null;
|
|
54
56
|
|
|
@@ -61,6 +63,7 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
|
|
|
61
63
|
|
|
62
64
|
constructor(data: PersistedSyncRulesContentData) {
|
|
63
65
|
Object.assign(this, data);
|
|
66
|
+
this.logger = defaultLogger.child({ prefix: `[${this.slot_name}] ` });
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
/**
|
|
@@ -73,7 +76,7 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
|
|
|
73
76
|
if (storageConfig == null) {
|
|
74
77
|
throw new ServiceError(
|
|
75
78
|
ErrorCode.PSYNC_S1005,
|
|
76
|
-
`Unsupported storage version ${this.storageVersion} for
|
|
79
|
+
`Unsupported storage version ${this.storageVersion} for replication stream ${this.id}`
|
|
77
80
|
);
|
|
78
81
|
}
|
|
79
82
|
return storageConfig;
|
|
@@ -99,10 +102,13 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
|
|
|
99
102
|
|
|
100
103
|
const precompiled = new PrecompiledSyncConfig(plan, compatibility, eventDefinitions, {
|
|
101
104
|
defaultSchema: options.defaultSchema,
|
|
102
|
-
engine: javaScriptExpressionEngine(compatibility),
|
|
103
105
|
sourceText: this.sync_rules_content
|
|
104
106
|
});
|
|
105
107
|
|
|
108
|
+
// Note: If the original content did not define a storage version, this will still set the storage version.
|
|
109
|
+
// This means asUpdateOptions will not change the storage version, even if the default changes.
|
|
110
|
+
precompiled.storageVersion = this.storageVersion;
|
|
111
|
+
|
|
106
112
|
const errors: YamlError[] = [];
|
|
107
113
|
if (this.compiled_plan.errors) {
|
|
108
114
|
for (const error of this.compiled_plan.errors) {
|
|
@@ -135,17 +141,19 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
|
|
|
135
141
|
return {
|
|
136
142
|
id: this.id,
|
|
137
143
|
slot_name: this.slot_name,
|
|
138
|
-
|
|
144
|
+
syncConfigWithErrors: config,
|
|
139
145
|
hydrationState,
|
|
140
|
-
|
|
141
|
-
return config.config.hydrate({ hydrationState });
|
|
146
|
+
hydratedSyncConfig: () => {
|
|
147
|
+
return config.config.hydrate({ hydrationState, sqlite: nodeSqlite(sqlite) });
|
|
142
148
|
}
|
|
143
149
|
};
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
asUpdateOptions(options?: Omit<UpdateSyncRulesOptions, 'config'>): UpdateSyncRulesOptions {
|
|
153
|
+
// defaultSchema is not relevant for the parsed version here
|
|
154
|
+
const parsed = this.parsed({ defaultSchema: 'not_applicable' });
|
|
147
155
|
return {
|
|
148
|
-
config: { yaml: this.sync_rules_content, plan: this.compiled_plan },
|
|
156
|
+
config: { yaml: this.sync_rules_content, plan: this.compiled_plan, parsed: parsed.syncConfigWithErrors },
|
|
149
157
|
...options
|
|
150
158
|
};
|
|
151
159
|
}
|
|
@@ -155,12 +163,12 @@ export abstract class PersistedSyncRulesContent implements PersistedSyncRulesCon
|
|
|
155
163
|
|
|
156
164
|
export interface PersistedSyncRules {
|
|
157
165
|
readonly id: number;
|
|
158
|
-
readonly
|
|
166
|
+
readonly syncConfigWithErrors: SyncConfigWithErrors;
|
|
159
167
|
readonly slot_name: string;
|
|
160
168
|
/**
|
|
161
169
|
* For testing only.
|
|
162
170
|
*/
|
|
163
171
|
readonly hydrationState: HydrationState;
|
|
164
172
|
|
|
165
|
-
|
|
173
|
+
hydratedSyncConfig(): HydratedSyncConfig;
|
|
166
174
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SourceTableRef } from '@powersync/service-sync-rules';
|
|
2
|
+
|
|
1
3
|
export interface ColumnDescriptor {
|
|
2
4
|
name: string;
|
|
3
5
|
/**
|
|
@@ -10,7 +12,7 @@ export interface ColumnDescriptor {
|
|
|
10
12
|
typeId?: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export interface SourceEntityDescriptor {
|
|
15
|
+
export interface SourceEntityDescriptor extends SourceTableRef {
|
|
14
16
|
/**
|
|
15
17
|
* The internal id of the source entity structure in the database.
|
|
16
18
|
* If undefined, the schema and name are used as the identifier.
|
|
@@ -23,4 +25,10 @@ export interface SourceEntityDescriptor {
|
|
|
23
25
|
* The columns that are used to uniquely identify a record in the source entity.
|
|
24
26
|
*/
|
|
25
27
|
replicaIdColumns: ColumnDescriptor[];
|
|
28
|
+
/**
|
|
29
|
+
* Whether the source always sends complete row data with each operation (e.g. Postgres REPLICA
|
|
30
|
+
* IDENTITY FULL). When true, no current_data copy is needed. Undefined means the source does not
|
|
31
|
+
* report this, in which case we default to keeping a copy.
|
|
32
|
+
*/
|
|
33
|
+
sendsCompleteRows?: boolean;
|
|
26
34
|
}
|