import PouchDB from 'pouchdb'; import {DiagramEntity, DiagramEventType} from "../diagram/types/diagramEntity"; import {Color3, Observable} from "@babylonjs/core"; import {AppConfigType} from "../util/appConfigType"; import {v4 as uuidv4} from 'uuid'; import axios from "axios"; import {DiagramManager} from "../diagram/diagramManager"; import log, {Logger} from "loglevel"; const logger: Logger = log.getLogger('PouchdbPersistenceManager'); export class PouchdbPersistenceManager { configObserver: Observable = new Observable(); updateObserver: Observable = new Observable(); removeObserver: Observable = new Observable(); //implement IPersistenceManager interface with pouchdb apis private db: PouchDB; private remote: PouchDB; private diagramListings: PouchDB; private user: string; constructor() { this.diagramListings = new PouchDB("diagramListings"); } public setDiagramManager(diagramManager: DiagramManager) { diagramManager.onDiagramEventObservable.add((evt) => { logger.debug(evt); switch (evt.type) { case DiagramEventType.CHANGECOLOR: this.changeColor(evt.oldColor, evt.newColor); break; case DiagramEventType.ADD: this.add(evt.entity); break; case DiagramEventType.REMOVE: this.remove(evt.entity.id); break; case DiagramEventType.MODIFY: case DiagramEventType.DROP: this.modify(evt.entity); break; default: //this.logger.warn('App', 'unknown diagram event type', evt); } }, 2); this.updateObserver.add((evt) => { logger.debug(evt); diagramManager.onDiagramEventObservable.notifyObservers({ type: DiagramEventType.ADD, entity: evt }, 1); }); this.removeObserver.add((entity) => { logger.debug(entity); diagramManager.onDiagramEventObservable.notifyObservers( {type: DiagramEventType.REMOVE, entity: entity}, 1); }); } private _currentDiagramId: string; public get currentDiagramId(): string { return this._currentDiagramId; } public set currentDiagramId(value: string) { this._currentDiagramId = value; try { const listing = this.diagramListings.get(value); } catch (err) { this.diagramListings.put({_id: value, name: "New Diagram"}); } this.db = new PouchDB(value); this.db.sync(this.remote, {live: true}); } public async add(entity: DiagramEntity) { if (!entity) { return; } const newEntity = {...entity, _id: entity.id}; try { this.db.put(newEntity); } catch (err) { logger.error(err); } } public async remove(id: string) { if (!id) { return; } try { const doc = await this.db.get(id); this.db.remove(doc); } catch (err) { logger.error(err); } } public async modify(entity: DiagramEntity) { if (!entity) { return; } try { const doc = await this.db.get(entity.id); const newDoc = {...doc, ...entity}; this.db.put(newDoc); } catch (err) { logger.error(err); } } public async getNewRelicData(): Promise { return []; } public async setNewRelicData(data: any[]): Promise { return data; } public async setConfig(config: AppConfigType, initial: boolean = false) { if (!initial) { localStorage.setItem('config', JSON.stringify(config)); } else { localStorage.setItem('config', JSON.stringify(config)); } } public getConfig(): AppConfigType { return JSON.parse(localStorage.getItem('config')) as AppConfigType; } public async initialize() { try { let current = this.getPath(); const configString = localStorage.getItem('config'); const config = JSON.parse(configString) as AppConfigType; if (!current && config.currentDiagramId) { this.db = new PouchDB(config.currentDiagramId); await this.beginSync(config.currentDiagramId); } else { if (current) { config.currentDiagramId = current; } else { config.currentDiagramId = uuidv4(); } this.db = new PouchDB(config.currentDiagramId); await this.beginSync(config.currentDiagramId); localStorage.setItem('config', JSON.stringify(config)); } this.configObserver.notifyObservers(config); } catch (err) { const defaultConfig = { _id: '1', demoCompleted: false, gridSnap: 1, rotateSnap: 0, createSnap: 0, turnSnap: 0, flyMode: true, currentDiagramId: 'public' } try { await this.setConfig(defaultConfig, true); } catch (err) { logger.error(err); } this.diagramListings.put({_id: defaultConfig.currentDiagramId, name: "New Diagram"}); this.db = new PouchDB(defaultConfig.currentDiagramId); await this.beginSync(defaultConfig.currentDiagramId); this.configObserver.notifyObservers(defaultConfig); } try { const all = await this.db.allDocs({include_docs: true}); for (const entity of all.rows) { logger.debug(entity.doc); this.updateObserver.notifyObservers(entity.doc, 1); } } catch (err) { logger.error(err); } } private getPath(): string { const path = window.location.pathname.split('/'); if (path.length == 3 && path[1]) { return path[2]; } else { return null; } } async changeColor(oldColor: Color3, newColor: Color3) { const all = await this.db.allDocs({include_docs: true}); for (const entity of all.rows) { logger.debug(`comparing ${entity.doc.color} to ${oldColor.toHexString()}`); if (entity.doc.color == oldColor.toHexString()) { entity.doc.color = newColor.toHexString(); this.db.put({...entity.doc, _rev: entity.doc._rev}); } } } sync() { } private syncDoc(info) { logger.debug(info); if (info.direction == 'pull') { const docs = info.change.docs; for (const doc of docs) { logger.debug(doc); if (doc._deleted) { logger.debug('Delete', doc); this.removeObserver.notifyObservers({id: doc._id, template: doc.template}, 1); } else { this.updateObserver.notifyObservers(doc, 1); } } } } private async beginSync(localName: string) { try { //const remoteDbName = "db1"; const userHex = ascii_to_hex(localName); const remoteDbName = 'userdb-' + userHex; const remoteUserName = localName; const password = localName; const dbs = await axios.get(import.meta.env.VITE_SYNCDB_ENDPOINT + 'list'); logger.debug(dbs.data); if (dbs.data.indexOf(remoteDbName) == -1) { logger.warn('sync target missing attempting to create'); const newdb = await axios.post(import.meta.env.VITE_CREATE_ENDPOINT, { "_id": "org.couchdb.user:" + localName, "name": localName, "password": localName, "roles": ["readers"], "type": "user" } ); if (newdb.status == 200) { logger.info('sync target created'); } else { return; } } const userEndpoint: string = import.meta.env.VITE_USER_ENDPOINT logger.debug(userEndpoint); logger.debug(remoteDbName); const target = await axios.get(userEndpoint); if (target.status != 200) { logger.info(target.statusText); return; } if (target.data && target.data.userCtx) { if (!target.data.userCtx.name || target.data.userCtx.name != remoteUserName) { const buildTarget = await axios.post(userEndpoint, {username: remoteUserName, password: password}); if (buildTarget.status != 200) { logger.info(buildTarget.statusText); return; } else { this.user = buildTarget.data.userCtx; logger.debug(this.user); } } } const remoteEndpoint: string = import.meta.env.VITE_SYNCDB_ENDPOINT; logger.debug(remoteEndpoint + remoteDbName); this.remote = new PouchDB(remoteEndpoint + remoteDbName, {auth: {username: remoteUserName, password: password}, skip_setup: true}); const dbInfo = await this.remote.info(); logger.debug(dbInfo); this.db.sync(this.remote, {live: true, retry: true}) .on('change', (info) => { this.syncDoc(info) }) .on('active', function (info) { logger.debug('sync active', info) }) .on('paused', function (info) { logger.debug('sync paused', info) }) .on('error', function (err) { logger.error('sync error', err) }); } catch (err) { logger.error(err); } } } function hex_to_ascii(input) { var hex = input.toString(); let output = ''; for (let n = 0; n < hex.length; n += 2) { output += String.fromCharCode(parseInt(hex.substr(n, 2), 16)); } return output; } function ascii_to_hex(str) { const arr1 = []; for (let n = 0, l = str.length; n < l; n++) { var hex = Number(str.charCodeAt(n)).toString(16); arr1.push(hex); } return arr1.join(''); }