UNPKG

fbp-graph

Version:
524 lines 20.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MemoryJournalStore = exports.JournalStore = exports.Journal = void 0; // FBP Graph Journal // (c) 2016-2020 Flowhub UG // (c) 2014 Jon Nordby // (c) 2013 Flowhub UG // (c) 2011-2012 Henri Bergius, Nemein // FBP Graph may be freely distributed under the MIT license const events_1 = require("events"); const clone = require("clone"); const JournalStore_1 = require("./JournalStore"); exports.JournalStore = JournalStore_1.default; const MemoryJournalStore_1 = require("./MemoryJournalStore"); exports.MemoryJournalStore = MemoryJournalStore_1.default; function entryToPrettyString(entry) { const a = entry.args; switch (entry.cmd) { case 'addNode': return `${a.id}(${a.component})`; case 'removeNode': return `DEL ${a.id}(${a.component})`; case 'renameNode': return `RENAME ${a.oldId} ${a.newId}`; case 'changeNode': return `META ${a.id}`; case 'addEdge': return `${a.from.node} ${a.from.port} -> ${a.to.port} ${a.to.node}`; case 'removeEdge': return `${a.from.node} ${a.from.port} -X> ${a.to.port} ${a.to.node}`; case 'changeEdge': return `META ${a.from.node} ${a.from.port} -> ${a.to.port} ${a.to.node}`; case 'addInitial': return `'${a.from.data}' -> ${a.to.port} ${a.to.node}`; case 'removeInitial': return `'${a.from.data}' -X> ${a.to.port} ${a.to.node}`; case 'startTransaction': return `>>> ${entry.rev}: ${a.id}`; case 'endTransaction': return `<<< ${entry.rev}: ${a.id}`; case 'changeProperties': return 'PROPERTIES'; case 'addGroup': return `GROUP ${a.name}`; case 'renameGroup': return `RENAME GROUP ${a.oldName} ${a.newName}`; case 'removeGroup': return `DEL GROUP ${a.name}`; case 'changeGroup': return `META GROUP ${a.name}`; case 'addInport': return `INPORT ${a.name}`; case 'removeInport': return `DEL INPORT ${a.name}`; case 'renameInport': return `RENAME INPORT ${a.oldId} ${a.newId}`; case 'changeInport': return `META INPORT ${a.name}`; case 'addOutport': return `OUTPORT ${a.name}`; case 'removeOutport': return `DEL OUTPORT ${a.name}`; case 'renameOutport': return `RENAME OUTPORT ${a.oldId} ${a.newId}`; case 'changeOutport': return `META OUTPORT ${a.name}`; default: throw new Error(`Unknown journal entry: ${entry.cmd}`); } } // To set, not just update (append) metadata function calculateMeta(oldMeta, newMeta) { const setMeta = {}; Object.keys(oldMeta).forEach((k) => { setMeta[k] = null; }); Object.keys(newMeta).forEach((k) => { const v = newMeta[k]; setMeta[k] = v; }); return setMeta; } // ## Journalling graph changes // // The Journal can follow graph changes, store them // and allows to recall previous revisions of the graph. // // Revisions stored in the journal follow the transactions of the graph. // It is not possible to operate on smaller changes than individual transactions. // Use startTransaction and endTransaction on Graph to structure the revisions logical changesets. class Journal extends events_1.EventEmitter { constructor(graph, metadata, store) { super(); this.graph = graph; // Entries added during this revision this.entries = []; // Whether we should respond to graph change notifications or not this.subscribed = true; this.store = store || new MemoryJournalStore_1.default(this.graph); if (this.store.countTransactions() === 0) { // Sync journal with current graph to start transaction history this.currentRevision = -1; this.startTransaction('initial', metadata || {}); this.graph.nodes.forEach((node) => { this.appendCommand('addNode', node); }); this.graph.edges.forEach((edge) => { this.appendCommand('addEdge', edge); }); this.graph.initializers.forEach((iip) => { this.appendCommand('addInitial', iip); }); if (Object.keys(this.graph.properties).length > 0) { this.appendCommand('changeProperties', this.graph.properties); } Object.keys(this.graph.inports).forEach((name) => { const port = this.graph.inports[name]; this.appendCommand('addInport', { name, port, }); }); Object.keys(this.graph.outports).forEach((name) => { const port = this.graph.outports[name]; this.appendCommand('addOutport', { name, port, }); }); this.graph.groups.forEach((group) => { this.appendCommand('addGroup', group); }); this.endTransaction('initial', metadata || {}); } else { // Persistent store, start with its latest rev this.currentRevision = this.store.lastRevision; } // Subscribe to graph changes this.graph.on('addNode', (node) => { this.appendCommand('addNode', node); }); this.graph.on('removeNode', (node) => { this.appendCommand('removeNode', node); }); this.graph.on('renameNode', (oldId, newId) => { const args = { oldId, newId, }; this.appendCommand('renameNode', args); }); this.graph.on('changeNode', (node, oldMeta) => { this.appendCommand('changeNode', { id: node.id, new: node.metadata, old: oldMeta, }); }); this.graph.on('addEdge', (edge) => { this.appendCommand('addEdge', edge); }); this.graph.on('removeEdge', (edge) => { this.appendCommand('removeEdge', edge); }); this.graph.on('changeEdge', (edge, oldMeta) => { this.appendCommand('changeEdge', { from: edge.from, to: edge.to, new: edge.metadata, old: oldMeta, }); }); this.graph.on('addInitial', (iip) => { this.appendCommand('addInitial', iip); }); this.graph.on('removeInitial', (iip) => { this.appendCommand('removeInitial', iip); }); this.graph.on('changeProperties', (newProps, oldProps) => this.appendCommand('changeProperties', { new: newProps, old: oldProps })); this.graph.on('addGroup', (group) => this.appendCommand('addGroup', group)); this.graph.on('renameGroup', (oldName, newName) => this.appendCommand('renameGroup', { oldName, newName, })); this.graph.on('removeGroup', (group) => this.appendCommand('removeGroup', group)); this.graph.on('changeGroup', (group, oldMeta) => this.appendCommand('changeGroup', { name: group.name, new: group.metadata, old: oldMeta })); this.graph.on('addExport', (exported) => this.appendCommand('addExport', exported)); this.graph.on('removeExport', (exported) => this.appendCommand('removeExport', exported)); this.graph.on('addInport', (name, port) => this.appendCommand('addInport', { name, port })); this.graph.on('removeInport', (name, port) => this.appendCommand('removeInport', { name, port })); this.graph.on('renameInport', (oldId, newId) => this.appendCommand('renameInport', { oldId, newId })); this.graph.on('changeInport', (name, port, oldMeta) => this.appendCommand('changeInport', { name, new: port.metadata, old: oldMeta })); this.graph.on('addOutport', (name, port) => this.appendCommand('addOutport', { name, port })); this.graph.on('removeOutport', (name, port) => this.appendCommand('removeOutport', { name, port })); this.graph.on('renameOutport', (oldId, newId) => this.appendCommand('renameOutport', { oldId, newId })); this.graph.on('changeOutport', (name, port, oldMeta) => this.appendCommand('changeOutport', { name, new: port.metadata, old: oldMeta })); this.graph.on('startTransaction', (id, meta) => { this.startTransaction(id, meta); }); this.graph.on('endTransaction', (id, meta) => { this.endTransaction(id, meta); }); } startTransaction(id, meta) { if (!this.subscribed) { return; } if (this.entries.length > 0) { throw Error('Inconsistent @entries'); } this.currentRevision += 1; this.appendCommand('startTransaction', { id, metadata: meta, }, this.currentRevision); } endTransaction(id, meta) { if (!this.subscribed) { return; } this.appendCommand('endTransaction', { id, metadata: meta, }, this.currentRevision); // TODO: this would be the place to refine @entries into // a minimal set of changes, like eliminating changes early in transaction // which were later reverted/overwritten this.store.putTransaction(this.currentRevision, this.entries); this.entries = []; } appendCommand(cmd, args, rev = null) { if (!this.subscribed) { return; } const entry = { cmd, args: clone(args), rev: rev, }; this.entries.push(entry); } executeEntry(entry) { const a = entry.args; switch (entry.cmd) { case 'addNode': { this.graph.addNode(a.id, a.component); break; } case 'removeNode': { this.graph.removeNode(a.id); break; } case 'renameNode': { this.graph.renameNode(a.oldId, a.newId); break; } case 'changeNode': { this.graph.setNodeMetadata(a.id, calculateMeta(a.old, a.new)); break; } case 'addEdge': { this.graph.addEdge(a.from.node, a.from.port, a.to.node, a.to.port); break; } case 'removeEdge': { this.graph.removeEdge(a.from.node, a.from.port, a.to.node, a.to.port); break; } case 'changeEdge': { this.graph.setEdgeMetadata(a.from.node, a.from.port, a.to.node, a.to.port, calculateMeta(a.old, a.new)); break; } case 'addInitial': { if (typeof a.to.index === 'number') { this.graph.addInitialIndex(a.from.data, a.to.node, a.to.port, a.to.index, a.metadata); } else { this.graph.addInitial(a.from.data, a.to.node, a.to.port, a.metadata); } break; } case 'removeInitial': { this.graph.removeInitial(a.to.node, a.to.port); break; } case 'startTransaction': { break; } case 'endTransaction': { break; } case 'changeProperties': { this.graph.setProperties(a.new); break; } case 'addGroup': { this.graph.addGroup(a.name, a.nodes, a.metadata); break; } case 'renameGroup': { this.graph.renameGroup(a.oldName, a.newName); break; } case 'removeGroup': { this.graph.removeGroup(a.name); break; } case 'changeGroup': { this.graph.setGroupMetadata(a.name, calculateMeta(a.old, a.new)); break; } case 'addInport': { this.graph.addInport(a.name, a.port.process, a.port.port, a.port.metadata); break; } case 'removeInport': { this.graph.removeInport(a.name); break; } case 'renameInport': { this.graph.renameInport(a.oldId, a.newId); break; } case 'changeInport': { this.graph.setInportMetadata(a.name, calculateMeta(a.old, a.new)); break; } case 'addOutport': { this.graph.addOutport(a.name, a.port.process, a.port.port, a.port.metadata(a.name)); break; } case 'removeOutport': { this.graph.removeOutport(a.name); break; } case 'renameOutport': { this.graph.renameOutport(a.oldId, a.newId); break; } case 'changeOutport': { this.graph.setOutportMetadata(a.name, calculateMeta(a.old, a.new)); break; } default: throw new Error(`Unknown journal entry: ${entry.cmd}`); } } executeEntryInversed(entry) { const a = entry.args; switch (entry.cmd) { case 'addNode': { this.graph.removeNode(a.id); break; } case 'removeNode': { this.graph.addNode(a.id, a.component); break; } case 'renameNode': { this.graph.renameNode(a.newId, a.oldId); break; } case 'changeNode': { this.graph.setNodeMetadata(a.id, calculateMeta(a.new, a.old)); break; } case 'addEdge': { this.graph.removeEdge(a.from.node, a.from.port, a.to.node, a.to.port); break; } case 'removeEdge': { this.graph.addEdge(a.from.node, a.from.port, a.to.node, a.to.port); break; } case 'changeEdge': { this.graph.setEdgeMetadata(a.from.node, a.from.port, a.to.node, a.to.port, calculateMeta(a.new, a.old)); break; } case 'addInitial': { this.graph.removeInitial(a.to.node, a.to.port); break; } case 'removeInitial': { if (typeof a.to.index === 'number') { this.graph.addInitialIndex(a.from.data, a.to.node, a.to.port, a.to.index, a.metadata); } else { this.graph.addInitial(a.from.data, a.to.node, a.to.port, a.metadata); } break; } case 'startTransaction': { break; } case 'endTransaction': { break; } case 'changeProperties': { this.graph.setProperties(a.old); break; } case 'addGroup': { this.graph.removeGroup(a.name); break; } case 'renameGroup': { this.graph.renameGroup(a.newName, a.oldName); break; } case 'removeGroup': { this.graph.addGroup(a.name, a.nodes, a.metadata); break; } case 'changeGroup': { this.graph.setGroupMetadata(a.name, calculateMeta(a.new, a.old)); break; } case 'addInport': { this.graph.removeInport(a.name); break; } case 'removeInport': { this.graph.addInport(a.name, a.port.process, a.port.port, a.port.metadata); break; } case 'renameInport': { this.graph.renameInport(a.newId, a.oldId); break; } case 'changeInport': { this.graph.setInportMetadata(a.name, calculateMeta(a.new, a.old)); break; } case 'addOutport': { this.graph.removeOutport(a.name); break; } case 'removeOutport': { this.graph.addOutport(a.name, a.port.process, a.port.port, a.port.metadata); break; } case 'renameOutport': { this.graph.renameOutport(a.newId, a.oldId); break; } case 'changeOutport': { this.graph.setOutportMetadata(a.name, calculateMeta(a.new, a.old)); break; } default: throw new Error(`Unknown journal entry: ${entry.cmd}`); } } moveToRevision(revId) { if (revId === this.currentRevision) { return; } this.subscribed = false; if (revId > this.currentRevision) { // Forward replay journal to revId for (let start = this.currentRevision + 1, r = start, end = revId, asc = start <= end; asc ? r <= end : r >= end; asc ? r += 1 : r -= 1) { this.store.fetchTransaction(r).forEach((entry) => { this.executeEntry(entry); }); } } else { // Move backwards, and apply inverse changes for (let r = this.currentRevision, end = revId + 1; r >= end; r -= 1) { // Apply entries in reverse order const entries = this.store.fetchTransaction(r).slice(0); entries.reverse(); entries.forEach((entry) => { this.executeEntryInversed(entry); }); } } this.currentRevision = revId; this.subscribed = true; } // ## Undoing & redoing // Undo the last graph change undo() { if (!this.canUndo()) { return; } this.moveToRevision(this.currentRevision - 1); } // If there is something to undo canUndo() { return this.currentRevision > 0; } // Redo the last undo redo() { if (!this.canRedo()) { return; } this.moveToRevision(this.currentRevision + 1); } // If there is something to redo canRedo() { return this.currentRevision < this.store.lastRevision; } // # Serializing // Render a pretty printed string of the journal. Changes are abbreviated toPrettyString(startRev = 0, endRevParam) { const endRev = endRevParam || this.store.lastRevision; const lines = []; for (let r = startRev, end = endRev, asc = startRev <= end; asc ? r < end : r > end; asc ? r += 1 : r -= 1) { const e = this.store.fetchTransaction(r); e.forEach((entry) => { lines.push(entryToPrettyString(entry)); }); } return lines.join('\n'); } // Serialize journal to JSON toJSON(startRev = 0, endRevParam = null) { const endRev = endRevParam || this.store.lastRevision; const entries = []; for (let r = startRev, end = endRev; r < end; r += 1) { const e = this.store.fetchTransaction(r); e.forEach((entry) => { entries.push(entryToPrettyString(entry)); }); } return entries; } save(file, callback) { const promise = new Promise((resolve, reject) => { const json = JSON.stringify(this.toJSON(), null, 4); const { writeFile } = require('fs'); writeFile(`${file}.json`, json, 'utf-8', (err) => { if (err) { reject(err); return; } resolve(); }); }); if (callback) { promise.then(() => { callback(null); }, callback); return; } return promise; } } exports.Journal = Journal; //# sourceMappingURL=Journal.js.map