@powersync/service-module-mongodb-storage 0.18.3 → 0.20.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 +53 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js +41 -0
- package/dist/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.js.map +1 -0
- package/dist/storage/MongoBucketStorage.d.ts +5 -1
- package/dist/storage/MongoBucketStorage.js +5 -1
- package/dist/storage/MongoBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoBucketBatch.d.ts +3 -0
- package/dist/storage/implementation/MongoBucketBatch.js.map +1 -1
- package/dist/storage/implementation/MongoCompactor.d.ts +15 -19
- package/dist/storage/implementation/MongoCompactor.js +68 -260
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoStorageProvider.js +26 -3
- package/dist/storage/implementation/MongoStorageProvider.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +10 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.js +13 -12
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +2 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +146 -34
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/dist/storage/implementation/common/PersistedBatch.d.ts +6 -0
- package/dist/storage/implementation/common/PersistedBatch.js +15 -8
- package/dist/storage/implementation/common/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +20 -0
- package/dist/storage/implementation/models.js.map +1 -1
- package/dist/storage/implementation/v1/MongoBucketBatchV1.d.ts +4 -0
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js +58 -84
- package/dist/storage/implementation/v1/MongoBucketBatchV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.d.ts +11 -2
- package/dist/storage/implementation/v1/MongoCompactorV1.js +250 -1
- package/dist/storage/implementation/v1/MongoCompactorV1.js.map +1 -1
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js +19 -11
- package/dist/storage/implementation/v1/MongoSyncBucketStorageV1.js.map +1 -1
- package/dist/storage/implementation/v1/SingleBucketStoreV1.d.ts +9 -10
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js +2 -8
- package/dist/storage/implementation/v1/SingleBucketStoreV1.js.map +1 -1
- package/dist/storage/implementation/v3/BucketDataContextV3.d.ts +13 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js +23 -0
- package/dist/storage/implementation/v3/BucketDataContextV3.js.map +1 -0
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js +35 -23
- package/dist/storage/implementation/v3/MongoBucketBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoChecksumsV3.d.ts +1 -0
- package/dist/storage/implementation/v3/MongoChecksumsV3.js +59 -103
- package/dist/storage/implementation/v3/MongoChecksumsV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoCompactorV3.d.ts +23 -2
- package/dist/storage/implementation/v3/MongoCompactorV3.js +354 -219
- package/dist/storage/implementation/v3/MongoCompactorV3.js.map +1 -1
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.d.ts +5 -0
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js +12 -11
- package/dist/storage/implementation/v3/MongoStoppedSyncConfigCleanup.js.map +1 -1
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.d.ts +5 -3
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js +108 -106
- package/dist/storage/implementation/v3/MongoSyncBucketStorageV3.js.map +1 -1
- package/dist/storage/implementation/v3/PersistedBatchV3.d.ts +5 -2
- package/dist/storage/implementation/v3/PersistedBatchV3.js +53 -15
- package/dist/storage/implementation/v3/PersistedBatchV3.js.map +1 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.d.ts +2 -1
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js +3 -0
- package/dist/storage/implementation/v3/VersionedPowerSyncMongoV3.js.map +1 -1
- package/dist/storage/implementation/v3/bucket-format.d.ts +10 -1
- package/dist/storage/implementation/v3/bucket-format.js +28 -9
- package/dist/storage/implementation/v3/bucket-format.js.map +1 -1
- package/dist/storage/implementation/v3/chunking.js +3 -1
- package/dist/storage/implementation/v3/chunking.js.map +1 -1
- package/dist/storage/implementation/v3/models.d.ts +43 -2
- package/dist/storage/implementation/v3/models.js.map +1 -1
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.d.ts +21 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js +44 -0
- package/dist/storage/implementation/v3/object-storage/BucketDataObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.d.ts +30 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js +12 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.d.ts +70 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js +146 -0
- package/dist/storage/implementation/v3/object-storage/ObjectStorageLifecycle.js.map +1 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.d.ts +42 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js +285 -0
- package/dist/storage/implementation/v3/object-storage/S3ObjectStorage.js.map +1 -0
- package/dist/storage/implementation/v3/source-table-utils.d.ts +9 -1
- package/dist/storage/implementation/v3/source-table-utils.js +20 -21
- package/dist/storage/implementation/v3/source-table-utils.js.map +1 -1
- package/dist/types/types.d.ts +35 -0
- package/dist/types/types.js +35 -1
- package/dist/types/types.js.map +1 -1
- package/dist/utils/test-utils.js +3 -1
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/util.d.ts +8 -1
- package/dist/utils/util.js +125 -2
- package/dist/utils/util.js.map +1 -1
- package/package.json +10 -7
- package/src/migrations/db/migrations/1782950400000-checkpoint-requested-at-index.ts +55 -0
- package/src/storage/MongoBucketStorage.ts +10 -2
- package/src/storage/implementation/MongoBucketBatch.ts +4 -0
- package/src/storage/implementation/MongoCompactor.ts +90 -314
- package/src/storage/implementation/MongoStorageProvider.ts +32 -3
- package/src/storage/implementation/MongoSyncBucketStorage.ts +23 -22
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +164 -37
- package/src/storage/implementation/common/PersistedBatch.ts +22 -10
- package/src/storage/implementation/models.ts +20 -0
- package/src/storage/implementation/v1/MongoBucketBatchV1.ts +76 -96
- package/src/storage/implementation/v1/MongoCompactorV1.ts +292 -8
- package/src/storage/implementation/v1/MongoSyncBucketStorageV1.ts +53 -40
- package/src/storage/implementation/v1/SingleBucketStoreV1.ts +18 -27
- package/src/storage/implementation/v3/BucketDataContextV3.ts +32 -0
- package/src/storage/implementation/v3/MongoBucketBatchV3.ts +40 -23
- package/src/storage/implementation/v3/MongoChecksumsV3.ts +75 -105
- package/src/storage/implementation/v3/MongoCompactorV3.ts +456 -258
- package/src/storage/implementation/v3/MongoStoppedSyncConfigCleanup.ts +21 -19
- package/src/storage/implementation/v3/MongoSyncBucketStorageV3.ts +122 -126
- package/src/storage/implementation/v3/PersistedBatchV3.ts +69 -16
- package/src/storage/implementation/v3/VersionedPowerSyncMongoV3.ts +5 -0
- package/src/storage/implementation/v3/bucket-format.ts +36 -9
- package/src/storage/implementation/v3/chunking.ts +3 -1
- package/src/storage/implementation/v3/models.ts +45 -1
- package/src/storage/implementation/v3/object-storage/BucketDataObjectStorage.ts +54 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorage.ts +29 -0
- package/src/storage/implementation/v3/object-storage/ObjectStorageLifecycle.ts +171 -0
- package/src/storage/implementation/v3/object-storage/S3ObjectStorage.ts +343 -0
- package/src/storage/implementation/v3/source-table-utils.ts +43 -35
- package/src/types/types.ts +42 -1
- package/src/utils/test-utils.ts +3 -1
- package/src/utils/util.ts +182 -4
- package/test/src/__snapshots__/storage_sync.test.ts.snap +1 -583
- package/test/src/cleanup-stopped-sync-configs.test.ts +24 -1
- package/test/src/helpers/MemoryObjectStorage.ts +53 -0
- package/test/src/helpers/s3TestFactory.ts +69 -0
- package/test/src/object_storage_lifecycle.test.ts +59 -0
- package/test/src/setup.ts +6 -1
- package/test/src/storage.test.ts +216 -2
- package/test/src/storage_compacting.test.ts +176 -187
- package/test/src/storage_object_storage_inline_threshold.test.ts +116 -0
- package/test/src/storage_s3_checksums.test.ts +191 -0
- package/test/src/storage_s3_compaction_lifecycle.test.ts +417 -0
- package/test/src/storage_s3_reading.test.ts +526 -0
- package/test/src/storage_s3_writing.test.ts +135 -0
- package/test/src/storage_sync.test.ts +124 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/storage/implementation/common/SingleBucketStore.d.ts +0 -54
- package/dist/storage/implementation/common/SingleBucketStore.js +0 -3
- package/dist/storage/implementation/common/SingleBucketStore.js.map +0 -1
- package/dist/storage/implementation/v3/SingleBucketStoreV3.d.ts +0 -18
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js +0 -84
- package/dist/storage/implementation/v3/SingleBucketStoreV3.js.map +0 -1
- package/src/storage/implementation/common/SingleBucketStore.ts +0 -63
- package/src/storage/implementation/v3/SingleBucketStoreV3.ts +0 -108
|
@@ -26,8 +26,9 @@ import { HydratedSyncConfig, ParameterLookupRows, ScopedParameterLookup } from '
|
|
|
26
26
|
import * as bson from 'bson';
|
|
27
27
|
import { LRUCache } from 'lru-cache';
|
|
28
28
|
import * as timers from 'timers/promises';
|
|
29
|
-
import {
|
|
29
|
+
import { DEFAULT_CLEAR_BATCH_THROTTLE_RATE } from '../../types/types.js';
|
|
30
30
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
31
|
+
import { DEFAULT_INLINE_THRESHOLD_BYTES } from './common/PersistedBatch.js';
|
|
31
32
|
import type { VersionedPowerSyncMongo } from './db.js';
|
|
32
33
|
import { StorageConfig } from './models.js';
|
|
33
34
|
import { MongoBucketBatchOptions } from './MongoBucketBatch.js';
|
|
@@ -37,12 +38,16 @@ import { MongoParameterCompactor } from './MongoParameterCompactor.js';
|
|
|
37
38
|
import { MongoParsedSyncConfigSet } from './MongoParsedSyncConfigSet.js';
|
|
38
39
|
import { MongoPersistedReplicationStream } from './MongoPersistedReplicationStream.js';
|
|
39
40
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
41
|
+
import { ObjectStorage } from './v3/object-storage/ObjectStorage.js';
|
|
40
42
|
|
|
41
43
|
export interface MongoSyncBucketStorageOptions {
|
|
42
44
|
checksumOptions?: Omit<MongoChecksumOptions, 'storageConfig'>;
|
|
43
45
|
readPreference?: mongo.ReadPreference;
|
|
44
46
|
checksumCacheTtlMs?: number;
|
|
47
|
+
clearBatchThrottleRate?: number;
|
|
45
48
|
storageConfig: StorageConfig;
|
|
49
|
+
objectStorage?: ObjectStorage;
|
|
50
|
+
inlineThresholdBytes?: number;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
interface InternalCheckpointChanges extends CheckpointChanges {
|
|
@@ -71,6 +76,9 @@ export abstract class MongoSyncBucketStorage
|
|
|
71
76
|
|
|
72
77
|
readonly checksums: MongoChecksums;
|
|
73
78
|
|
|
79
|
+
readonly objectStorage?: ObjectStorage;
|
|
80
|
+
readonly inlineThresholdBytes: number;
|
|
81
|
+
|
|
74
82
|
/**
|
|
75
83
|
* Canonical parsed sync config sets, keyed by defaultSchema.
|
|
76
84
|
*
|
|
@@ -83,6 +91,7 @@ export abstract class MongoSyncBucketStorage
|
|
|
83
91
|
public readonly logger: Logger;
|
|
84
92
|
public readonly storageConfig: StorageConfig;
|
|
85
93
|
public readonly readPreference: mongo.ReadPreference | undefined;
|
|
94
|
+
public readonly clearBatchThrottleRate: number;
|
|
86
95
|
#storageInitialized = false;
|
|
87
96
|
|
|
88
97
|
constructor(
|
|
@@ -95,7 +104,12 @@ export abstract class MongoSyncBucketStorage
|
|
|
95
104
|
) {
|
|
96
105
|
super();
|
|
97
106
|
this.storageConfig = options.storageConfig;
|
|
107
|
+
this.objectStorage = options.objectStorage;
|
|
108
|
+
// Keep small chunks inline in MongoDB rather than offloading them to S3.
|
|
109
|
+
// Configurable via object_storage.inline_threshold_bytes.
|
|
110
|
+
this.inlineThresholdBytes = options.inlineThresholdBytes ?? DEFAULT_INLINE_THRESHOLD_BYTES;
|
|
98
111
|
this.readPreference = options.readPreference;
|
|
112
|
+
this.clearBatchThrottleRate = options.clearBatchThrottleRate ?? DEFAULT_CLEAR_BATCH_THROTTLE_RATE;
|
|
99
113
|
this.db = factory.db.versioned(this.storageConfig);
|
|
100
114
|
this.checksums = this.createMongoChecksums(options);
|
|
101
115
|
this.writeCheckpointAPI = new MongoWriteCheckpointAPI({
|
|
@@ -134,7 +148,9 @@ export abstract class MongoSyncBucketStorage
|
|
|
134
148
|
this.writeCheckpointAPI.setWriteCheckpointMode(mode);
|
|
135
149
|
}
|
|
136
150
|
|
|
137
|
-
createManagedWriteCheckpoints(
|
|
151
|
+
createManagedWriteCheckpoints(
|
|
152
|
+
checkpoints: storage.ManagedWriteCheckpointOptions[]
|
|
153
|
+
): Promise<storage.CreateManagedWriteCheckpointsResult> {
|
|
138
154
|
return this.writeCheckpointAPI.createManagedWriteCheckpoints(checkpoints);
|
|
139
155
|
}
|
|
140
156
|
|
|
@@ -218,7 +234,9 @@ export abstract class MongoSyncBucketStorage
|
|
|
218
234
|
skipExistingRows: options.skipExistingRows ?? false,
|
|
219
235
|
markRecordUnavailable: options.markRecordUnavailable,
|
|
220
236
|
hooks: options.hooks,
|
|
221
|
-
tracer: options.tracer
|
|
237
|
+
tracer: options.tracer,
|
|
238
|
+
objectStorage: this.objectStorage,
|
|
239
|
+
inlineThresholdBytes: this.inlineThresholdBytes
|
|
222
240
|
};
|
|
223
241
|
}
|
|
224
242
|
|
|
@@ -258,13 +276,13 @@ export abstract class MongoSyncBucketStorage
|
|
|
258
276
|
checkpoint: MongoReplicationCheckpoint,
|
|
259
277
|
dataBuckets: storage.BucketDataRequest[],
|
|
260
278
|
options?: storage.BucketDataBatchOptions
|
|
261
|
-
): AsyncIterable<storage.SyncBucketDataChunk>;
|
|
279
|
+
): AsyncIterable<storage.SyncBucketDataChunk | storage.SyncBucketDataBatchEnd>;
|
|
262
280
|
|
|
263
281
|
async *getBucketDataBatch(
|
|
264
282
|
checkpoint: storage.ReplicationCheckpoint,
|
|
265
283
|
dataBuckets: storage.BucketDataRequest[],
|
|
266
284
|
options?: storage.BucketDataBatchOptions
|
|
267
|
-
): AsyncIterable<storage.SyncBucketDataChunk> {
|
|
285
|
+
): AsyncIterable<storage.SyncBucketDataChunk | storage.SyncBucketDataBatchEnd> {
|
|
268
286
|
yield* this.getBucketDataBatchImpl(checkpoint as MongoReplicationCheckpoint, dataBuckets, options);
|
|
269
287
|
}
|
|
270
288
|
|
|
@@ -331,23 +349,6 @@ export abstract class MongoSyncBucketStorage
|
|
|
331
349
|
this.#storageInitialized = false;
|
|
332
350
|
}
|
|
333
351
|
|
|
334
|
-
protected async clearDeleteMany(
|
|
335
|
-
label: string,
|
|
336
|
-
operation: () => Promise<mongo.DeleteResult>,
|
|
337
|
-
signal?: AbortSignal
|
|
338
|
-
): Promise<void> {
|
|
339
|
-
await retryOnMongoMaxTimeMSExpired(operation, {
|
|
340
|
-
signal,
|
|
341
|
-
abortMessage: 'Aborted clearing data',
|
|
342
|
-
retryDelayMs: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS / 5,
|
|
343
|
-
onRetry: () => {
|
|
344
|
-
this.logger.info(
|
|
345
|
-
`Cleared batch of ${label} in ${lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS}ms, continuing...`
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
|
|
351
352
|
async reportError(e: any): Promise<void> {
|
|
352
353
|
const message = String(e.message ?? 'Replication failure');
|
|
353
354
|
await this.db.sync_rules.updateOne(
|
|
@@ -28,21 +28,26 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
28
28
|
|
|
29
29
|
async createManagedWriteCheckpoints(
|
|
30
30
|
checkpoints: storage.ManagedWriteCheckpointOptions[]
|
|
31
|
-
): Promise<
|
|
31
|
+
): Promise<storage.CreateManagedWriteCheckpointsResult> {
|
|
32
32
|
if (this.writeCheckpointMode !== storage.WriteCheckpointMode.MANAGED) {
|
|
33
33
|
throw new framework.ServiceAssertionError(
|
|
34
34
|
`Attempting to create a managed Write Checkpoint when the current Write Checkpoint mode is set to "${this.writeCheckpointMode}"`
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const uniqueCheckpoints =
|
|
38
|
+
const uniqueCheckpoints = storage.uniqueManagedWriteCheckpoints(checkpoints);
|
|
39
39
|
if (uniqueCheckpoints.length == 0) {
|
|
40
|
-
return new Map();
|
|
40
|
+
return { writeCheckpoints: new Map(), shouldAdvance: false };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
let shouldAdvance = false;
|
|
44
|
+
const writeCheckpoints = new Map<string, bigint>();
|
|
45
|
+
const generatedCheckpoints = uniqueCheckpoints.filter((checkpoint) => checkpoint.checkpoint_request_id == null);
|
|
46
|
+
const suppliedCheckpoints = uniqueCheckpoints.filter((checkpoint) => checkpoint.checkpoint_request_id != null);
|
|
47
|
+
|
|
48
|
+
if (generatedCheckpoints.length == 1) {
|
|
44
49
|
// For the common case of a single checkpoint, we can do this in a single request.
|
|
45
|
-
const { user_id, heads: lsns } =
|
|
50
|
+
const { user_id, heads: lsns } = generatedCheckpoints[0];
|
|
46
51
|
const doc = await this.db.write_checkpoints.findOneAndUpdate(
|
|
47
52
|
{
|
|
48
53
|
user_id
|
|
@@ -52,6 +57,11 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
52
57
|
lsns,
|
|
53
58
|
processed_at_lsn: null
|
|
54
59
|
},
|
|
60
|
+
// Unset rather than set to null, to keep the document out of the
|
|
61
|
+
// partial checkpoint_requested_at index used for retention cleanup.
|
|
62
|
+
$unset: {
|
|
63
|
+
checkpoint_requested_at: 1
|
|
64
|
+
},
|
|
55
65
|
$inc: {
|
|
56
66
|
client_id: 1n
|
|
57
67
|
}
|
|
@@ -59,29 +69,123 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
59
69
|
{ upsert: true, returnDocument: 'after' }
|
|
60
70
|
);
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
// Generated checkpoints always reset processed_at_lsn, so they are pending
|
|
73
|
+
// and the source marker must be forced.
|
|
74
|
+
shouldAdvance = true;
|
|
75
|
+
writeCheckpoints.set(doc!.user_id, doc!.client_id);
|
|
76
|
+
} else if (generatedCheckpoints.length > 1) {
|
|
77
|
+
// For more than one generated checkpoint, this gives a constant 2 requests.
|
|
78
|
+
await this.db.write_checkpoints.bulkWrite(
|
|
79
|
+
generatedCheckpoints.map(({ user_id, heads: lsns }) => ({
|
|
80
|
+
updateOne: {
|
|
81
|
+
filter: { user_id },
|
|
82
|
+
update: {
|
|
83
|
+
$set: {
|
|
84
|
+
lsns,
|
|
85
|
+
processed_at_lsn: null
|
|
86
|
+
},
|
|
87
|
+
$unset: {
|
|
88
|
+
checkpoint_requested_at: 1
|
|
89
|
+
},
|
|
90
|
+
$inc: {
|
|
91
|
+
client_id: 1n
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
upsert: true
|
|
95
|
+
}
|
|
96
|
+
}))
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const userIds = generatedCheckpoints.map((checkpoint) => checkpoint.user_id);
|
|
100
|
+
const docs = await this.db.write_checkpoints
|
|
101
|
+
.find(
|
|
102
|
+
{
|
|
103
|
+
user_id: { $in: userIds }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
projection: {
|
|
107
|
+
user_id: 1,
|
|
108
|
+
client_id: 1
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
.toArray();
|
|
113
|
+
|
|
114
|
+
shouldAdvance = true;
|
|
115
|
+
for (const doc of docs) {
|
|
116
|
+
writeCheckpoints.set(doc.user_id, doc.client_id);
|
|
117
|
+
}
|
|
63
118
|
}
|
|
64
119
|
|
|
65
|
-
|
|
120
|
+
if (suppliedCheckpoints.length > 0) {
|
|
121
|
+
const suppliedResult = await this.createSuppliedManagedWriteCheckpoints(suppliedCheckpoints);
|
|
122
|
+
shouldAdvance ||= suppliedResult.shouldAdvance;
|
|
123
|
+
for (const [userId, writeCheckpoint] of suppliedResult.writeCheckpoints) {
|
|
124
|
+
writeCheckpoints.set(userId, writeCheckpoint);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { writeCheckpoints, shouldAdvance };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async createSuppliedManagedWriteCheckpoints(checkpoints: storage.ManagedWriteCheckpointOptions[]) {
|
|
132
|
+
const checkpointRequestedAt = new Date();
|
|
133
|
+
|
|
66
134
|
await this.db.write_checkpoints.bulkWrite(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
135
|
+
checkpoints.map((checkpoint) => {
|
|
136
|
+
const { user_id, heads: lsns } = checkpoint;
|
|
137
|
+
const checkpointRequestId = checkpoint.checkpoint_request_id!;
|
|
138
|
+
|
|
139
|
+
// Supplied request ids are monotonic. updateMany preserves the existing
|
|
140
|
+
// upsert behavior for first checkpoint requests, while also advancing
|
|
141
|
+
// every duplicate row for the user_id that is lower than this request id.
|
|
142
|
+
const shouldApplyRequestId = {
|
|
143
|
+
$or: [
|
|
144
|
+
{ $eq: [{ $ifNull: ['$client_id', null] }, null] },
|
|
145
|
+
{ $lt: ['$client_id', { $literal: checkpointRequestId }] }
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
// Equal retries keep their original source head but refresh retention.
|
|
149
|
+
const shouldRefreshRequest = {
|
|
150
|
+
$or: [
|
|
151
|
+
{ $eq: [{ $ifNull: ['$client_id', null] }, null] },
|
|
152
|
+
{ $lte: ['$client_id', { $literal: checkpointRequestId }] }
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
updateMany: {
|
|
158
|
+
filter: { user_id },
|
|
159
|
+
update: [
|
|
160
|
+
{
|
|
161
|
+
$set: {
|
|
162
|
+
user_id,
|
|
163
|
+
client_id: {
|
|
164
|
+
$cond: [shouldApplyRequestId, { $literal: checkpointRequestId }, '$client_id']
|
|
165
|
+
},
|
|
166
|
+
lsns: {
|
|
167
|
+
$cond: [shouldApplyRequestId, { $literal: lsns }, '$lsns']
|
|
168
|
+
},
|
|
169
|
+
processed_at_lsn: {
|
|
170
|
+
$cond: [shouldApplyRequestId, null, '$processed_at_lsn']
|
|
171
|
+
},
|
|
172
|
+
checkpoint_requested_at: {
|
|
173
|
+
$cond: [shouldRefreshRequest, { $literal: checkpointRequestedAt }, '$checkpoint_requested_at']
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
upsert: true
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
})
|
|
82
182
|
);
|
|
83
183
|
|
|
84
|
-
|
|
184
|
+
// Fetch the final ids separately so stale requests can still return the
|
|
185
|
+
// checkpoint currently stored. If duplicate rows already exist for a
|
|
186
|
+
// user_id, use the highest stored id instead of whichever document MongoDB
|
|
187
|
+
// happens to return first.
|
|
188
|
+
const userIds = checkpoints.map((checkpoint) => checkpoint.user_id);
|
|
85
189
|
const docs = await this.db.write_checkpoints
|
|
86
190
|
.find(
|
|
87
191
|
{
|
|
@@ -90,13 +194,28 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
90
194
|
{
|
|
91
195
|
projection: {
|
|
92
196
|
user_id: 1,
|
|
93
|
-
client_id: 1
|
|
197
|
+
client_id: 1,
|
|
198
|
+
processed_at_lsn: 1
|
|
94
199
|
}
|
|
95
200
|
}
|
|
96
201
|
)
|
|
97
202
|
.toArray();
|
|
203
|
+
const maxDocsByUser = new Map<string, (typeof docs)[number]>();
|
|
204
|
+
for (const doc of docs) {
|
|
205
|
+
const existing = maxDocsByUser.get(doc.user_id);
|
|
206
|
+
if (
|
|
207
|
+
existing == null ||
|
|
208
|
+
doc.client_id > existing.client_id ||
|
|
209
|
+
(doc.client_id == existing.client_id && existing.processed_at_lsn != null && doc.processed_at_lsn == null)
|
|
210
|
+
) {
|
|
211
|
+
maxDocsByUser.set(doc.user_id, doc);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
98
214
|
|
|
99
|
-
return
|
|
215
|
+
return {
|
|
216
|
+
writeCheckpoints: new Map([...maxDocsByUser].map(([userId, doc]) => [userId, doc.client_id])),
|
|
217
|
+
shouldAdvance: [...maxDocsByUser.values()].some((doc) => doc.processed_at_lsn == null)
|
|
218
|
+
};
|
|
100
219
|
}
|
|
101
220
|
|
|
102
221
|
async lastWriteCheckpoint(filters: storage.LastWriteCheckpointFilters): Promise<bigint | null> {
|
|
@@ -225,19 +344,27 @@ export async function batchCreateCustomWriteCheckpoints(
|
|
|
225
344
|
}
|
|
226
345
|
|
|
227
346
|
await db.custom_write_checkpoints.bulkWrite(
|
|
228
|
-
checkpoints.map((checkpointOptions) =>
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
},
|
|
238
|
-
upsert: true
|
|
347
|
+
checkpoints.map((checkpointOptions) => {
|
|
348
|
+
const set: Record<string, unknown> = {
|
|
349
|
+
checkpoint: checkpointOptions.checkpoint,
|
|
350
|
+
sync_rules_id: checkpointOptions.sync_rules_id,
|
|
351
|
+
op_id: opId
|
|
352
|
+
};
|
|
353
|
+
if (checkpointOptions.checkpoint_requested_at != null) {
|
|
354
|
+
set.checkpoint_requested_at = checkpointOptions.checkpoint_requested_at;
|
|
239
355
|
}
|
|
240
|
-
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
updateOne: {
|
|
359
|
+
filter: { user_id: checkpointOptions.user_id, sync_rules_id: checkpointOptions.sync_rules_id },
|
|
360
|
+
update: {
|
|
361
|
+
$set: set,
|
|
362
|
+
...(checkpointOptions.checkpoint_requested_at == null ? { $unset: { checkpoint_requested_at: 1 } } : {})
|
|
363
|
+
},
|
|
364
|
+
upsert: true
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}),
|
|
241
368
|
{ session }
|
|
242
369
|
);
|
|
243
370
|
}
|
|
@@ -10,6 +10,7 @@ import { currentBucketKey, MAX_ROW_SIZE } from '../MongoBucketBatchShared.js';
|
|
|
10
10
|
import { MongoIdSequence } from '../MongoIdSequence.js';
|
|
11
11
|
import type { VersionedPowerSyncMongo } from '../db.js';
|
|
12
12
|
import { TaggedBucketParameterDocument } from '../models.js';
|
|
13
|
+
import { ObjectStorage } from '../v3/object-storage/ObjectStorage.js';
|
|
13
14
|
import { BucketDataDoc, BucketKey } from './BucketDataDoc.js';
|
|
14
15
|
import { SourceRecordBucketState, SourceRecordLookupState } from './SourceRecordStore.js';
|
|
15
16
|
|
|
@@ -34,6 +35,8 @@ const MAX_TRANSACTION_BATCH_SIZE = 30_000_000;
|
|
|
34
35
|
*/
|
|
35
36
|
const MAX_TRANSACTION_DOC_COUNT = 2_000;
|
|
36
37
|
|
|
38
|
+
export const DEFAULT_INLINE_THRESHOLD_BYTES = 16 * 1024;
|
|
39
|
+
|
|
37
40
|
export interface SaveBucketDataOptions {
|
|
38
41
|
op_seq: MongoIdSequence;
|
|
39
42
|
sourceKey: storage.ReplicaId;
|
|
@@ -60,6 +63,8 @@ export interface UpsertCurrentDataOptions {
|
|
|
60
63
|
|
|
61
64
|
export interface PersistedBatchOptions {
|
|
62
65
|
logger?: Logger;
|
|
66
|
+
objectStorage?: ObjectStorage;
|
|
67
|
+
inlineThresholdBytes?: number;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
@@ -74,6 +79,9 @@ export abstract class PersistedBatch {
|
|
|
74
79
|
bucketParameters: TaggedBucketParameterDocument[] = [];
|
|
75
80
|
bucketStates: Map<string, BucketStateUpdate> = new Map();
|
|
76
81
|
|
|
82
|
+
protected readonly objectStorage?: ObjectStorage;
|
|
83
|
+
protected readonly inlineThresholdBytes: number = DEFAULT_INLINE_THRESHOLD_BYTES;
|
|
84
|
+
|
|
77
85
|
/**
|
|
78
86
|
* For debug logging only.
|
|
79
87
|
*/
|
|
@@ -93,6 +101,10 @@ export abstract class PersistedBatch {
|
|
|
93
101
|
) {
|
|
94
102
|
this.currentSize = writtenSize;
|
|
95
103
|
this.logger = options?.logger ?? defaultLogger;
|
|
104
|
+
this.objectStorage = options?.objectStorage;
|
|
105
|
+
if (options?.inlineThresholdBytes != null) {
|
|
106
|
+
this.inlineThresholdBytes = options.inlineThresholdBytes;
|
|
107
|
+
}
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
saveBucketData(options: SaveBucketDataOptions) {
|
|
@@ -309,32 +321,32 @@ export abstract class PersistedBatch {
|
|
|
309
321
|
const replicationLag = Math.round((Date.now() - options.oldestUncommittedChange.getTime()) / 1000);
|
|
310
322
|
|
|
311
323
|
this.logger.info(
|
|
312
|
-
`Flushed ${this.bucketDataCount}
|
|
324
|
+
`Flushed: ${this.bucketDataCount} ops, ${this.bucketParameters.length} index entries, ${
|
|
313
325
|
this.currentDataCount
|
|
314
|
-
}
|
|
326
|
+
} records. ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}. Replication lag: ${replicationLag}s`,
|
|
315
327
|
{
|
|
316
328
|
flushed: {
|
|
317
329
|
duration: duration,
|
|
318
330
|
size: this.currentSize,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
331
|
+
bucket_ops_count: this.bucketDataCount,
|
|
332
|
+
parameter_indexes_count: this.bucketParameters.length,
|
|
333
|
+
source_records_count: this.currentDataCount,
|
|
322
334
|
replication_lag_seconds: replicationLag
|
|
323
335
|
}
|
|
324
336
|
}
|
|
325
337
|
);
|
|
326
338
|
} else {
|
|
327
339
|
this.logger.info(
|
|
328
|
-
`Flushed ${this.bucketDataCount}
|
|
340
|
+
`Flushed: ${this.bucketDataCount} ops, ${this.bucketParameters.length} index entries, ${
|
|
329
341
|
this.currentDataCount
|
|
330
|
-
}
|
|
342
|
+
} records. ${Math.round(this.currentSize / 1024)}kb in ${duration}ms. Last op_id: ${this.debugLastOpId}`,
|
|
331
343
|
{
|
|
332
344
|
flushed: {
|
|
333
345
|
duration: duration,
|
|
334
346
|
size: this.currentSize,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
347
|
+
bucket_ops_count: this.bucketDataCount,
|
|
348
|
+
parameter_indexes_count: this.bucketParameters.length,
|
|
349
|
+
source_records_count: this.currentDataCount
|
|
338
350
|
}
|
|
339
351
|
}
|
|
340
352
|
);
|
|
@@ -78,6 +78,11 @@ export interface BucketDataProperties {
|
|
|
78
78
|
row_id?: string;
|
|
79
79
|
checksum: bigint;
|
|
80
80
|
data: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* V1-only.
|
|
83
|
+
*
|
|
84
|
+
* V3 stores this on the BucketDataDocumentV3 instead of the individual ops.
|
|
85
|
+
*/
|
|
81
86
|
target_op?: bigint | null;
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -105,6 +110,10 @@ export interface SourceTableDocument {
|
|
|
105
110
|
replica_id_columns2: { name: string; type_oid?: number; type?: string }[] | undefined;
|
|
106
111
|
snapshot_done: boolean | undefined;
|
|
107
112
|
snapshot_status: SourceTableDocumentSnapshotStatus | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Source-specific metadata. Absent for legacy records.
|
|
115
|
+
*/
|
|
116
|
+
source_metadata?: storage.JsonValue;
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
export interface SourceTableDocumentSnapshotStatus {
|
|
@@ -273,6 +282,11 @@ export interface CustomWriteCheckpointDocument {
|
|
|
273
282
|
* This is not unique - multiple write checkpoints can have the same op_id.
|
|
274
283
|
*/
|
|
275
284
|
op_id?: InternalOpId;
|
|
285
|
+
/**
|
|
286
|
+
* Set when this checkpoint was created from a client-supplied checkpoint
|
|
287
|
+
* request rather than a persistent custom write checkpoint.
|
|
288
|
+
*/
|
|
289
|
+
checkpoint_requested_at?: Date | null;
|
|
276
290
|
}
|
|
277
291
|
|
|
278
292
|
export interface WriteCheckpointDocument {
|
|
@@ -286,6 +300,12 @@ export interface WriteCheckpointDocument {
|
|
|
286
300
|
* between two checkpoints.
|
|
287
301
|
*/
|
|
288
302
|
processed_at_lsn: string | null;
|
|
303
|
+
/**
|
|
304
|
+
* Set when this checkpoint was created from a client-supplied checkpoint request
|
|
305
|
+
* rather than a server-generated id. A missing/null value means this is not a
|
|
306
|
+
* checkpoint request.
|
|
307
|
+
*/
|
|
308
|
+
checkpoint_requested_at?: Date | null;
|
|
289
309
|
}
|
|
290
310
|
|
|
291
311
|
export interface InstanceDocument {
|