Hook dataflow.js into Syndicate/js; add "during ... actor { ... }"

This commit is contained in:
Tony Garnock-Jones 2016-08-07 15:33:09 -04:00
parent e2575c3ea1
commit 41693b897c
24 changed files with 408 additions and 208 deletions

View File

@ -42,15 +42,14 @@ var forEachChild = (function () {
return forEachChild;
})();
function buildActor(constructorES5, nameExpOpt, block) {
function buildActor(nameExpOpt, block) {
var nameExpStr;
if (nameExpOpt.numChildren === 1) {
nameExpStr = ', ' + nameExpOpt.asES5;
} else {
nameExpStr = '';
}
return 'Syndicate.Actor.spawnActor(new '+constructorES5+', '+
'function() ' + block.asES5 + nameExpStr + ');';
return 'Syndicate.Actor.spawnActor(function() ' + block.asES5 + nameExpStr + ');';
}
function buildFacet(facetBlock, transitionBlock) {
@ -58,11 +57,11 @@ function buildFacet(facetBlock, transitionBlock) {
'\nSyndicate.Actor.createFacet()' +
(facetBlock ? facetBlock.asES5 : '') +
(transitionBlock ? transitionBlock.asES5 : '') +
'.completeBuild(); })();';
'.completeBuild(); }).call(this);';
}
function buildOnEvent(isTerminal, eventType, subscription, projection, bindings, body) {
return '\n.onEvent(' + isTerminal + ', ' + JSON.stringify(eventType) + ', ' +
return '\n.onEvent(Syndicate.Actor.PRIORITY_NORMAL, ' + isTerminal + ', ' + JSON.stringify(eventType) + ', ' +
subscription + ', ' + projection +
', (function(' + bindings.join(', ') + ') ' + body + '))';
}
@ -86,11 +85,8 @@ function buildCaseEvent(eventPattern, body) {
}
var modifiedSourceActions = {
ActorStatement_noConstructor: function(_actor, _namedOpt, nameExpOpt, block) {
return buildActor('Object()', nameExpOpt, block);
},
ActorStatement_withConstructor: function(_actor, ctorExp, _namedOpt, nameExpOpt, block) {
return buildActor(ctorExp.asES5, nameExpOpt, block);
ActorStatement: function(_actor, _namedOpt, nameExpOpt, block) {
return buildActor(nameExpOpt, block);
},
DataspaceStatement_ground: function(_ground, _dataspace, maybeId, block) {
@ -133,6 +129,22 @@ var modifiedSourceActions = {
label + ', ' + JSON.stringify(formals) + ');';
},
FieldDeclarationStatement: function(_field, memberExpr, _eq, initExpr, sc) {
return 'Syndicate.Actor.declareField(' + memberExpr.memberObjectExpr.asES5 + ', ' +
memberExpr.memberPropExpr.asES5 + ', ' + initExpr.asES5 + ')' +
sc.interval.contents;
},
MemberExpression_fieldRefExp: function (_field, memberExpr) {
return 'Syndicate.Actor.referenceField(' + memberExpr.memberObjectExpr.asES5 + ', ' +
memberExpr.memberPropExpr.asES5 + ')';
},
UnaryExpression_fieldDelExp: function (_delete, _field, memberExpr) {
return 'Syndicate.Actor.deleteField(' + memberExpr.memberObjectExpr.asES5 + ', ' +
memberExpr.memberPropExpr.asES5 + ')';
},
SendMessageStatement: function(_colons, expr, sc) {
return 'Syndicate.Dataspace.send(' + expr.asES5 + ')' + sc.interval.contents;
},
@ -165,6 +177,9 @@ var modifiedSourceActions = {
FacetSituation_onEvent: function (_on, _event, id, block) {
return '\n.addOnEventHandler((function(' + id.asES5 + ') ' + block.asES5 + '))';
},
FacetSituation_dataflow: function (_dataflow, block) {
return '\n.addDataflow((function () ' + block.asES5 + '))';
},
FacetSituation_during: function(_during, pattern, facetBlock) {
var cachedAssertionVar = gensym('cachedAssertion');
return buildOnEvent(false,
@ -184,6 +199,28 @@ var modifiedSourceActions = {
'{}') +
'.completeBuild(); }');
},
FacetSituation_duringActor: function(_during, pattern, _actor, _named, nameExpOpt, facetBlock) {
var cachedAssertionVar = gensym('cachedAssertion');
var actorBlock = {
asES5: '{ ' + facetBlock.facetVarDecls +
'\nSyndicate.Actor.createFacet()' +
facetBlock.asES5 +
buildOnEvent(true,
'retracted',
pattern.instantiatedSubscription(cachedAssertionVar),
pattern.instantiatedProjection(cachedAssertionVar),
[],
'{}') +
'.completeBuild(); }'
};
return buildOnEvent(false,
'asserted',
pattern.subscription,
pattern.projection,
pattern.bindings,
'{ var '+cachedAssertionVar+' = '+pattern.instantiatedAssertion+';'+
'\n' + buildActor(nameExpOpt, actorBlock) + ' }');
},
AssertWhenClause: function(_when, _lparen, expr, _rparen) {
return expr.asES5;
@ -199,6 +236,24 @@ var modifiedSourceActions = {
semantics.extendAttribute('modifiedSource', modifiedSourceActions);
semantics.addAttribute('memberObjectExpr', {
MemberExpression_propRefExp: function(objExpr, _dot, id) {
return objExpr;
},
MemberExpression_arrayRefExp: function(objExpr, _lbrack, propExpr, _rbrack) {
return objExpr;
}
});
semantics.addAttribute('memberPropExpr', {
MemberExpression_propRefExp: function(objExpr, _dot, id) {
return { asES5: JSON.stringify(id.interval.contents) };
},
MemberExpression_arrayRefExp: function(objExpr, _lbrack, propExpr, _rbrack) {
return propExpr;
}
});
semantics.addAttribute('facetVarDecls', {
FacetBlock: function (_leftParen, varDecls, _init, _situations, _done, _rightParen) {
return varDecls.asES5.join(' ');
@ -269,7 +324,7 @@ semantics.addAttribute('instantiatedAssertion', {
var fragments = [];
fragments.push('(function() { var _ = Syndicate.__; return ');
children.forEach(function (c) { fragments.push(c.buildSubscription('instantiated')); });
fragments.push('; })()');
fragments.push('; }).call(this)');
return fragments.join('');
}
});
@ -345,6 +400,12 @@ semantics.addOperation('buildSubscription(mode)', {
}
},
MemberExpression_fieldRefExp: function (_field, memberExpr) {
return 'Syndicate.Actor.referenceField(' +
memberExpr.memberObjectExpr.buildSubscription(this.args.mode) + ', ' +
memberExpr.memberPropExpr.buildSubscription(this.args.mode) + ')';
},
identifier: function(_name) {
var i = this.interval.contents;
if (i[0] === '$' && i.length > 1) {

View File

@ -7,10 +7,13 @@ message type deposit(amount);
ground dataspace {
actor {
this.balance = 0;
field this.balance = 0;
react {
assert account(this.balance);
dataflow {
console.log("Balance inside account is", this.balance);
}
on message deposit($amount) {
this.balance += amount;
}

View File

@ -20,19 +20,19 @@ assertion type foo(x, y);
ground dataspace {
actor {
var x = 123;
field this.x = 123;
react {
assert foo(x, 999);
assert foo(this.x, 999);
during foo(x, $v) {
during foo(this.x, $v) {
do {
console.log('x=', x, 'v=', v);
if (x === 123) {
x = 124;
console.log('x=', this.x, 'v=', v);
if (this.x === 123) {
this.x = 124;
}
}
finally {
console.log('finally for x=', x, 'v=', v);
console.log('finally for x=', this.x, 'v=', v);
}
}
}

View File

@ -1,5 +1,17 @@
// bin/syndicatec compiler/demo-filesystem.js | node
// Good output:
//
// At least one reader exists for: hello.txt
// hello.txt has content undefined
// hello.txt has content "a"
// hello.txt has content undefined
// hello.txt has content "c"
// hello.txt has content "quit demo"
// The hello.txt file contained 'quit demo', so we will quit
// second observer sees that hello.txt content is "final contents"
// No remaining readers exist for: hello.txt
var Syndicate = require('./src/main.js');
assertion type file(name, content) = "file";
@ -17,16 +29,16 @@ ground dataspace {
do {
console.log("At least one reader exists for:", name);
}
assert file(name, this.files[name]);
assert file(name, field this.files[name]);
finally {
console.log("No remaining readers exist for:", name);
}
}
on message saveFile($name, $newcontent) {
this.files[name] = newcontent;
field this.files[name] = newcontent;
}
on message deleteFile($name) {
delete this.files[name];
delete field this.files[name];
}
}
}

View File

@ -11,13 +11,13 @@ Syndicate <: ES5 {
| DataspaceStatement
| ActorFacetStatement
| AssertionTypeDeclarationStatement
| FieldDeclarationStatement
| SendMessageStatement
FunctionBodyBlock = "{" FunctionBody "}" // odd that this isn't in es5.ohm somewhere
ActorStatement
= actor ~named CallExpression (named Expression<withIn>)? FunctionBodyBlock -- withConstructor
| actor (named Expression<withIn>)? FunctionBodyBlock -- noConstructor
= actor (named Expression<withIn>)? FunctionBodyBlock
DataspaceStatement
= ground dataspace identifier? FunctionBodyBlock -- ground
@ -31,22 +31,33 @@ Syndicate <: ES5 {
AssertionTypeDeclarationStatement
= (assertion | message) type identifier "(" FormalParameterList ")" ("=" stringLiteral)? #(sc)
FieldDeclarationStatement = field MemberExpression "=" AssignmentExpression<withIn> #(sc)
MemberExpression += field MemberExpression -- fieldRefExp
UnaryExpression += delete field MemberExpression -- fieldDelExp
SendMessageStatement = "::" Expression<withIn> #(sc)
//---------------------------------------------------------------------------
// Ongoing event handlers.
FacetBlock = "{" (VariableStatement | FunctionDeclaration)* FacetInitBlock? FacetSituation* FacetDoneBlock? "}"
FacetBlock = "{"
(VariableStatement | FieldDeclarationStatement | FunctionDeclaration)*
FacetInitBlock?
FacetSituation*
FacetDoneBlock?
"}"
FacetStateTransitionBlock = "{" FacetStateTransition* "}"
FacetInitBlock = do FunctionBodyBlock
FacetDoneBlock = finally FunctionBodyBlock
FacetSituation
= assert FacetPattern AssertWhenClause? #(sc) -- assert
| on FacetEventPattern FunctionBodyBlock -- event
| on event identifier FunctionBodyBlock -- onEvent
| during FacetPattern FacetBlock -- during
= assert FacetPattern AssertWhenClause? #(sc) -- assert
| on FacetEventPattern FunctionBodyBlock -- event
| on event identifier FunctionBodyBlock -- onEvent
| dataflow FunctionBodyBlock -- dataflow
| during FacetPattern FacetBlock -- during
| during FacetPattern actor (named Expression<withIn>)? FacetBlock -- duringActor
AssertWhenClause = when "(" Expression<withIn> ")"
@ -76,9 +87,11 @@ Syndicate <: ES5 {
assert = "assert" ~identifierPart
asserted = "asserted" ~identifierPart
assertion = "assertion" ~identifierPart
dataflow = "dataflow" ~identifierPart
dataspace = "dataspace" ~identifierPart
during = "during" ~identifierPart
event = "event" ~identifierPart
field = "field" ~identifierPart
ground = "ground" ~identifierPart
message = "message" ~identifierPart
metalevel = "metalevel" ~identifierPart

1
js/examples/button/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.expanded.js

View File

@ -0,0 +1,7 @@
all: index.expanded.js
%.expanded.js: %.js
../../bin/syndicatec $< > $@ || (rm -f $@; false)
clean:
rm -f *.expanded.js

View File

@ -1,14 +0,0 @@
"use strict";
new Syndicate.Ground(function () {
Syndicate.UI.spawnUIDriver();
Syndicate.Actor.spawnActor(new Object(), function() {
var counter = 0;
var ui = new Syndicate.UI.Anchor();
Syndicate.Actor.createFacet()
.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(ui.html('#button-label',''+counter), 0); }))
.onEvent(false, "message", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(Syndicate.UI.globalEvent('#counter','click',_), 0); }), (function() { var _ = Syndicate.__; return { assertion: Syndicate.UI.globalEvent('#counter','click',_), metalevel: 0 }; }), (function() {
counter++;
})).completeBuild();
});
}).startStepping();

View File

@ -2,12 +2,12 @@ ground dataspace {
Syndicate.UI.spawnUIDriver();
actor {
var counter = 0;
field this.counter = 0;
var ui = new Syndicate.UI.Anchor();
react {
assert ui.html('#button-label', '' + counter);
assert ui.html('#button-label', '' + this.counter);
on message Syndicate.UI.globalEvent('#counter', 'click', _) {
counter++;
this.counter++;
}
}
}

View File

@ -17,6 +17,8 @@ function spawnChatApp() {
actor {
var ui = new Syndicate.UI.Anchor();
field this.nym;
field this.status;
react {
on asserted inputValue('#nym', $v) { this.nym = v; }
on asserted inputValue('#status', $v) { this.status = v; }
@ -83,17 +85,11 @@ assertion type inputValue(selector, value);
function spawnInputChangeMonitor() {
actor {
react {
on asserted Syndicate.observe(inputValue($selector, _)) {
actor {
this.value = $(selector).val();
react {
assert inputValue(selector, this.value);
on message Syndicate.UI.globalEvent(selector, 'change', $e) {
this.value = e.target.value;
}
} until {
case retracted Syndicate.observe(inputValue(selector, _));
}
during Syndicate.observe(inputValue($selector, _)) actor {
field this.value = $(selector).val();
assert inputValue(selector, this.value);
on message Syndicate.UI.globalEvent(selector, 'change', $e) {
this.value = e.target.value;
}
}
}

View File

@ -60,7 +60,7 @@ function spawnRemoteListener() {
function spawnStoveSwitch() {
actor {
this.powerOn = false;
field this.powerOn = false;
this.ui = new Syndicate.UI.Anchor();
react {
assert componentPresent('stove switch');
@ -85,7 +85,7 @@ function spawnStoveSwitch() {
function spawnPowerDrawMonitor() {
actor {
this.watts = 0;
field this.watts = 0;
this.ui = new Syndicate.UI.Anchor();
react {
assert componentPresent('power draw monitor');

View File

@ -40,7 +40,7 @@ ground dataspace G {
var geocoder = new google.maps.Geocoder();
var wsurl_base = 'wss://demo-broker.syndicate-lang.org:8443/location/';
var wsurl = wsurl_base + group_element.value.trim();
field this.wsurl = wsurl_base + group_element.value.trim();
var watchId = ('geolocation' in navigator)
&& navigator.geolocation.watchPosition(Syndicate.Dataspace.wrap(function (pos) {
@ -62,21 +62,21 @@ ground dataspace G {
}));
react {
var currentLocation = null;
field this.currentLocation = null;
var selectedMarker = null;
assert brokerConnection(wsurl);
assert toBroker(wsurl, currentLocation) when (currentLocation);
assert brokerConnection(this.wsurl);
assert toBroker(this.wsurl, this.currentLocation) when (this.currentLocation);
on message Syndicate.UI.globalEvent('#my_email', 'change', _) {
var v = email_element.value.trim();
if (currentLocation) currentLocation[1] = v;
if (this.currentLocation) this.currentLocation = this.currentLocation.set(1, v);
localStorage.my_email = v;
}
on message Syndicate.UI.globalEvent('#group', 'change', _) {
localStorage.group = group_element.value.trim();
wsurl = wsurl_base + group_element.value.trim();
this.wsurl = wsurl_base + group_element.value.trim();
}
on message Syndicate.UI.globalEvent('#findMarker', 'click', $e) {
@ -87,10 +87,10 @@ ground dataspace G {
}
on message ($loc = locationRecord(_, _, _, _, _)) {
currentLocation = loc;
this.currentLocation = loc;
}
during fromBroker(wsurl, locationRecord($id, $email, _, _, _)) {
during fromBroker(this.wsurl, locationRecord($id, $email, _, _, _)) {
var ui = new Syndicate.UI.Anchor();
var marker = new google.maps.Marker({
map: map,
@ -131,7 +131,7 @@ ground dataspace G {
selectMarker();
if (latestPosition) map.panTo(latestPosition);
}
on asserted fromBroker(wsurl, locationRecord(id, email, $timestamp, $lat, $lng)) {
on asserted fromBroker(this.wsurl, locationRecord(id, email, $timestamp, $lat, $lng)) {
latestTimestamp = new Date(timestamp);
latestPosition = {lat: lat, lng: lng};
marker.setPosition(latestPosition);

View File

@ -14,8 +14,8 @@ ground dataspace G {
var color = tinycolor('hsl ' + (Math.random() * 360 | 0) + ' 100% 50%').toHexString();
var x = 0;
var y = 0;
var publishedX = x;
var publishedY = y;
field this.publishedX = x;
field this.publishedY = y;
function clamp(v) {
var limit = 9.8;
@ -28,10 +28,10 @@ ground dataspace G {
assert Syndicate.UI.uiAttribute('rect#my_color', 'fill', color);
assert toBroker(wsurl, point(color, publishedX, publishedY));
assert toBroker(wsurl, point(color, this.publishedX, this.publishedY));
on message Syndicate.Timer.periodicTick(100) {
publishedX = x;
publishedY = y;
this.publishedX = x;
this.publishedY = y;
}
on message Syndicate.UI.windowEvent('deviceorientation', $e) {

View File

@ -6,16 +6,16 @@ ground dataspace {
actor {
react until {
case asserted Syndicate.observe(beep(_)) {
var counter = 0;
field this.counter = 0;
react {
do {
:: beep(counter++);
:: beep(this.counter++);
}
on message beep(_) {
:: beep(counter++);
:: beep(this.counter++);
}
} until {
case (counter >= 10);
case (this.counter > 10);
}
}
}

View File

@ -4,6 +4,10 @@ ground dataspace G {
actor {
var ui = new Syndicate.UI.Anchor();
field this.angle;
field this.handX;
field this.handY;
react {
assert ui.html('#clock',
'<svg width="300px" viewBox="0 0 100 100">'+

View File

@ -20,7 +20,7 @@ function spawnModel() {
function spawnView() {
actor named 'view' {
var ui = new Syndicate.UI.Anchor();
var orderColumn = 2;
field this.orderColumn = 2;
function cell(text) {
// Should escape text in a real application.
@ -28,13 +28,13 @@ function spawnView() {
}
react {
on message setSortColumn($c) { orderColumn = c; }
on message setSortColumn($c) { this.orderColumn = c; }
during person($id, $firstName, $lastName, $address, $age) {
assert ui.context(id)
.html('table#the-table tbody',
'<tr>' + [id, firstName, lastName, address, age].map(cell).join('') + '</tr>',
[id, firstName, lastName, address, age][orderColumn]);
[id, firstName, lastName, address, age][this.orderColumn]);
}
}
}

View File

@ -26,24 +26,24 @@ function piece(text, pos, lo, hi, cls) {
function spawnGui() {
actor {
this.text = '';
this.pos = 0;
this.highlightState = false;
this.updateDisplay = function () {
var text = this.text;
var pos = this.pos;
var highlight = this.highlightState;
var hLeft = highlight ? highlight[0] : 0;
var hRight = highlight ? highlight[1] : 0;
document.getElementById("fieldContents").innerHTML = highlight
? piece(text, pos, 0, hLeft, "normal") +
piece(text, pos, hLeft, hRight, "highlight") +
piece(text, pos, hRight, text.length + 1, "normal")
: piece(text, pos, 0, text.length + 1, "normal");
};
react {
field this.text = '';
field this.pos = 0;
field this.highlightState = false;
dataflow {
var text = this.text;
var pos = this.pos;
var highlight = this.highlightState;
var hLeft = highlight ? highlight[0] : 0;
var hRight = highlight ? highlight[1] : 0;
document.getElementById("fieldContents").innerHTML = highlight
? piece(text, pos, 0, hLeft, "normal") +
piece(text, pos, hLeft, hRight, "highlight") +
piece(text, pos, hRight, text.length + 1, "normal")
: piece(text, pos, 0, text.length + 1, "normal");
}
on message globalEvent("#inputRow", "+keydown", $event) {
switch (event.keyCode) {
case 37 /* left */: :: fieldCommand("cursorLeft"); break;
@ -67,12 +67,10 @@ function spawnGui() {
on asserted fieldContents($text, $pos) {
this.text = text;
this.pos = pos;
this.updateDisplay();
}
on asserted highlight($state) {
this.highlightState = state;
this.updateDisplay();
}
}
}
@ -83,10 +81,10 @@ function spawnGui() {
function spawnModel() {
actor {
this.fieldValue = "initial";
this.cursorPos = this.fieldValue.length; /* positions address gaps between characters */
react {
field this.fieldValue = "initial";
field this.cursorPos = this.fieldValue.length; /* positions address gaps between characters */
assert fieldContents(this.fieldValue, this.cursorPos);
on message fieldCommand("cursorLeft") {
@ -126,29 +124,28 @@ function spawnModel() {
function spawnSearch() {
actor {
this.fieldValue = "";
this.highlight = false;
this.search = function () {
var searchtext = document.getElementById("searchBox").value;
if (searchtext) {
var pos = this.fieldValue.indexOf(searchtext);
this.highlight = (pos !== -1) && [pos, pos + searchtext.length];
} else {
this.highlight = false;
}
};
react {
field this.searchtext = document.getElementById("searchBox").value;
field this.fieldValue = "";
field this.highlight = false;
assert highlight(this.highlight);
dataflow {
if (this.searchtext) {
var pos = this.fieldValue.indexOf(this.searchtext);
this.highlight = (pos !== -1) && [pos, pos + this.searchtext.length];
} else {
this.highlight = false;
}
}
on message globalEvent("#searchBox", "input", $event) {
this.search();
this.searchtext = document.getElementById("searchBox").value;
}
on asserted fieldContents($text, _) {
this.fieldValue = text;
this.search();
}
}
}

View File

@ -25,11 +25,11 @@ assertion type show(completed);
function todoListItemModel(initialId, initialTitle, initialCompleted) {
actor {
this.id = initialId;
this.title = initialTitle;
this.completed = initialCompleted;
react {
field this.id = initialId;
field this.title = initialTitle;
field this.completed = initialCompleted;
assert todo(this.id, this.title, this.completed);
on message setCompleted(this.id, $v) { this.completed = v; }
@ -58,8 +58,9 @@ function getTemplate(id) {
function todoListItemView(id) {
actor {
this.ui = new Syndicate.UI.Anchor();
this.editing = false;
react {
field this.editing = false;
during todo(id, $title, $completed) {
during show(completed) {
assert this.ui.html('.todo-list',
@ -158,15 +159,15 @@ ground dataspace G {
}
actor {
var completedCount = 0;
var activeCount = 0;
react {
on asserted todo($id, _, $completed) { if (completed) completedCount++; else activeCount++; }
on retracted todo($id, _, $completed) { if (completed) completedCount--; else activeCount--; }
assert activeTodoCount(activeCount);
assert completedTodoCount(completedCount);
assert totalTodoCount(activeCount + completedCount);
assert allCompleted() when (completedCount > 0 && activeCount === 0);
field this.completedCount = 0;
field this.activeCount = 0;
on asserted todo($id, _, $c) { if (c) this.completedCount++; else this.activeCount++; }
on retracted todo($id, _, $c) { if (c) this.completedCount--; else this.activeCount--; }
assert activeTodoCount(this.activeCount);
assert completedTodoCount(this.completedCount);
assert totalTodoCount(this.activeCount + this.completedCount);
assert allCompleted() when (this.completedCount > 0 && this.activeCount === 0);
}
}

View File

@ -91,19 +91,27 @@ function seller() {
/// We give our actor two state variables: a dictionary recording our
/// inventory of books (mapping title to price), and a counter
/// tracking the next order ID to be allocated.
///
/// We mark each property (entry) in the `books` table as a *field* so
/// that dependency-tracking on a per-book basis is enabled. As a result,
/// when a book is sold, any client still interested in its price will
/// learn that the book is no longer available.
///
/// We do not enable dependency-tracking for either the `books` table
/// itself or the `nextOrderId` field: nothing depends on tracking
/// changes in their values.
this.books = {
"The Wind in the Willows": 3.95,
"Catch 22": 2.22,
"Candide": 34.95
};
this.books = {};
field this.books["The Wind in the Willows"] = 3.95;
field this.books["Catch 22"] = 2.22;
field this.books["Candide"] = 34.95;
this.nextOrderId = 10001483;
/// Looking up a price yields `false` if no such book is in our
/// inventory.
this.priceOf = function (title) {
return (title in this.books) && this.books[title];
return (title in this.books) && field this.books[title];
};
/// The seller responds to interest in bookQuotes by asserting a
@ -134,7 +142,7 @@ function seller() {
/// replying to the orderer.
var orderId = this.nextOrderId++;
delete this.books[title];
delete field this.books[title];
actor {
whileRelevantAssert(

View File

@ -8,20 +8,22 @@ var Mux = require('./mux.js');
var Patch = require('./patch.js');
var Trie = require('./trie.js');
var Util = require('./util.js');
var Dataflow = require('./dataflow.js');
//---------------------------------------------------------------------------
function spawnActor(state, bootFn, optName) {
Dataspace.spawn(new Actor(state, bootFn, optName));
function spawnActor(bootFn, optName) {
Dataspace.spawn(new Actor(bootFn, optName));
}
function Actor(state, bootFn, optName) {
this.state = state;
function Actor(bootFn, optName) {
this.fields = {};
this.facets = Immutable.Set();
this.mux = new Mux.Mux();
this.previousKnowledge = Trie.emptyTrie;
this.knowledge = Trie.emptyTrie;
this.pendingActions = [];
this.dataflowGraph = new Dataflow.Graph();
if (typeof optName !== 'undefined') {
this.name = optName;
@ -29,36 +31,82 @@ function Actor(state, bootFn, optName) {
this.boot = function() {
var self = this;
withCurrentFacet(null, function () {
bootFn.call(self.state);
withCurrentFacet(self, null, function () {
bootFn.call(self.fields);
});
self.checkForTermination();
this.quiesce();
};
}
Actor.current = null;
(function () {
var priorities = ['PRIORITY_QUERY_HIGH',
'PRIORITY_QUERY',
'PRIORITY_QUERY_HANDLER',
'PRIORITY_NORMAL'];
for (var i = 0; i < priorities.length; i++) {
Actor[priorities[i]] = i;
}
})();
Actor.prototype.nextPendingAction = function (probe) {
for (var i = 0; i < this.pendingActions.length; i++) {
var q = this.pendingActions[i];
if (q.length > 0) {
return probe ? true : q.shift();
}
}
return false;
};
Actor.prototype.handleEvent = function(e) {
var actor = this;
if (e.type === 'stateChange') {
this.previousKnowledge = this.knowledge;
this.knowledge = e.patch.updateInterests(this.knowledge);
}
if (this.pendingActions.length > 0) {
if (this.nextPendingAction(true)) {
throw new Error('Syndicate: pendingActions must not be nonempty at start of handleEvent');
}
this.facets.forEach(function (f) {
withCurrentFacet(f, function () { f.handleEvent(e); });
withCurrentFacet(actor, f, function () { f.handleEvent(e); });
});
while (this.pendingActions.length) {
var entry = this.pendingActions.shift();
withCurrentFacet(entry.facet, entry.action);
this.quiesce();
};
Actor.prototype.quiesce = function() {
var actor = this;
while (true) {
var entry = this.nextPendingAction(false);
if (!entry) break;
withCurrentFacet(actor, entry.facet, entry.action);
this.dataflowGraph.repairDamage(function (subjectId) {
var facet = subjectId[0];
var endpoint = subjectId[1];
withCurrentFacet(actor, facet, function () {
// TODO: coalesce patches within a single actor
var aggregate = Patch.emptyPatch;
var patch = Patch.retract(__).andThen(endpoint.subscriptionFn.call(facet.fields));
var r = facet.actor.mux.updateStream(endpoint.eid, patch);
aggregate = aggregate.andThen(r.deltaAggregate);
Dataspace.stateChange(aggregate);
});
});
}
this.facets.forEach(function (f) {
withCurrentFacet(f, function () { f.refresh(); });
});
this.checkForTermination();
};
Actor.prototype.pushAction = function (a) {
this.pendingActions.push({facet: Facet.current, action: a});
Actor.prototype.pushAction = function (a, priorityOpt) {
var priority = typeof priorityOpt === 'undefined' ? Actor.PRIORITY_NORMAL : priorityOpt;
while (this.pendingActions.length < priority + 1) {
this.pendingActions.push([]);
}
this.pendingActions[priority].push({facet: Facet.current, action: a});
};
Actor.prototype.addFacet = function(facet) {
@ -88,29 +136,34 @@ function Facet(actor) {
this.doneBlocks = Immutable.List();
this.children = Immutable.Set();
this.parent = Facet.current;
this.fields = Dataflow.Graph.newScope((this.parent && this.parent.fields) || actor.fields);
this.terminated = false;
}
Facet.current = null;
function withCurrentFacet(facet, f) {
var previous = Facet.current;
function withCurrentFacet(actor, facet, f) {
var previousActor = Actor.current;
var previousFacet = Facet.current;
Actor.current = actor;
Facet.current = facet;
var result;
try {
result = f();
} catch (e) {
Facet.current = previous;
Actor.current = previousActor;
Facet.current = previousFacet;
throw e;
}
Facet.current = previous;
Actor.current = previousActor;
Facet.current = previousFacet;
return result;
}
Facet.prototype.handleEvent = function(e) {
var facet = this;
facet.endpoints.forEach(function(endpoint) {
endpoint.handlerFn.call(facet.actor.state, e);
endpoint.handlerFn.call(facet.fields, e);
});
};
@ -118,27 +171,47 @@ Facet.prototype.addAssertion = function(assertionFn) {
return this.addEndpoint(new Endpoint(assertionFn, function(e) {}));
};
Facet.prototype.addOnEventHandler = function(handler) {
Facet.prototype.addOnEventHandler = function(handler, priorityOpt) {
var facet = this;
return this.addEndpoint(new Endpoint(function () { return Patch.emptyPatch; }, function (e) {
facet.actor.pushAction(function () { handler(e); });
facet.actor.pushAction(function () { handler(e); }, priorityOpt);
}));
};
Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projectionFn, handlerFn) {
Facet.prototype.addDataflow = function(subjectFunction) {
var facet = this;
return this.addEndpoint(new Endpoint(function () {
var subjectId = facet.actor.dataflowGraph.currentSubjectId;
facet.actor.pushAction(function () {
facet.actor.dataflowGraph.withSubject(subjectId, function () {
subjectFunction.call(facet.fields);
});
});
return Patch.emptyPatch;
}, function (e) {}));
};
Facet.prototype.onEvent = function(priority,
isTerminal,
eventType,
subscriptionFn,
projectionFn,
handlerFn)
{
var facet = this;
switch (eventType) {
case 'message':
return this.addEndpoint(new Endpoint(subscriptionFn, function(e) {
if (e.type === 'message') {
var proj = projectionFn.call(facet.actor.state);
var proj = projectionFn.call(facet.fields);
var spec = Patch.prependAtMeta(proj.assertion, proj.metalevel);
var match = Trie.matchPattern(e.message, spec);
// console.log(match);
if (match) {
if (isTerminal) { facet.terminate(); }
facet.actor.pushAction(function () { Util.kwApply(handlerFn, facet.actor.state, match); });
facet.actor.pushAction(function () { Util.kwApply(handlerFn, facet.fields, match); },
priority);
}
}
}));
@ -147,7 +220,7 @@ Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projec
case 'retracted':
return this.addEndpoint(new Endpoint(subscriptionFn, function(e) {
if (e.type === 'stateChange') {
var proj = projectionFn.call(facet.actor.state);
var proj = projectionFn.call(facet.fields);
var spec = Patch.prependAtMeta(proj.assertion, proj.metalevel);
var objects = Trie.projectObjects(eventType === 'asserted'
? e.patch.added
@ -163,8 +236,8 @@ Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projec
facet.terminate();
}
facet.actor.pushAction(function () {
Util.kwApply(handlerFn, facet.actor.state, o);
});
Util.kwApply(handlerFn, facet.fields, o);
}, priority);
}
});
}
@ -172,17 +245,17 @@ Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projec
}));
case 'risingEdge':
var endpoint = new Endpoint(function() { return Patch.emptyPatch; },
function(e) {
var newValue = subscriptionFn.call(facet.actor.state);
if (newValue && !this.currentValue) {
if (isTerminal) { facet.terminate(); }
facet.actor.pushAction(function () {
handlerFn.call(facet.actor.state);
});
}
this.currentValue = newValue;
});
var endpoint = new Endpoint(function() {
var newValue = subscriptionFn.call(facet.fields);
if (newValue && !this.currentValue) {
if (isTerminal) { facet.terminate(); }
facet.actor.pushAction(function () {
handlerFn.call(facet.fields);
}, priority);
}
this.currentValue = newValue;
return Patch.emptyPatch;
}, function(e) {});
endpoint.currentValue = false;
return this.addEndpoint(endpoint);
@ -206,11 +279,15 @@ Facet.prototype.interestWas = function(assertedOrRetracted, pat) {
};
Facet.prototype.addEndpoint = function(endpoint) {
var patch = endpoint.subscriptionFn.call(this.actor.state);
var r = this.actor.mux.addStream(patch);
this.endpoints = this.endpoints.set(r.pid, endpoint);
var facet = this;
var patch = facet.actor.dataflowGraph.withSubject([facet, endpoint], function () {
return endpoint.subscriptionFn.call(facet.fields);
});
var r = facet.actor.mux.addStream(patch);
endpoint.eid = r.pid;
facet.endpoints = facet.endpoints.set(endpoint.eid, endpoint);
Dataspace.stateChange(r.deltaAggregate);
return this; // for chaining
return facet; // for chaining
};
Facet.prototype.addInitBlock = function(thunk) {
@ -223,28 +300,17 @@ Facet.prototype.addDoneBlock = function(thunk) {
return this;
};
Facet.prototype.refresh = function() {
var facet = this;
var aggregate = Patch.emptyPatch;
this.endpoints.forEach(function(endpoint, eid) {
var patch = Patch.retract(__).andThen(endpoint.subscriptionFn.call(facet.actor.state));
var r = facet.actor.mux.updateStream(eid, patch);
aggregate = aggregate.andThen(r.deltaAggregate);
});
Dataspace.stateChange(aggregate);
};
Facet.prototype.completeBuild = function() {
var facet = this;
this.actor.addFacet(this);
if (this.parent) {
this.parent.children = this.parent.children.add(this);
}
withCurrentFacet(facet, function () {
facet.initBlocks.forEach(function(b) { b.call(facet.actor.state); });
withCurrentFacet(this.actor, facet, function () {
facet.initBlocks.forEach(function(b) { b.call(facet.fields); });
});
var initialEvent = _Dataspace.stateChange(new Patch.Patch(facet.actor.knowledge, Trie.emptyTrie));
withCurrentFacet(facet, function () { facet.handleEvent(initialEvent); });
withCurrentFacet(this.actor, facet, function () { facet.handleEvent(initialEvent); });
};
Facet.prototype.terminate = function() {
@ -267,13 +333,13 @@ Facet.prototype.terminate = function() {
this.actor.removeFacet(this);
withCurrentFacet(facet, function () {
facet.doneBlocks.forEach(function(b) { b.call(facet.actor.state); });
});
this.children.forEach(function (child) {
child.terminate();
});
withCurrentFacet(this.actor, facet, function () {
facet.doneBlocks.forEach(function(b) { b.call(facet.fields); });
});
};
//---------------------------------------------------------------------------
@ -281,9 +347,37 @@ Facet.prototype.terminate = function() {
function Endpoint(subscriptionFn, handlerFn) {
this.subscriptionFn = subscriptionFn;
this.handlerFn = handlerFn;
this.eid = 'uninitialized_eid'; // initialized later
}
//---------------------------------------------------------------------------
function referenceField(obj, prop) {
if (!(prop in obj)) {
Actor.current.dataflowGraph.recordObservation(Immutable.List.of(obj, prop));
}
return obj[prop];
}
function declareField(obj, prop, init) {
if (prop in obj) {
obj[prop] = init;
} else {
Actor.current.dataflowGraph.defineObservableProperty(obj, prop, init, {
objectId: Immutable.List.of(obj, prop)
});
}
}
function deleteField(obj, prop) {
Actor.current.dataflowGraph.recordDamage(Immutable.List.of(obj, prop));
return delete obj[prop];
}
//---------------------------------------------------------------------------
module.exports.spawnActor = spawnActor;
module.exports.createFacet = createFacet;
module.exports.referenceField = referenceField;
module.exports.declareField = declareField;
module.exports.deleteField = deleteField;

View File

@ -9,7 +9,6 @@ function Graph() {
this.edgesReverse = Immutable.Map();
this.damagedNodes = Immutable.Set();
this.currentSubjectId = null;
this.enforceSubjectPresence = true;
}
Graph.prototype.withSubject = function (subjectId, f) {
@ -30,8 +29,6 @@ Graph.prototype.recordObservation = function (objectId) {
if (this.currentSubjectId) {
this.edgesForward = MapSet.add(this.edgesForward, objectId, this.currentSubjectId);
this.edgesReverse = MapSet.add(this.edgesReverse, this.currentSubjectId, objectId);
} else if (this.enforceSubjectPresence) {
throw new Error('Attempt to observe ' + objectId + ' with no currentSubjectId');
}
};
@ -80,7 +77,7 @@ Graph.prototype.repairDamage = function (repairNode) {
Graph.prototype.defineObservableProperty = function (obj, prop, value, maybeOptions) {
var graph = this;
var options = typeof maybeOptions === 'undefined' ? {} : maybeOptions;
var objectId = '__' + (options.baseId || prop);
var objectId = options.objectId || '__' + prop;
Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,

View File

@ -33,6 +33,7 @@ module.exports.Reflect = require("./reflect.js");
module.exports.WakeDetector = require("./wake-detector-driver.js");
module.exports.Codec = require("./codec.js");
module.exports.Broker = require("./broker.js");
module.exports.Dataflow = require("./dataflow.js");
module.exports.Patch = require("./patch.js");
copyKeys(['emptyPatch',

View File

@ -54,6 +54,19 @@ function Structure(meta, fields) {
}
}
Structure.prototype.clone = function () {
return new Structure(this.meta, this.fields);
};
Structure.prototype.get = function (index) {
return this[index];
};
Structure.prototype.set = function (index, value) {
var s = this.clone();
s[index] = s.fields[index] = value;
};
function reviveStructs(j) {
if (Array.isArray(j)) {
return j.map(reviveStructs);

View File

@ -7,7 +7,7 @@ var Dataflow = require('../src/dataflow.js');
function Cell(graph, initialValue, name) {
this.objectId = graph.defineObservableProperty(this, 'value', initialValue, {
baseId: name,
objectId: name,
noopGuard: function (a, b) {
return a === b;
}
@ -50,7 +50,6 @@ describe('dataflow edges, damage and subjects', function () {
describe('DerivedCell', function () {
describe('simple case', function () {
var g = new Dataflow.Graph();
g.enforceSubjectPresence = false;
var c = DerivedCell(g, 'c', function () { return 123; });
var d = DerivedCell(g, 'd', function () { return c.value * 2; });
it('should be properly initialized', function () {
@ -79,7 +78,6 @@ describe('DerivedCell', function () {
describe('a more complex case', function () {
var g = new Dataflow.Graph();
g.enforceSubjectPresence = false;
function add(a, b) { return a + b; }
var xs = new Cell(g, Immutable.List.of(1, 2, 3, 4), 'xs');
@ -132,7 +130,6 @@ describe('DerivedCell', function () {
describe('scopes', function () {
var g = new Dataflow.Graph();
g.enforceSubjectPresence = false;
function buildScopes() {
var rootScope = {};
@ -147,6 +144,9 @@ describe('scopes', function () {
expect(ss.root.p).to.be(123);
expect(ss.mid.p).to.be(123);
expect(ss.outer.p).to.be(123);
expect('p' in ss.root).to.be(true);
expect('p' in ss.mid).to.be(true);
expect('p' in ss.outer).to.be(true);
});
it('should make changes at root visible at leaves', function () {
@ -173,6 +173,9 @@ describe('scopes', function () {
expect(ss.outer.p).to.be(123);
expect(ss.mid.p).to.be(undefined);
expect(ss.root.p).to.be(undefined);
expect('p' in ss.root).to.be(false);
expect('p' in ss.mid).to.be(false);
expect('p' in ss.outer).to.be(true);
});
it('should hide middle definitions from roots but show to leaves', function () {
@ -181,5 +184,8 @@ describe('scopes', function () {
expect(ss.outer.p).to.be(123);
expect(ss.mid.p).to.be(123);
expect(ss.root.p).to.be(undefined);
expect('p' in ss.root).to.be(false);
expect('p' in ss.mid).to.be(true);
expect('p' in ss.outer).to.be(true);
});
});