@powersync/service-module-mongodb-storage 0.0.0-dev-20250310210938 → 0.0.0-dev-20250312112247
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 +7 -6
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.d.ts +3 -0
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js +28 -0
- package/dist/migrations/db/migrations/1741697235857-bucket-state-index.js.map +1 -0
- package/dist/storage/implementation/MongoCompactor.js +14 -1
- package/dist/storage/implementation/MongoCompactor.js.map +1 -1
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +8 -2
- package/dist/storage/implementation/MongoSyncBucketStorage.js +101 -9
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/PersistedBatch.d.ts +8 -0
- package/dist/storage/implementation/PersistedBatch.js +47 -0
- package/dist/storage/implementation/PersistedBatch.js.map +1 -1
- package/dist/storage/implementation/db.d.ts +2 -1
- package/dist/storage/implementation/db.js +3 -0
- package/dist/storage/implementation/db.js.map +1 -1
- package/dist/storage/implementation/models.d.ts +8 -0
- package/package.json +6 -6
- package/src/migrations/db/migrations/1741697235857-bucket-state-index.ts +40 -0
- package/src/storage/implementation/MongoCompactor.ts +19 -1
- package/src/storage/implementation/MongoSyncBucketStorage.ts +130 -13
- package/src/storage/implementation/PersistedBatch.ts +55 -0
- package/src/storage/implementation/db.ts +4 -0
- package/src/storage/implementation/models.ts +9 -0
- package/test/src/setup.ts +3 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/storage/implementation/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,SAAS,MAAM,gCAAgC,CAAC;AAE5D,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/storage/implementation/db.ts"],"names":[],"mappings":"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"}
|
|
@@ -69,6 +69,14 @@ export interface SourceTableDocument {
|
|
|
69
69
|
}[] | undefined;
|
|
70
70
|
snapshot_done: boolean | undefined;
|
|
71
71
|
}
|
|
72
|
+
export interface BucketStateDocument {
|
|
73
|
+
_id: {
|
|
74
|
+
g: number;
|
|
75
|
+
b: string;
|
|
76
|
+
};
|
|
77
|
+
last_op: bigint;
|
|
78
|
+
op_count: number;
|
|
79
|
+
}
|
|
72
80
|
export interface IdSequenceDocument {
|
|
73
81
|
_id: string;
|
|
74
82
|
op_id: bigint;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@powersync/service-module-mongodb-storage",
|
|
3
3
|
"repository": "https://github.com/powersync-ja/powersync-service",
|
|
4
4
|
"types": "dist/index.d.ts",
|
|
5
|
-
"version": "0.0.0-dev-
|
|
5
|
+
"version": "0.0.0-dev-20250312112247",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"license": "FSL-1.1-Apache-2.0",
|
|
8
8
|
"type": "module",
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"lru-cache": "^10.2.2",
|
|
29
29
|
"uuid": "^9.0.1",
|
|
30
30
|
"@powersync/lib-services-framework": "0.5.3",
|
|
31
|
-
"@powersync/service-core": "0.0.0-dev-
|
|
31
|
+
"@powersync/service-core": "0.0.0-dev-20250312112247",
|
|
32
32
|
"@powersync/service-jsonbig": "0.17.10",
|
|
33
|
-
"@powersync/service-sync-rules": "0.
|
|
34
|
-
"@powersync/service-types": "0.
|
|
35
|
-
"@powersync/lib-service-mongodb": "0.0.0-dev-
|
|
33
|
+
"@powersync/service-sync-rules": "0.0.0-dev-20250312112247",
|
|
34
|
+
"@powersync/service-types": "0.9.0",
|
|
35
|
+
"@powersync/lib-service-mongodb": "0.0.0-dev-20250312112247"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/uuid": "^9.0.4",
|
|
39
|
-
"@powersync/service-core-tests": "0.0.0-dev-
|
|
39
|
+
"@powersync/service-core-tests": "0.0.0-dev-20250312112247"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "tsc -b",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { migrations } from '@powersync/service-core';
|
|
2
|
+
import * as storage from '../../../storage/storage-index.js';
|
|
3
|
+
import { MongoStorageConfig } from '../../../types/types.js';
|
|
4
|
+
|
|
5
|
+
const INDEX_NAME = 'bucket_updates';
|
|
6
|
+
|
|
7
|
+
export const up: migrations.PowerSyncMigrationFunction = async (context) => {
|
|
8
|
+
const {
|
|
9
|
+
service_context: { configuration }
|
|
10
|
+
} = context;
|
|
11
|
+
const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await db.bucket_state.createIndex(
|
|
15
|
+
{
|
|
16
|
+
'_id.g': 1,
|
|
17
|
+
last_op: 1
|
|
18
|
+
},
|
|
19
|
+
{ name: INDEX_NAME, unique: true }
|
|
20
|
+
);
|
|
21
|
+
} finally {
|
|
22
|
+
await db.client.close();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const down: migrations.PowerSyncMigrationFunction = async (context) => {
|
|
27
|
+
const {
|
|
28
|
+
service_context: { configuration }
|
|
29
|
+
} = context;
|
|
30
|
+
|
|
31
|
+
const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (await db.bucket_state.indexExists(INDEX_NAME)) {
|
|
35
|
+
await db.bucket_state.dropIndex(INDEX_NAME);
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
await db.client.close();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -314,10 +314,12 @@ export class MongoCompactor {
|
|
|
314
314
|
let lastOpId: BucketDataKey | null = null;
|
|
315
315
|
let targetOp: bigint | null = null;
|
|
316
316
|
let gotAnOp = false;
|
|
317
|
+
let numberOfOpsToClear = 0;
|
|
317
318
|
for await (let op of query.stream()) {
|
|
318
319
|
if (op.op == 'MOVE' || op.op == 'REMOVE' || op.op == 'CLEAR') {
|
|
319
320
|
checksum = utils.addChecksums(checksum, op.checksum);
|
|
320
321
|
lastOpId = op._id;
|
|
322
|
+
numberOfOpsToClear += 1;
|
|
321
323
|
if (op.op != 'CLEAR') {
|
|
322
324
|
gotAnOp = true;
|
|
323
325
|
}
|
|
@@ -337,7 +339,7 @@ export class MongoCompactor {
|
|
|
337
339
|
return;
|
|
338
340
|
}
|
|
339
341
|
|
|
340
|
-
logger.info(`Flushing CLEAR at ${lastOpId?.o}`);
|
|
342
|
+
logger.info(`Flushing CLEAR for ${numberOfOpsToClear} ops at ${lastOpId?.o}`);
|
|
341
343
|
await this.db.bucket_data.deleteMany(
|
|
342
344
|
{
|
|
343
345
|
_id: {
|
|
@@ -362,6 +364,22 @@ export class MongoCompactor {
|
|
|
362
364
|
},
|
|
363
365
|
{ session }
|
|
364
366
|
);
|
|
367
|
+
|
|
368
|
+
// Note: This does not update anything if there is no existing state
|
|
369
|
+
await this.db.bucket_state.updateOne(
|
|
370
|
+
{
|
|
371
|
+
_id: {
|
|
372
|
+
g: this.group_id,
|
|
373
|
+
b: bucket
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
$inc: {
|
|
378
|
+
op_count: 1 - numberOfOpsToClear
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{ session }
|
|
382
|
+
);
|
|
365
383
|
},
|
|
366
384
|
{
|
|
367
385
|
writeConcern: { w: 'majority' },
|
|
@@ -9,27 +9,29 @@ import {
|
|
|
9
9
|
} from '@powersync/lib-services-framework';
|
|
10
10
|
import {
|
|
11
11
|
BroadcastIterable,
|
|
12
|
-
CHECKPOINT_INVALIDATE_ALL,
|
|
13
12
|
CheckpointChanges,
|
|
14
13
|
GetCheckpointChangesOptions,
|
|
15
14
|
InternalOpId,
|
|
16
15
|
internalToExternalOpId,
|
|
17
16
|
ProtocolOpId,
|
|
18
17
|
ReplicationCheckpoint,
|
|
19
|
-
SourceTable,
|
|
20
18
|
storage,
|
|
21
19
|
utils,
|
|
22
|
-
WatchWriteCheckpointOptions
|
|
20
|
+
WatchWriteCheckpointOptions,
|
|
21
|
+
CHECKPOINT_INVALIDATE_ALL,
|
|
22
|
+
deserializeParameterLookup
|
|
23
23
|
} from '@powersync/service-core';
|
|
24
|
-
import { SqliteJsonRow,
|
|
24
|
+
import { SqliteJsonRow, ParameterLookup, SqlSyncRules } from '@powersync/service-sync-rules';
|
|
25
25
|
import * as bson from 'bson';
|
|
26
26
|
import { wrapWithAbort } from 'ix/asynciterable/operators/withabort.js';
|
|
27
|
+
import { LRUCache } from 'lru-cache';
|
|
27
28
|
import * as timers from 'timers/promises';
|
|
28
29
|
import { MongoBucketStorage } from '../MongoBucketStorage.js';
|
|
29
30
|
import { PowerSyncMongo } from './db.js';
|
|
30
31
|
import {
|
|
31
32
|
BucketDataDocument,
|
|
32
33
|
BucketDataKey,
|
|
34
|
+
BucketStateDocument,
|
|
33
35
|
SourceKey,
|
|
34
36
|
SourceTableDocument,
|
|
35
37
|
SyncRuleCheckpointState,
|
|
@@ -39,6 +41,7 @@ import { MongoBucketBatch } from './MongoBucketBatch.js';
|
|
|
39
41
|
import { MongoCompactor } from './MongoCompactor.js';
|
|
40
42
|
import { MongoWriteCheckpointAPI } from './MongoWriteCheckpointAPI.js';
|
|
41
43
|
import { idPrefixFilter, mapOpEntry, readSingleBatch } from './util.js';
|
|
44
|
+
import { JSONBig } from '@powersync/service-jsonbig';
|
|
42
45
|
|
|
43
46
|
export class MongoSyncBucketStorage
|
|
44
47
|
extends BaseObserver<storage.SyncRulesBucketStorageListener>
|
|
@@ -154,7 +157,7 @@ export class MongoSyncBucketStorage
|
|
|
154
157
|
|
|
155
158
|
await callback(batch);
|
|
156
159
|
await batch.flush();
|
|
157
|
-
if (batch.last_flushed_op) {
|
|
160
|
+
if (batch.last_flushed_op != null) {
|
|
158
161
|
return { flushed_op: batch.last_flushed_op };
|
|
159
162
|
} else {
|
|
160
163
|
return null;
|
|
@@ -252,7 +255,7 @@ export class MongoSyncBucketStorage
|
|
|
252
255
|
return result!;
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
async getParameterSets(checkpoint: utils.InternalOpId, lookups:
|
|
258
|
+
async getParameterSets(checkpoint: utils.InternalOpId, lookups: ParameterLookup[]): Promise<SqliteJsonRow[]> {
|
|
256
259
|
const lookupFilter = lookups.map((lookup) => {
|
|
257
260
|
return storage.serializeLookup(lookup);
|
|
258
261
|
});
|
|
@@ -585,6 +588,13 @@ export class MongoSyncBucketStorage
|
|
|
585
588
|
{ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
|
|
586
589
|
);
|
|
587
590
|
|
|
591
|
+
await this.db.bucket_state.deleteMany(
|
|
592
|
+
{
|
|
593
|
+
_id: idPrefixFilter<BucketStateDocument['_id']>({ g: this.group_id }, ['b'])
|
|
594
|
+
},
|
|
595
|
+
{ maxTimeMS: lib_mongo.db.MONGO_CLEAR_OPERATION_TIMEOUT_MS }
|
|
596
|
+
);
|
|
597
|
+
|
|
588
598
|
await this.db.source_tables.deleteMany(
|
|
589
599
|
{
|
|
590
600
|
group_id: this.group_id
|
|
@@ -795,12 +805,7 @@ export class MongoSyncBucketStorage
|
|
|
795
805
|
|
|
796
806
|
const updates: CheckpointChanges =
|
|
797
807
|
lastCheckpoint == null
|
|
798
|
-
?
|
|
799
|
-
invalidateDataBuckets: true,
|
|
800
|
-
invalidateParameterBuckets: true,
|
|
801
|
-
updatedDataBuckets: [],
|
|
802
|
-
updatedParameterBucketDefinitions: []
|
|
803
|
-
}
|
|
808
|
+
? CHECKPOINT_INVALIDATE_ALL
|
|
804
809
|
: await this.getCheckpointChanges({
|
|
805
810
|
lastCheckpoint: lastCheckpoint,
|
|
806
811
|
nextCheckpoint: checkpoint
|
|
@@ -869,7 +874,119 @@ export class MongoSyncBucketStorage
|
|
|
869
874
|
return pipeline;
|
|
870
875
|
}
|
|
871
876
|
|
|
877
|
+
private async getDataBucketChanges(
|
|
878
|
+
options: GetCheckpointChangesOptions
|
|
879
|
+
): Promise<Pick<CheckpointChanges, 'updatedDataBuckets' | 'invalidateDataBuckets'>> {
|
|
880
|
+
const bucketStateUpdates = await this.db.bucket_state
|
|
881
|
+
.find(
|
|
882
|
+
{
|
|
883
|
+
// We have an index on (_id.g, last_op).
|
|
884
|
+
'_id.g': this.group_id,
|
|
885
|
+
last_op: { $gt: BigInt(options.lastCheckpoint) }
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
projection: {
|
|
889
|
+
'_id.b': 1
|
|
890
|
+
},
|
|
891
|
+
limit: 1001,
|
|
892
|
+
batchSize: 1001,
|
|
893
|
+
singleBatch: true
|
|
894
|
+
}
|
|
895
|
+
)
|
|
896
|
+
.toArray();
|
|
897
|
+
|
|
898
|
+
const buckets = bucketStateUpdates.map((doc) => doc._id.b);
|
|
899
|
+
const invalidateDataBuckets = buckets.length > 1000;
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
invalidateDataBuckets: invalidateDataBuckets,
|
|
903
|
+
updatedDataBuckets: invalidateDataBuckets ? [] : buckets
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private async getParameterBucketChanges(
|
|
908
|
+
options: GetCheckpointChangesOptions
|
|
909
|
+
): Promise<Pick<CheckpointChanges, 'updatedParameterLookups' | 'invalidateParameterBuckets'>> {
|
|
910
|
+
// TODO: limit max query running time
|
|
911
|
+
const parameterUpdates = await this.db.bucket_parameters
|
|
912
|
+
.find(
|
|
913
|
+
{
|
|
914
|
+
_id: { $gt: BigInt(options.lastCheckpoint), $lt: BigInt(options.nextCheckpoint) },
|
|
915
|
+
'key.g': this.group_id
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
projection: {
|
|
919
|
+
lookup: 1
|
|
920
|
+
},
|
|
921
|
+
limit: 1001,
|
|
922
|
+
batchSize: 1001,
|
|
923
|
+
singleBatch: true
|
|
924
|
+
}
|
|
925
|
+
)
|
|
926
|
+
.toArray();
|
|
927
|
+
const invalidateParameterUpdates = parameterUpdates.length > 1000;
|
|
928
|
+
|
|
929
|
+
return {
|
|
930
|
+
invalidateParameterBuckets: invalidateParameterUpdates,
|
|
931
|
+
updatedParameterLookups: invalidateParameterUpdates
|
|
932
|
+
? new Set<string>()
|
|
933
|
+
: new Set<string>(parameterUpdates.map((p) => JSONBig.stringify(deserializeParameterLookup(p.lookup))))
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// TODO:
|
|
938
|
+
// We can optimize this by implementing it like ChecksumCache: We can use partial cache results to do
|
|
939
|
+
// more efficient lookups in some cases.
|
|
940
|
+
private checkpointChangesCache = new LRUCache<string, CheckpointChanges, { options: GetCheckpointChangesOptions }>({
|
|
941
|
+
max: 50,
|
|
942
|
+
maxSize: 10 * 1024 * 1024,
|
|
943
|
+
sizeCalculation: (value: CheckpointChanges) => {
|
|
944
|
+
const paramSize = [...value.updatedParameterLookups].reduce<number>((a, b) => a + b.length, 0);
|
|
945
|
+
const bucketSize = [...value.updatedDataBuckets].reduce<number>((a, b) => a + b.length, 0);
|
|
946
|
+
return 100 + paramSize + bucketSize;
|
|
947
|
+
},
|
|
948
|
+
fetchMethod: async (_key, _staleValue, options) => {
|
|
949
|
+
return this.getCheckpointChangesInternal(options.context.options);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
private _hasDynamicBucketsCached: boolean | undefined = undefined;
|
|
954
|
+
|
|
955
|
+
private hasDynamicBucketQueries(): boolean {
|
|
956
|
+
if (this._hasDynamicBucketsCached != null) {
|
|
957
|
+
return this._hasDynamicBucketsCached;
|
|
958
|
+
}
|
|
959
|
+
const syncRules = this.getParsedSyncRules({
|
|
960
|
+
defaultSchema: 'default' // n/a
|
|
961
|
+
});
|
|
962
|
+
const hasDynamicBuckets = syncRules.hasDynamicBucketQueries();
|
|
963
|
+
this._hasDynamicBucketsCached = hasDynamicBuckets;
|
|
964
|
+
return hasDynamicBuckets;
|
|
965
|
+
}
|
|
966
|
+
|
|
872
967
|
async getCheckpointChanges(options: GetCheckpointChangesOptions): Promise<CheckpointChanges> {
|
|
873
|
-
|
|
968
|
+
if (!this.hasDynamicBucketQueries()) {
|
|
969
|
+
// Special case when we have no dynamic parameter queries.
|
|
970
|
+
// In this case, we can avoid doing any queries.
|
|
971
|
+
return {
|
|
972
|
+
invalidateDataBuckets: true,
|
|
973
|
+
updatedDataBuckets: [],
|
|
974
|
+
invalidateParameterBuckets: false,
|
|
975
|
+
updatedParameterLookups: new Set<string>()
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const key = `${options.lastCheckpoint}_${options.nextCheckpoint}`;
|
|
979
|
+
const result = await this.checkpointChangesCache.fetch(key, { context: { options } });
|
|
980
|
+
return result!;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private async getCheckpointChangesInternal(options: GetCheckpointChangesOptions): Promise<CheckpointChanges> {
|
|
984
|
+
const dataUpdates = await this.getDataBucketChanges(options);
|
|
985
|
+
const parameterUpdates = await this.getParameterBucketChanges(options);
|
|
986
|
+
|
|
987
|
+
return {
|
|
988
|
+
...dataUpdates,
|
|
989
|
+
...parameterUpdates
|
|
990
|
+
};
|
|
874
991
|
}
|
|
875
992
|
}
|
|
@@ -11,6 +11,7 @@ import { PowerSyncMongo } from './db.js';
|
|
|
11
11
|
import {
|
|
12
12
|
BucketDataDocument,
|
|
13
13
|
BucketParameterDocument,
|
|
14
|
+
BucketStateDocument,
|
|
14
15
|
CurrentBucket,
|
|
15
16
|
CurrentDataDocument,
|
|
16
17
|
SourceKey
|
|
@@ -48,6 +49,7 @@ export class PersistedBatch {
|
|
|
48
49
|
bucketData: mongo.AnyBulkWriteOperation<BucketDataDocument>[] = [];
|
|
49
50
|
bucketParameters: mongo.AnyBulkWriteOperation<BucketParameterDocument>[] = [];
|
|
50
51
|
currentData: mongo.AnyBulkWriteOperation<CurrentDataDocument>[] = [];
|
|
52
|
+
bucketStates: Map<string, BucketStateUpdate> = new Map();
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
55
|
* For debug logging only.
|
|
@@ -66,6 +68,19 @@ export class PersistedBatch {
|
|
|
66
68
|
this.currentSize = writtenSize;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
private incrementBucket(bucket: string, op_id: InternalOpId) {
|
|
72
|
+
let existingState = this.bucketStates.get(bucket);
|
|
73
|
+
if (existingState) {
|
|
74
|
+
existingState.lastOp = op_id;
|
|
75
|
+
existingState.incrementCount += 1;
|
|
76
|
+
} else {
|
|
77
|
+
this.bucketStates.set(bucket, {
|
|
78
|
+
lastOp: op_id,
|
|
79
|
+
incrementCount: 1
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
saveBucketData(options: {
|
|
70
85
|
op_seq: MongoIdSequence;
|
|
71
86
|
sourceKey: storage.ReplicaId;
|
|
@@ -120,6 +135,7 @@ export class PersistedBatch {
|
|
|
120
135
|
}
|
|
121
136
|
}
|
|
122
137
|
});
|
|
138
|
+
this.incrementBucket(k.bucket, op_id);
|
|
123
139
|
}
|
|
124
140
|
|
|
125
141
|
for (let bd of remaining_buckets.values()) {
|
|
@@ -147,6 +163,7 @@ export class PersistedBatch {
|
|
|
147
163
|
}
|
|
148
164
|
});
|
|
149
165
|
this.currentSize += 200;
|
|
166
|
+
this.incrementBucket(bd.bucket, op_id);
|
|
150
167
|
}
|
|
151
168
|
}
|
|
152
169
|
|
|
@@ -277,6 +294,14 @@ export class PersistedBatch {
|
|
|
277
294
|
});
|
|
278
295
|
}
|
|
279
296
|
|
|
297
|
+
if (this.bucketStates.size > 0) {
|
|
298
|
+
await db.bucket_state.bulkWrite(this.getBucketStateUpdates(), {
|
|
299
|
+
session,
|
|
300
|
+
// Per-bucket operation - order doesn't matter
|
|
301
|
+
ordered: false
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
280
305
|
const duration = performance.now() - startAt;
|
|
281
306
|
logger.info(
|
|
282
307
|
`powersync_${this.group_id} Flushed ${this.bucketData.length} + ${this.bucketParameters.length} + ${
|
|
@@ -287,7 +312,37 @@ export class PersistedBatch {
|
|
|
287
312
|
this.bucketData = [];
|
|
288
313
|
this.bucketParameters = [];
|
|
289
314
|
this.currentData = [];
|
|
315
|
+
this.bucketStates.clear();
|
|
290
316
|
this.currentSize = 0;
|
|
291
317
|
this.debugLastOpId = null;
|
|
292
318
|
}
|
|
319
|
+
|
|
320
|
+
private getBucketStateUpdates(): mongo.AnyBulkWriteOperation<BucketStateDocument>[] {
|
|
321
|
+
return Array.from(this.bucketStates.entries()).map(([bucket, state]) => {
|
|
322
|
+
return {
|
|
323
|
+
updateOne: {
|
|
324
|
+
filter: {
|
|
325
|
+
_id: {
|
|
326
|
+
g: this.group_id,
|
|
327
|
+
b: bucket
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
update: {
|
|
331
|
+
$set: {
|
|
332
|
+
last_op: state.lastOp
|
|
333
|
+
},
|
|
334
|
+
$inc: {
|
|
335
|
+
op_count: state.incrementCount
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
upsert: true
|
|
339
|
+
}
|
|
340
|
+
} satisfies mongo.AnyBulkWriteOperation<BucketStateDocument>;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interface BucketStateUpdate {
|
|
346
|
+
lastOp: InternalOpId;
|
|
347
|
+
incrementCount: number;
|
|
293
348
|
}
|
|
@@ -6,6 +6,7 @@ import { MongoStorageConfig } from '../../types/types.js';
|
|
|
6
6
|
import {
|
|
7
7
|
BucketDataDocument,
|
|
8
8
|
BucketParameterDocument,
|
|
9
|
+
BucketStateDocument,
|
|
9
10
|
CurrentDataDocument,
|
|
10
11
|
CustomWriteCheckpointDocument,
|
|
11
12
|
IdSequenceDocument,
|
|
@@ -33,6 +34,7 @@ export class PowerSyncMongo {
|
|
|
33
34
|
readonly write_checkpoints: mongo.Collection<WriteCheckpointDocument>;
|
|
34
35
|
readonly instance: mongo.Collection<InstanceDocument>;
|
|
35
36
|
readonly locks: mongo.Collection<lib_mongo.locks.Lock>;
|
|
37
|
+
readonly bucket_state: mongo.Collection<BucketStateDocument>;
|
|
36
38
|
|
|
37
39
|
readonly client: mongo.MongoClient;
|
|
38
40
|
readonly db: mongo.Db;
|
|
@@ -55,6 +57,7 @@ export class PowerSyncMongo {
|
|
|
55
57
|
this.write_checkpoints = db.collection('write_checkpoints');
|
|
56
58
|
this.instance = db.collection('instance');
|
|
57
59
|
this.locks = this.db.collection('locks');
|
|
60
|
+
this.bucket_state = this.db.collection('bucket_state');
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
@@ -70,6 +73,7 @@ export class PowerSyncMongo {
|
|
|
70
73
|
await this.write_checkpoints.deleteMany({});
|
|
71
74
|
await this.instance.deleteOne({});
|
|
72
75
|
await this.locks.deleteMany({});
|
|
76
|
+
await this.bucket_state.deleteMany({});
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/**
|
|
@@ -75,6 +75,15 @@ export interface SourceTableDocument {
|
|
|
75
75
|
snapshot_done: boolean | undefined;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export interface BucketStateDocument {
|
|
79
|
+
_id: {
|
|
80
|
+
g: number;
|
|
81
|
+
b: string;
|
|
82
|
+
};
|
|
83
|
+
last_op: bigint;
|
|
84
|
+
op_count: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
export interface IdSequenceDocument {
|
|
79
88
|
_id: string;
|
|
80
89
|
op_id: bigint;
|
package/test/src/setup.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { container } from '@powersync/lib-services-framework';
|
|
2
|
+
import { test_utils } from '@powersync/service-core-tests';
|
|
2
3
|
import { beforeAll, beforeEach } from 'vitest';
|
|
3
|
-
import { METRICS_HELPER } from '@powersync/service-core-tests';
|
|
4
4
|
|
|
5
5
|
beforeAll(async () => {
|
|
6
6
|
// Executes for every test file
|
|
7
7
|
container.registerDefaults();
|
|
8
|
+
await test_utils.initMetrics();
|
|
8
9
|
});
|
|
9
10
|
|
|
10
11
|
beforeEach(async () => {
|
|
11
|
-
|
|
12
|
+
await test_utils.resetMetrics();
|
|
12
13
|
});
|