immersive2/src/integration/database/pouchData.ts
Michael Mainguy a772372b2b Add public URL sharing with express-pouchdb sync
- Add express-pouchdb for self-hosted PouchDB sync server
- Public databases (/db/public/:db) accessible without auth
- Add auth middleware for public/private database access
- Simplify share button to copy current URL to clipboard
- Move feature config from static JSON to dynamic API endpoint
- Add PouchDB sync to PouchData class for real-time collaboration
- Fix Express 5 compatibility by patching req.query
- Skip express.json() for /pouchdb routes (stream handling)
- Remove unused PouchdbPersistenceManager and old share system

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 11:49:56 -06:00

189 lines
7.3 KiB
TypeScript

import {Observable} from "@babylonjs/core";
import {DiagramEntity, DiagramEventType} from "../../diagram/types/diagramEntity";
import {DiagramManager} from "../../diagram/diagramManager";
import {DiagramEventObserverMask} from "../../diagram/types/diagramEventObserverMask";
import log, {Logger} from "loglevel";
import PouchDB from 'pouchdb';
import {importDiagramFromJSON, DiagramExport} from "../../util/functions/exportDiagramAsJSON";
import {isPublicPath, getRemoteDbPath} from "../../util/functions/getPath";
export class PouchData {
public readonly onDBEntityUpdateObservable: Observable<DiagramEntity> = new Observable<DiagramEntity>();
public readonly onDBEntityRemoveObservable: Observable<DiagramEntity> = new Observable<DiagramEntity>();
private _db: PouchDB;
private _remote: PouchDB;
private _diagramManager: DiagramManager;
private _logger: Logger = log.getLogger('PouchData');
private _dbName: string;
private _syncHandler: any;
constructor(dbname: string) {
this._db = new PouchDB(dbname);
this._dbName = dbname;
// Start sync for public databases
this.initSync();
}
/**
* Initialize sync with remote express-pouchdb for public databases
*/
private initSync() {
const remoteDbPath = getRemoteDbPath();
const isPublic = isPublicPath();
if (!remoteDbPath || !isPublic) {
this._logger.debug('[Sync] Not a public path, skipping remote sync');
return;
}
const remoteUrl = `${window.location.origin}/pouchdb/${remoteDbPath}`;
this._logger.info(`[Sync] Connecting to remote: ${remoteUrl}`);
this._remote = new PouchDB(remoteUrl);
// Start live bidirectional sync
this._syncHandler = this._db.sync(this._remote, { live: true, retry: true })
.on('change', (info) => {
this._logger.debug('[Sync] Change:', info.direction, info.change.docs.length, 'docs');
// Process incoming changes
if (info.direction === 'pull' && info.change && info.change.docs) {
info.change.docs.forEach((doc) => {
if (doc._deleted) {
this.onDBEntityRemoveObservable.notifyObservers(doc);
} else if (doc.id && doc.id !== 'metadata') {
this.onDBEntityUpdateObservable.notifyObservers(doc);
}
});
}
})
.on('paused', (info) => {
this._logger.debug('[Sync] Paused - up to date');
})
.on('active', () => {
this._logger.debug('[Sync] Active - syncing');
})
.on('error', (err) => {
this._logger.error('[Sync] Error:', err);
});
}
public setDiagramManager(diagramManager: DiagramManager) {
this._diagramManager = diagramManager;
diagramManager.onDiagramEventObservable.add((evt) => {
this._logger.debug(evt);
if (!evt?.entity) {
this._logger.warn('no entity');
return;
}
if (!evt?.entity?.id) {
this._logger.warn('no entity id');
return;
}
switch (evt.type) {
case DiagramEventType.REMOVE:
this.remove(evt.entity.id);
break;
case DiagramEventType.ADD:
case DiagramEventType.MODIFY:
case DiagramEventType.DROP:
this.upsert(evt.entity);
break;
default:
this._logger.warn('unknown diagram event type', evt);
}
}, DiagramEventObserverMask.TO_DB);
this.onDBEntityUpdateObservable.add((evt) => {
this._logger.debug(evt);
if (evt.id != 'metadata') {
diagramManager.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.ADD,
entity: evt
}, DiagramEventObserverMask.FROM_DB);
} else {
}
});
this.onDBEntityRemoveObservable.add((entity) => {
this._logger.debug(entity);
diagramManager.onDiagramEventObservable.notifyObservers(
{type: DiagramEventType.REMOVE, entity: entity}, DiagramEventObserverMask.FROM_DB);
});
this._db.allDocs({include_docs: true}).then(async (docs) => {
// Check if this is the demo database and it's empty
if (this._dbName === 'demo' && docs.rows.length === 0) {
this._logger.info('Demo database is empty, loading template...');
await this.loadDemoTemplate();
// Re-fetch docs after loading template
const updatedDocs = await this._db.allDocs({include_docs: true});
updatedDocs.rows.forEach((row) => {
if (row.doc.id != 'metadata') {
diagramManager.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.ADD,
entity: row.doc
}, DiagramEventObserverMask.FROM_DB);
}
});
} else {
docs.rows.forEach((row) => {
if (row.doc.id != 'metadata') {
diagramManager.onDiagramEventObservable.notifyObservers({
type: DiagramEventType.ADD,
entity: row.doc
}, DiagramEventObserverMask.FROM_DB);
}
});
}
});
}
private async loadDemoTemplate(): Promise<void> {
try {
// Fetch the demo template from public/templates/demo.json
const response = await fetch('/templates/demo.json');
if (!response.ok) {
this._logger.error('Failed to fetch demo template:', response.statusText);
return;
}
const templateData: DiagramExport = await response.json();
// Import the template into the current database
await importDiagramFromJSON(templateData, this._dbName);
this._logger.info('Demo template loaded successfully');
} catch (error) {
this._logger.error('Error loading demo template:', error);
}
}
public async remove(id: string) {
if (!id) {
return;
}
try {
const doc = await this._db.get(id);
await this._db.remove(doc);
} catch (err) {
this._logger.error(err);
}
}
public async upsert(entity: DiagramEntity) {
if (!entity) {
return;
}
if (entity.template == '#image-template' && !entity.image) {
this._logger.error('no image data', entity);
return;
}
let doc = null;
try {
doc = await this._db.get(entity.id, {conflicts: true, include_docs: true});
await this._db.put({_id: doc._id, _rev: doc._rev, ...entity});
} catch (err) {
await this._db.put({_id: entity.id, ...entity});
}
if (doc && doc._conflicts) {
this._logger.warn('CONFLICTS!', doc._conflicts);
}
}
}