@powersync/service-module-mongodb 0.16.0 → 0.17.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.
@@ -13,12 +13,15 @@ import {
13
13
  TimeValuePrecision
14
14
  } from '@powersync/service-sync-rules';
15
15
 
16
- import { ErrorCode, ServiceError } from '@powersync/lib-services-framework';
16
+ import { ErrorCode, logger, ServiceAssertionError, ServiceError } from '@powersync/lib-services-framework';
17
17
  import { MongoLSN } from '../common/MongoLSN.js';
18
- import { CHECKPOINTS_COLLECTION } from './replication-utils.js';
19
18
 
20
- export function getMongoRelation(source: mongo.ChangeStreamNameSpace): storage.SourceEntityDescriptor {
19
+ export function getMongoRelation(
20
+ source: mongo.ChangeStreamNameSpace,
21
+ connectionTag: string
22
+ ): storage.SourceEntityDescriptor {
21
23
  return {
24
+ connectionTag,
22
25
  name: source.coll,
23
26
  schema: source.db,
24
27
  // Not relevant for MongoDB - we use db + coll name as the identifier
@@ -175,30 +178,53 @@ export async function createCheckpoint(
175
178
  db: mongo.Db,
176
179
  id: mongo.ObjectId | string
177
180
  ): Promise<string> {
178
- const session = client.startSession();
179
- try {
180
- // We use an unique id per process, and clear documents on startup.
181
- // This is so that we can filter events for our own process only, and ignore
182
- // events from other processes.
183
- await db.collection(CHECKPOINTS_COLLECTION).findOneAndUpdate(
184
- {
185
- _id: id as any
186
- },
187
- {
188
- $inc: { i: 1 }
189
- },
190
- {
191
- upsert: true,
192
- returnDocument: 'after',
193
- session
181
+ const TRIES = 2;
182
+ for (let i = 0; i < TRIES; i++) {
183
+ try {
184
+ return await createCheckpointInner(client, db, id);
185
+ } catch (e) {
186
+ if (i < TRIES - 1) {
187
+ logger.warn(`Failed to create checkpoint on attempt ${i + 1}`, e);
188
+ } else {
189
+ throw e;
194
190
  }
195
- );
196
- const time = session.operationTime!;
197
- // TODO: Use the above when we support custom write checkpoints
198
- return new MongoLSN({ timestamp: time }).comparable;
199
- } finally {
200
- await session.endSession();
191
+ }
192
+ }
193
+ throw new ServiceAssertionError(`Unreachable code`);
194
+ }
195
+
196
+ async function createCheckpointInner(
197
+ client: mongo.MongoClient,
198
+ db: mongo.Db,
199
+ id: mongo.ObjectId | string
200
+ ): Promise<string> {
201
+ // We use an unique id per process, and clear documents on startup.
202
+ // This is so that we can filter events for our own process only, and ignore
203
+ // events from other processes.
204
+
205
+ // We use a command instead of a regular update to avoid auto retries on writes.
206
+ // An auto retry on the write can trigger a weird edge case where the change stream event
207
+ // has the clusterTime of the first write, while the returned operation time is for the second no-op write.
208
+ // Instead, we do manual retries, which does not have the same write de-duplication logic.
209
+ // A sentinal-based approach would be better here, but that is a much bigger change.
210
+
211
+ const response = await db.command({
212
+ findAndModify: '_powersync_checkpoints',
213
+ query: {
214
+ _id: id as any
215
+ },
216
+ new: true,
217
+ upsert: true,
218
+ update: {
219
+ $inc: { i: 1 }
220
+ }
221
+ });
222
+
223
+ const time = response.operationTime as mongo.Timestamp | undefined;
224
+ if (time == null) {
225
+ throw new ServiceError(ErrorCode.PSYNC_S1004, `clusterTime not available for checkpoint`);
201
226
  }
227
+ return new MongoLSN({ timestamp: time }).comparable;
202
228
  }
203
229
 
204
230
  const mongoTimeOptions: DateTimeSourceOptions = {