@powersync/service-module-mongodb-storage 0.0.0-dev-20250313091552 → 0.0.0-dev-20250317113118
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 +11 -4
- package/dist/storage/implementation/MongoSyncBucketStorage.d.ts +2 -2
- package/dist/storage/implementation/MongoSyncBucketStorage.js +37 -14
- package/dist/storage/implementation/MongoSyncBucketStorage.js.map +1 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.d.ts +6 -1
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js +166 -0
- package/dist/storage/implementation/MongoWriteCheckpointAPI.js.map +1 -1
- package/package.json +4 -4
- package/src/storage/implementation/MongoSyncBucketStorage.ts +49 -16
- package/src/storage/implementation/MongoWriteCheckpointAPI.ts +221 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { mongo } from '@powersync/lib-service-mongodb';
|
|
1
2
|
import * as framework from '@powersync/lib-services-framework';
|
|
2
|
-
import { storage } from '@powersync/service-core';
|
|
3
|
+
import { storage, WatchUserWriteCheckpointOptions, WriteCheckpointResult } from '@powersync/service-core';
|
|
3
4
|
import { PowerSyncMongo } from './db.js';
|
|
5
|
+
import { CustomWriteCheckpointDocument, WriteCheckpointDocument } from './models.js';
|
|
6
|
+
import { Demultiplexer, DemultiplexerValue } from '@powersync/service-core/src/streams/Demultiplexer.js';
|
|
4
7
|
|
|
5
8
|
export type MongoCheckpointAPIOptions = {
|
|
6
9
|
db: PowerSyncMongo;
|
|
@@ -93,6 +96,223 @@ export class MongoWriteCheckpointAPI implements storage.WriteCheckpointAPI {
|
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
watchUserWriteCheckpoint(options: WatchUserWriteCheckpointOptions): AsyncIterable<storage.WriteCheckpointResult> {
|
|
100
|
+
switch (this.writeCheckpointMode) {
|
|
101
|
+
case storage.WriteCheckpointMode.CUSTOM:
|
|
102
|
+
return this.watchCustomWriteCheckpoint(options);
|
|
103
|
+
case storage.WriteCheckpointMode.MANAGED:
|
|
104
|
+
return this.watchManagedWriteCheckpoint(options);
|
|
105
|
+
default:
|
|
106
|
+
throw new Error('Invalid write checkpoint mode');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private sharedIter = new Demultiplexer<WriteCheckpointResult>((signal) => {
|
|
111
|
+
const clusterTimePromise = (async () => {
|
|
112
|
+
const hello = await this.db.db.command({ hello: 1 });
|
|
113
|
+
// Note: This is not valid on sharded clusters.
|
|
114
|
+
const startClusterTime = hello.lastWrite?.majorityOpTime?.ts as mongo.Timestamp;
|
|
115
|
+
startClusterTime;
|
|
116
|
+
return startClusterTime;
|
|
117
|
+
})();
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
iterator: this.watchAllManagedWriteCheckpoints(clusterTimePromise, signal),
|
|
121
|
+
getFirstValue: async (user_id: string) => {
|
|
122
|
+
// Potential race conditions we cater for:
|
|
123
|
+
|
|
124
|
+
// Case 1: changestream is behind.
|
|
125
|
+
// We get a doc now, then the same or older doc again later.
|
|
126
|
+
// No problem!
|
|
127
|
+
|
|
128
|
+
// Case 2: Query is behind. I.e. doc has been created, and emitted on the changestream, but the query doesn't see it yet.
|
|
129
|
+
// Not possible luckily, but can we make sure?
|
|
130
|
+
|
|
131
|
+
// Case 3: changestream delays openeing. A doc is created after our query here, but before the changestream is opened.
|
|
132
|
+
// Awaiting clusterTimePromise should be sufficient here, but as a sanity check we also confirm that our query
|
|
133
|
+
// timestamp is > the startClusterTime.
|
|
134
|
+
|
|
135
|
+
const changeStreamStart = await clusterTimePromise;
|
|
136
|
+
|
|
137
|
+
let doc = null as WriteCheckpointDocument | null;
|
|
138
|
+
let clusterTime = null as mongo.Timestamp | null;
|
|
139
|
+
|
|
140
|
+
await this.db.client.withSession(async (session) => {
|
|
141
|
+
doc = await this.db.write_checkpoints.findOne(
|
|
142
|
+
{
|
|
143
|
+
user_id: user_id
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
session
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
const time = session.clusterTime?.clusterTime ?? null;
|
|
150
|
+
clusterTime = time;
|
|
151
|
+
});
|
|
152
|
+
if (clusterTime == null) {
|
|
153
|
+
throw new framework.ServiceAssertionError('Could not get clusterTime for write checkpoint');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (clusterTime.lessThan(changeStreamStart)) {
|
|
157
|
+
throw new framework.ServiceAssertionError(
|
|
158
|
+
'clusterTime for write checkpoint is older than changestream start'
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (doc == null) {
|
|
163
|
+
return {
|
|
164
|
+
id: null,
|
|
165
|
+
lsn: null
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
id: doc.client_id,
|
|
171
|
+
lsn: doc.lsns['1']
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
private async *watchAllManagedWriteCheckpoints(
|
|
178
|
+
clusterTimePromise: Promise<mongo.BSON.Timestamp>,
|
|
179
|
+
signal: AbortSignal
|
|
180
|
+
): AsyncGenerator<DemultiplexerValue<WriteCheckpointResult>> {
|
|
181
|
+
const clusterTime = await clusterTimePromise;
|
|
182
|
+
|
|
183
|
+
const stream = this.db.write_checkpoints.watch(
|
|
184
|
+
[{ $match: { operationType: { $in: ['insert', 'update', 'replace'] } } }],
|
|
185
|
+
{
|
|
186
|
+
fullDocument: 'updateLookup',
|
|
187
|
+
startAtOperationTime: clusterTime
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const hello = await this.db.db.command({ hello: 1 });
|
|
192
|
+
// Note: This is not valid on sharded clusters.
|
|
193
|
+
const startClusterTime = hello.lastWrite?.majorityOpTime?.ts as mongo.Timestamp;
|
|
194
|
+
if (startClusterTime == null) {
|
|
195
|
+
throw new framework.ServiceAssertionError('Could not get clusterTime');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
signal.onabort = () => {
|
|
199
|
+
stream.close();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (signal.aborted) {
|
|
203
|
+
stream.close();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
for await (let event of stream) {
|
|
208
|
+
if (!('fullDocument' in event) || event.fullDocument == null) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const user_id = event.fullDocument.user_id;
|
|
213
|
+
yield {
|
|
214
|
+
key: user_id,
|
|
215
|
+
value: {
|
|
216
|
+
id: event.fullDocument.client_id,
|
|
217
|
+
lsn: event.fullDocument.lsns['1']
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async *watchManagedWriteCheckpoint(
|
|
224
|
+
options: WatchUserWriteCheckpointOptions
|
|
225
|
+
): AsyncIterable<storage.WriteCheckpointResult> {
|
|
226
|
+
const stream = this.sharedIter.subscribe(options.user_id, options.signal);
|
|
227
|
+
|
|
228
|
+
let lastId = -1n;
|
|
229
|
+
|
|
230
|
+
for await (let doc of stream) {
|
|
231
|
+
// Guard against out-of-order events
|
|
232
|
+
if (lastId == -1n || (doc.id != null && doc.id > lastId)) {
|
|
233
|
+
yield doc;
|
|
234
|
+
if (doc.id != null) {
|
|
235
|
+
lastId = doc.id;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async *watchCustomWriteCheckpoint(
|
|
242
|
+
options: WatchUserWriteCheckpointOptions
|
|
243
|
+
): AsyncIterable<storage.WriteCheckpointResult> {
|
|
244
|
+
const { user_id, sync_rules_id, signal } = options;
|
|
245
|
+
|
|
246
|
+
let doc = null as CustomWriteCheckpointDocument | null;
|
|
247
|
+
let clusterTime = null as mongo.Timestamp | null;
|
|
248
|
+
|
|
249
|
+
await this.db.client.withSession(async (session) => {
|
|
250
|
+
doc = await this.db.custom_write_checkpoints.findOne(
|
|
251
|
+
{
|
|
252
|
+
user_id: user_id,
|
|
253
|
+
sync_rules_id: sync_rules_id
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
session
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
const time = session.clusterTime?.clusterTime ?? null;
|
|
260
|
+
clusterTime = time;
|
|
261
|
+
});
|
|
262
|
+
if (clusterTime == null) {
|
|
263
|
+
throw new framework.ServiceAssertionError('Could not get clusterTime');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const stream = this.db.custom_write_checkpoints.watch(
|
|
267
|
+
[
|
|
268
|
+
{
|
|
269
|
+
$match: {
|
|
270
|
+
'fullDocument.user_id': user_id,
|
|
271
|
+
'fullDocument.sync_rules_id': sync_rules_id,
|
|
272
|
+
operationType: { $in: ['insert', 'update', 'replace'] }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
{
|
|
277
|
+
fullDocument: 'updateLookup',
|
|
278
|
+
startAtOperationTime: clusterTime
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
signal.onabort = () => {
|
|
283
|
+
stream.close();
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
if (signal.aborted) {
|
|
287
|
+
stream.close();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let lastId = -1n;
|
|
292
|
+
|
|
293
|
+
if (doc != null) {
|
|
294
|
+
yield {
|
|
295
|
+
id: doc.checkpoint,
|
|
296
|
+
lsn: null
|
|
297
|
+
};
|
|
298
|
+
lastId = doc.checkpoint;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for await (let event of stream) {
|
|
302
|
+
if (!('fullDocument' in event) || event.fullDocument == null) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
// Guard against out-of-order events
|
|
306
|
+
if (event.fullDocument.checkpoint > lastId) {
|
|
307
|
+
yield {
|
|
308
|
+
id: event.fullDocument.checkpoint,
|
|
309
|
+
lsn: null
|
|
310
|
+
};
|
|
311
|
+
lastId = event.fullDocument.checkpoint;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
96
316
|
protected async lastCustomWriteCheckpoint(filters: storage.CustomWriteCheckpointFilters) {
|
|
97
317
|
const { user_id, sync_rules_id } = filters;
|
|
98
318
|
const lastWriteCheckpoint = await this.db.custom_write_checkpoints.findOne({
|