import type { Cursor } from "@apibara/protocol"; import type { BulkWriteOptions, ClientSession, Collection, CollectionOptions, Db, DeleteOptions, Document, Filter, FindCursor, FindOneAndUpdateOptions, FindOptions, InsertManyResult, InsertOneOptions, InsertOneResult, MatchKeysAndValues, OptionalUnlessRequiredId, UpdateFilter, UpdateOptions, UpdateResult, WithId, } from "mongodb"; export class MongoStorage { constructor( private db: Db, private session: ClientSession, private endCursor?: Cursor, ) {} collection( name: string, options?: CollectionOptions, ) { const collection = this.db.collection(name, options); return new MongoCollection( this.session, collection, this.endCursor, ); } } export type MongoCursor = { from: bigint | null; to: bigint | null; }; export type CursoredSchema = TSchema & { _cursor: MongoCursor; }; export class MongoCollection { constructor( private session: ClientSession, private collection: Collection, private endCursor?: Cursor, ) {} async insertOne( doc: OptionalUnlessRequiredId, options?: InsertOneOptions, ): Promise> { return await this.collection.insertOne( { ...doc, _cursor: { from: this.endCursor?.orderKey ?? null, to: null, } as MongoCursor, }, { ...options, session: this.session }, ); } async insertMany( docs: ReadonlyArray>, options?: BulkWriteOptions, ): Promise> { return await this.collection.insertMany( docs.map((doc) => ({ ...doc, _cursor: { from: this.endCursor?.orderKey ?? null, to: null, } as MongoCursor, })), { ...options, session: this.session }, ); } async updateOne( filter: Filter, update: UpdateFilter, options?: UpdateOptions, ): Promise> { // 1. Find and update the document, getting the old version const oldDoc = await this.collection.findOneAndUpdate( { ...filter, "_cursor.to": null, }, { ...update, $set: { ...update.$set, "_cursor.from": this.endCursor?.orderKey ?? null, } as unknown as MatchKeysAndValues, }, { ...options, session: this.session, returnDocument: "before", } as FindOneAndUpdateOptions, ); // 2. If we found and updated a document, insert its old version if (oldDoc) { const { _id, ...doc } = oldDoc; await this.collection.insertOne( { ...doc, _cursor: { ...oldDoc._cursor, to: this.endCursor?.orderKey ?? null, }, } as unknown as OptionalUnlessRequiredId, { session: this.session }, ); } // 3. Return an UpdateResult-compatible object return { acknowledged: true, modifiedCount: oldDoc ? 1 : 0, upsertedId: null, upsertedCount: 0, matchedCount: oldDoc ? 1 : 0, }; } async updateMany( filter: Filter, update: UpdateFilter, options?: UpdateOptions, ): Promise> { // 1. Find all documents matching the filter that are latest (to: null) const oldDocs = await this.collection .find( { ...filter, "_cursor.to": null, }, { session: this.session }, ) .toArray(); // 2. Update to the new values with updateMany // (setting _cursor.from to endCursor, leaving _cursor.to unchanged) const updateResult = await this.collection.updateMany( { ...filter, "_cursor.to": null, }, { ...update, $set: { ...update.$set, "_cursor.from": this.endCursor?.orderKey ?? null, } as unknown as MatchKeysAndValues, }, { ...options, session: this.session }, ); // 3. Adjust the cursor.to of the old values const oldDocsWithUpdatedCursor = oldDocs.map(({ _id, ...doc }) => ({ ...doc, _cursor: { ...doc._cursor, to: this.endCursor?.orderKey ?? null, }, })); // 4. Insert the old values back into the db if (oldDocsWithUpdatedCursor.length > 0) { await this.collection.insertMany( oldDocsWithUpdatedCursor as unknown as OptionalUnlessRequiredId[], { session: this.session }, ); } return updateResult; } async deleteOne( filter: Filter, options?: DeleteOptions, ): Promise> { return await this.collection.updateOne( { ...filter, "_cursor.to": null, }, { $set: { "_cursor.to": this.endCursor?.orderKey ?? null, } as unknown as MatchKeysAndValues, }, { ...options, session: this.session }, ); } async deleteMany( filter?: Filter, options?: DeleteOptions, ): Promise> { return await this.collection.updateMany( { ...((filter ?? {}) as Filter), "_cursor.to": null, }, { $set: { "_cursor.to": this.endCursor?.orderKey ?? null, } as unknown as MatchKeysAndValues, }, { ...options, session: this.session }, ); } async findOne( filter: Filter, options?: Omit, ): Promise | null> { return await this.collection.findOne( { ...filter, "_cursor.to": null, }, { ...options, session: this.session }, ); } find( filter: Filter, options?: FindOptions, ): FindCursor> { return this.collection.find( { ...filter, "_cursor.to": null, }, { ...options, session: this.session }, ); } }