@powersync/service-core 1.14.0 → 1.15.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 +32 -0
- package/LICENSE +3 -3
- package/dist/api/api-metrics.js +5 -0
- package/dist/api/api-metrics.js.map +1 -1
- package/dist/api/diagnostics.js +1 -1
- package/dist/api/diagnostics.js.map +1 -1
- package/dist/metrics/open-telemetry/util.d.ts +0 -3
- package/dist/metrics/open-telemetry/util.js +18 -13
- package/dist/metrics/open-telemetry/util.js.map +1 -1
- package/dist/routes/compression.d.ts +19 -0
- package/dist/routes/compression.js +70 -0
- package/dist/routes/compression.js.map +1 -0
- package/dist/routes/configure-fastify.d.ts +40 -5
- package/dist/routes/endpoints/socket-route.js +24 -9
- package/dist/routes/endpoints/socket-route.js.map +1 -1
- package/dist/routes/endpoints/sync-rules.js +1 -27
- package/dist/routes/endpoints/sync-rules.js.map +1 -1
- package/dist/routes/endpoints/sync-stream.d.ts +80 -10
- package/dist/routes/endpoints/sync-stream.js +17 -12
- package/dist/routes/endpoints/sync-stream.js.map +1 -1
- package/dist/storage/BucketStorage.d.ts +1 -1
- package/dist/storage/BucketStorage.js.map +1 -1
- package/dist/storage/BucketStorageBatch.d.ts +4 -4
- package/dist/storage/BucketStorageBatch.js.map +1 -1
- package/dist/storage/ChecksumCache.d.ts +4 -19
- package/dist/storage/ChecksumCache.js +4 -0
- package/dist/storage/ChecksumCache.js.map +1 -1
- package/dist/storage/ReplicationEventPayload.d.ts +2 -2
- package/dist/storage/SyncRulesBucketStorage.d.ts +9 -0
- package/dist/storage/SyncRulesBucketStorage.js.map +1 -1
- package/dist/sync/BucketChecksumState.d.ts +40 -10
- package/dist/sync/BucketChecksumState.js +154 -18
- package/dist/sync/BucketChecksumState.js.map +1 -1
- package/dist/sync/RequestTracker.d.ts +7 -1
- package/dist/sync/RequestTracker.js +22 -2
- package/dist/sync/RequestTracker.js.map +1 -1
- package/dist/sync/sync.d.ts +3 -3
- package/dist/sync/sync.js +23 -42
- package/dist/sync/sync.js.map +1 -1
- package/dist/sync/util.js +1 -1
- package/dist/sync/util.js.map +1 -1
- package/dist/util/protocol-types.d.ts +153 -9
- package/dist/util/protocol-types.js +41 -6
- package/dist/util/protocol-types.js.map +1 -1
- package/dist/util/utils.d.ts +18 -3
- package/dist/util/utils.js +33 -9
- package/dist/util/utils.js.map +1 -1
- package/package.json +14 -14
- package/src/api/api-metrics.ts +6 -0
- package/src/api/diagnostics.ts +1 -1
- package/src/metrics/open-telemetry/util.ts +22 -21
- package/src/routes/compression.ts +75 -0
- package/src/routes/endpoints/socket-route.ts +24 -9
- package/src/routes/endpoints/sync-rules.ts +1 -28
- package/src/routes/endpoints/sync-stream.ts +18 -15
- package/src/storage/BucketStorage.ts +2 -2
- package/src/storage/BucketStorageBatch.ts +10 -4
- package/src/storage/ChecksumCache.ts +8 -22
- package/src/storage/ReplicationEventPayload.ts +2 -2
- package/src/storage/SyncRulesBucketStorage.ts +12 -0
- package/src/sync/BucketChecksumState.ts +192 -29
- package/src/sync/RequestTracker.ts +27 -2
- package/src/sync/sync.ts +53 -51
- package/src/sync/util.ts +1 -1
- package/src/util/protocol-types.ts +138 -10
- package/src/util/utils.ts +59 -12
- package/test/src/checksum_cache.test.ts +6 -8
- package/test/src/routes/mocks.ts +59 -0
- package/test/src/routes/stream.test.ts +84 -0
- package/test/src/sync/BucketChecksumState.test.ts +340 -42
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type Negotiator from 'negotiator';
|
|
2
|
+
import { PassThrough, pipeline, Readable, Transform } from 'node:stream';
|
|
3
|
+
import * as zlib from 'node:zlib';
|
|
4
|
+
import { RequestTracker } from '../sync/RequestTracker.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compress a streamed response.
|
|
8
|
+
*
|
|
9
|
+
* `@fastify/compress` can do something similar, but does not appear to work as well on streamed responses.
|
|
10
|
+
* The manual implementation is simple enough, and gives us more control over the low-level details.
|
|
11
|
+
*
|
|
12
|
+
* @param negotiator Negotiator from the request, to negotiate response encoding
|
|
13
|
+
* @param stream plain-text stream
|
|
14
|
+
* @returns
|
|
15
|
+
*/
|
|
16
|
+
export function maybeCompressResponseStream(
|
|
17
|
+
negotiator: Negotiator,
|
|
18
|
+
stream: Readable,
|
|
19
|
+
tracker: RequestTracker
|
|
20
|
+
): { stream: Readable; encodingHeaders: { 'content-encoding'?: string } } {
|
|
21
|
+
const encoding = (negotiator as any).encoding(['identity', 'gzip', 'zstd'], { preferred: 'zstd' });
|
|
22
|
+
const transform = createCompressionTransform(encoding);
|
|
23
|
+
if (transform == null) {
|
|
24
|
+
// No matching compression supported - leave stream as-is
|
|
25
|
+
return {
|
|
26
|
+
stream,
|
|
27
|
+
encodingHeaders: {}
|
|
28
|
+
};
|
|
29
|
+
} else {
|
|
30
|
+
tracker.setCompressed(encoding);
|
|
31
|
+
return {
|
|
32
|
+
stream: transformStream(stream, transform, tracker),
|
|
33
|
+
encodingHeaders: { 'content-encoding': encoding }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createCompressionTransform(encoding: string | undefined): Transform | null {
|
|
39
|
+
if (encoding == 'zstd') {
|
|
40
|
+
// Available since Node v23.8.0, v22.15.0
|
|
41
|
+
// This does the actual compression in a background thread pool.
|
|
42
|
+
return zlib.createZstdCompress({
|
|
43
|
+
// We need to flush the frame after every new input chunk, to avoid delaying data
|
|
44
|
+
// in the output stream.
|
|
45
|
+
flush: zlib.constants.ZSTD_e_flush,
|
|
46
|
+
params: {
|
|
47
|
+
// Default compression level is 3. We reduce this slightly to limit CPU overhead
|
|
48
|
+
[zlib.constants.ZSTD_c_compressionLevel]: 2
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
} else if (encoding == 'gzip') {
|
|
52
|
+
return zlib.createGzip({
|
|
53
|
+
// We need to flush the frame after every new input chunk, to avoid delaying data
|
|
54
|
+
// in the output stream.
|
|
55
|
+
flush: zlib.constants.Z_SYNC_FLUSH
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function transformStream(source: Readable, transform: Transform, tracker: RequestTracker) {
|
|
62
|
+
// pipe does not forward error events automatically, resulting in unhandled error
|
|
63
|
+
// events. This forwards it.
|
|
64
|
+
const out = new PassThrough();
|
|
65
|
+
const trackingTransform = new Transform({
|
|
66
|
+
transform(chunk, _encoding, callback) {
|
|
67
|
+
tracker.addCompressedDataSent(chunk.length);
|
|
68
|
+
callback(null, chunk);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
pipeline(source, transform, trackingTransform, out, (err) => {
|
|
72
|
+
if (err) out.destroy(err);
|
|
73
|
+
});
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
@@ -11,7 +11,7 @@ import { APIMetric } from '@powersync/service-types';
|
|
|
11
11
|
export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
12
12
|
router.reactiveStream<util.StreamingSyncRequest, any>(SyncRoutes.STREAM, {
|
|
13
13
|
validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
|
|
14
|
-
handler: async ({ context, params, responder, observer, initialN, signal: upstreamSignal }) => {
|
|
14
|
+
handler: async ({ context, params, responder, observer, initialN, signal: upstreamSignal, connection }) => {
|
|
15
15
|
const { service_context, logger } = context;
|
|
16
16
|
const { routerEngine, metricsEngine, syncContext } = service_context;
|
|
17
17
|
|
|
@@ -58,8 +58,6 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
const syncParams = new RequestParameters(context.token_payload!, params.parameters ?? {});
|
|
62
|
-
|
|
63
61
|
const {
|
|
64
62
|
storageEngine: { activeBucketStorage }
|
|
65
63
|
} = service_context;
|
|
@@ -86,16 +84,21 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
86
84
|
|
|
87
85
|
metricsEngine.getUpDownCounter(APIMetric.CONCURRENT_CONNECTIONS).add(1);
|
|
88
86
|
const tracker = new sync.RequestTracker(metricsEngine);
|
|
87
|
+
if (connection.tracker.encoding) {
|
|
88
|
+
// Must be set before we start the stream
|
|
89
|
+
tracker.setCompressed(connection.tracker.encoding);
|
|
90
|
+
}
|
|
89
91
|
try {
|
|
90
92
|
for await (const data of sync.streamResponse({
|
|
91
93
|
syncContext: syncContext,
|
|
92
94
|
bucketStorage: bucketStorage,
|
|
93
|
-
syncRules:
|
|
95
|
+
syncRules: {
|
|
96
|
+
syncRules,
|
|
97
|
+
version: bucketStorage.group_id
|
|
98
|
+
},
|
|
94
99
|
params: {
|
|
95
|
-
...params
|
|
96
|
-
binary_data: true // always true for web sockets
|
|
100
|
+
...params
|
|
97
101
|
},
|
|
98
|
-
syncParams,
|
|
99
102
|
token: context!.token_payload!,
|
|
100
103
|
tokenStreamOptions: {
|
|
101
104
|
// RSocket handles keepalive events by default
|
|
@@ -103,7 +106,8 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
103
106
|
},
|
|
104
107
|
tracker,
|
|
105
108
|
signal,
|
|
106
|
-
logger
|
|
109
|
+
logger,
|
|
110
|
+
isEncodingAsBson: true
|
|
107
111
|
})) {
|
|
108
112
|
if (signal.aborted) {
|
|
109
113
|
break;
|
|
@@ -116,7 +120,7 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
116
120
|
const serialized = sync.syncLineToBson(data);
|
|
117
121
|
responder.onNext({ data: serialized }, false);
|
|
118
122
|
requestedN--;
|
|
119
|
-
tracker.
|
|
123
|
+
tracker.addPlaintextDataSynced(serialized.length);
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
if (requestedN <= 0 && !signal.aborted) {
|
|
@@ -153,6 +157,17 @@ export const syncStreamReactive: SocketRouteGenerator = (router) =>
|
|
|
153
157
|
responder.onComplete();
|
|
154
158
|
removeStopHandler();
|
|
155
159
|
disposer();
|
|
160
|
+
if (connection.tracker.encoding) {
|
|
161
|
+
// Technically, this may not be unique to this specific stream, since there could be multiple
|
|
162
|
+
// rsocket streams on the same websocket connection. We don't have a way to track compressed bytes
|
|
163
|
+
// on individual streams, and we generally expect 1 stream per connection, so this is a reasonable
|
|
164
|
+
// approximation.
|
|
165
|
+
// If there are multiple streams, bytes written would be split arbitrarily across them, but the
|
|
166
|
+
// total should be correct.
|
|
167
|
+
// For non-compressed cases, this is tracked by the stream itself.
|
|
168
|
+
const socketBytes = connection.tracker.getBytesWritten();
|
|
169
|
+
tracker.addCompressedDataSent(socketBytes);
|
|
170
|
+
}
|
|
156
171
|
logger.info(`Sync stream complete`, {
|
|
157
172
|
...tracker.getLogMeta(),
|
|
158
173
|
stream_ms: Date.now() - streamStart,
|
|
@@ -202,34 +202,7 @@ async function debugSyncRules(apiHandler: RouteAPI, sync_rules: string) {
|
|
|
202
202
|
|
|
203
203
|
return {
|
|
204
204
|
valid: true,
|
|
205
|
-
bucket_definitions: rules.
|
|
206
|
-
let all_parameter_queries = [...d.parameterQueries.values()].flat();
|
|
207
|
-
let all_data_queries = [...d.dataQueries.values()].flat();
|
|
208
|
-
return {
|
|
209
|
-
name: d.name,
|
|
210
|
-
bucket_parameters: d.bucketParameters,
|
|
211
|
-
global_parameter_queries: d.globalParameterQueries.map((q) => {
|
|
212
|
-
return {
|
|
213
|
-
sql: q.sql
|
|
214
|
-
};
|
|
215
|
-
}),
|
|
216
|
-
parameter_queries: all_parameter_queries.map((q) => {
|
|
217
|
-
return {
|
|
218
|
-
sql: q.sql,
|
|
219
|
-
table: q.sourceTable,
|
|
220
|
-
input_parameters: q.inputParameters
|
|
221
|
-
};
|
|
222
|
-
}),
|
|
223
|
-
|
|
224
|
-
data_queries: all_data_queries.map((q) => {
|
|
225
|
-
return {
|
|
226
|
-
sql: q.sql,
|
|
227
|
-
table: q.sourceTable,
|
|
228
|
-
columns: q.columnOutputNames()
|
|
229
|
-
};
|
|
230
|
-
})
|
|
231
|
-
};
|
|
232
|
-
}),
|
|
205
|
+
bucket_definitions: rules.bucketSources.map((source) => source.debugRepresentation()),
|
|
233
206
|
source_tables: resolved_tables,
|
|
234
207
|
data_tables: rules.debugGetOutputTables()
|
|
235
208
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { ErrorCode, errors,
|
|
2
|
-
import { RequestParameters } from '@powersync/service-sync-rules';
|
|
3
|
-
import { Readable } from 'stream';
|
|
1
|
+
import { ErrorCode, errors, router, schema } from '@powersync/lib-services-framework';
|
|
4
2
|
import Negotiator from 'negotiator';
|
|
3
|
+
import { Readable } from 'stream';
|
|
5
4
|
|
|
6
5
|
import * as sync from '../../sync/sync-index.js';
|
|
7
6
|
import * as util from '../../util/util-index.js';
|
|
@@ -10,6 +9,7 @@ import { authUser } from '../auth.js';
|
|
|
10
9
|
import { routeDefinition } from '../router.js';
|
|
11
10
|
|
|
12
11
|
import { APIMetric } from '@powersync/service-types';
|
|
12
|
+
import { maybeCompressResponseStream } from '../compression.js';
|
|
13
13
|
|
|
14
14
|
export enum SyncRoutes {
|
|
15
15
|
STREAM = '/sync/stream'
|
|
@@ -31,10 +31,11 @@ export const syncStreamed = routeDefinition({
|
|
|
31
31
|
const userAgent = headers['x-user-agent'] ?? headers['user-agent'];
|
|
32
32
|
const clientId = payload.params.client_id;
|
|
33
33
|
const streamStart = Date.now();
|
|
34
|
+
const negotiator = new Negotiator(payload.request);
|
|
34
35
|
// This falls back to JSON unless there's preference for the bson-stream in the Accept header.
|
|
35
|
-
const useBson =
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const useBson = payload.request.headers.accept
|
|
37
|
+
? negotiator.mediaType(supportedContentTypes) == concatenatedBsonContentType
|
|
38
|
+
: false;
|
|
38
39
|
|
|
39
40
|
logger.defaultMeta = {
|
|
40
41
|
...logger.defaultMeta,
|
|
@@ -52,9 +53,6 @@ export const syncStreamed = routeDefinition({
|
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const params: util.StreamingSyncRequest = payload.params;
|
|
56
|
-
const syncParams = new RequestParameters(payload.context.token_payload!, payload.params.parameters ?? {});
|
|
57
|
-
|
|
58
56
|
const bucketStorage = await storageEngine.activeBucketStorage.getActiveStorage();
|
|
59
57
|
|
|
60
58
|
if (bucketStorage == null) {
|
|
@@ -74,20 +72,24 @@ export const syncStreamed = routeDefinition({
|
|
|
74
72
|
const syncLines = sync.streamResponse({
|
|
75
73
|
syncContext: syncContext,
|
|
76
74
|
bucketStorage,
|
|
77
|
-
syncRules:
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
syncRules: {
|
|
76
|
+
syncRules,
|
|
77
|
+
version: bucketStorage.group_id
|
|
78
|
+
},
|
|
79
|
+
params: payload.params,
|
|
80
80
|
token: payload.context.token_payload!,
|
|
81
81
|
tracker,
|
|
82
82
|
signal: controller.signal,
|
|
83
|
-
logger
|
|
83
|
+
logger,
|
|
84
|
+
isEncodingAsBson: useBson
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
const byteContents = useBson ? sync.bsonLines(syncLines) : sync.ndjson(syncLines);
|
|
87
|
-
const
|
|
88
|
+
const plainStream = Readable.from(sync.transformToBytesTracked(byteContents, tracker), {
|
|
88
89
|
objectMode: false,
|
|
89
90
|
highWaterMark: 16 * 1024
|
|
90
91
|
});
|
|
92
|
+
const { stream, encodingHeaders } = maybeCompressResponseStream(negotiator, plainStream, tracker);
|
|
91
93
|
|
|
92
94
|
// Best effort guess on why the stream was closed.
|
|
93
95
|
// We use the `??=` operator everywhere, so that we catch the first relevant
|
|
@@ -122,7 +124,8 @@ export const syncStreamed = routeDefinition({
|
|
|
122
124
|
return new router.RouterResponse({
|
|
123
125
|
status: 200,
|
|
124
126
|
headers: {
|
|
125
|
-
'Content-Type': useBson ? concatenatedBsonContentType : ndJsonContentType
|
|
127
|
+
'Content-Type': useBson ? concatenatedBsonContentType : ndJsonContentType,
|
|
128
|
+
...encodingHeaders
|
|
126
129
|
},
|
|
127
130
|
data: stream,
|
|
128
131
|
afterSend: async (details) => {
|
|
@@ -39,8 +39,8 @@ export enum SyncRuleState {
|
|
|
39
39
|
export const DEFAULT_DOCUMENT_BATCH_LIMIT = 1000;
|
|
40
40
|
export const DEFAULT_DOCUMENT_CHUNK_LIMIT_BYTES = 1 * 1024 * 1024;
|
|
41
41
|
|
|
42
|
-
export function mergeToast(record: ToastableSqliteRow
|
|
43
|
-
const newRecord: ToastableSqliteRow = {};
|
|
42
|
+
export function mergeToast<V>(record: ToastableSqliteRow<V>, persisted: ToastableSqliteRow<V>): ToastableSqliteRow<V> {
|
|
43
|
+
const newRecord: ToastableSqliteRow<V> = {};
|
|
44
44
|
for (let key in record) {
|
|
45
45
|
if (typeof record[key] == 'undefined') {
|
|
46
46
|
newRecord[key] = persisted[key];
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ObserverClient } from '@powersync/lib-services-framework';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
EvaluatedParameters,
|
|
4
|
+
EvaluatedRow,
|
|
5
|
+
SqliteInputRow,
|
|
6
|
+
SqliteRow,
|
|
7
|
+
ToastableSqliteRow
|
|
8
|
+
} from '@powersync/service-sync-rules';
|
|
3
9
|
import { BSON } from 'bson';
|
|
4
10
|
import { ReplicationEventPayload } from './ReplicationEventPayload.js';
|
|
5
11
|
import { SourceTable, TableSnapshotStatus } from './SourceTable.js';
|
|
@@ -132,7 +138,7 @@ export interface SaveInsert {
|
|
|
132
138
|
sourceTable: SourceTable;
|
|
133
139
|
before?: undefined;
|
|
134
140
|
beforeReplicaId?: undefined;
|
|
135
|
-
after:
|
|
141
|
+
after: SqliteInputRow;
|
|
136
142
|
afterReplicaId: ReplicaId;
|
|
137
143
|
}
|
|
138
144
|
|
|
@@ -143,7 +149,7 @@ export interface SaveUpdate {
|
|
|
143
149
|
/**
|
|
144
150
|
* This is only present when the id has changed, and will only contain replica identity columns.
|
|
145
151
|
*/
|
|
146
|
-
before?:
|
|
152
|
+
before?: SqliteInputRow;
|
|
147
153
|
beforeReplicaId?: ReplicaId;
|
|
148
154
|
|
|
149
155
|
/**
|
|
@@ -158,7 +164,7 @@ export interface SaveUpdate {
|
|
|
158
164
|
export interface SaveDelete {
|
|
159
165
|
tag: SaveOperationTag.DELETE;
|
|
160
166
|
sourceTable: SourceTable;
|
|
161
|
-
before?:
|
|
167
|
+
before?: SqliteInputRow;
|
|
162
168
|
beforeReplicaId: ReplicaId;
|
|
163
169
|
after?: undefined;
|
|
164
170
|
afterReplicaId?: undefined;
|
|
@@ -1,40 +1,21 @@
|
|
|
1
1
|
import { OrderedSet } from '@js-sdsl/ordered-set';
|
|
2
2
|
import { LRUCache } from 'lru-cache/min';
|
|
3
3
|
import { BucketChecksum } from '../util/protocol-types.js';
|
|
4
|
-
import { addBucketChecksums, ChecksumMap, InternalOpId } from '../util/utils.js';
|
|
4
|
+
import { addBucketChecksums, ChecksumMap, InternalOpId, PartialChecksum } from '../util/utils.js';
|
|
5
5
|
|
|
6
6
|
interface ChecksumFetchContext {
|
|
7
7
|
fetch(bucket: string): Promise<BucketChecksum>;
|
|
8
8
|
checkpoint: InternalOpId;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export interface PartialChecksum {
|
|
12
|
-
bucket: string;
|
|
13
|
-
/**
|
|
14
|
-
* 32-bit unsigned hash.
|
|
15
|
-
*/
|
|
16
|
-
partialChecksum: number;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Count of operations - informational only.
|
|
20
|
-
*/
|
|
21
|
-
partialCount: number;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* True if the queried operations contains (starts with) a CLEAR
|
|
25
|
-
* operation, indicating that the partial checksum is the full
|
|
26
|
-
* checksum, and must not be added to a previously-cached checksum.
|
|
27
|
-
*/
|
|
28
|
-
isFullChecksum: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
11
|
export interface FetchPartialBucketChecksum {
|
|
32
12
|
bucket: string;
|
|
33
13
|
start?: InternalOpId;
|
|
34
14
|
end: InternalOpId;
|
|
35
15
|
}
|
|
36
16
|
|
|
37
|
-
export type
|
|
17
|
+
export type PartialOrFullChecksum = PartialChecksum | BucketChecksum;
|
|
18
|
+
export type PartialChecksumMap = Map<string, PartialOrFullChecksum>;
|
|
38
19
|
|
|
39
20
|
export type FetchChecksums = (batch: FetchPartialBucketChecksum[]) => Promise<PartialChecksumMap>;
|
|
40
21
|
|
|
@@ -127,6 +108,11 @@ export class ChecksumCache {
|
|
|
127
108
|
});
|
|
128
109
|
}
|
|
129
110
|
|
|
111
|
+
clear() {
|
|
112
|
+
this.cache.clear();
|
|
113
|
+
this.bucketCheckpoints.clear();
|
|
114
|
+
}
|
|
115
|
+
|
|
130
116
|
async getChecksums(checkpoint: InternalOpId, buckets: string[]): Promise<BucketChecksum[]> {
|
|
131
117
|
const checksums = await this.getChecksumMap(checkpoint, buckets);
|
|
132
118
|
// Return results in the same order as the request
|
|
@@ -4,8 +4,8 @@ import { BucketStorageBatch, SaveOp } from './BucketStorageBatch.js';
|
|
|
4
4
|
|
|
5
5
|
export type EventData = {
|
|
6
6
|
op: SaveOp;
|
|
7
|
-
before?: sync_rules.
|
|
8
|
-
after?: sync_rules.
|
|
7
|
+
before?: sync_rules.SqliteInputRow;
|
|
8
|
+
after?: sync_rules.SqliteInputRow;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export type ReplicationEventPayload = {
|
|
@@ -62,6 +62,11 @@ export interface SyncRulesBucketStorage
|
|
|
62
62
|
|
|
63
63
|
compact(options?: CompactOptions): Promise<void>;
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Lightweight "compact" process to populate the checksum cache, if any.
|
|
67
|
+
*/
|
|
68
|
+
populatePersistentChecksumCache(options?: Pick<CompactOptions, 'signal' | 'maxOpId'>): Promise<void>;
|
|
69
|
+
|
|
65
70
|
// ## Read operations
|
|
66
71
|
|
|
67
72
|
getCheckpoint(): Promise<ReplicationCheckpoint>;
|
|
@@ -108,6 +113,11 @@ export interface SyncRulesBucketStorage
|
|
|
108
113
|
* Returns zero checksums for any buckets not found.
|
|
109
114
|
*/
|
|
110
115
|
getChecksums(checkpoint: util.InternalOpId, buckets: string[]): Promise<util.ChecksumMap>;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clear checksum cache. Primarily intended for tests.
|
|
119
|
+
*/
|
|
120
|
+
clearChecksumCache(): void;
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
export interface SyncRulesBucketStorageListener {
|
|
@@ -208,6 +218,8 @@ export interface CompactOptions {
|
|
|
208
218
|
* Internal/testing use: Cache size for compacting parameters.
|
|
209
219
|
*/
|
|
210
220
|
compactParameterCacheLimit?: number;
|
|
221
|
+
|
|
222
|
+
signal?: AbortSignal;
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
export interface ClearStorageOptions {
|