Skip to content

Commit

Permalink
added the ability to specify a merge strategy when creating a new pro…
Browse files Browse the repository at this point in the history
…pagator
  • Loading branch information
eldritchreality committed May 4, 2016
1 parent 504911e commit 0a0a8a2
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 20 deletions.
1 change: 1 addition & 0 deletions probe.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down
32 changes: 23 additions & 9 deletions propagator-cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions propagator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,7 +40,9 @@ function Propagator(func,input,output) {

return output;
}


this.merge = mergeStrategy || mergeByUpdate

this.propagate = function propagate() {

var outputValues = applyFuncToInputs();
Expand Down
11 changes: 10 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
53 changes: 45 additions & 8 deletions test/test-propagator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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))
})
Expand All @@ -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))
})
Expand All @@ -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))
})

Expand All @@ -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)

})

Expand Down

0 comments on commit 0a0a8a2

Please sign in to comment.