[]
As a supplementary example of Custom Database Adapter, this document provides a reference implementation of a filesystem-based database adapter.
import { promises as fs } from "fs";
import * as path from "path";
import { Db } from '@mescius/js-collaboration-ot';
/**
* Example directory structure: E:\\my-storage
* ├── room1/
* │ ├── documents.json
* │ └── operations
* └── op_0.json
* └── op_1.json
* │ └── snapshot_fragments
* └── default.json
* ├── room2/
* │ └── ...
*/
export class FileStorage extends Db {
constructor(basePath) {
super();
this.basePath = basePath;
}
async getDocument(roomId) {
return this.readFile(this.getDocumentsPath(roomId));
}
async getSnapshot(roomId) {
const document = await this.getDocument(roomId);
if (!document) return undefined;
const fragments = await this.getFragments(roomId);
return {
id: document.id,
v: document.snapshotVersion,
type: document.type,
fragments,
};
}
async getFragment(roomId, fragmentId) {
const fragmentPath = path.join(this.getSnapshotsPath(roomId), `${fragmentId}.json`);
return this.readFile(fragmentPath);
}
async getFragments(roomId) {
const fragmentsDir = this.getSnapshotsPath(roomId);
const files = await this.listFiles(fragmentsDir);
const fragments = {};
for (const file of files) {
const fragmentId = path.basename(file, '.json');
fragments[fragmentId] = await this.getFragment(roomId, fragmentId);
}
return fragments;
}
async getOps(roomId, fromVersion, toVersion) {
const operationsDir = this.getOperationsPath(roomId);
const files = await this.listFiles(operationsDir);
const ops = [];
for (const file of files) {
const version = parseInt(path.basename(file, '.json').split('_')[1], 10);
if (version >= fromVersion && (toVersion === undefined || version < toVersion)) {
const op = await this.readFile(path.join(operationsDir, file));
if (op) ops.push(op);
}
}
return ops.sort((a, b) => a.v - b.v);
}
async commitOp(id, op, document) {
const roomPath = this.getRoomPath(id);
await this.ensureDirExists(roomPath);
const currentDoc = await this.getDocument(id);
if (op.create) {
if (currentDoc) return false;
await this.writeDocumentFile(id, document);
await this.writeOpFile(id, op);
} else if (op.del) {
if (!currentDoc) return false;
await fs.rmdir(roomPath, { recursive: true });
} else {
if (!currentDoc || op.v !== currentDoc.version) return false;
await this.writeDocumentFile(id, document);
await this.writeOpFile(id, op);
}
return true;
}
async commitSnapshot(roomId, snapshot) {
const document = await this.getDocument(roomId);
if (!document || snapshot.fromVersion !== document.snapshotVersion || snapshot.v <= document.snapshotVersion) {
return false;
}
const snapshotsDir = this.getSnapshotsPath(roomId);
await this.ensureDirExists(snapshotsDir);
const { deleteSnapshot, createFragments, updateFragments, deleteFragments } = snapshot.fragmentsChanges;
if (deleteSnapshot) {
await fs.rmdir(snapshotsDir, { recursive: true });
} else {
if (createFragments) {
for (const [key, value] of Object.entries(createFragments)) {
const fragmentPath = path.join(snapshotsDir, `${key}.json`);
await this.writeFile(fragmentPath, value);
}
}
if (updateFragments) {
for (const [key, value] of Object.entries(updateFragments)) {
const fragmentPath = path.join(snapshotsDir, `${key}.json`);
await this.writeFile(fragmentPath, value);
}
}
if (deleteFragments) {
for (const key of deleteFragments) {
const fragmentPath = path.join(snapshotsDir, `${key}.json`);
await this.deleteFile(fragmentPath);
}
}
}
await this.writeDocumentFile(roomId, { ...document, snapshotVersion: snapshot.v });
return true;
}
async ensureDirExists(dirPath) {
try {
await fs.mkdir(dirPath, { recursive: true });
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
}
async writeDocumentFile(id, document) {
const docPath = this.getDocumentsPath(id);
await this.writeFile(docPath, document);
}
async writeOpFile(id, op) {
const dirPath = this.getOperationsPath(id);
const opPath = path.join(dirPath, `op_${op.v}.json`);
await this.ensureDirExists(dirPath);
await this.writeFile(opPath, op);
}
async writeFile(filePath, data) {
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
}
async readFile(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
} catch (err) {
if (err.code === 'ENOENT') return undefined;
throw err;
}
}
async deleteFile(filePath) {
try {
await fs.unlink(filePath);
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
}
async listFiles(dirPath) {
try {
return await fs.readdir(dirPath);
} catch (err) {
if (err.code === 'ENOENT') return [];
throw err;
}
}
getRoomPath(roomId) {
return path.join(this.basePath, roomId);
}
getDocumentsPath(roomId) {
return path.join(this.getRoomPath(roomId), 'documents.json');
}
getOperationsPath(roomId) {
return path.join(this.getRoomPath(roomId), 'operations');
}
getSnapshotsPath(roomId) {
return path.join(this.getRoomPath(roomId), 'snapshot_fragments');
}
async close() {
// No resources to close for file-based storage.
}
}