[]
        
(Showing Draft Content)

Custom Database Adapter

If the built-in adapters do not meet your needs, you can implement the IDatabaseAdapter interface to create a custom adapter.

Below is an example.

import { IOp, ISnapshot, IDocument, ICommitSnapshot, IDatabaseAdapter, Db } from '@mescius/js-collaboration-ot';

export class SampleDb<S = unknown, T = unknown> extends Db<S, T> implements IDatabaseAdapter<S, T> {
    private ops: Map<string, IOp<T>[]> = new Map();
    private documents: Map<string, IDocument> = new Map();
    private fragments: Map<string, Map<string, S>> = new Map();

    async getOps(id: string, from: number, to?: number): Promise<IOp<T>[]> {
        const allOps = this.ops.get(id) || [];
        const toVersion = to ?? Number.MAX_SAFE_INTEGER;
        return clone(allOps.filter(op => op.v >= from && op.v < toVersion));
    }

    async getDocument(id: string): Promise<IDocument | null> {
        if (!this.documents.has(id)) return null;
        return clone(this.documents.get(id));
    }

    async getSnapshot(id: string): Promise<ISnapshot<S> | null> {
        if (!this.documents.has(id)) return null;

        const doc = this.documents.get(id);
        const map = this.fragments.get(id);
        const fragments = map ? clone(Object.fromEntries(map)) : {};

        return {
            id,
            v: doc.snapshotVersion,
            type: doc.type,
            fragments: fragments
        };
    }

    async getFragment(id: string, fragmentId: string): Promise<{ version: number; data: S | null; } | null> {
        if (!this.documents.has(id)) return null;

        const doc = this.documents.get(id)!;
        const fragment = this.fragments.get(id)?.get(fragmentId) ?? null;
        return {
            version: doc.snapshotVersion,
            data: clone(fragment)
        };
    }

    async commitOp(id: string, op: IOp<T>, document: IDocument): Promise<boolean> {
        if (op.create) {
            if (this.documents.has(id)) return false;
            this.documents.set(id, clone(document));
            this.ops.set(id, [clone(op)]);
            this.fragments.set(id, new Map());
            return true;
        }

        if (!this.documents.has(id)) return false;

        const docRecord = this.documents.get(id)!;

        // Ensure snapshot version matches database version to prevent concurrent commit issues
        if (docRecord.version !== op.v) {
            return false;
        }

        this.ops.get(id).push(clone(op));
        docRecord.version = document.version;

        return true;
    }

    async commitSnapshot(id: string, snapshot: ICommitSnapshot<S>): Promise<boolean> {
        if (!this.documents.has(id)) return false;

        const doc = this.documents.get(id)!;
        // Verify snapshot starting version matches database version to prevent update conflicts
        if (doc.snapshotVersion !== snapshot.fromVersion) {
            return false;
        }

        doc.snapshotVersion = snapshot.v;

        if (snapshot.fragmentsChanges) {
            const { createFragments, updateFragments, deleteFragments, deleteSnapshot, setSnapshotFragments } = snapshot.fragmentsChanges;

            if (deleteSnapshot) {
                this.fragments.delete(id);
            } else if (setSnapshotFragments) {
                // Handle full replacement
                this.fragments.set(id, new Map(Object.entries(setSnapshotFragments as Record<string, S>)));
            } else {
                const fragments = this.fragments.get(id);

                // Handle creates and updates
                Object.entries({ ...createFragments, ...updateFragments }).forEach(([key, value]) => {
                    fragments.set(key, value as S);
                });

                // Handle deletes
                deleteFragments?.forEach(key => fragments.delete(key));
            }
        }
        return true;
    }

    async close(): Promise<void> {
        // Nothing to do for in-memory Database
    }
}

function clone<T>(obj: T): T {
    return JSON.parse(JSON.stringify(obj));
}

Use a Custom Database Adapter

import { DocumentServices, IDatabaseAdapter, MemoryDb } from '@mescius/js-collaboration-ot';

const dbAdapter: IDatabaseAdapter = new SampleDb();

const docService = new DocumentServices({ db: dbAdapter });

More Example

File Database Adapter