diff --git a/doc/demand-matcher.dot b/doc/demand-matcher.dot new file mode 100644 index 0000000..4ba3941 --- /dev/null +++ b/doc/demand-matcher.dot @@ -0,0 +1,57 @@ +digraph G { + node[shape=box]; + + // s0000 idle + // s1000 error + // s0100 supply + // s1100 running + // s1010 starting + // s0011 starting_unwanted + // s1011 starting_doomed + // s0101 unwanted + // s1101 running_doomed + + idle -> starting [label="D+/start"]; + supply -> starting [label="D+,S-/start"]; + error -> idle [label="D-"]; + error -> running [label="S+"]; + error -> unwanted [label="D-,S+"]; + running -> unwanted [label="D-"]; + running -> error [label="S-/error"]; + + unwanted -> idle [label="S-"]; + unwanted -> starting [label="D+,S-/start"]; + running_doomed -> starting [label="S-/start"]; + running_doomed -> idle [label="D-,S-"]; + + starting -> starting_unwanted [label="D-"]; + starting -> running [label="S+"]; + starting -> unwanted [label="D-,S+"]; + starting_unwanted -> unwanted [label="S+"]; + starting_unwanted -> running_doomed [label="D+,S+"]; + starting_doomed -> running_doomed [label="S+"]; + starting_doomed -> unwanted [label="D-,S+"]; + + + idle -> supply [label="S+"]; + idle -> running [label="D+S+"]; + supply -> running [label="D+"]; + supply -> idle [label="S-"]; + running -> idle [label="D-,S-"]; + unwanted -> running_doomed [label="D+"]; + running_doomed -> unwanted [label="D-"]; + starting_unwanted -> starting_doomed [label="D+"]; + starting_doomed -> starting_unwanted [label="D-"]; + + + // s0001 -> impossible [label="any"]; + // s0010 -> impossible [label="any"]; + + // s1001 -> impossible [label="any"]; + + // s0110 -> impossible [label="any"]; + // s1110 -> impossible [label="any"]; + // s0111 -> impossible [label="any"]; + // s1111 -> impossible [label="any"]; + +} \ No newline at end of file diff --git a/doc/demand-matcher.md b/doc/demand-matcher.md new file mode 100644 index 0000000..48e1e76 --- /dev/null +++ b/doc/demand-matcher.md @@ -0,0 +1,376 @@ +# Demand-matching and Supervision + +The Demand Matcher pattern (in `demand-matcher.rkt` and in +`demand-matcher.js`'s `DemandMatcher` class) tracks assertions +representing some abstract *demand* for a resource, and causes the +creation or acquisition of matching *supply* of that resource. + +To do this, it tracks the state of each *instance* of the resource. +Each resource instance (called a "task") is uniquely identified by a +projection of the dataspace. + +The basic idea is that: + + - When demand for a task is detected, it is started. + + - Each started task signals its presence to the DemandMatcher. + + - When demand drops, the task should detect this and exit. + + - If the task exits unexpectedly, this is an error, and the + DemandMatcher prints a warning. + +## Latency causes problems + +However, because there can be some latency between requesting the +start of a task and its signalling its presence to the DemandMatcher, +we can't just figure out what to do based on the presence or absence +of demand and supply for a task. We also need to track a few more bits +of information. + +When demand for a task drops briefly, we *expect* a drop in supply, +*even demand increases again before we detect a supply drop*. For this +reason, in some circumstances, the default task supervision strategy +of DemandMatcher *recreates* supply on supply drop in some +circumstances. It keeps track of whether a supply increase is +expected, and of whether a supply decrease is expected for each task. + +It becomes an important part of the DemandMatcher protocol for a task +instance to always drop its supply assertion in response to a drop in +demand. This works well in Syndicate implementations that preserve all +assertion transitions, but not at all well where brief transitions may +be elided. In those cases, we will have to reach for a more heuristic +approach involving something akin to Erlang's "Maximum Restart +Intensity" and/or other kinds of time-based decision. For now though, +the precise case works fine. + +While it seems simple enough to imagine, the details are rather +fiddly. + +## Working out the algorithm that defaultTaskSupervisor should use + +We may assume some expected task behaviour: that it will eventually +assert supply, and *then* upon demand drop eventually exit. + + ◇(supply ∧ (¬demand ⇒ ◇ terminate)) (?!?!) + +### Complete table of actions + +Each row in this table describes actions taken in a particular +circumstance by `defaultTaskSupervisor`. + +The table has seven columns: + - `∃D`, whether demand for the task exists currently + - `∃S`, whether supply for the task exists currently + - `ΔD`, whether (and in which direction) demand is changing now + - `ΔS`, whether (and in which direction) supply is changing now + - `expS+`, whether we expect a supply increase at some point in future + - `expS-`, whether we expect a supply decrease at some point in future + - and an action to take in this circumstance. + +The first two values are drawn from the state of the DemandMatcher; +the second two, from the patch event the DemandMatcher is currently +processing; and the third two are private state variables of the task +supervisor itself. + + ∃D ∃S ΔD ΔS expS+ expS- Action + --------------------------------------------------------------------------- + - - + - - Start task, set expS+ + - - + - - No action (but slightly weird) + - - + + - - No action (but slightly weird) + - Y + - - No action (pre-extant supply) + - Y - - - No action + - Y + - - - Start task, set expS+ + Y - - - - Demand goes after unexpected supply drop + Y - + - - Spontaneous recovery from unexpected supply drop + Y - - + - - Spontaneous recovery from unexpected supply drop; set expS- + Y Y - - - Set expS- + Y Y - - - Unexpected supply drop error + Y Y - - - - No action (but slightly weird) + + - - + - Y Impossible (expS- would be clear or expS+ set) + - - + - Y Impossible (expS- would be clear or expS+ set) + - - + + - Y Impossible (expS- would be clear or expS+ set) + - Y + - Y No action + - Y - - Y Clear expS- + - Y + - - Y Clear expS-, start task, set expS+ + Y - - - Y Impossible (expS+ would be set) + Y - + - Y Impossible (expS+ would be set) + Y - - + - Y Impossible (expS+ would be set) + Y Y - - Y No action + Y Y - - Y Clear expS-, start task, set expS+ + Y Y - - - Y Clear expS- + + - - + Y - Impossible (expS+ would be clear or expS- set) + - - + Y - Impossible (expS+ would be clear or expS- set) + - - + + Y - Impossible (expS+ would be clear or expS- set) + - Y + Y - Impossible (expS+ would be clear) + - Y - Y - Impossible (expS+ would be clear) + - Y + - Y - Impossible (expS+ would be clear) + Y - - Y - Set expS- + Y - + Y - Clear expS+ + Y - - + Y - Clear expS+, set expS- + Y Y - Y - Impossible (expS+ would be clear) + Y Y - Y - Impossible (expS+ would be clear) + Y Y - - Y - Impossible (expS+ would be clear) + + - - + Y Y No action + - - + Y Y Clear expS+ + - - + + Y Y Clear expS+ + - Y + Y Y Impossible (expS+ would be clear) + - Y - Y Y Impossible (expS+ would be clear) + - Y + - Y Y Impossible (expS+ would be clear) + Y - - Y Y No action + Y - + Y Y Clear expS+ + Y - - + Y Y Clear expS+ + Y Y - Y Y Impossible (expS+ would be clear) + Y Y - Y Y Impossible (expS+ would be clear) + Y Y - - Y Y Impossible (expS+ would be clear) + +#### Actions and transitions involving actions + +From the table, we learn that the possible actions are: + + - `START`, Start task, set expS+ + - `EXPDROP`, Set expS- + - `GOTDROP`, Clear expS- + - `RUNNING`, Clear expS+ + +There are also a couple of pseudo-actions, `ERROR` for an unexpected +supply drop, and `RECOVER` for circumstances marking spontaneous +recovery after an unexpected supply drop. + +The final four columns in this table are the new states of the +DemandMatcher and the task supervisor. + + ∃D ∃S ΔD ΔS expS+ expS- Actions Next: ∃D ∃S expS+ expS- + --------------------------------------------------------------------------- + - - + - - START Y - Y - + - Y + - - - START Y - Y - + Y - - - - RECOVERY - - - - + Y - + - - RECOVER Y Y - - + Y - - + - - RECOVER EXPDROP - Y - Y + Y Y - - - EXPDROP - Y - Y + Y Y - - - ERROR Y - - - + + - Y - - Y GOTDROP - - - - + - Y + - - Y GOTDROP START Y - Y - + Y Y - - Y GOTDROP START Y - Y - + Y Y - - - Y GOTDROP - - - - + + Y - - Y - EXPDROP - - Y Y + Y - + Y - RUNNING Y Y - - + Y - - + Y - RUNNING EXPDROP - Y - Y + + - - + Y Y RUNNING - Y - Y + - - + + Y Y RUNNING Y Y - Y + Y - + Y Y RUNNING Y Y - Y + Y - - + Y Y RUNNING - Y - Y + +#### Impossible states + +Some states are impossible to reach. + +It is impossible for neither supply nor demand to exist, when either +but not both of a rise or a drop in supply is expected: + + ∃D ∃S ΔD ΔS expS+ expS- + --------------------------------------------------------------------------- + - - - Y Impossible (expS- would be clear or expS+ set) + - - Y - Impossible (expS+ would be clear or expS- set) + +It is impossible for demand but no supply to exist, when a drop in +supply is expected but no rise in supply is expected: + + ∃D ∃S ΔD ΔS expS+ expS- + --------------------------------------------------------------------------- + Y - - Y Impossible (expS+ would be set) + +It is impossible for supply to exist while a rise in supply is +expected: + + ∃D ∃S ΔD ΔS expS+ expS- + --------------------------------------------------------------------------- + - Y Y - Impossible (expS+ would be clear) + Y Y Y - Impossible (expS+ would be clear) + - Y Y Y Impossible (expS+ would be clear) + Y Y Y Y Impossible (expS+ would be clear) + +#### Transitions involving only DemandMatcher state change + +Where no task supervisor state changes and no actions are needed: + + ∃D ∃S ΔD ΔS expS+ expS- Actions Next: ∃D ∃S expS+ expS- + --------------------------------------------------------------------------- + - - + - - - Y - - + - - + + - - Y Y - - + - Y + - - Y Y - - + - Y - - - - - - - + Y Y - - - - - - - - + - Y + - Y Y Y - Y + Y Y - - Y - Y - Y + - - + Y Y Y - Y Y + Y - - Y Y - - Y Y + +### Transition diagram + +![DemandMatcher task supervisor transition diagram](demand-matcher.png) + +### From state machine to implementation + +We can give the reachable states reasonable names: + + ∃D ∃S expS+ expS- Name + --------------------------------------- + - - - - IDLE + Y - Y - STARTING + Y Y - - RUNNING + - Y - Y UNWANTED + + Y - Y Y STARTING_DOOMED + - - Y Y STARTING_UNWANTED + Y Y - Y RUNNING_DOOMED + + - Y - - SUPPLY + Y - - - ERROR + +However, writing out the full state machine in terms of these states +doesn't exploit all the redundancy in the machine. + +Instead, let's group transitions by their effects on the task +supervisor's state, the "expected" bits. There are only four possible +actions (excluding warnings related to recovery etc.): + + START - set expS+ (and start a task instance) + RUNNING - clear expS+ + EXPDROP - set expS- + GOTDROP - clear expS- + +--------------------------------------------------------------------------- + +Leave expS+ alone, set expS-: + Y - - + - - EXPDROP - Y - Y + Y Y - - - EXPDROP - Y - Y + Y - - Y - EXPDROP - - Y Y + +Leave expS+ alone, clear expS-: + - Y - - Y GOTDROP - - - - + Y Y - - - Y GOTDROP - - - - + +Set expS+, leave expS- alone: + - - + - - START Y - Y - + - Y + - - - START Y - Y - + +Set expS+, clear expS-: + - Y + - - Y START GOTDROP Y - Y - + Y Y - - Y START GOTDROP Y - Y - + +Clear expS+, leave expS- alone: + Y - + Y - RUNNING Y Y - - + - - + Y Y RUNNING - Y - Y + - - + + Y Y RUNNING Y Y - Y + Y - + Y Y RUNNING Y Y - Y + Y - - + Y Y RUNNING - Y - Y + +Clear expS+, set expS-: + Y - - + Y - RUNNING EXPDROP - Y - Y + +--------------------------------------------------------------------------- + +Now, let's look at those grouped by specific action (some rows will +appear twice, because some rows involve more than one action): + +Expdrop: + Y - - + - - EXPDROP - Y - Y + Y Y - - - EXPDROP - Y - Y + Y - - Y - EXPDROP - - Y Y + Y - - + Y - RUNNING EXPDROP - Y - Y + + - "Set expS- whenever a drop in demand is detected, and either (a) + increase in supply is detected, (b) supply exists and is not + falling, or (c) supply is expected to exist." + +Gotdrop: + - Y - - Y GOTDROP - - - - + Y Y - - - Y GOTDROP - - - - + - Y + - - Y START GOTDROP Y - Y - + Y Y - - Y START GOTDROP Y - Y - + + - "Clear expS- whenever a drop in supply is detected." + +Start: + - - + - - START Y - Y - + - Y + - - - START Y - Y - + - Y + - - Y START GOTDROP Y - Y - + Y Y - - Y START GOTDROP Y - Y - + + - "Set expS+ and start a task whenever expS+ is clear and demand + becomes or remains high and supply becomes or remains low UNLESS + demand is already high, supply drops, and expS- is clear, which is + the 'unexpected drop' error case." + +Running: + Y - + Y - RUNNING Y Y - - + - - + Y Y RUNNING - Y - Y + - - + + Y Y RUNNING Y Y - Y + Y - + Y Y RUNNING Y Y - Y + Y - - + Y Y RUNNING - Y - Y + Y - - + Y - RUNNING EXPDROP - Y - Y + + - "Clear expS+ whenever supply increases." + +--------------------------------------------------------------------------- + +Now let's take those rules and check them against the full rulebase: + +"Set expS- whenever a drop in demand is detected, and either (a) +increase in supply is detected, (b) supply exists and is not +falling, or (c) supply is expected to exist." + + Y - - + - - RECOVER EXPDROP - Y - Y + Y Y - - - EXPDROP - Y - Y + Y - - Y - EXPDROP - - Y Y + Y - - + Y - RUNNING EXPDROP - Y - Y + Y - - + Y Y RUNNING - Y - Y + Y Y - - Y - Y - Y + Y - - Y Y - - Y Y + +"Clear expS- whenever a drop in supply is detected." + + - Y + - - - START Y - Y - + Y Y - - - ERROR Y - - - + - Y - - Y GOTDROP - - - - + - Y + - - Y GOTDROP START Y - Y - + Y Y - - Y GOTDROP START Y - Y - + Y Y - - - Y GOTDROP - - - - + - Y - - - - - - - + Y Y - - - - - - - - + +"Set expS+ and start a task whenever expS+ is clear and demand +becomes or remains high and supply becomes or remains low UNLESS +demand is already high, supply drops, and expS- is clear, which is +the 'unexpected drop' error case." + + - - + - - START Y - Y - + - Y + - - - START Y - Y - + Y Y - - - ERROR Y - - - + - Y + - - Y GOTDROP START Y - Y - + Y Y - - Y GOTDROP START Y - Y - + +"Clear expS+ whenever supply increases." + + Y - + - - RECOVER Y Y - - + Y - - + - - RECOVER EXPDROP - Y - Y + Y - + Y - RUNNING Y Y - - + Y - - + Y - RUNNING EXPDROP - Y - Y + - - + Y Y RUNNING - Y - Y + - - + + Y Y RUNNING Y Y - Y + Y - + Y Y RUNNING Y Y - Y + Y - - + Y Y RUNNING - Y - Y + - - + - - - Y - - + - - + + - - Y Y - - + +By looking at the next-state columns corresponding to the action +described, we can see that each predicate used to decide whether to +set or clear each state bit is a sound overapproximation of the +behaviour we want. diff --git a/doc/demand-matcher.png b/doc/demand-matcher.png new file mode 100644 index 0000000..110dafd Binary files /dev/null and b/doc/demand-matcher.png differ diff --git a/js/src/broker.js b/js/src/broker.js index 4f2e30e..0073e17 100644 --- a/js/src/broker.js +++ b/js/src/broker.js @@ -31,12 +31,12 @@ function spawnBrokerClientDriver() { Dataspace.spawn( new DemandMatcher([brokerConnection(URL)], [brokerConnection(URL)], + function (c) { + Dataspace.spawn(new BrokerClientConnection(c.url)); + }, { demandMetaLevel: 1, - supplyMetaLevel: 0, - onDemandIncrease: function (c) { - Dataspace.spawn(new BrokerClientConnection(c.url)); - } + supplyMetaLevel: 0 })); })); } diff --git a/js/src/demand-matcher.js b/js/src/demand-matcher.js index 900c285..c8ab9b7 100644 --- a/js/src/demand-matcher.js +++ b/js/src/demand-matcher.js @@ -3,6 +3,100 @@ var Trie = require('./trie.js'); var Patch = require('./patch.js'); var Util = require('./util.js'); +/////////////////////////////////////////////////////////////////////////// +// Protocol between DemandMatcher and taskSupervisor functions + +// Bits: +var IS_CHANGING = 1; +var IS_PRESENT = 2; + +// Bit combinations: +var LOW = 0 ; +var RISING = IS_CHANGING ; +var HIGH = IS_PRESENT ; +var FALLING = IS_PRESENT | IS_CHANGING ; + +/////////////////////////////////////////////////////////////////////////// +// Default task supervision strategy. See syndicate/doc/demand-matcher.md. + +function defaultTaskSupervisor(demandState, supplyState, supervisionState, taskFn, errorFn) { + var oldESI = supervisionState ? supervisionState.expectSupplyIncrease : false; + var oldESD = supervisionState ? supervisionState.expectSupplyDecrease : false; + + var newESI = oldESI; + var newESD = oldESD; + + if ((demandState === FALLING) && ((supplyState === RISING) || + (supplyState === HIGH) || + oldESI)) { + newESD = true; + } + + if (!oldESI && ((demandState === RISING) || + (demandState === HIGH)) && ((supplyState === LOW) || + (supplyState === FALLING))) { + if ((demandState === HIGH) && !oldESD) { + errorFn("Syndicate: DemandMatcher detected unexpected drop in supply"); + } else { + taskFn(); + newESI = true; + } + } + + if (supplyState === FALLING) newESD = false; + if (supplyState === RISING) newESI = false; + + if (newESI || newESD) { + return { expectSupplyIncrease: newESI, expectSupplyDecrease: newESD }; + } else { + return null; + } +} + +/////////////////////////////////////////////////////////////////////////// +// DemandMatcher itself + +function DemandMatcher(demandSpecs, supplySpecs, startTask, options) { + options = Util.extend({ + metaLevel: 0, + demandMetaLevel: null, + supplyMetaLevel: null, + taskSupervisor: defaultTaskSupervisor, + }, options); + + if (typeof startTask !== 'function') { + throw new Error("Syndicate: DemandMatcher expects 'startTask' function as third argument"); + } + + this.demandProjectionNames = ensureMatchingProjectionNames(demandSpecs); + this.supplyProjectionNames = ensureMatchingProjectionNames(supplySpecs); + ensureMatchingProjectionNames([demandSpecs[0], supplySpecs[0]]); + + this.demandSpecs = demandSpecs; + this.supplySpecs = supplySpecs; + + this.demandPatterns = demandSpecs.map(function (s) { return Trie.projectionToPattern(s); }); + this.supplyPatterns = supplySpecs.map(function (s) { return Trie.projectionToPattern(s); }); + + this.demandMetaLevel = + (options.demandMetaLevel === null) ? options.metaLevel : options.demandMetaLevel; + this.supplyMetaLevel = + (options.supplyMetaLevel === null) ? options.metaLevel : options.supplyMetaLevel; + + function metaWrap(n) { + return function (s) { return Patch.prependAtMeta(s, n); }; + } + this.demandProjections = demandSpecs.map(metaWrap(this.demandMetaLevel)); + this.supplyProjections = supplySpecs.map(metaWrap(this.supplyMetaLevel)); + + this.taskSupervisor = options.taskSupervisor; + this.startTask = startTask; + + this.currentDemand = Immutable.Set(); + this.currentSupply = Immutable.Set(); + this.supervisionStates = Immutable.Map(); +} + function ensureMatchingProjectionNames(specs) { if (!(specs.length > 0)) { throw new Error("Syndicate: DemandMatcher needs at least one spec"); @@ -21,52 +115,6 @@ function ensureMatchingProjectionNames(specs) { return names; } -function defaultHandler(side, movement) { - return function (captures) { - console.error("Syndicate: Unhandled "+movement+" in "+side, captures); - }; -} - -function DemandMatcher(demandSpecs, supplySpecs, options) { - options = Util.extend({ - metaLevel: 0, - demandMetaLevel: null, - supplyMetaLevel: null, - onDemandIncrease: defaultHandler('demand', 'increase'), - onDemandDecrease: function (captures) {}, - onSupplyIncrease: function (captures) {}, - onSupplyDecrease: defaultHandler('supply', 'decrease') - }, options); - - this.demandProjectionNames = ensureMatchingProjectionNames(demandSpecs); - this.supplyProjectionNames = ensureMatchingProjectionNames(supplySpecs); - - this.demandSpecs = demandSpecs; - this.supplySpecs = supplySpecs; - - this.demandPatterns = demandSpecs.map(function (s) { return Trie.projectionToPattern(s); }); - this.supplyPatterns = supplySpecs.map(function (s) { return Trie.projectionToPattern(s); }); - - this.demandMetaLevel = - (options.demandMetaLevel === null) ? options.metaLevel : options.demandMetaLevel; - this.supplyMetaLevel = - (options.supplyMetaLevel === null) ? options.metaLevel : options.supplyMetaLevel; - - function metaWrap(n) { - return function (s) { return Patch.prependAtMeta(s, n); }; - } - this.demandProjections = demandSpecs.map(metaWrap(this.demandMetaLevel)); - this.supplyProjections = supplySpecs.map(metaWrap(this.supplyMetaLevel)); - - this.onDemandIncrease = options.onDemandIncrease; - this.onDemandDecrease = options.onDemandDecrease; - this.onSupplyIncrease = options.onSupplyIncrease; - this.onSupplyDecrease = options.onSupplyDecrease; - - this.currentDemand = Immutable.Set(); - this.currentSupply = Immutable.Set(); -} - DemandMatcher.prototype.boot = function () { var p = Patch.emptyPatch; function extend(ml) { @@ -83,6 +131,50 @@ DemandMatcher.prototype.handleEvent = function (e) { } }; +DemandMatcher.prototype.handlePatch = function (p) { + var self = this; + + var dN = self.demandProjectionNames.length; + var sN = self.supplyProjectionNames.length; + var addedDemand = self.extractKeys(p.added, self.demandProjections, dN, 'demand'); + var removedDemand = self.extractKeys(p.removed, self.demandProjections, dN, 'demand'); + var addedSupply = self.extractKeys(p.added, self.supplyProjections, sN, 'supply'); + var removedSupply = self.extractKeys(p.removed, self.supplyProjections, sN, 'supply'); + + // Though the added and removed sets of patches are always disjoint, + // *after projection* this may not hold. Cancel out any overlaps. + var demandOverlap = addedDemand.intersect(removedDemand); + var supplyOverlap = addedSupply.intersect(removedSupply); + addedDemand = addedDemand.subtract(demandOverlap); + removedDemand = removedDemand.subtract(demandOverlap); + addedSupply = addedSupply.subtract(supplyOverlap); + removedSupply = removedSupply.subtract(supplyOverlap); + + var allTasks = addedDemand.union(addedSupply).union(removedDemand).union(removedSupply); + + allTasks.forEach(function (captures) { + function taskFn() { + self.startTask(Trie.captureToObject(captures, self.demandProjectionNames)); + } + function errorFn(msg) { + console.error(msg, captures); + } + + var demandState = computeState(self.currentDemand, addedDemand, removedDemand, captures); + var supplyState = computeState(self.currentSupply, addedSupply, removedSupply, captures); + var oldSupervisionState = self.supervisionStates.get(captures, null); + var newSupervisionState = self.taskSupervisor(demandState, + supplyState, + oldSupervisionState, + taskFn, + errorFn); + self.supervisionStates = self.supervisionStates.set(captures, newSupervisionState); + }); + + self.currentSupply = self.currentSupply.union(addedSupply).subtract(removedSupply); + self.currentDemand = self.currentDemand.union(addedDemand).subtract(removedDemand); +}; + DemandMatcher.prototype.extractKeys = function (trie, projections, keyCount, whichSide) { var ks = Immutable.Set(); projections.forEach(function (proj) { @@ -97,53 +189,11 @@ DemandMatcher.prototype.extractKeys = function (trie, projections, keyCount, whi return ks; }; -DemandMatcher.prototype.handlePatch = function (p) { - var self = this; - - var dN = self.demandProjectionNames.length; - var sN = self.supplyProjectionNames.length; - var addedDemand = this.extractKeys(p.added, self.demandProjections, dN, 'demand'); - var removedDemand = this.extractKeys(p.removed, self.demandProjections, dN, 'demand'); - var addedSupply = this.extractKeys(p.added, self.supplyProjections, sN, 'supply'); - var removedSupply = this.extractKeys(p.removed, self.supplyProjections, sN, 'supply'); - - // Though the added and removed sets of patches are always disjoint, - // *after projection* this may not hold. Cancel out any overlaps. - var demandOverlap = addedDemand.intersect(removedDemand); - var supplyOverlap = addedSupply.intersect(removedSupply); - addedDemand = addedDemand.subtract(demandOverlap); - removedDemand = removedDemand.subtract(demandOverlap); - addedSupply = addedSupply.subtract(supplyOverlap); - removedSupply = removedSupply.subtract(supplyOverlap); - - self.currentSupply = self.currentSupply.union(addedSupply); - self.currentDemand = self.currentDemand.subtract(removedDemand); - - removedSupply.forEach(function (captures) { - if (self.currentDemand.has(captures)) { - self.onSupplyDecrease(Trie.captureToObject(captures, self.supplyProjectionNames)); - } - }); - addedSupply.forEach(function (captures) { - if (!self.currentDemand.has(captures)) { - self.onSupplyIncrease(Trie.captureToObject(captures, self.supplyProjectionNames)); - } - }); - - removedDemand.forEach(function (captures) { - if (self.currentSupply.has(captures)) { - self.onDemandDecrease(Trie.captureToObject(captures, self.demandProjectionNames)); - } - }); - addedDemand.forEach(function (captures) { - if (!self.currentSupply.has(captures)) { - self.onDemandIncrease(Trie.captureToObject(captures, self.demandProjectionNames)); - } - }); - - self.currentSupply = self.currentSupply.subtract(removedSupply); - self.currentDemand = self.currentDemand.union(addedDemand); -}; +function computeState(current, added, removed, captures) { + var isPresent = current.has(captures); + var isChanging = added.has(captures) || removed.has(captures); + return (isPresent ? IS_PRESENT : 0) | (isChanging ? IS_CHANGING : 0); +} /////////////////////////////////////////////////////////////////////////// diff --git a/js/src/timer-driver.js b/js/src/timer-driver.js index 0ee8162..4d8757b 100644 --- a/js/src/timer-driver.js +++ b/js/src/timer-driver.js @@ -15,18 +15,14 @@ function spawnTimerDriver() { Dataspace.spawn( new DemandMatcher([Patch.observe(periodicTick(_$('intervalMS')))], [Patch.advertise(periodicTick(_$('intervalMS')))], - { - onDemandIncrease: function (c) { - Dataspace.spawn(new Tick(c.intervalMS)); - } + function (c) { + Dataspace.spawn(new Tick(c.intervalMS)); })); Dataspace.spawn( new DemandMatcher([Patch.observe(timeLaterThan(_$('deadlineMS')))], [Patch.advertise(timeLaterThan(_$('deadlineMS')))], - { - onDemandIncrease: function (c) { - Dataspace.spawn(new Alarm(c.deadlineMS)); - } + function (c) { + Dataspace.spawn(new Alarm(c.deadlineMS)); })); } diff --git a/js/src/ui.js b/js/src/ui.js index 0013a43..8848f1f 100644 --- a/js/src/ui.js +++ b/js/src/ui.js @@ -78,29 +78,23 @@ function spawnUIDriver(options) { Dataspace.spawn( new DemandMatcher([Patch.observe(globalEventProj)], [Patch.advertise(globalEventProj)], - { - onDemandIncrease: function (c) { - Dataspace.spawn(new GlobalEventSupply(c.selector, c.eventType)); - } + function (c) { + Dataspace.spawn(new GlobalEventSupply(c.selector, c.eventType)); })); var windowEventProj = windowEvent(_$('eventType'), __); Dataspace.spawn( new DemandMatcher([Patch.observe(windowEventProj)], [Patch.advertise(windowEventProj)], - { - onDemandIncrease: function (c) { - Dataspace.spawn(new WindowEventSupply(c.eventType)); - } + function (c) { + Dataspace.spawn(new WindowEventSupply(c.eventType)); })); Dataspace.spawn( new DemandMatcher([uiFragment(_$('fragmentId'), __, __)], [uiFragmentExists(_$('fragmentId'))], - { - onDemandIncrease: function (c) { - Dataspace.spawn(new UIFragment(c.fragmentId)); - } + function (c) { + Dataspace.spawn(new UIFragment(c.fragmentId)); })); Dataspace.spawn(new LocationHashTracker(options.defaultLocationHash || '/'));