diff --git a/probe.js b/probe.js index c6cde9a..e9c8ee9 100644 --- a/probe.js +++ b/probe.js @@ -3,6 +3,7 @@ var assert = require("assert") var probe = function addProbe (name,cell) { + assert(typeof name === "string","You have to name a probe with a string, you can't name it with a",typeof name); assert(cell instanceof Cell, "You need to attach the probe to a Cell object"); diff --git a/propagator-cell.js b/propagator-cell.js index f8501ab..a31feec 100644 --- a/propagator-cell.js +++ b/propagator-cell.js @@ -6,25 +6,39 @@ function Cell () { this.contents = undefined this.listeners = new Set() - this.lastUpdater = undefined + this.updateHistory = [] function updateListeners(caller) { caller = caller || false self.listeners.forEach((elem) => {if(caller !== elem) elem()}) ; } + + - this.update = function update(update,caller) { + this.update = function update(newVal,caller) { + + function doUpdate() { + + if (self.contents === undefined) self.contents = newVal + else if(typeof caller === "string") self.contents = newVal //not sure about this, it's deliberately permissive + else if(typeof caller !== "undefined" && caller.merge instanceof Function) { + self.contents = caller.merge(self.contents,newVal,self.updateHistory.slice()) // make the caller sort out the merge conflict + assert(typeof self.contents !== "undefined","This merge strategy failed to return a value: "+ caller.merge.toString()) // insist the caller actually does sort out the merge conflict + } + else throw new Error("You need to identify yourself to change an existing value. Pass either an identifying string, or a propagator object.") //caller is not a string or a valid propagator therefore caller is improperly defined + + } + //check invariants - if (typeof update !== "undefined","Tried to update a cell without sending a value") + if (typeof newVal !== "undefined","Tried to update a cell without sending a value") caller = caller || false - //check permissions - if (self.lastUpdater && caller != self.lastUpdater) throw new Error("Tried to change a value owned by another propagator") - if (update === self.contents) return; + //Do update + if (newVal === self.contents) return; + else doUpdate(); - //do update - self.contents = update; - self.lastUpdater = caller || self.lastUpdater + //append to history + self.updateHistory.push({"caller":caller,"value":self.contents}) updateListeners(caller) return self diff --git a/propagator.js b/propagator.js index 34db46b..c789632 100644 --- a/propagator.js +++ b/propagator.js @@ -7,14 +7,22 @@ function coerceToArray(object) { else return object; } -function Propagator(func,input,output) { +function mergeByUpdate(prev,update) { + return update; +} + +function Propagator(func,input,output,mergeStrategy) { var self = this assert(func && input && output, "You need to call the propagator constructor with all three of a function, input cell and output cell") + assert(func instanceof Function, "You need to call a propagator with a function as the first argument") assert(input instanceof Cell || input instanceof Array, "Propagators read from cells or lists of cells, not " + typeof input) assert(output instanceof Cell, "Propagators output to single cells, not " + typeof output) assert(input !== output, "Propagators shouldn't write out to the same cell they read from.") if (input instanceof Array) input.forEach( (cell) => assert(cell instanceof Cell,"All inputs need to be cells, not " + typeof cell ) ); + if (typeof mergeStrategy != "undefined") { + assert(mergeStrategy instanceof Function, "your merge callback "+ mergeStrategy.toString() + " must be a function not a " + typeof mergeStrategy) + } this.input = coerceToArray(input); this.output = output; @@ -32,7 +40,9 @@ function Propagator(func,input,output) { return output; } - + + this.merge = mergeStrategy || mergeByUpdate + this.propagate = function propagate() { var outputValues = applyFuncToInputs(); diff --git a/readme.md b/readme.md index 8494de3..648fbf2 100644 --- a/readme.md +++ b/readme.md @@ -27,10 +27,19 @@ This is the primary interface to the module, you can access all constructors as methods: -* constructor: takes a function, a list of input cells, and and output cell. Registers a function on the input cells that sets the output cells whenever the input cells change value. +* constructor: takes a function, a list of input cells, an output cell, and optionally a merge-strategy function. Registers a function on the input cells that sets the output cells whenever the input cells change value. * makeCell: returns a newly created blank cell. * addProbe: adds a probe with a "name" to a Cell. Api as listed for probe.js +##### Merge Strategy Function: + +A function (optionally) passed to the propagator constructor. It is called by the cell when the propagator tries to update a cell that already holds a value, and merges the new value with the old value to provide a new value for the cell. + +* It is given three arguments (previousValue, UpdatedValue and cellHistory), it should return a new value for the cell. +* If no merge-strategy is passed to the propagator constuctor, the default merge strategy (called update) is used. Update simply overwrites the old value with the new value. + + + #### Constants *Not implemented yet* Constants are propagators which only set a scalar value to their output cells. diff --git a/test/test-propagator.js b/test/test-propagator.js index 572ea51..c7146e1 100644 --- a/test/test-propagator.js +++ b/test/test-propagator.js @@ -22,7 +22,7 @@ describe("The propagator constructor",function() { var mockOutputCell = Propagator.makeCell() var identityFunc = ((x) => x) - mockInputCell.update(5) + mockInputCell.update(5,"mocha test script") var testPropagator = new Propagator(identityFunc,mockInputCell,mockOutputCell); @@ -34,10 +34,10 @@ describe("The propagator constructor",function() { var mockOutputCell = Propagator.makeCell() var add1to = ((x) => x+1) - mockInputCell.update(5) + mockInputCell.update(5,"mocha test script") var testPropagator = new Propagator(add1to,mockInputCell,mockOutputCell); - mockInputCell.update(10) + mockInputCell.update(10,"mocha test script") expect(mockOutputCell.getContents()).to.equal(add1to(10)) }) @@ -50,7 +50,7 @@ describe("The propagator constructor",function() { var addValuesTogether = ((x,y,z) => x+y+z) var testPropagator = new Propagator(addValuesTogether,[mockInputCell1,mockInputCell2,mockInputCell3],mockOutputCell); - mockInputCell1.update(10) + mockInputCell1.update(10,"mocha test script") expect(mockOutputCell.getContents()).to.equal(addValuesTogether(10,1,1)) }) @@ -64,13 +64,13 @@ describe("The propagator constructor",function() { var testPropagator = new Propagator(addValuesTogether,[mockInputCell1,mockInputCell2,mockInputCell3],mockOutputCell); - mockInputCell1.update(1) + mockInputCell1.update(1,"mocha test script") expect(mockOutputCell.getContents()).to.be.undefined; - mockInputCell2.update(10) + mockInputCell2.update(10,"mocha test script") expect(mockOutputCell.getContents()).to.be.undefined; - mockInputCell3.update(100) + mockInputCell3.update(100,"mocha test script") expect(mockOutputCell.getContents()).to.equal(addValuesTogether(1,10,100)) }) @@ -79,12 +79,49 @@ describe("The propagator constructor",function() { expect( () => new Propagator() ).to.throw(Error) }) - it("should throw an error when the constructor is called something other than a function and cells", function(){ + it("should throw an error when the constructor is called something other than a function, cells and optionally a merge strategy", function(){ + expect( () => new Propagator("I'm not a function",Propagator.makeCell(),Propagator.makeCell()) ).to.throw(Error) expect( () => new Propagator((x) => x,"not a cell",["really not a cell"]) ).to.throw(Error) expect( () => new Propagator((x) => x,Propagator.makeCell(),["really not a cell"]) ).to.throw(Error) expect( () => new Propagator((x) => x,["really not a cell","me either"],Propagator.makeCell()) ).to.throw(Error) expect( () => new Propagator((x) => x,[Propagator.makeCell(),"still not a cell yet"],Propagator.makeCell()) ).to.throw(Error) + expect( () => new Propagator((x) => x,[Propagator.makeCell(),"still not a cell yet"],Propagator.makeCell()) ).to.throw(Error) + expect( () => new Propagator((x) => x,Propagator.makeCell(),Propagator.makeCell(),"I'm not a function") ).to.throw(Error) + + }) + + it("should cope when a merge strategy is defined as part of the arguments",function(){ + var mockInputCell = Propagator.makeCell() + var mockOutputCell = Propagator.makeCell() + var add1to = ((x) => x+1) + var mergeByAdding = ((x,y) => x + y) + + expect( (() => new Propagator(add1to,mockInputCell,mockOutputCell,mergeByAdding)).bind(this) ).not.to.throw(Error); + + }) + + it("should throw an error when the merge strategy fails to return a value",function(){ + var mockInputCell = Propagator.makeCell() + var mockOutputCell = Propagator.makeCell() + var add1to = ((x) => x+1) + var mergeByMistake = function fail(x,y) { x + y } //no return + + mockInputCell.update(5,"mocha test script") + mockOutputCell.update(500,"mocha test script") + expect( (() => new Propagator(add1to,mockInputCell,mockOutputCell,mergeByMistake)).bind(this) ).to.throw(Error); + + }) + + it("should successfully run a correctly defined merge strategy when updating a cell which already has a value",function(){ + var mockInputCell = Propagator.makeCell().update(5,"mocha test script") + var mockOutputCell = Propagator.makeCell().update(10,"mocha test script") + var add1to = ((x) => x+1) + var mergeByAdding = ((x,y) => x + y) + + var testPropagator = new Propagator(add1to,mockInputCell,mockOutputCell,mergeByAdding) + + expect(mockOutputCell.getContents()).to.equal(16) // 10 + add1to(5) })