@powersync/service-core 1.11.2 → 1.12.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 +56 -0
- package/dist/auth/CachedKeyCollector.js +2 -7
- package/dist/auth/CachedKeyCollector.js.map +1 -1
- package/dist/auth/CompoundKeyCollector.js.map +1 -1
- package/dist/auth/KeyCollector.d.ts +2 -2
- package/dist/auth/KeyStore.js +32 -14
- package/dist/auth/KeyStore.js.map +1 -1
- package/dist/auth/RemoteJWKSCollector.d.ts +1 -0
- package/dist/auth/RemoteJWKSCollector.js +39 -16
- package/dist/auth/RemoteJWKSCollector.js.map +1 -1
- package/dist/auth/auth-index.d.ts +1 -0
- package/dist/auth/auth-index.js +1 -0
- package/dist/auth/auth-index.js.map +1 -1
- package/dist/auth/utils.d.ts +6 -0
- package/dist/auth/utils.js +97 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/entry/commands/compact-action.js +4 -1
- package/dist/entry/commands/compact-action.js.map +1 -1
- package/dist/entry/commands/migrate-action.js +4 -1
- package/dist/entry/commands/migrate-action.js.map +1 -1
- package/dist/entry/commands/test-connection-action.js +4 -1
- package/dist/entry/commands/test-connection-action.js.map +1 -1
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js +1 -1
- package/dist/metrics/open-telemetry/OpenTelemetryMetricsFactory.js.map +1 -1
- package/dist/routes/RouterEngine.d.ts +2 -0
- package/dist/routes/RouterEngine.js +15 -10
- package/dist/routes/RouterEngine.js.map +1 -1
- package/dist/routes/auth.d.ts +5 -16
- package/dist/routes/auth.js +6 -4
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/configure-fastify.d.ts +3 -21
- package/dist/routes/configure-fastify.js +3 -6
- package/dist/routes/configure-fastify.js.map +1 -1
- package/dist/routes/configure-rsocket.js +28 -14
- package/dist/routes/configure-rsocket.js.map +1 -1
- package/dist/routes/endpoints/admin.js.map +1 -1
- package/dist/routes/endpoints/checkpointing.d.ts +4 -28
- package/dist/routes/endpoints/checkpointing.js.map +1 -1
- package/dist/routes/endpoints/route-endpoints-index.d.ts +1 -0
- package/dist/routes/endpoints/route-endpoints-index.js +1 -0
- package/dist/routes/endpoints/route-endpoints-index.js.map +1 -1
- package/dist/routes/endpoints/socket-route.js +22 -8
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.d.ts +2 -14
- package/dist/routes/endpoints/sync-stream.js +28 -9
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/routes/route-register.js +10 -6
- package/dist/routes/route-register.js.map +1 -1
- package/dist/routes/router.d.ts +8 -7
- package/dist/routes/router.js.map +1 -1
- package/dist/runner/teardown.js +4 -1
- package/dist/runner/teardown.js.map +1 -1
- package/dist/storage/SyncRulesBucketStorage.d.ts +3 -3
- package/dist/storage/storage-metrics.js +1 -1
- package/dist/storage/storage-metrics.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +40 -18
- package/dist/sync/BucketChecksumState.js +120 -72
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +22 -1
- package/dist/sync/RequestTracker.js +51 -2
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/sync.d.ts +3 -5
- package/dist/sync/sync.js +48 -33
- package/dist/sync/sync.js.map +1 -1
- package/dist/system/ServiceContext.d.ts +19 -4
- package/dist/system/ServiceContext.js +20 -8
- package/dist/system/ServiceContext.js.map +1 -1
- package/dist/util/config/collectors/config-collector.js +4 -33
- package/dist/util/config/collectors/config-collector.js.map +1 -1
- package/dist/util/config/collectors/impl/yaml-env.d.ts +7 -0
- package/dist/util/config/collectors/impl/yaml-env.js +59 -0
- package/dist/util/config/collectors/impl/yaml-env.js.map +1 -0
- package/dist/util/config/compound-config-collector.js +18 -1
- package/dist/util/config/compound-config-collector.js.map +1 -1
- package/dist/util/config/types.d.ts +11 -0
- package/dist/util/protocol-types.d.ts +2 -2
- package/package.json +6 -7
- package/src/auth/CachedKeyCollector.ts +4 -6
- package/src/auth/CompoundKeyCollector.ts +2 -1
- package/src/auth/KeyCollector.ts +2 -2
- package/src/auth/KeyStore.ts +45 -20
- package/src/auth/RemoteJWKSCollector.ts +39 -16
- package/src/auth/auth-index.ts +1 -0
- package/src/auth/utils.ts +102 -0
- package/src/entry/commands/compact-action.ts +4 -1
- package/src/entry/commands/migrate-action.ts +4 -1
- package/src/entry/commands/test-connection-action.ts +4 -1
- package/src/metrics/open-telemetry/OpenTelemetryMetricsFactory.ts +1 -1
- package/src/routes/RouterEngine.ts +21 -11
- package/src/routes/auth.ts +7 -6
- package/src/routes/configure-fastify.ts +6 -8
- package/src/routes/configure-rsocket.ts +33 -18
- package/src/routes/endpoints/admin.ts +5 -5
- package/src/routes/endpoints/checkpointing.ts +2 -2
- package/src/routes/endpoints/route-endpoints-index.ts +1 -0
- package/src/routes/endpoints/socket-route.ts +27 -11
- package/src/routes/endpoints/sync-rules.ts +4 -4
- package/src/routes/endpoints/sync-stream.ts +34 -11
- package/src/routes/route-register.ts +10 -7
- package/src/routes/router.ts +11 -4
- package/src/runner/teardown.ts +5 -1
- package/src/storage/SyncRulesBucketStorage.ts +3 -3
- package/src/storage/storage-metrics.ts +1 -1
- package/src/sync/BucketChecksumState.ts +160 -75
- package/src/sync/RequestTracker.ts +70 -3
- package/src/sync/sync.ts +70 -47
- package/src/system/ServiceContext.ts +31 -12
- package/src/util/config/collectors/config-collector.ts +4 -40
- package/src/util/config/collectors/impl/yaml-env.ts +67 -0
- package/src/util/config/compound-config-collector.ts +22 -5
- package/src/util/config/types.ts +13 -0
- package/src/util/protocol-types.ts +2 -2
- package/test/src/auth.test.ts +29 -11
- package/test/src/config.test.ts +72 -0
- package/test/src/sync/BucketChecksumState.test.ts +32 -18
- package/tsconfig.tsbuildinfo +1 -1
package/src/sync/sync.ts
CHANGED
|
@@ -7,12 +7,12 @@ import * as auth from '../auth/auth-index.js';
|
|
|
7
7
|
import * as storage from '../storage/storage-index.js';
|
|
8
8
|
import * as util from '../util/util-index.js';
|
|
9
9
|
|
|
10
|
-
import { logger } from '@powersync/lib-services-framework';
|
|
11
|
-
import { BucketChecksumState } from './BucketChecksumState.js';
|
|
10
|
+
import { Logger, logger as defaultLogger } from '@powersync/lib-services-framework';
|
|
11
|
+
import { BucketChecksumState, CheckpointLine } from './BucketChecksumState.js';
|
|
12
12
|
import { mergeAsyncIterables } from '../streams/streams-index.js';
|
|
13
13
|
import { acquireSemaphoreAbortable, settledPromise, tokenStream, TokenStreamOptions } from './util.js';
|
|
14
14
|
import { SyncContext } from './SyncContext.js';
|
|
15
|
-
import { RequestTracker } from './RequestTracker.js';
|
|
15
|
+
import { OperationsSentStats, RequestTracker, statsForBatch } from './RequestTracker.js';
|
|
16
16
|
|
|
17
17
|
export interface SyncStreamParameters {
|
|
18
18
|
syncContext: SyncContext;
|
|
@@ -21,6 +21,7 @@ export interface SyncStreamParameters {
|
|
|
21
21
|
params: util.StreamingSyncRequest;
|
|
22
22
|
syncParams: RequestParameters;
|
|
23
23
|
token: auth.JwtPayload;
|
|
24
|
+
logger?: Logger;
|
|
24
25
|
/**
|
|
25
26
|
* If this signal is aborted, the stream response ends as soon as possible, without error.
|
|
26
27
|
*/
|
|
@@ -35,6 +36,8 @@ export async function* streamResponse(
|
|
|
35
36
|
): AsyncIterable<util.StreamingSyncLine | string | null> {
|
|
36
37
|
const { syncContext, bucketStorage, syncRules, params, syncParams, token, tokenStreamOptions, tracker, signal } =
|
|
37
38
|
options;
|
|
39
|
+
const logger = options.logger ?? defaultLogger;
|
|
40
|
+
|
|
38
41
|
// We also need to be able to abort, so we create our own controller.
|
|
39
42
|
const controller = new AbortController();
|
|
40
43
|
if (signal) {
|
|
@@ -57,7 +60,8 @@ export async function* streamResponse(
|
|
|
57
60
|
params,
|
|
58
61
|
syncParams,
|
|
59
62
|
tracker,
|
|
60
|
-
controller.signal
|
|
63
|
+
controller.signal,
|
|
64
|
+
logger
|
|
61
65
|
);
|
|
62
66
|
// Merge the two streams, and abort as soon as one of the streams end.
|
|
63
67
|
const merged = mergeAsyncIterables([stream, ki], controller.signal);
|
|
@@ -77,11 +81,6 @@ export async function* streamResponse(
|
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
|
|
80
|
-
export type BucketSyncState = {
|
|
81
|
-
description?: BucketDescription; // Undefined if the bucket has not yet been resolved by us.
|
|
82
|
-
start_op_id: util.InternalOpId;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
84
|
async function* streamResponseInner(
|
|
86
85
|
syncContext: SyncContext,
|
|
87
86
|
bucketStorage: storage.SyncRulesBucketStorage,
|
|
@@ -89,7 +88,8 @@ async function* streamResponseInner(
|
|
|
89
88
|
params: util.StreamingSyncRequest,
|
|
90
89
|
syncParams: RequestParameters,
|
|
91
90
|
tracker: RequestTracker,
|
|
92
|
-
signal: AbortSignal
|
|
91
|
+
signal: AbortSignal,
|
|
92
|
+
logger: Logger
|
|
93
93
|
): AsyncGenerator<util.StreamingSyncLine | string | null> {
|
|
94
94
|
const { raw_data, binary_data } = params;
|
|
95
95
|
|
|
@@ -103,7 +103,8 @@ async function* streamResponseInner(
|
|
|
103
103
|
initialBucketPositions: params.buckets?.map((bucket) => ({
|
|
104
104
|
name: bucket.name,
|
|
105
105
|
after: BigInt(bucket.after)
|
|
106
|
-
}))
|
|
106
|
+
})),
|
|
107
|
+
logger: logger
|
|
107
108
|
});
|
|
108
109
|
const stream = bucketStorage.watchCheckpointChanges({
|
|
109
110
|
user_id: checkpointUserId,
|
|
@@ -111,16 +112,29 @@ async function* streamResponseInner(
|
|
|
111
112
|
});
|
|
112
113
|
const newCheckpoints = stream[Symbol.asyncIterator]();
|
|
113
114
|
|
|
115
|
+
type CheckpointAndLine = {
|
|
116
|
+
checkpoint: bigint;
|
|
117
|
+
line: CheckpointLine | null;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
async function waitForNewCheckpointLine(): Promise<IteratorResult<CheckpointAndLine>> {
|
|
121
|
+
const next = await newCheckpoints.next();
|
|
122
|
+
if (next.done) {
|
|
123
|
+
return { done: true, value: undefined };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const line = await checksumState.buildNextCheckpointLine(next.value);
|
|
127
|
+
return { done: false, value: { checkpoint: next.value.base.checkpoint, line } };
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
try {
|
|
115
|
-
let nextCheckpointPromise:
|
|
116
|
-
| Promise<PromiseSettledResult<IteratorResult<storage.StorageCheckpointUpdate>>>
|
|
117
|
-
| undefined;
|
|
131
|
+
let nextCheckpointPromise: Promise<PromiseSettledResult<IteratorResult<CheckpointAndLine>>> | undefined;
|
|
118
132
|
|
|
119
133
|
do {
|
|
120
134
|
if (!nextCheckpointPromise) {
|
|
121
135
|
// Wrap in a settledPromise, so that abort errors after the parent stopped iterating
|
|
122
136
|
// does not result in uncaught errors.
|
|
123
|
-
nextCheckpointPromise = settledPromise(
|
|
137
|
+
nextCheckpointPromise = settledPromise(waitForNewCheckpointLine());
|
|
124
138
|
}
|
|
125
139
|
const next = await nextCheckpointPromise;
|
|
126
140
|
nextCheckpointPromise = undefined;
|
|
@@ -130,7 +144,7 @@ async function* streamResponseInner(
|
|
|
130
144
|
if (next.value.done) {
|
|
131
145
|
break;
|
|
132
146
|
}
|
|
133
|
-
const line =
|
|
147
|
+
const line = next.value.value.line;
|
|
134
148
|
if (line == null) {
|
|
135
149
|
// No update to sync
|
|
136
150
|
continue;
|
|
@@ -138,7 +152,10 @@ async function* streamResponseInner(
|
|
|
138
152
|
|
|
139
153
|
const { checkpointLine, bucketsToFetch } = line;
|
|
140
154
|
|
|
155
|
+
// Since yielding can block, we update the state just before yielding the line.
|
|
156
|
+
line.advance();
|
|
141
157
|
yield checkpointLine;
|
|
158
|
+
|
|
142
159
|
// Start syncing data for buckets up to the checkpoint. As soon as we have completed at least one priority and
|
|
143
160
|
// at least 1000 operations, we also start listening for new checkpoints concurrently. When a new checkpoint comes
|
|
144
161
|
// in while we're still busy syncing data for lower priorities, interrupt the current operation and start syncing
|
|
@@ -164,30 +181,32 @@ async function* streamResponseInner(
|
|
|
164
181
|
function maybeRaceForNewCheckpoint() {
|
|
165
182
|
if (syncedOperations >= 1000 && nextCheckpointPromise === undefined) {
|
|
166
183
|
nextCheckpointPromise = (async () => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
184
|
+
while (true) {
|
|
185
|
+
const next = await settledPromise(waitForNewCheckpointLine());
|
|
186
|
+
if (next.status == 'rejected') {
|
|
187
|
+
abortCheckpointController.abort();
|
|
188
|
+
} else if (!next.value.done) {
|
|
189
|
+
if (next.value.value.line == null) {
|
|
190
|
+
// There's a new checkpoint that doesn't affect this sync stream. Keep listening, but don't
|
|
191
|
+
// interrupt this batch.
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// A new sync line can be emitted. Stop running the bucketDataInBatches() iterations, making the
|
|
196
|
+
// main flow reach the new checkpoint.
|
|
197
|
+
abortCheckpointController.abort();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return next;
|
|
173
201
|
}
|
|
174
|
-
|
|
175
|
-
return next;
|
|
176
202
|
})();
|
|
177
203
|
}
|
|
178
204
|
}
|
|
179
205
|
|
|
180
|
-
function markOperationsSent(
|
|
181
|
-
syncedOperations +=
|
|
182
|
-
tracker.addOperationsSynced(
|
|
183
|
-
|
|
184
|
-
// There is a bug with interrupting checkpoints where:
|
|
185
|
-
// 1. User is in the middle of syncing a large batch of data (for example initial sync).
|
|
186
|
-
// 2. A new checkpoint comes in, which interrupts the batch.
|
|
187
|
-
// 3. However, the new checkpoint does not contain any new data for this connection, so nothing further is synced.
|
|
188
|
-
// This then causes the client to wait indefinitely for the remaining data or checkpoint_complete message. That is
|
|
189
|
-
// only resolved when a new checkpoint comes in that does have data for this connection, or the connection is restarted.
|
|
190
|
-
// maybeRaceForNewCheckpoint();
|
|
206
|
+
function markOperationsSent(stats: OperationsSentStats) {
|
|
207
|
+
syncedOperations += stats.total;
|
|
208
|
+
tracker.addOperationsSynced(stats);
|
|
209
|
+
maybeRaceForNewCheckpoint();
|
|
191
210
|
}
|
|
192
211
|
|
|
193
212
|
// This incrementally updates dataBuckets with each individual bucket position.
|
|
@@ -201,9 +220,9 @@ async function* streamResponseInner(
|
|
|
201
220
|
yield* bucketDataInBatches({
|
|
202
221
|
syncContext: syncContext,
|
|
203
222
|
bucketStorage: bucketStorage,
|
|
204
|
-
checkpoint: next.value.value.
|
|
223
|
+
checkpoint: next.value.value.checkpoint,
|
|
205
224
|
bucketsToFetch: buckets,
|
|
206
|
-
|
|
225
|
+
checkpointLine: line,
|
|
207
226
|
raw_data,
|
|
208
227
|
binary_data,
|
|
209
228
|
onRowsSent: markOperationsSent,
|
|
@@ -212,7 +231,8 @@ async function* streamResponseInner(
|
|
|
212
231
|
user_id: syncParams.user_id,
|
|
213
232
|
// Passing null here will emit a full sync complete message at the end. If we pass a priority, we'll emit a partial
|
|
214
233
|
// sync complete message instead.
|
|
215
|
-
forPriority: !isLast ? priority : null
|
|
234
|
+
forPriority: !isLast ? priority : null,
|
|
235
|
+
logger
|
|
216
236
|
});
|
|
217
237
|
}
|
|
218
238
|
|
|
@@ -229,9 +249,10 @@ interface BucketDataRequest {
|
|
|
229
249
|
syncContext: SyncContext;
|
|
230
250
|
bucketStorage: storage.SyncRulesBucketStorage;
|
|
231
251
|
checkpoint: util.InternalOpId;
|
|
252
|
+
/** Contains current bucket state. Modified by the request as data is sent. */
|
|
253
|
+
checkpointLine: CheckpointLine;
|
|
254
|
+
/** Subset of checkpointLine.bucketsToFetch, filtered by priority. */
|
|
232
255
|
bucketsToFetch: BucketDescription[];
|
|
233
|
-
/** Contains current bucket state. Modified by the request as data is sent. */
|
|
234
|
-
checksumState: BucketChecksumState;
|
|
235
256
|
raw_data: boolean | undefined;
|
|
236
257
|
binary_data: boolean | undefined;
|
|
237
258
|
/** Signals that the connection was aborted and that streaming should stop ASAP. */
|
|
@@ -243,7 +264,8 @@ interface BucketDataRequest {
|
|
|
243
264
|
abort_batch: AbortSignal;
|
|
244
265
|
user_id?: string;
|
|
245
266
|
forPriority: BucketPriority | null;
|
|
246
|
-
onRowsSent: (
|
|
267
|
+
onRowsSent: (stats: OperationsSentStats) => void;
|
|
268
|
+
logger: Logger;
|
|
247
269
|
}
|
|
248
270
|
|
|
249
271
|
async function* bucketDataInBatches(request: BucketDataRequest) {
|
|
@@ -292,12 +314,13 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
|
|
|
292
314
|
bucketStorage: storage,
|
|
293
315
|
checkpoint,
|
|
294
316
|
bucketsToFetch,
|
|
295
|
-
|
|
317
|
+
checkpointLine,
|
|
296
318
|
raw_data,
|
|
297
319
|
binary_data,
|
|
298
320
|
abort_connection,
|
|
299
321
|
abort_batch,
|
|
300
|
-
onRowsSent
|
|
322
|
+
onRowsSent,
|
|
323
|
+
logger
|
|
301
324
|
} = request;
|
|
302
325
|
|
|
303
326
|
let checkpointInvalidated = false;
|
|
@@ -322,12 +345,12 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
|
|
|
322
345
|
}
|
|
323
346
|
// Optimization: Only fetch buckets for which the checksums have changed since the last checkpoint
|
|
324
347
|
// For the first batch, this will be all buckets.
|
|
325
|
-
const filteredBuckets =
|
|
348
|
+
const filteredBuckets = checkpointLine.getFilteredBucketPositions(bucketsToFetch);
|
|
326
349
|
const dataBatches = storage.getBucketDataBatch(checkpoint, filteredBuckets);
|
|
327
350
|
|
|
328
351
|
let has_more = false;
|
|
329
352
|
|
|
330
|
-
for await (let {
|
|
353
|
+
for await (let { chunkData: r, targetOp } of dataBatches) {
|
|
331
354
|
// Abort in current batch if the connection is closed
|
|
332
355
|
if (abort_connection.aborted) {
|
|
333
356
|
return;
|
|
@@ -369,9 +392,9 @@ async function* bucketDataBatch(request: BucketDataRequest): AsyncGenerator<Buck
|
|
|
369
392
|
// iterator memory in case if large data sent.
|
|
370
393
|
yield { data: null, done: false };
|
|
371
394
|
}
|
|
372
|
-
onRowsSent(r
|
|
395
|
+
onRowsSent(statsForBatch(r));
|
|
373
396
|
|
|
374
|
-
|
|
397
|
+
checkpointLine.updateBucketPosition({ bucket: r.bucket, nextAfter: BigInt(r.next_after), hasMore: r.has_more });
|
|
375
398
|
|
|
376
399
|
// Check if syncing bucket data is supposed to stop before fetching more data
|
|
377
400
|
// from storage.
|
|
@@ -6,18 +6,34 @@ import { PowerSyncMigrationManager } from '../migrations/PowerSyncMigrationManag
|
|
|
6
6
|
import * as replication from '../replication/replication-index.js';
|
|
7
7
|
import * as routes from '../routes/routes-index.js';
|
|
8
8
|
import * as storage from '../storage/storage-index.js';
|
|
9
|
-
import * as utils from '../util/util-index.js';
|
|
10
9
|
import { SyncContext } from '../sync/SyncContext.js';
|
|
10
|
+
import * as utils from '../util/util-index.js';
|
|
11
11
|
|
|
12
12
|
export interface ServiceContext {
|
|
13
13
|
configuration: utils.ResolvedPowerSyncConfig;
|
|
14
14
|
lifeCycleEngine: LifeCycledSystem;
|
|
15
15
|
metricsEngine: metrics.MetricsEngine;
|
|
16
16
|
replicationEngine: replication.ReplicationEngine | null;
|
|
17
|
-
routerEngine: routes.RouterEngine
|
|
17
|
+
routerEngine: routes.RouterEngine;
|
|
18
18
|
storageEngine: storage.StorageEngine;
|
|
19
19
|
migrations: PowerSyncMigrationManager;
|
|
20
20
|
syncContext: SyncContext;
|
|
21
|
+
serviceMode: ServiceContextMode;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export enum ServiceContextMode {
|
|
25
|
+
API = utils.ServiceRunner.API,
|
|
26
|
+
SYNC = utils.ServiceRunner.SYNC,
|
|
27
|
+
UNIFIED = utils.ServiceRunner.UNIFIED,
|
|
28
|
+
COMPACT = 'compact',
|
|
29
|
+
MIGRATION = 'migration',
|
|
30
|
+
TEARDOWN = 'teardown',
|
|
31
|
+
TEST_CONNECTION = 'test-connection'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ServiceContextOptions {
|
|
35
|
+
serviceMode: ServiceContextMode;
|
|
36
|
+
configuration: utils.ResolvedPowerSyncConfig;
|
|
21
37
|
}
|
|
22
38
|
|
|
23
39
|
/**
|
|
@@ -26,11 +42,18 @@ export interface ServiceContext {
|
|
|
26
42
|
* This controls registering, initializing and the lifecycle of various services.
|
|
27
43
|
*/
|
|
28
44
|
export class ServiceContextContainer implements ServiceContext {
|
|
45
|
+
configuration: utils.ResolvedPowerSyncConfig;
|
|
29
46
|
lifeCycleEngine: LifeCycledSystem;
|
|
30
47
|
storageEngine: storage.StorageEngine;
|
|
31
48
|
syncContext: SyncContext;
|
|
49
|
+
routerEngine: routes.RouterEngine;
|
|
50
|
+
serviceMode: ServiceContextMode;
|
|
51
|
+
|
|
52
|
+
constructor(options: ServiceContextOptions) {
|
|
53
|
+
this.serviceMode = options.serviceMode;
|
|
54
|
+
const { configuration } = options;
|
|
55
|
+
this.configuration = configuration;
|
|
32
56
|
|
|
33
|
-
constructor(public configuration: utils.ResolvedPowerSyncConfig) {
|
|
34
57
|
this.lifeCycleEngine = new LifeCycledSystem();
|
|
35
58
|
|
|
36
59
|
this.storageEngine = new storage.StorageEngine({
|
|
@@ -42,6 +65,11 @@ export class ServiceContextContainer implements ServiceContext {
|
|
|
42
65
|
stop: (storageEngine) => storageEngine.shutDown()
|
|
43
66
|
});
|
|
44
67
|
|
|
68
|
+
this.routerEngine = new routes.RouterEngine();
|
|
69
|
+
this.lifeCycleEngine.withLifecycle(this.routerEngine, {
|
|
70
|
+
stop: (routerEngine) => routerEngine.shutDown()
|
|
71
|
+
});
|
|
72
|
+
|
|
45
73
|
this.syncContext = new SyncContext({
|
|
46
74
|
maxDataFetchConcurrency: configuration.api_parameters.max_data_fetch_concurrency,
|
|
47
75
|
maxBuckets: configuration.api_parameters.max_buckets_per_connection,
|
|
@@ -55,21 +83,12 @@ export class ServiceContextContainer implements ServiceContext {
|
|
|
55
83
|
// Migrations should be executed before the system starts
|
|
56
84
|
start: () => migrationManager[Symbol.asyncDispose]()
|
|
57
85
|
});
|
|
58
|
-
|
|
59
|
-
this.lifeCycleEngine.withLifecycle(this.storageEngine, {
|
|
60
|
-
start: (storageEngine) => storageEngine.start(),
|
|
61
|
-
stop: (storageEngine) => storageEngine.shutDown()
|
|
62
|
-
});
|
|
63
86
|
}
|
|
64
87
|
|
|
65
88
|
get replicationEngine(): replication.ReplicationEngine | null {
|
|
66
89
|
return container.getOptional(replication.ReplicationEngine);
|
|
67
90
|
}
|
|
68
91
|
|
|
69
|
-
get routerEngine(): routes.RouterEngine | null {
|
|
70
|
-
return container.getOptional(routes.RouterEngine);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
92
|
get metricsEngine(): metrics.MetricsEngine {
|
|
74
93
|
return container.getImplementation(metrics.MetricsEngine);
|
|
75
94
|
}
|
|
@@ -1,34 +1,18 @@
|
|
|
1
|
-
import * as t from 'ts-codec';
|
|
2
1
|
import * as yaml from 'yaml';
|
|
3
2
|
|
|
4
|
-
import { configFile } from '@powersync/service-types';
|
|
5
3
|
import { schema } from '@powersync/lib-services-framework';
|
|
4
|
+
import { configFile } from '@powersync/service-types';
|
|
6
5
|
|
|
7
6
|
import { RunnerConfig } from '../types.js';
|
|
7
|
+
import { YamlEnvTag } from './impl/yaml-env.js';
|
|
8
8
|
|
|
9
9
|
export enum ConfigFileFormat {
|
|
10
10
|
YAML = 'yaml',
|
|
11
11
|
JSON = 'json'
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Environment variables can be substituted into the YAML config
|
|
16
|
-
* when parsing if the environment variable name starts with this prefix.
|
|
17
|
-
* Attempting to substitute any other environment variable will throw an exception.
|
|
18
|
-
*
|
|
19
|
-
* Example of substitution:
|
|
20
|
-
* storage:
|
|
21
|
-
* type: mongodb
|
|
22
|
-
* uri: !env PS_MONGO_URI
|
|
23
|
-
*/
|
|
24
|
-
const YAML_ENV_PREFIX = 'PS_';
|
|
25
|
-
|
|
26
14
|
// ts-codec itself doesn't give great validation errors, so we use json schema for that
|
|
27
|
-
const configSchemaValidator = schema
|
|
28
|
-
.parseJSONSchema(
|
|
29
|
-
t.generateJSONSchema(configFile.powerSyncConfig, { allowAdditional: true, parsers: [configFile.portParser] })
|
|
30
|
-
)
|
|
31
|
-
.validator();
|
|
15
|
+
const configSchemaValidator = schema.parseJSONSchema(configFile.PowerSyncConfigJSONSchema).validator();
|
|
32
16
|
|
|
33
17
|
export abstract class ConfigCollector {
|
|
34
18
|
abstract get name(): string;
|
|
@@ -105,27 +89,7 @@ export abstract class ConfigCollector {
|
|
|
105
89
|
schema: 'core',
|
|
106
90
|
keepSourceTokens: true,
|
|
107
91
|
lineCounter,
|
|
108
|
-
customTags: [
|
|
109
|
-
{
|
|
110
|
-
tag: '!env',
|
|
111
|
-
resolve(envName: string, onError: (error: string) => void) {
|
|
112
|
-
if (!envName.startsWith(YAML_ENV_PREFIX)) {
|
|
113
|
-
onError(
|
|
114
|
-
`Attempting to substitute environment variable ${envName} is not allowed. Variables must start with "${YAML_ENV_PREFIX}"`
|
|
115
|
-
);
|
|
116
|
-
return envName;
|
|
117
|
-
}
|
|
118
|
-
const value = process.env[envName];
|
|
119
|
-
if (typeof value == 'undefined') {
|
|
120
|
-
onError(
|
|
121
|
-
`Attempted to substitute environment variable "${envName}" which is undefined. Set this variable on the environment.`
|
|
122
|
-
);
|
|
123
|
-
return envName;
|
|
124
|
-
}
|
|
125
|
-
return value;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
]
|
|
92
|
+
customTags: [YamlEnvTag]
|
|
129
93
|
});
|
|
130
94
|
|
|
131
95
|
if (parsed.errors.length) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as yaml from 'yaml';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment variables can be substituted into the YAML config
|
|
5
|
+
* when parsing if the environment variable name starts with this prefix.
|
|
6
|
+
* Attempting to substitute any other environment variable will throw an exception.
|
|
7
|
+
*
|
|
8
|
+
* Example of substitution:
|
|
9
|
+
* storage:
|
|
10
|
+
* type: mongodb
|
|
11
|
+
* uri: !env PS_MONGO_URI
|
|
12
|
+
*/
|
|
13
|
+
const YAML_ENV_PREFIX = 'PS_';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Custom YAML tag which performs string environment variable substitution
|
|
17
|
+
* Allows for type casting string environment variables to boolean or number
|
|
18
|
+
* by using the syntax !env PS_MONGO_PORT::number or !env PS_USE_SUPABASE::boolean
|
|
19
|
+
*/
|
|
20
|
+
export const YamlEnvTag: yaml.ScalarTag = {
|
|
21
|
+
tag: '!env',
|
|
22
|
+
resolve(envName: string, onError: (error: string) => void) {
|
|
23
|
+
if (!envName.startsWith(YAML_ENV_PREFIX)) {
|
|
24
|
+
onError(
|
|
25
|
+
`Attempting to substitute environment variable ${envName} is not allowed. Variables must start with "${YAML_ENV_PREFIX}"`
|
|
26
|
+
);
|
|
27
|
+
return envName;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// allow type casting if the envName contains a type suffix
|
|
31
|
+
// e.g. PS_MONGO_PORT::number or PS_USE_SUPABASE::boolean
|
|
32
|
+
const [name, type = 'string'] = envName.split('::');
|
|
33
|
+
|
|
34
|
+
let value = process.env[name];
|
|
35
|
+
|
|
36
|
+
if (typeof value == 'undefined') {
|
|
37
|
+
onError(
|
|
38
|
+
`Attempted to substitute environment variable "${envName}" which is undefined. Set this variable on the environment.`
|
|
39
|
+
);
|
|
40
|
+
return envName;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
switch (type) {
|
|
44
|
+
case 'string':
|
|
45
|
+
return value;
|
|
46
|
+
case 'number':
|
|
47
|
+
const numberValue = Number(value);
|
|
48
|
+
if (Number.isNaN(numberValue)) {
|
|
49
|
+
onError(`Environment variable "${envName}" is not a valid number. Got: "${value}".`);
|
|
50
|
+
return envName;
|
|
51
|
+
}
|
|
52
|
+
return numberValue;
|
|
53
|
+
case 'boolean':
|
|
54
|
+
if (value?.toLowerCase() == 'true') {
|
|
55
|
+
return true;
|
|
56
|
+
} else if (value?.toLowerCase() == 'false') {
|
|
57
|
+
return false;
|
|
58
|
+
} else {
|
|
59
|
+
onError(`Environment variable "${envName}" is not a boolean. Expected "true" or "false", got "${value}".`);
|
|
60
|
+
return envName;
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
onError(`Environment variable "${envName}" has an invalid type suffix "${type}".`);
|
|
64
|
+
return envName;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -5,11 +5,6 @@ import { ConfigCollector } from './collectors/config-collector.js';
|
|
|
5
5
|
import { Base64ConfigCollector } from './collectors/impl/base64-config-collector.js';
|
|
6
6
|
import { FallbackConfigCollector } from './collectors/impl/fallback-config-collector.js';
|
|
7
7
|
import { FileSystemConfigCollector } from './collectors/impl/filesystem-config-collector.js';
|
|
8
|
-
import { Base64SyncRulesCollector } from './sync-rules/impl/base64-sync-rules-collector.js';
|
|
9
|
-
import { FileSystemSyncRulesCollector } from './sync-rules/impl/filesystem-sync-rules-collector.js';
|
|
10
|
-
import { InlineSyncRulesCollector } from './sync-rules/impl/inline-sync-rules-collector.js';
|
|
11
|
-
import { SyncRulesCollector } from './sync-rules/sync-collector.js';
|
|
12
|
-
import { ResolvedPowerSyncConfig, RunnerConfig, SyncRulesConfig } from './types.js';
|
|
13
8
|
import {
|
|
14
9
|
DEFAULT_MAX_BUCKETS_PER_CONNECTION,
|
|
15
10
|
DEFAULT_MAX_CONCURRENT_CONNECTIONS,
|
|
@@ -17,6 +12,11 @@ import {
|
|
|
17
12
|
DEFAULT_MAX_PARAMETER_QUERY_RESULTS,
|
|
18
13
|
DEFAULT_MAX_POOL_SIZE
|
|
19
14
|
} from './defaults.js';
|
|
15
|
+
import { Base64SyncRulesCollector } from './sync-rules/impl/base64-sync-rules-collector.js';
|
|
16
|
+
import { FileSystemSyncRulesCollector } from './sync-rules/impl/filesystem-sync-rules-collector.js';
|
|
17
|
+
import { InlineSyncRulesCollector } from './sync-rules/impl/inline-sync-rules-collector.js';
|
|
18
|
+
import { SyncRulesCollector } from './sync-rules/sync-collector.js';
|
|
19
|
+
import { ResolvedPowerSyncConfig, RunnerConfig, SyncRulesConfig } from './types.js';
|
|
20
20
|
|
|
21
21
|
export type CompoundConfigCollectorOptions = {
|
|
22
22
|
/**
|
|
@@ -159,6 +159,23 @@ export class CompoundConfigCollector {
|
|
|
159
159
|
internal_service_endpoint:
|
|
160
160
|
baseConfig.telemetry?.internal_service_endpoint ?? 'https://pulse.journeyapps.com/v1/metrics'
|
|
161
161
|
},
|
|
162
|
+
healthcheck: {
|
|
163
|
+
/**
|
|
164
|
+
* Default to legacy mode if no probes config is provided.
|
|
165
|
+
* If users provide a config, all options require explicit opt-in.
|
|
166
|
+
*/
|
|
167
|
+
probes: baseConfig.healthcheck?.probes
|
|
168
|
+
? {
|
|
169
|
+
use_filesystem: baseConfig.healthcheck.probes.use_filesystem ?? false,
|
|
170
|
+
use_http: baseConfig.healthcheck.probes.use_http ?? false,
|
|
171
|
+
use_legacy: baseConfig.healthcheck.probes.use_legacy ?? false
|
|
172
|
+
}
|
|
173
|
+
: {
|
|
174
|
+
use_filesystem: false,
|
|
175
|
+
use_http: false,
|
|
176
|
+
use_legacy: true
|
|
177
|
+
}
|
|
178
|
+
},
|
|
162
179
|
api_parameters: {
|
|
163
180
|
max_buckets_per_connection:
|
|
164
181
|
baseConfig.api?.parameters?.max_buckets_per_connection ?? DEFAULT_MAX_BUCKETS_PER_CONNECTION,
|
package/src/util/config/types.ts
CHANGED
|
@@ -69,5 +69,18 @@ export type ResolvedPowerSyncConfig = {
|
|
|
69
69
|
|
|
70
70
|
/** Prefix for postgres replication slot names. May eventually be connection-specific. */
|
|
71
71
|
slot_name_prefix: string;
|
|
72
|
+
|
|
73
|
+
healthcheck: {
|
|
74
|
+
probes: {
|
|
75
|
+
use_filesystem: boolean;
|
|
76
|
+
use_http: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* @deprecated This maintains backwards compatibility with the legacy default.
|
|
79
|
+
* Explicit probe configuration should be used instead.
|
|
80
|
+
*/
|
|
81
|
+
use_legacy: boolean;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
72
85
|
parameters: Record<string, number | string | boolean | null>;
|
|
73
86
|
};
|
|
@@ -117,11 +117,11 @@ export interface SyncBucketData {
|
|
|
117
117
|
bucket: string;
|
|
118
118
|
data: OplogEntry[];
|
|
119
119
|
/**
|
|
120
|
-
* True if
|
|
120
|
+
* True if there _could_ be more data for this bucket, and another request must be made.
|
|
121
121
|
*/
|
|
122
122
|
has_more: boolean;
|
|
123
123
|
/**
|
|
124
|
-
* The `after` specified in the request.
|
|
124
|
+
* The `after` specified in the request, or the next_after from the previous batch returned.
|
|
125
125
|
*/
|
|
126
126
|
after: ProtocolOpId;
|
|
127
127
|
/**
|