UNPKG

noflo

Version:

Flow-Based Programming environment for JavaScript

324 lines (285 loc) 13.6 kB
# NoFlo - Flow-Based Programming for JavaScript # (c) 2016 TheGrid (Rituwall Inc.) # (c) 2014 Jon Nordby # (c) 2013 TheGrid (Rituwall Inc.) # (c) 2011-2012 Henri Bergius, Nemein # NoFlo may be freely distributed under the MIT license # {EventEmitter} = require 'events' clone = require('./Utils').clone entryToPrettyString = (entry) -> a = entry.args return switch entry.cmd when 'addNode' then "#{a.id}(#{a.component})" when 'removeNode' then "DEL #{a.id}(#{a.component})" when 'renameNode' then "RENAME #{a.oldId} #{a.newId}" when 'changeNode' then "META #{a.id}" when 'addEdge' then "#{a.from.node} #{a.from.port} -> #{a.to.port} #{a.to.node}" when 'removeEdge' then "#{a.from.node} #{a.from.port} -X> #{a.to.port} #{a.to.node}" when 'changeEdge' then "META #{a.from.node} #{a.from.port} -> #{a.to.port} #{a.to.node}" when 'addInitial' then "'#{a.from.data}' -> #{a.to.port} #{a.to.node}" when 'removeInitial' then "'#{a.from.data}' -X> #{a.to.port} #{a.to.node}" when 'startTransaction' then ">>> #{entry.rev}: #{a.id}" when 'endTransaction' then "<<< #{entry.rev}: #{a.id}" when 'changeProperties' then "PROPERTIES" when 'addGroup' then "GROUP #{a.name}" when 'renameGroup' then "RENAME GROUP #{a.oldName} #{a.newName}" when 'removeGroup' then "DEL GROUP #{a.name}" when 'changeGroup' then "META GROUP #{a.name}" when 'addInport' then "INPORT #{a.name}" when 'removeInport' then "DEL INPORT #{a.name}" when 'renameInport' then "RENAME INPORT #{a.oldId} #{a.newId}" when 'changeInport' then "META INPORT #{a.name}" when 'addOutport' then "OUTPORT #{a.name}" when 'removeOutport' then "DEL OUTPORT #{a.name}" when 'renameOutport' then "RENAME OUTPORT #{a.oldId} #{a.newId}" when 'changeOutport' then "META OUTPORT #{a.name}" else throw new Error("Unknown journal entry: #{entry.cmd}") # To set, not just update (append) metadata calculateMeta = (oldMeta, newMeta) -> setMeta = {} for k, v of oldMeta setMeta[k] = null for k, v of newMeta setMeta[k] = v return setMeta class JournalStore extends EventEmitter lastRevision: 0 constructor: (@graph) -> @lastRevision = 0 putTransaction: (revId, entries) -> @lastRevision = revId if revId > @lastRevision @emit 'transaction', revId fetchTransaction: (revId, entries) -> class MemoryJournalStore extends JournalStore constructor: (graph) -> super graph @transactions = [] putTransaction: (revId, entries) -> super revId, entries @transactions[revId] = entries fetchTransaction: (revId) -> return @transactions[revId] # ## 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 EventEmitter graph: null entries: [] # Entries added during this revision subscribed: true # Whether we should respond to graph change notifications or not constructor: (graph, metadata, store) -> @graph = graph @entries = [] @subscribed = true @store = store || new MemoryJournalStore @graph if @store.transactions.length is 0 # Sync journal with current graph to start transaction history @currentRevision = -1 @startTransaction 'initial', metadata @appendCommand 'addNode', node for node in @graph.nodes @appendCommand 'addEdge', edge for edge in @graph.edges @appendCommand 'addInitial', iip for iip in @graph.initializers @appendCommand 'changeProperties', @graph.properties, {} if Object.keys(@graph.properties).length > 0 @appendCommand 'addInport', {name: k, port: v} for k,v of @graph.inports @appendCommand 'addOutport', {name: k, port: v} for k,v of @graph.outports @appendCommand 'addGroup', group for group in @graph.groups @endTransaction 'initial', metadata else # Persistent store, start with its latest rev @currentRevision = @store.lastRevision # Subscribe to graph changes @graph.on 'addNode', (node) => @appendCommand 'addNode', node @graph.on 'removeNode', (node) => @appendCommand 'removeNode', node @graph.on 'renameNode', (oldId, newId) => args = oldId: oldId newId: newId @appendCommand 'renameNode', args @graph.on 'changeNode', (node, oldMeta) => @appendCommand 'changeNode', {id: node.id, new: node.metadata, old: oldMeta} @graph.on 'addEdge', (edge) => @appendCommand 'addEdge', edge @graph.on 'removeEdge', (edge) => @appendCommand 'removeEdge', edge @graph.on 'changeEdge', (edge, oldMeta) => @appendCommand 'changeEdge', {from: edge.from, to: edge.to, new: edge.metadata, old: oldMeta} @graph.on 'addInitial', (iip) => @appendCommand 'addInitial', iip @graph.on 'removeInitial', (iip) => @appendCommand 'removeInitial', iip @graph.on 'changeProperties', (newProps, oldProps) => @appendCommand 'changeProperties', {new: newProps, old: oldProps} @graph.on 'addGroup', (group) => @appendCommand 'addGroup', group @graph.on 'renameGroup', (oldName, newName) => @appendCommand 'renameGroup', oldName: oldName newName: newName @graph.on 'removeGroup', (group) => @appendCommand 'removeGroup', group @graph.on 'changeGroup', (group, oldMeta) => @appendCommand 'changeGroup', {name: group.name, new: group.metadata, old: oldMeta} @graph.on 'addExport', (exported) => @appendCommand 'addExport', exported @graph.on 'removeExport', (exported) => @appendCommand 'removeExport', exported @graph.on 'addInport', (name, port) => @appendCommand 'addInport', {name: name, port: port} @graph.on 'removeInport', (name, port) => @appendCommand 'removeInport', {name: name, port: port} @graph.on 'renameInport', (oldId, newId) => @appendCommand 'renameInport', {oldId: oldId, newId: newId} @graph.on 'changeInport', (name, port, oldMeta) => @appendCommand 'changeInport', {name: name, new: port.metadata, old: oldMeta} @graph.on 'addOutport', (name, port) => @appendCommand 'addOutport', {name: name, port: port} @graph.on 'removeOutport', (name, port) => @appendCommand 'removeOutport', {name: name, port: port} @graph.on 'renameOutport', (oldId, newId) => @appendCommand 'renameOutport', {oldId: oldId, newId: newId} @graph.on 'changeOutport', (name, port, oldMeta) => @appendCommand 'changeOutport', {name: name, new: port.metadata, old: oldMeta} @graph.on 'startTransaction', (id, meta) => @startTransaction id, meta @graph.on 'endTransaction', (id, meta) => @endTransaction id, meta startTransaction: (id, meta) => return if not @subscribed if @entries.length > 0 throw Error("Inconsistent @entries") @currentRevision++ @appendCommand 'startTransaction', {id: id, metadata: meta}, @currentRevision endTransaction: (id, meta) => return if not @subscribed @appendCommand 'endTransaction', {id: id, metadata: meta}, @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 @store.putTransaction @currentRevision, @entries @entries = [] appendCommand: (cmd, args, rev) -> return if not @subscribed entry = cmd: cmd args: clone args entry.rev = rev if rev? @entries.push(entry) executeEntry: (entry) -> a = entry.args switch entry.cmd when 'addNode' then @graph.addNode a.id, a.component when 'removeNode' then @graph.removeNode a.id when 'renameNode' then @graph.renameNode a.oldId, a.newId when 'changeNode' then @graph.setNodeMetadata a.id, calculateMeta(a.old, a.new) when 'addEdge' then @graph.addEdge a.from.node, a.from.port, a.to.node, a.to.port when 'removeEdge' then @graph.removeEdge a.from.node, a.from.port, a.to.node, a.to.port when 'changeEdge' then @graph.setEdgeMetadata a.from.node, a.from.port, a.to.node, a.to.port, calculateMeta(a.old, a.new) when 'addInitial' then @graph.addInitial a.from.data, a.to.node, a.to.port when 'removeInitial' then @graph.removeInitial a.to.node, a.to.port when 'startTransaction' then null when 'endTransaction' then null when 'changeProperties' then @graph.setProperties a.new when 'addGroup' then @graph.addGroup a.name, a.nodes, a.metadata when 'renameGroup' then @graph.renameGroup a.oldName, a.newName when 'removeGroup' then @graph.removeGroup a.name when 'changeGroup' then @graph.setGroupMetadata a.name, calculateMeta(a.old, a.new) when 'addInport' then @graph.addInport a.name, a.port.process, a.port.port, a.port.metadata when 'removeInport' then @graph.removeInport a.name when 'renameInport' then @graph.renameInport a.oldId, a.newId when 'changeInport' then @graph.setInportMetadata a.name, calculateMeta(a.old, a.new) when 'addOutport' then @graph.addOutport a.name, a.port.process, a.port.port, a.port.metadata a.name when 'removeOutport' then @graph.removeOutport when 'renameOutport' then @graph.renameOutport a.oldId, a.newId when 'changeOutport' then @graph.setOutportMetadata a.name, calculateMeta(a.old, a.new) else throw new Error("Unknown journal entry: #{entry.cmd}") executeEntryInversed: (entry) -> a = entry.args switch entry.cmd when 'addNode' then @graph.removeNode a.id when 'removeNode' then @graph.addNode a.id, a.component when 'renameNode' then @graph.renameNode a.newId, a.oldId when 'changeNode' then @graph.setNodeMetadata a.id, calculateMeta(a.new, a.old) when 'addEdge' then @graph.removeEdge a.from.node, a.from.port, a.to.node, a.to.port when 'removeEdge' then @graph.addEdge a.from.node, a.from.port, a.to.node, a.to.port when 'changeEdge' then @graph.setEdgeMetadata a.from.node, a.from.port, a.to.node, a.to.port, calculateMeta(a.new, a.old) when 'addInitial' then @graph.removeInitial a.to.node, a.to.port when 'removeInitial' then @graph.addInitial a.from.data, a.to.node, a.to.port when 'startTransaction' then null when 'endTransaction' then null when 'changeProperties' then @graph.setProperties a.old when 'addGroup' then @graph.removeGroup a.name when 'renameGroup' then @graph.renameGroup a.newName, a.oldName when 'removeGroup' then @graph.addGroup a.name, a.nodes, a.metadata when 'changeGroup' then @graph.setGroupMetadata a.name, calculateMeta(a.new, a.old) when 'addInport' then @graph.removeInport a.name when 'removeInport' then @graph.addInport a.name, a.port.process, a.port.port, a.port.metadata when 'renameInport' then @graph.renameInport a.newId, a.oldId when 'changeInport' then @graph.setInportMetadata a.name, calculateMeta(a.new, a.old) when 'addOutport' then @graph.removeOutport a.name when 'removeOutport' then @graph.addOutport a.name, a.port.process, a.port.port, a.port.metadata when 'renameOutport' then @graph.renameOutport a.newId, a.oldId when 'changeOutport' then @graph.setOutportMetadata a.name, calculateMeta(a.new, a.old) else throw new Error("Unknown journal entry: #{entry.cmd}") moveToRevision: (revId) -> return if revId == @currentRevision @subscribed = false if revId > @currentRevision # Forward replay journal to revId for r in [@currentRevision+1..revId] @executeEntry entry for entry in @store.fetchTransaction r else # Move backwards, and apply inverse changes for r in [@currentRevision..revId+1] by -1 entries = @store.fetchTransaction r for i in [entries.length-1..0] by -1 @executeEntryInversed entries[i] @currentRevision = revId @subscribed = true # ## Undoing & redoing # Undo the last graph change undo: () -> return unless @canUndo() @moveToRevision(@currentRevision-1) # If there is something to undo canUndo: () -> return @currentRevision > 0 # Redo the last undo redo: () -> return unless @canRedo() @moveToRevision(@currentRevision+1) # If there is something to redo canRedo: () -> return @currentRevision < @store.lastRevision ## Serializing # Render a pretty printed string of the journal. Changes are abbreviated toPrettyString: (startRev, endRev) -> startRev |= 0 endRev |= @store.lastRevision lines = [] for r in [startRev...endRev] e = @store.fetchTransaction r lines.push (entryToPrettyString entry) for entry in e return lines.join('\n') # Serialize journal to JSON toJSON: (startRev, endRev) -> startRev |= 0 endRev |= @store.lastRevision entries = [] for r in [startRev...endRev] by 1 entries.push (entryToPrettyString entry) for entry in @store.fetchTransaction r return entries save: (file, success) -> json = JSON.stringify @toJSON(), null, 4 require('fs').writeFile "#{file}.json", json, "utf-8", (err, data) -> throw err if err success file exports.Journal = Journal exports.JournalStore = JournalStore exports.MemoryJournalStore = MemoryJournalStore