[]
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));
}import { DocumentServices, IDatabaseAdapter, MemoryDb } from '@mescius/js-collaboration-ot';
const dbAdapter: IDatabaseAdapter = new SampleDb();
const docService = new DocumentServices({ db: dbAdapter });