Rewrite JS DemandMatcher to handle important latency-related corner cases.
This commit is contained in:
parent
68cde5be6c
commit
628ba87c54
|
@ -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"];
|
||||
|
||||
}
|
|
@ -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.
|
Binary file not shown.
After Width: | Height: | Size: 69 KiB |
|
@ -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
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
|
|
@ -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));
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
18
js/src/ui.js
18
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 || '/'));
|
||||
|
|
Loading…
Reference in New Issue