actor{react{...}} ==> actor{...} for JS

This commit is contained in:
Tony Garnock-Jones 2016-08-25 13:12:32 +01:00
parent c61ed644ce
commit 138bab9ba6
19 changed files with 678 additions and 810 deletions

View File

@ -42,28 +42,29 @@ var forEachChild = (function () {
return forEachChild; return forEachChild;
})(); })();
function buildActor(nameExpOpt, block) { function buildActor(nameExpOpt, block, withReact) {
var nameExpStr; var nameExpStr;
if (nameExpOpt.numChildren === 1) { if (nameExpOpt.numChildren === 1) {
nameExpStr = ', ' + nameExpOpt.asES5; nameExpStr = ', ' + nameExpOpt.asES5;
} else { } else {
nameExpStr = ''; nameExpStr = '';
} }
return 'Syndicate.Actor.spawnActor(function() ' + block.asES5 + nameExpStr + ');'; return 'Syndicate.Actor.spawnActor(function() ' +
(withReact ? reactWrap(block.asES5) : block.asES5) +
nameExpStr + ');';
} }
function buildFacet(facetBlock, transitionBlock) { function reactWrap(blockCode) {
return '(function () { ' + (facetBlock ? facetBlock.facetVarDecls : '') + return '{ Syndicate.Actor.Facet.build((function () { ' +
'\nSyndicate.Actor.createFacet()' + blockCode +
(facetBlock ? facetBlock.asES5 : '') + ' }).bind(this)); }';
(transitionBlock ? transitionBlock.asES5 : '') +
'.completeBuild(); }).call(this);';
} }
function buildOnEvent(isTerminal, eventType, subscription, projection, bindings, body) { function buildOnEvent(isTerminal, eventType, subscription, projection, bindings, body) {
return '\n.onEvent(Syndicate.Actor.PRIORITY_NORMAL, ' + isTerminal + ', ' + JSON.stringify(eventType) + ', ' + return 'Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, ' + isTerminal + ', ' +
JSON.stringify(eventType) + ', ' +
subscription + ', ' + projection + subscription + ', ' + projection +
', (function(' + bindings.join(', ') + ') ' + body + '))'; ', (function(' + bindings.join(', ') + ') ' + body + '));';
} }
function buildCaseEvent(eventPattern, body) { function buildCaseEvent(eventPattern, body) {
@ -85,8 +86,11 @@ function buildCaseEvent(eventPattern, body) {
} }
var modifiedSourceActions = { var modifiedSourceActions = {
ActorStatement: function(_actor, _namedOpt, nameExpOpt, block) { ActorStatement_noReact: function(_actorStar, _namedOpt, nameExpOpt, block) {
return buildActor(nameExpOpt, block); return buildActor(nameExpOpt, block, false);
},
ActorStatement_withReact: function(_actor, _namedOpt, nameExpOpt, block) {
return buildActor(nameExpOpt, block, true);
}, },
DataspaceStatement_ground: function(_ground, _dataspace, maybeId, block) { DataspaceStatement_ground: function(_ground, _dataspace, maybeId, block) {
@ -101,14 +105,8 @@ var modifiedSourceActions = {
return 'Syndicate.Dataspace.spawn(new Dataspace(function () ' + block.asES5 + '));'; return 'Syndicate.Dataspace.spawn(new Dataspace(function () ' + block.asES5 + '));';
}, },
ActorFacetStatement_state: function(_state, facetBlock, _until, transitionBlock) { ActorFacetStatement: function(_react, block) {
return buildFacet(facetBlock, transitionBlock); return '(function () ' + reactWrap(block.asES5) + ').call(this);';
},
ActorFacetStatement_until: function(_react, _until, transitionBlock) {
return buildFacet(null, transitionBlock);
},
ActorFacetStatement_forever: function(_forever, facetBlock) {
return buildFacet(facetBlock, null);
}, },
AssertionTypeDeclarationStatement: function(_assertion, AssertionTypeDeclarationStatement: function(_assertion,
@ -151,24 +149,17 @@ var modifiedSourceActions = {
return 'Syndicate.Dataspace.send(' + expr.asES5 + ')' + sc.interval.contents; return 'Syndicate.Dataspace.send(' + expr.asES5 + ')' + sc.interval.contents;
}, },
FacetBlock: function(_leftParen, _varStmts, init, situations, done, _rightParen) { ActorEndpointStatement_start: function (_on, _start, block) {
return (init ? init.asES5 : '') + situations.asES5.join('') + (done ? done.asES5 : ''); return 'Syndicate.Actor.Facet.current.addInitBlock((function() ' + block.asES5 + '));';
}, },
FacetStateTransitionBlock: function(_leftParen, transitions, _rightParen) { ActorEndpointStatement_stop: function (_on, _stop, block) {
return transitions.asES5.join(''); return 'Syndicate.Actor.Facet.current.addDoneBlock((function() ' + block.asES5 + '));';
}, },
ActorEndpointStatement_assert: function(_assert, expr, whenClause, _sc) {
FacetInitBlock: function(_init, block) { return 'Syndicate.Actor.Facet.current.addAssertion(' +
return '\n.addInitBlock((function() ' + block.asES5 + '))'; buildSubscription([expr], 'assert', 'pattern', whenClause, null) + ');';
}, },
FacetDoneBlock: function(_done, block) { ActorEndpointStatement_event: function(_on, eventPattern, block) {
return '\n.addDoneBlock((function() ' + block.asES5 + '))';
},
FacetSituation_assert: function(_assert, expr, whenClause, _sc) {
return '\n.addAssertion(' + buildSubscription([expr], 'assert', 'pattern', whenClause, null) + ')';
},
FacetSituation_event: function(_on, eventPattern, block) {
return buildOnEvent(false, return buildOnEvent(false,
eventPattern.eventType, eventPattern.eventType,
eventPattern.subscription, eventPattern.subscription,
@ -176,63 +167,59 @@ var modifiedSourceActions = {
eventPattern.bindings, eventPattern.bindings,
block.asES5); block.asES5);
}, },
FacetSituation_onEvent: function (_on, _event, id, block) { ActorEndpointStatement_onEvent: function (_on, _event, id, block) {
return '\n.addOnEventHandler((function(' + id.asES5 + ') ' + block.asES5 + '))'; return 'Syndicate.Actor.Facet.current.addOnEventHandler((function(' + id.asES5 + ') ' +
block.asES5 + '));';
}, },
FacetSituation_dataflow: function (_dataflow, block) { ActorEndpointStatement_stopOnWithCont: function(_stop, _on, eventPattern, block) {
return '\n.addDataflow((function () ' + block.asES5 + '))'; return buildCaseEvent(eventPattern, block.asES5);
}, },
FacetSituation_during: function(_during, pattern, facetBlock) { ActorEndpointStatement_stopOnNoCont: function(_stop, _on, eventPattern, _sc) {
return buildCaseEvent(eventPattern, '{}');
},
ActorEndpointStatement_dataflow: function (_dataflow, block) {
return 'Syndicate.Actor.Facet.current.addDataflow((function () ' + block.asES5 + '));';
},
ActorEndpointStatement_during: function(_during, pattern, block) {
var cachedAssertionVar = gensym('cachedAssertion'); var cachedAssertionVar = gensym('cachedAssertion');
return buildOnEvent(false, return buildOnEvent(false,
'asserted', 'asserted',
pattern.subscription, pattern.subscription,
pattern.projection, pattern.projection,
pattern.bindings, pattern.bindings,
'{ ' + facetBlock.facetVarDecls + '{\n' +
'\nvar '+cachedAssertionVar+' = '+pattern.instantiatedAssertion+';'+ 'var '+cachedAssertionVar+' = '+pattern.instantiatedAssertion+';\n'+
'\nSyndicate.Actor.createFacet()' + reactWrap(block.asES5 + '\n' +
facetBlock.asES5 + buildOnEvent(true,
buildOnEvent(true, 'retracted',
'retracted', pattern.instantiatedSubscription(cachedAssertionVar),
pattern.instantiatedSubscription(cachedAssertionVar), pattern.instantiatedProjection(cachedAssertionVar),
pattern.instantiatedProjection(cachedAssertionVar), [],
[], '{}')) + '}');
'{}') +
'.completeBuild(); }');
}, },
FacetSituation_duringActor: function(_during, pattern, _actor, _named, nameExpOpt, facetBlock) { ActorEndpointStatement_duringActor: function(_during, pattern, _actor, _named, nameExpOpt, block)
{
var cachedAssertionVar = gensym('cachedAssertion'); var cachedAssertionVar = gensym('cachedAssertion');
var actorBlock = { var actorBlock = {
asES5: '{ ' + facetBlock.facetVarDecls + asES5: reactWrap(block.asES5 + '\n' +
'\nSyndicate.Actor.createFacet()' + buildOnEvent(true,
facetBlock.asES5 + 'retracted',
buildOnEvent(true, pattern.instantiatedSubscription(cachedAssertionVar),
'retracted', pattern.instantiatedProjection(cachedAssertionVar),
pattern.instantiatedSubscription(cachedAssertionVar), [],
pattern.instantiatedProjection(cachedAssertionVar), '{}'))
[],
'{}') +
'.completeBuild(); }'
}; };
return buildOnEvent(false, return buildOnEvent(false,
'asserted', 'asserted',
pattern.subscription, pattern.subscription,
pattern.projection, pattern.projection,
pattern.bindings, pattern.bindings,
'{ var '+cachedAssertionVar+' = '+pattern.instantiatedAssertion+';'+ '{ var '+cachedAssertionVar+' = '+pattern.instantiatedAssertion+';\n'+
'\n' + buildActor(nameExpOpt, actorBlock) + ' }'); buildActor(nameExpOpt, actorBlock, true) + ' }');
}, },
AssertWhenClause: function(_when, _lparen, expr, _rparen) { AssertWhenClause: function(_when, _lparen, expr, _rparen) {
return expr.asES5; return expr.asES5;
},
FacetStateTransition_withContinuation: function(_case, eventPattern, block) {
return buildCaseEvent(eventPattern, block.asES5);
},
FacetStateTransition_noContinuation: function(_case, eventPattern, _sc) {
return buildCaseEvent(eventPattern, '{}');
} }
}; };
@ -256,11 +243,7 @@ semantics.addAttribute('memberPropExpr', {
} }
}); });
semantics.addAttribute('facetVarDecls', { ///////////////////////////////////////////////////////////////////////////
FacetBlock: function (_leftParen, varDecls, _init, _situations, _done, _rightParen) {
return varDecls.asES5.join(' ');
}
});
semantics.addAttribute('asSyndicateStructureArguments', { semantics.addAttribute('asSyndicateStructureArguments', {
FormalParameterList: function(formals) { FormalParameterList: function(formals) {

View File

@ -8,39 +8,29 @@ message type deposit(amount);
ground dataspace { ground dataspace {
actor { actor {
field this.balance = 0; field this.balance = 0;
assert account(this.balance);
react { dataflow {
assert account(this.balance); console.log("Balance inside account is", this.balance);
dataflow { }
console.log("Balance inside account is", this.balance); on message deposit($amount) {
} this.balance += amount;
on message deposit($amount) {
this.balance += amount;
}
} }
} }
actor { actor {
react { on asserted account($balance) {
on asserted account($balance) { console.log("Balance is now", balance);
console.log("Balance is now", balance);
}
} }
} }
actor { actor {
react { on start {
do { console.log("Waiting for account.");
console.log("Waiting for account."); }
} stop on asserted Syndicate.observe(deposit(_)) {
finally { console.log("Account became ready.");
console.log("Account became ready."); :: deposit(+100);
} :: deposit(-30);
} until {
case asserted Syndicate.observe(deposit(_)) {
:: deposit(+100);
:: deposit(-30);
}
} }
} }
} }

View File

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

View File

@ -24,53 +24,47 @@ ground dataspace {
actor { actor {
this.files = {}; this.files = {};
react { during Syndicate.observe(file($name, _)) {
during Syndicate.observe(file($name, _)) { on start {
do { console.log("At least one reader exists for:", name);
console.log("At least one reader exists for:", name);
}
assert file(name, field this.files[name]);
finally {
console.log("No remaining readers exist for:", name);
}
} }
on message saveFile($name, $newcontent) { assert file(name, field this.files[name]);
field this.files[name] = newcontent; on stop {
} console.log("No remaining readers exist for:", name);
on message deleteFile($name) {
delete field this.files[name];
} }
} }
on message saveFile($name, $newcontent) {
field this.files[name] = newcontent;
}
on message deleteFile($name) {
delete field this.files[name];
}
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// A simple demo client of the file system // A simple demo client of the file system
actor { actor {
react { on asserted file("hello.txt", $content) {
on asserted file("hello.txt", $content) { console.log("hello.txt has content", JSON.stringify(content));
console.log("hello.txt has content", JSON.stringify(content));
}
} until {
case asserted file("hello.txt", "quit demo") {
console.log("The hello.txt file contained 'quit demo', so we will quit");
}
} }
react until { stop on asserted file("hello.txt", "quit demo") {
case asserted Syndicate.observe(saveFile(_, _)) { console.log("The hello.txt file contained 'quit demo', so we will quit");
:: saveFile("hello.txt", "a"); }
:: deleteFile("hello.txt"); }
:: saveFile("hello.txt", "c");
:: saveFile("hello.txt", "quit demo"); actor {
:: saveFile("hello.txt", "final contents"); stop on asserted Syndicate.observe(saveFile(_, _)) {
actor { :: saveFile("hello.txt", "a");
react until { :: deleteFile("hello.txt");
case asserted file("hello.txt", $content) { :: saveFile("hello.txt", "c");
console.log("second observer sees that hello.txt content is", :: saveFile("hello.txt", "quit demo");
JSON.stringify(content)); :: saveFile("hello.txt", "final contents");
} actor {
} stop on asserted file("hello.txt", $content) {
console.log("second observer sees that hello.txt content is",
JSON.stringify(content));
} }
} }
} }

View File

@ -60,69 +60,58 @@ assertion type view(str);
ground dataspace { ground dataspace {
actor { actor {
react { field this.title = "first";
field this.title = "first"; assert todo(this.title);
assert todo(this.title); on message 3 {
on message 3 { this.title = "second";
this.title = "second";
}
} }
} }
actor { actor {
react { assert show();
assert show(); }
actor {
field this.editing = false;
during todo($title) {
on start { console.log('OUTER++', title); }
during show() {
on start { console.log('++', title); }
assert view((this.editing ? 'EDIT ' : 'VIEW ') + title);
on stop { console.log('--', title); }
}
on stop { console.log('OUTER--', title); }
}
on message 1 {
this.editing = true;
:: 2;
}
on message 2 {
:: 3;
this.editing = false;
} }
} }
actor { actor {
react { on start { :: 0; }
field this.editing = false; stop on message 0 {
:: 1;
during todo($title) {
do { console.log('OUTER++', title); }
during show() {
do { console.log('++', title); }
assert view((this.editing ? 'EDIT ' : 'VIEW ') + title);
finally { console.log('--', title); }
}
finally { console.log('OUTER--', title); }
}
on message 1 {
this.editing = true;
:: 2;
}
on message 2 {
:: 3;
this.editing = false;
}
} }
} }
actor { actor {
react { field this.count = 0;
do { :: 0; } on retracted view($x) { console.log('VIEW--', x); }
} until { on asserted view($x) {
case message 0 { console.log('VIEW++', x);
:: 1; if (x === 'VIEW second') {
} this.count++;
} if (this.count === 1) {
} console.log("Kicking off second edit cycle");
:: 1;
actor {
react {
field this.count = 0;
on retracted view($x) { console.log('VIEW--', x); }
on asserted view($x) {
console.log('VIEW++', x);
if (x === 'VIEW second') {
this.count++;
if (this.count === 1) {
console.log("Kicking off second edit cycle");
:: 1;
}
} }
} }
} }

View File

@ -15,33 +15,28 @@ assertion type entry(key, val);
ground dataspace { ground dataspace {
actor named 'listener' { actor named 'listener' {
react { assert ready('listener');
assert ready('listener'); on asserted entry($key, _) {
on asserted entry($key, _) { console.log('key asserted', key);
console.log('key asserted', key); react {
react { on asserted entry(key, $value) { console.log('binding', key, '--->', value); }
on asserted entry(key, $value) { console.log('binding', key, '--->', value); } on retracted entry(key, $value) { console.log('binding', key, '-/->', value); }
on retracted entry(key, $value) { console.log('binding', key, '-/->', value); } stop on retracted entry(key, _) {
} until { console.log('key retracted', key);
case retracted entry(key, _) {
console.log('key retracted', key);
}
} }
} }
} }
} }
actor named 'other-listener' { actor named 'other-listener' {
react { assert ready('other-listener');
assert ready('other-listener'); during entry($key, _) {
during entry($key, _) { on start { console.log('(other-listener) key asserted', key); }
do { console.log('(other-listener) key asserted', key); } during entry(key, $value) {
during entry(key, $value) { on start { console.log('(other-listener) binding', key, '--->', value); }
do { console.log('(other-listener) binding', key, '--->', value); } on stop { console.log('(other-listener) binding', key, '-/->', value); }
finally { console.log('(other-listener) binding', key, '-/->', value); }
}
finally { console.log('(other-listener) key retracted', key); }
} }
on stop { console.log('(other-listener) key retracted', key); }
} }
} }
@ -49,36 +44,33 @@ ground dataspace {
console.log('pause'); console.log('pause');
react { react {
assert ready('pause'); assert ready('pause');
} until { on asserted ready('pause') {
case asserted ready('pause') {
return k(); return k();
} }
} }
} }
actor named 'driver' { actor named 'driver' {
react until { stop on asserted ready('listener') {
case asserted ready('listener') { react {
react until { stop on asserted ready('other-listener') {
case asserted ready('other-listener') { Dataspace.stateChange(Patch.assert(entry('a', 1)));
Dataspace.stateChange(Patch.assert(entry('a', 1))); Dataspace.stateChange(Patch.assert(entry('a', 2)));
Dataspace.stateChange(Patch.assert(entry('a', 2))); Dataspace.stateChange(Patch.assert(entry('b', 3)));
Dataspace.stateChange(Patch.assert(entry('b', 3))); Dataspace.stateChange(Patch.assert(entry('c', 33)));
Dataspace.stateChange(Patch.assert(entry('c', 33))); Dataspace.stateChange(Patch.assert(entry('a', 4)));
Dataspace.stateChange(Patch.assert(entry('a', 4))); Dataspace.stateChange(Patch.assert(entry('a', 5)));
Dataspace.stateChange(Patch.assert(entry('a', 5))); pause(function () {
Dataspace.stateChange(Patch.retract(entry('a', 2)));
Dataspace.stateChange(Patch.retract(entry('c', 33)));
Dataspace.stateChange(Patch.assert(entry('a', 9)));
pause(function () { pause(function () {
Dataspace.stateChange(Patch.retract(entry('a', 2))); Dataspace.stateChange(Patch.retract(entry('a', __)));
Dataspace.stateChange(Patch.retract(entry('c', 33)));
Dataspace.stateChange(Patch.assert(entry('a', 9)));
pause(function () { pause(function () {
Dataspace.stateChange(Patch.retract(entry('a', __))); console.log('done');
pause(function () {
console.log('done');
});
}); });
}); });
} });
} }
} }
} }

View File

@ -10,6 +10,7 @@ Syndicate <: ES5 {
+= ActorStatement += ActorStatement
| DataspaceStatement | DataspaceStatement
| ActorFacetStatement | ActorFacetStatement
| ActorEndpointStatement
| AssertionTypeDeclarationStatement | AssertionTypeDeclarationStatement
| FieldDeclarationStatement | FieldDeclarationStatement
| SendMessageStatement | SendMessageStatement
@ -17,16 +18,29 @@ Syndicate <: ES5 {
FunctionBodyBlock = "{" FunctionBody "}" // odd that this isn't in es5.ohm somewhere FunctionBodyBlock = "{" FunctionBody "}" // odd that this isn't in es5.ohm somewhere
ActorStatement ActorStatement
= actor (named Expression<withIn>)? FunctionBodyBlock = actorStar (named Expression<withIn>)? FunctionBodyBlock -- noReact
| actor (named Expression<withIn>)? FunctionBodyBlock -- withReact
DataspaceStatement DataspaceStatement
= ground dataspace identifier? FunctionBodyBlock -- ground = ground dataspace identifier? FunctionBodyBlock -- ground
| dataspace FunctionBodyBlock -- normal | dataspace FunctionBodyBlock -- normal
ActorFacetStatement ActorFacetStatement
= react FacetBlock until FacetStateTransitionBlock -- state = react FunctionBodyBlock
| react until FacetStateTransitionBlock -- until
| react FacetBlock -- forever ActorEndpointStatement
= on start FunctionBodyBlock -- start
| on stop FunctionBodyBlock -- stop
| assert FacetPattern AssertWhenClause? #(sc) -- assert
| on FacetEventPattern FunctionBodyBlock -- event
| on event identifier FunctionBodyBlock -- onEvent
| stop on FacetTransitionEventPattern FunctionBodyBlock -- stopOnWithCont
| stop on FacetTransitionEventPattern #(sc) -- stopOnNoCont
| dataflow FunctionBodyBlock -- dataflow
| during FacetPattern FunctionBodyBlock -- during
| during FacetPattern actor (named Expression<withIn>)? FunctionBodyBlock -- duringActor
AssertWhenClause = when "(" Expression<withIn> ")"
AssertionTypeDeclarationStatement AssertionTypeDeclarationStatement
= (assertion | message) type identifier "(" FormalParameterList ")" ("=" stringLiteral)? #(sc) = (assertion | message) type identifier "(" FormalParameterList ")" ("=" stringLiteral)? #(sc)
@ -37,30 +51,6 @@ Syndicate <: ES5 {
SendMessageStatement = "::" Expression<withIn> #(sc) SendMessageStatement = "::" Expression<withIn> #(sc)
//---------------------------------------------------------------------------
// Ongoing event handlers.
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
| dataflow FunctionBodyBlock -- dataflow
| during FacetPattern FacetBlock -- during
| during FacetPattern actor (named Expression<withIn>)? FacetBlock -- duringActor
AssertWhenClause = when "(" Expression<withIn> ")"
FacetEventPattern FacetEventPattern
= message FacetPattern -- messageEvent = message FacetPattern -- messageEvent
| asserted FacetPattern -- assertedEvent | asserted FacetPattern -- assertedEvent
@ -70,10 +60,6 @@ Syndicate <: ES5 {
= FacetEventPattern -- facetEvent = FacetEventPattern -- facetEvent
| "(" Expression<withIn> ")" -- risingEdge | "(" Expression<withIn> ")" -- risingEdge
FacetStateTransition
= case FacetTransitionEventPattern FunctionBodyBlock -- withContinuation
| case FacetTransitionEventPattern #(sc) -- noContinuation
FacetPattern FacetPattern
= LeftHandSideExpression metalevel decimalIntegerLiteral -- withMetalevel = LeftHandSideExpression metalevel decimalIntegerLiteral -- withMetalevel
| LeftHandSideExpression -- noMetalevel | LeftHandSideExpression -- noMetalevel
@ -83,7 +69,8 @@ Syndicate <: ES5 {
// we don't want to make them unavailable to programs as // we don't want to make them unavailable to programs as
// identifiers. // identifiers.
actor = "actor" ~identifierPart actorStar = "actor*" ~identifierPart
actor = "actor" ~("*" | identifierPart)
assert = "assert" ~identifierPart assert = "assert" ~identifierPart
asserted = "asserted" ~identifierPart asserted = "asserted" ~identifierPart
assertion = "assertion" ~identifierPart assertion = "assertion" ~identifierPart
@ -99,7 +86,8 @@ Syndicate <: ES5 {
on = "on" ~identifierPart on = "on" ~identifierPart
react = "react" ~identifierPart react = "react" ~identifierPart
retracted = "retracted" ~identifierPart retracted = "retracted" ~identifierPart
start = "start" ~identifierPart
stop = "stop" ~identifierPart
type = "type" ~identifierPart type = "type" ~identifierPart
until = "until" ~identifierPart
when = "when" ~identifierPart when = "when" ~identifierPart
} }

View File

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

View File

@ -19,37 +19,36 @@ function spawnChatApp() {
var ui = new Syndicate.UI.Anchor(); var ui = new Syndicate.UI.Anchor();
field this.nym; field this.nym;
field this.status; field this.status;
react {
on asserted inputValue('#nym', $v) { this.nym = v; }
on asserted inputValue('#status', $v) { this.status = v; }
on asserted brokerConnected($url) { outputState('connected to ' + url); } on asserted inputValue('#nym', $v) { this.nym = v; }
on retracted brokerConnected($url) { outputState('disconnected from ' + url); } on asserted inputValue('#status', $v) { this.status = v; }
during inputValue('#wsurl', $url) { on asserted brokerConnected($url) { outputState('connected to ' + url); }
assert brokerConnection(url); on retracted brokerConnected($url) { outputState('disconnected from ' + url); }
on message Syndicate.WakeDetector.wakeEvent() { during inputValue('#wsurl', $url) {
:: forceBrokerDisconnect(url); assert brokerConnection(url);
}
assert toBroker(url, present(this.nym, this.status)); on message Syndicate.WakeDetector.wakeEvent() {
during fromBroker(url, present($who, $status)) { :: forceBrokerDisconnect(url);
assert ui.context(who, status) }
.html('#nymlist',
Mustache.render($('#nym_template').html(), { who: who, status: status }));
}
on message Syndicate.UI.globalEvent('#send_chat', 'click', _) { assert toBroker(url, present(this.nym, this.status));
var inp = $("#chat_input"); during fromBroker(url, present($who, $status)) {
var utterance = inp.val(); assert ui.context(who, status)
inp.val(""); .html('#nymlist',
if (utterance) :: toBroker(url, says(this.nym, utterance)); Mustache.render($('#nym_template').html(), { who: who, status: status }));
} }
on message fromBroker(url, says($who, $what)) { on message Syndicate.UI.globalEvent('#send_chat', 'click', _) {
outputUtterance(who, what); var inp = $("#chat_input");
} var utterance = inp.val();
inp.val("");
if (utterance) :: toBroker(url, says(this.nym, utterance));
}
on message fromBroker(url, says($who, $what)) {
outputUtterance(who, what);
} }
} }
} }
@ -84,13 +83,11 @@ assertion type inputValue(selector, value);
function spawnInputChangeMonitor() { function spawnInputChangeMonitor() {
actor { actor {
react { during Syndicate.observe(inputValue($selector, _)) actor {
during Syndicate.observe(inputValue($selector, _)) actor { field this.value = $(selector).val();
field this.value = $(selector).val(); assert inputValue(selector, this.value);
assert inputValue(selector, this.value); on message Syndicate.UI.globalEvent(selector, 'change', $e) {
on message Syndicate.UI.globalEvent(selector, 'change', $e) { this.value = e.target.value;
this.value = e.target.value;
}
} }
} }
} }

View File

@ -11,10 +11,8 @@ assertion type componentPresent(name);
function spawnTV() { function spawnTV() {
actor { actor {
var ui = new Syndicate.UI.Anchor(); var ui = new Syndicate.UI.Anchor();
react { during tvAlert($text) {
during tvAlert($text) { assert ui.context(text).html('#tv', Mustache.render($('#alert_template').html(), { text: text }));
assert ui.context(text).html('#tv', Mustache.render($('#alert_template').html(), { text: text }));
}
} }
} }
} }
@ -24,11 +22,9 @@ function spawnTV() {
function spawnRemoteControl() { function spawnRemoteControl() {
actor { actor {
react { assert componentPresent('remote control');
assert componentPresent('remote control'); on message Syndicate.UI.globalEvent('#remote-control', 'click', _) {
on message Syndicate.UI.globalEvent('#remote-control', 'click', _) { :: remoteClick();
:: remoteClick();
}
} }
} }
} }
@ -41,15 +37,13 @@ function spawnRemoteListener() {
// state, if we've been clicked, turn it off. We don't do this // state, if we've been clicked, turn it off. We don't do this
// here, for simplicity. // here, for simplicity.
react { on asserted powerDraw($watts) {
on asserted powerDraw($watts) { this.stoveIsOn = watts > 0;
this.stoveIsOn = watts > 0; }
}
on message remoteClick() { on message remoteClick() {
if (this.stoveIsOn) { if (this.stoveIsOn) {
:: switchAction(false); :: switchAction(false);
}
} }
} }
} }
@ -62,24 +56,23 @@ function spawnStoveSwitch() {
actor { actor {
field this.powerOn = false; field this.powerOn = false;
this.ui = new Syndicate.UI.Anchor(); this.ui = new Syndicate.UI.Anchor();
react {
assert componentPresent('stove switch');
assert switchState(this.powerOn);
assert this.ui.html('#stove-switch', assert componentPresent('stove switch');
Mustache.render($('#stove_element_template').html(), assert switchState(this.powerOn);
{ imgurl: ("img/stove-coil-element-" +
(this.powerOn ? "hot" : "cold") + ".jpg") }));
on message Syndicate.UI.globalEvent('#stove-switch-on', 'click', _) { this.powerOn = true; } assert this.ui.html('#stove-switch',
on message Syndicate.UI.globalEvent('#stove-switch-off', 'click', _) { this.powerOn = false; } Mustache.render($('#stove_element_template').html(),
{ imgurl: ("img/stove-coil-element-" +
(this.powerOn ? "hot" : "cold") + ".jpg") }));
on message switchAction($newState) { on message Syndicate.UI.globalEvent('#stove-switch-on', 'click', _) { this.powerOn = true; }
this.powerOn = newState; on message Syndicate.UI.globalEvent('#stove-switch-off', 'click', _) { this.powerOn = false; }
}
} until { on message switchAction($newState) {
case message Syndicate.UI.globalEvent('#kill-stove-switch', 'click', _); this.powerOn = newState;
} }
stop on message Syndicate.UI.globalEvent('#kill-stove-switch', 'click', _);
} }
} }
@ -87,19 +80,18 @@ function spawnPowerDrawMonitor() {
actor { actor {
field this.watts = 0; field this.watts = 0;
this.ui = new Syndicate.UI.Anchor(); this.ui = new Syndicate.UI.Anchor();
react {
assert componentPresent('power draw monitor');
assert powerDraw(this.watts);
assert this.ui.html('#power-draw-meter', assert componentPresent('power draw monitor');
Mustache.render($('#power_draw_template').html(), { watts: this.watts })); assert powerDraw(this.watts);
on asserted switchState($on) { assert this.ui.html('#power-draw-meter',
this.watts = on ? 1500 : 0; Mustache.render($('#power_draw_template').html(), { watts: this.watts }));
}
} until { on asserted switchState($on) {
case message Syndicate.UI.globalEvent('#kill-power-draw-monitor', 'click', _); this.watts = on ? 1500 : 0;
} }
stop on message Syndicate.UI.globalEvent('#kill-power-draw-monitor', 'click', _);
} }
} }
@ -108,18 +100,16 @@ function spawnPowerDrawMonitor() {
function spawnTimeoutListener() { function spawnTimeoutListener() {
actor { actor {
react { during powerDraw($watts) {
during powerDraw($watts) { on start {
do { if (watts > 0) {
if (watts > 0) { var powerOnTime = Date.now();
var powerOnTime = Date.now(); react {
react { on asserted Syndicate.Timer.timeLaterThan(powerOnTime + 3000) {
on asserted Syndicate.Timer.timeLaterThan(powerOnTime + 3000) { react { assert tvAlert('Stove on too long?'); }
react { assert tvAlert('Stove on too long?'); } }
} on asserted Syndicate.Timer.timeLaterThan(powerOnTime + 10000) {
on asserted Syndicate.Timer.timeLaterThan(powerOnTime + 10000) { $("img.flames").show();
$("img.flames").show();
}
} }
} }
} }
@ -130,17 +120,14 @@ function spawnTimeoutListener() {
// function spawnTimeoutListener() { // function spawnTimeoutListener() {
// actor { // actor {
// react { // on asserted powerDraw($watts) {
// on asserted powerDraw($watts) { // if (watts > 0) {
// if (watts > 0) { // var powerOnTime = Date.now();
// var powerOnTime = Date.now(); // react {
// react { // on asserted Syndicate.Timer.timeLaterThan(powerOnTime + 3000) {
// on asserted Syndicate.Timer.timeLaterThan(powerOnTime + 3000) { // react { assert tvAlert('Stove on too long?'); }
// react { assert tvAlert('Stove on too long?'); }
// }
// } until {
// case asserted powerDraw(0); // alt: on retracted powerDraw(watts);
// } // }
// stop on asserted powerDraw(0); // alt: on retracted powerDraw(watts);
// } // }
// } // }
// } // }
@ -151,16 +138,14 @@ function spawnTimeoutListener() {
// actor { // actor {
// this.mostRecentTime = 0; // this.mostRecentTime = 0;
// this.powerOnTime = null; // this.powerOnTime = null;
// react { // on asserted powerDraw($watts) {
// on asserted powerDraw($watts) { // this.powerOnTime = (watts > 0) ? this.mostRecentTime : null;
// this.powerOnTime = (watts > 0) ? this.mostRecentTime : null;
// }
// on message Syndicate.Timer.periodicTick(200) {
// this.mostRecentTime = Date.now();
// }
// assert tvAlert('Stove on too long?')
// when (this.powerOnTime !== null && this.mostRecentTime - this.powerOnTime > 3000);
// } // }
// on message Syndicate.Timer.periodicTick(200) {
// this.mostRecentTime = Date.now();
// }
// assert tvAlert('Stove on too long?')
// when (this.powerOnTime !== null && this.mostRecentTime - this.powerOnTime > 3000);
// } // }
// } // }
@ -169,13 +154,10 @@ function spawnTimeoutListener() {
function spawnFailureMonitor() { function spawnFailureMonitor() {
actor { actor {
react { on retracted componentPresent($who) {
on retracted componentPresent($who) { react {
react { assert tvAlert('FAILURE: ' + who);
assert tvAlert('FAILURE: ' + who); stop on asserted componentPresent(who);
} until {
case asserted componentPresent(who);
}
} }
} }
} }
@ -185,7 +167,7 @@ function spawnFailureMonitor() {
// Chaos Monkey // Chaos Monkey
function spawnChaosMonkey() { function spawnChaosMonkey() {
actor { actor* {
monitorComponent('power draw monitor', monitorComponent('power draw monitor',
'#spawn-power-draw-monitor', '#spawn-power-draw-monitor',
'#kill-power-draw-monitor', '#kill-power-draw-monitor',
@ -201,11 +183,11 @@ function spawnChaosMonkey() {
var jKillButtons = $(killButtonSelector); var jKillButtons = $(killButtonSelector);
react { react {
during componentPresent(name) { during componentPresent(name) {
do { on start {
jSpawnButtons.prop('disabled', true); jSpawnButtons.prop('disabled', true);
jKillButtons.prop('disabled', false); jKillButtons.prop('disabled', false);
} }
finally { on stop {
jSpawnButtons.prop('disabled', false); jSpawnButtons.prop('disabled', false);
jKillButtons.prop('disabled', true); jKillButtons.prop('disabled', true);
} }

View File

@ -61,87 +61,85 @@ ground dataspace G {
timeout: 15000 timeout: 15000
})); }));
react { field this.currentLocation = null;
field this.currentLocation = null; var selectedMarker = null;
var selectedMarker = null;
assert brokerConnection(this.wsurl); assert brokerConnection(this.wsurl);
assert toBroker(this.wsurl, this.currentLocation) when (this.currentLocation); assert toBroker(this.wsurl, this.currentLocation) when (this.currentLocation);
on message Syndicate.UI.globalEvent('#my_email', 'change', _) { on message Syndicate.UI.globalEvent('#my_email', 'change', _) {
var v = email_element.value.trim(); var v = email_element.value.trim();
if (this.currentLocation) this.currentLocation = this.currentLocation.set(1, v); if (this.currentLocation) this.currentLocation = this.currentLocation.set(1, v);
localStorage.my_email = v; localStorage.my_email = v;
}
on message Syndicate.UI.globalEvent('#group', 'change', _) {
localStorage.group = group_element.value.trim();
this.wsurl = wsurl_base + group_element.value.trim();
}
on message Syndicate.UI.globalEvent('#findMarker', 'click', $e) {
:: findMarker(document.getElementById('markerList').value);
}
on message Syndicate.UI.globalEvent('#markerList', 'change', $e) {
:: findMarker(document.getElementById('markerList').value);
}
on message ($loc = locationRecord(_, _, _, _, _)) {
this.currentLocation = loc;
}
during fromBroker(this.wsurl, locationRecord($id, $email, _, _, _)) {
var ui = new Syndicate.UI.Anchor();
var marker = new google.maps.Marker({
map: map,
clickable: true,
icon: 'https://www.gravatar.com/avatar/' + md5(email.trim().toLowerCase()) + '?s=32&d=retro'
});
var latestTimestamp = null;
var latestPosition = null;
function selectMarker() {
selectedMarker = marker;
updateInfoWindow();
infoWindow.open(map, marker);
} }
function updateInfoWindow() {
on message Syndicate.UI.globalEvent('#group', 'change', _) { if (selectedMarker === marker && latestPosition && latestTimestamp) {
localStorage.group = group_element.value.trim(); geocoder.geocode({'location': latestPosition}, function (results, status) {
this.wsurl = wsurl_base + group_element.value.trim(); if (status === google.maps.GeocoderStatus.OK && results[0]) {
} infoWindow.setContent(Mustache.render(document.getElementById('info').innerHTML, {
email: email,
on message Syndicate.UI.globalEvent('#findMarker', 'click', $e) { timestamp: latestTimestamp ? latestTimestamp.toString() : '',
:: findMarker(document.getElementById('markerList').value); address: results[0].formatted_address
} }));
on message Syndicate.UI.globalEvent('#markerList', 'change', $e) { }
:: findMarker(document.getElementById('markerList').value); });
}
on message ($loc = locationRecord(_, _, _, _, _)) {
this.currentLocation = loc;
}
during fromBroker(this.wsurl, locationRecord($id, $email, _, _, _)) {
var ui = new Syndicate.UI.Anchor();
var marker = new google.maps.Marker({
map: map,
clickable: true,
icon: 'https://www.gravatar.com/avatar/' + md5(email.trim().toLowerCase()) + '?s=32&d=retro'
});
var latestTimestamp = null;
var latestPosition = null;
function selectMarker() {
selectedMarker = marker;
updateInfoWindow();
infoWindow.open(map, marker);
} }
function updateInfoWindow() { }
if (selectedMarker === marker && latestPosition && latestTimestamp) { on start {
geocoder.geocode({'location': latestPosition}, function (results, status) { marker.addListener('click', Syndicate.Dataspace.wrap(function () {
if (status === google.maps.GeocoderStatus.OK && results[0]) {
infoWindow.setContent(Mustache.render(document.getElementById('info').innerHTML, {
email: email,
timestamp: latestTimestamp ? latestTimestamp.toString() : '',
address: results[0].formatted_address
}));
}
});
}
}
do {
marker.addListener('click', Syndicate.Dataspace.wrap(function () {
selectMarker();
}));
}
assert ui.html('#markerList',
Mustache.render(document.getElementById('markerList-option').innerHTML, {
id: id,
email: email
}));
on message findMarker(id) {
selectMarker(); selectMarker();
if (latestPosition) map.panTo(latestPosition); }));
} }
on asserted fromBroker(this.wsurl, locationRecord(id, email, $timestamp, $lat, $lng)) { assert ui.html('#markerList',
latestTimestamp = new Date(timestamp); Mustache.render(document.getElementById('markerList-option').innerHTML, {
latestPosition = {lat: lat, lng: lng}; id: id,
marker.setPosition(latestPosition); email: email
marker.setTitle(email + ' ' + latestTimestamp.toTimeString()); }));
updateInfoWindow(); on message findMarker(id) {
} selectMarker();
finally { if (latestPosition) map.panTo(latestPosition);
marker.setMap(null); }
if (selectedMarker === marker) selectedMarker = null; on asserted fromBroker(this.wsurl, locationRecord(id, email, $timestamp, $lat, $lng)) {
} latestTimestamp = new Date(timestamp);
latestPosition = {lat: lat, lng: lng};
marker.setPosition(latestPosition);
marker.setTitle(email + ' ' + latestTimestamp.toTimeString());
updateInfoWindow();
}
on stop {
marker.setMap(null);
if (selectedMarker === marker) selectedMarker = null;
} }
} }
} }

View File

@ -23,32 +23,31 @@ ground dataspace G {
} }
var wsurl = 'wss://demo-broker.syndicate-lang.org:8443/'; var wsurl = 'wss://demo-broker.syndicate-lang.org:8443/';
react {
assert brokerConnection(wsurl);
assert Syndicate.UI.uiAttribute('rect#my_color', 'fill', color); assert brokerConnection(wsurl);
assert toBroker(wsurl, point(color, this.publishedX, this.publishedY)); assert Syndicate.UI.uiAttribute('rect#my_color', 'fill', color);
on message Syndicate.Timer.periodicTick(100) {
this.publishedX = x;
this.publishedY = y;
}
on message Syndicate.UI.windowEvent('deviceorientation', $e) { assert toBroker(wsurl, point(color, this.publishedX, this.publishedY));
var scale = 0.5; on message Syndicate.Timer.periodicTick(100) {
x = clamp(e.gamma * scale); this.publishedX = x;
y = clamp((e.beta - 40) * scale); this.publishedY = y;
} }
during fromBroker(wsurl, point($oc, $ox, $oy)) { on message Syndicate.UI.windowEvent('deviceorientation', $e) {
assert ui.context(oc) var scale = 0.5;
.html('#container', x = clamp(e.gamma * scale);
Mustache.render(document.getElementById('circle-template').innerHTML, { y = clamp((e.beta - 40) * scale);
color: oc, }
x: ox,
y: oy during fromBroker(wsurl, point($oc, $ox, $oy)) {
})); assert ui.context(oc)
} .html('#container',
Mustache.render(document.getElementById('circle-template').innerHTML, {
color: oc,
x: ox,
y: oy
}));
} }
} }
} }

View File

@ -4,28 +4,23 @@ ground dataspace {
console.log('starting ground boot'); console.log('starting ground boot');
actor { actor {
react until { stop on asserted Syndicate.observe(beep(_)) {
case asserted Syndicate.observe(beep(_)) { field this.counter = 0;
field this.counter = 0; react {
react { on start {
do { :: beep(this.counter++);
:: beep(this.counter++);
}
on message beep(_) {
:: beep(this.counter++);
}
} until {
case (this.counter > 10);
} }
on message beep(_) {
:: beep(this.counter++);
}
stop on (this.counter > 10);
} }
} }
} }
actor { actor {
react { on message beep($counter) {
on message beep($counter) { console.log("beep!", counter);
console.log("beep!", counter);
}
} }
} }
} }

View File

@ -8,19 +8,17 @@ ground dataspace G {
field this.handX; field this.handX;
field this.handY; field this.handY;
react { assert ui.html('#clock',
assert ui.html('#clock', '<svg width="300px" viewBox="0 0 100 100">'+
'<svg width="300px" viewBox="0 0 100 100">'+ '<circle fill="#0B79CE" r=45 cx=50 cy=50 />'+
'<circle fill="#0B79CE" r=45 cx=50 cy=50 />'+ '<line stroke="#023963" x1=50 y1=50 x2='+this.handX+' y2='+this.handY+' />'+
'<line stroke="#023963" x1=50 y1=50 x2='+this.handX+' y2='+this.handY+' />'+ '</svg>')
'</svg>')
when (typeof this.angle === 'number'); when (typeof this.angle === 'number');
on message Syndicate.Timer.periodicTick(1000) { on message Syndicate.Timer.periodicTick(1000) {
this.angle = ((((Date.now() / 1000) % 60) / 60) - 0.25) * 2 * Math.PI; this.angle = ((((Date.now() / 1000) % 60) / 60) - 0.25) * 2 * Math.PI;
this.handX = 50 + 40 * Math.cos(this.angle); this.handX = 50 + 40 * Math.cos(this.angle);
this.handY = 50 + 40 * Math.sin(this.angle); this.handY = 50 + 40 * Math.sin(this.angle);
}
} }
} }
} }

View File

@ -3,9 +3,7 @@ message type setSortColumn(number);
function newRow(id, firstName, lastName, address, age) { function newRow(id, firstName, lastName, address, age) {
actor named ('model' + id) { actor named ('model' + id) {
react { assert person(id, firstName, lastName, address, age);
assert person(id, firstName, lastName, address, age);
}
} }
} }
@ -27,25 +25,21 @@ function spawnView() {
return '<td>' + text + '</td>'; return '<td>' + text + '</td>';
} }
react { on message setSortColumn($c) { this.orderColumn = c; }
on message setSortColumn($c) { this.orderColumn = c; }
during person($id, $firstName, $lastName, $address, $age) { during person($id, $firstName, $lastName, $address, $age) {
assert ui.context(id) assert ui.context(id)
.html('table#the-table tbody', .html('table#the-table tbody',
'<tr>' + [id, firstName, lastName, address, age].map(cell).join('') + '</tr>', '<tr>' + [id, firstName, lastName, address, age].map(cell).join('') + '</tr>',
[id, firstName, lastName, address, age][this.orderColumn]); [id, firstName, lastName, address, age][this.orderColumn]);
}
} }
} }
} }
function spawnController() { function spawnController() {
actor named 'controller' { actor named 'controller' {
react { on message Syndicate.UI.globalEvent('table#the-table th', 'click', $e) {
on message Syndicate.UI.globalEvent('table#the-table th', 'click', $e) { :: setSortColumn(JSON.parse(e.target.dataset.column));
:: setSortColumn(JSON.parse(e.target.dataset.column));
}
} }
} }
} }

View File

@ -26,52 +26,50 @@ function piece(text, pos, lo, hi, cls) {
function spawnGui() { function spawnGui() {
actor { actor {
react { field this.text = '';
field this.text = ''; field this.pos = 0;
field this.pos = 0; field this.highlightState = false;
field this.highlightState = false;
dataflow { dataflow {
var text = this.text; var text = this.text;
var pos = this.pos; var pos = this.pos;
var highlight = this.highlightState; var highlight = this.highlightState;
var hLeft = highlight ? highlight[0] : 0; var hLeft = highlight ? highlight[0] : 0;
var hRight = highlight ? highlight[1] : 0; var hRight = highlight ? highlight[1] : 0;
document.getElementById("fieldContents").innerHTML = highlight document.getElementById("fieldContents").innerHTML = highlight
? piece(text, pos, 0, hLeft, "normal") + ? piece(text, pos, 0, hLeft, "normal") +
piece(text, pos, hLeft, hRight, "highlight") + piece(text, pos, hLeft, hRight, "highlight") +
piece(text, pos, hRight, text.length + 1, "normal") piece(text, pos, hRight, text.length + 1, "normal")
: piece(text, pos, 0, text.length + 1, "normal"); : piece(text, pos, 0, text.length + 1, "normal");
} }
on message globalEvent("#inputRow", "+keydown", $event) { on message globalEvent("#inputRow", "+keydown", $event) {
switch (event.keyCode) { switch (event.keyCode) {
case 37 /* left */: :: fieldCommand("cursorLeft"); break; case 37 /* left */: :: fieldCommand("cursorLeft"); break;
case 39 /* right */: :: fieldCommand("cursorRight"); break; case 39 /* right */: :: fieldCommand("cursorRight"); break;
case 9 /* tab */: /* ignore */ break; case 9 /* tab */: /* ignore */ break;
case 8 /* backspace */: case 8 /* backspace */:
event.preventDefault(); // that this works here is a minor miracle event.preventDefault(); // that this works here is a minor miracle
:: fieldCommand("backspace"); :: fieldCommand("backspace");
break; break;
default: break; default: break;
}
} }
}
on message globalEvent("#inputRow", "keypress", $event) { on message globalEvent("#inputRow", "keypress", $event) {
var character = String.fromCharCode(event.charCode); var character = String.fromCharCode(event.charCode);
if (event.charCode && character) { if (event.charCode && character) {
:: fieldCommand(["insert", character]); :: fieldCommand(["insert", character]);
}
} }
}
on asserted fieldContents($text, $pos) { on asserted fieldContents($text, $pos) {
this.text = text; this.text = text;
this.pos = pos; this.pos = pos;
} }
on asserted highlight($state) { on asserted highlight($state) {
this.highlightState = state; this.highlightState = state;
}
} }
} }
} }
@ -81,40 +79,38 @@ function spawnGui() {
function spawnModel() { function spawnModel() {
actor { actor {
react { field this.fieldValue = "initial";
field this.fieldValue = "initial"; field this.cursorPos = this.fieldValue.length; /* positions address gaps between characters */
field this.cursorPos = this.fieldValue.length; /* positions address gaps between characters */
assert fieldContents(this.fieldValue, this.cursorPos); assert fieldContents(this.fieldValue, this.cursorPos);
on message fieldCommand("cursorLeft") { on message fieldCommand("cursorLeft") {
this.cursorPos--; this.cursorPos--;
if (this.cursorPos < 0) if (this.cursorPos < 0)
this.cursorPos = 0; this.cursorPos = 0;
}
on message fieldCommand("cursorRight") {
this.cursorPos++;
if (this.cursorPos > this.fieldValue.length)
this.cursorPos = this.fieldValue.length;
}
on message fieldCommand("backspace") {
if (this.cursorPos > 0) {
this.fieldValue =
this.fieldValue.substring(0, this.cursorPos - 1) +
this.fieldValue.substring(this.cursorPos);
this.cursorPos--;
} }
}
on message fieldCommand("cursorRight") { on message fieldCommand(["insert", $newText]) {
this.cursorPos++; this.fieldValue =
if (this.cursorPos > this.fieldValue.length) this.fieldValue.substring(0, this.cursorPos) +
this.cursorPos = this.fieldValue.length; newText +
} this.fieldValue.substring(this.cursorPos);
this.cursorPos += newText.length;
on message fieldCommand("backspace") {
if (this.cursorPos > 0) {
this.fieldValue =
this.fieldValue.substring(0, this.cursorPos - 1) +
this.fieldValue.substring(this.cursorPos);
this.cursorPos--;
}
}
on message fieldCommand(["insert", $newText]) {
this.fieldValue =
this.fieldValue.substring(0, this.cursorPos) +
newText +
this.fieldValue.substring(this.cursorPos);
this.cursorPos += newText.length;
}
} }
} }
} }
@ -124,29 +120,27 @@ function spawnModel() {
function spawnSearch() { function spawnSearch() {
actor { actor {
react { field this.searchtext = document.getElementById("searchBox").value;
field this.searchtext = document.getElementById("searchBox").value; field this.fieldValue = "";
field this.fieldValue = ""; field this.highlight = false;
field this.highlight = false;
assert highlight(this.highlight); assert highlight(this.highlight);
dataflow { dataflow {
if (this.searchtext) { if (this.searchtext) {
var pos = this.fieldValue.indexOf(this.searchtext); var pos = this.fieldValue.indexOf(this.searchtext);
this.highlight = (pos !== -1) && [pos, pos + this.searchtext.length]; this.highlight = (pos !== -1) && [pos, pos + this.searchtext.length];
} else { } else {
this.highlight = false; this.highlight = false;
}
} }
}
on message globalEvent("#searchBox", "input", $event) { on message globalEvent("#searchBox", "input", $event) {
this.searchtext = document.getElementById("searchBox").value; this.searchtext = document.getElementById("searchBox").value;
} }
on asserted fieldContents($text, _) { on asserted fieldContents($text, _) {
this.fieldValue = text; this.fieldValue = text;
}
} }
} }
} }

View File

@ -25,23 +25,21 @@ assertion type show(completed);
function todoListItemModel(initialId, initialTitle, initialCompleted) { function todoListItemModel(initialId, initialTitle, initialCompleted) {
actor { actor {
react { field this.id = initialId;
field this.id = initialId; field this.title = initialTitle;
field this.title = initialTitle; field this.completed = initialCompleted;
field this.completed = initialCompleted;
assert todo(this.id, this.title, this.completed); stop on message deleteTodo(this.id);
on message setCompleted(this.id, $v) { this.completed = v; } assert todo(this.id, this.title, this.completed);
on message setAllCompleted($v) { this.completed = v; }
on message setTitle(this.id, $v) { this.title = v; } on message setCompleted(this.id, $v) { this.completed = v; }
on message setAllCompleted($v) { this.completed = v; }
on message clearCompletedTodos() { on message setTitle(this.id, $v) { this.title = v; }
if (this.completed) :: deleteTodo(this.id);
} on message clearCompletedTodos() {
} until { if (this.completed) :: deleteTodo(this.id);
case message deleteTodo(this.id);
} }
} }
} }
@ -57,53 +55,51 @@ function getTemplate(id) {
function todoListItemView(id) { function todoListItemView(id) {
actor { actor {
stop on retracted todo(id, _, _);
this.ui = new Syndicate.UI.Anchor(); this.ui = new Syndicate.UI.Anchor();
react { field this.editing = false;
field this.editing = false;
during todo(id, $title, $completed) { during todo(id, $title, $completed) {
during show(completed) { during show(completed) {
assert this.ui.html('.todo-list', assert this.ui.html('.todo-list',
Mustache.render(getTemplate(this.editing Mustache.render(getTemplate(this.editing
? 'todo-list-item-edit-template' ? 'todo-list-item-edit-template'
: 'todo-list-item-view-template'), : 'todo-list-item-view-template'),
{ {
id: id, id: id,
title: title, title: title,
completed_class: completed ? "completed" : "", completed_class: completed ? "completed" : "",
checked: completed ? "checked" : "", checked: completed ? "checked" : "",
}), }),
id); id);
}
} }
}
on message this.ui.event('.toggle', 'change', $e) { on message this.ui.event('.toggle', 'change', $e) {
:: setCompleted(id, e.target.checked); :: setCompleted(id, e.target.checked);
} }
on message this.ui.event('.destroy', 'click', _) { on message this.ui.event('.destroy', 'click', _) {
:: deleteTodo(id); :: deleteTodo(id);
} }
on message this.ui.event('label', 'dblclick', _) { on message this.ui.event('label', 'dblclick', _) {
this.editing = true; this.editing = true;
} }
on message this.ui.event('input.edit', 'keyup', $e) { on message this.ui.event('input.edit', 'keyup', $e) {
if (e.keyCode === ESCAPE_KEY_CODE || e.keyCode === ENTER_KEY_CODE) { if (e.keyCode === ESCAPE_KEY_CODE || e.keyCode === ENTER_KEY_CODE) {
this.editing = false;
}
}
on message this.ui.event('input.edit', 'blur', $e) {
this.editing = false; this.editing = false;
} }
on message this.ui.event('input.edit', 'change', $e) { }
var newTitle = e.target.value.trim(); on message this.ui.event('input.edit', 'blur', $e) {
:: (newTitle ? setTitle(id, newTitle) : deleteTodo(id)); this.editing = false;
this.editing = false; }
} on message this.ui.event('input.edit', 'change', $e) {
} until { var newTitle = e.target.value.trim();
case retracted todo(id, _, _); :: (newTitle ? setTitle(id, newTitle) : deleteTodo(id));
this.editing = false;
} }
} }
} }
@ -114,80 +110,72 @@ ground dataspace G {
Syndicate.UI.spawnUIDriver(); Syndicate.UI.spawnUIDriver();
actor { actor {
react { on message Syndicate.UI.globalEvent('.new-todo', 'change', $e) {
on message Syndicate.UI.globalEvent('.new-todo', 'change', $e) { var newTitle = e.target.value.trim();
var newTitle = e.target.value.trim(); if (newTitle) :: createTodo(newTitle);
if (newTitle) :: createTodo(newTitle); e.target.value = "";
e.target.value = "";
}
} }
} }
actor { actor {
this.ui = new Syndicate.UI.Anchor(); this.ui = new Syndicate.UI.Anchor();
react { during activeTodoCount($count) {
during activeTodoCount($count) { assert this.ui.context('count').html('.todo-count strong', '' + count);
assert this.ui.context('count').html('.todo-count strong', '' + count); assert this.ui.context('plural').html('.todo-count span.s', 's') when (count !== 1);
assert this.ui.context('plural').html('.todo-count span.s', 's') when (count !== 1); }
}
during totalTodoCount(0) { during totalTodoCount(0) {
assert Syndicate.UI.uiAttribute('section.main', 'class', 'hidden'); assert Syndicate.UI.uiAttribute('section.main', 'class', 'hidden');
assert Syndicate.UI.uiAttribute('footer.footer', 'class', 'hidden'); assert Syndicate.UI.uiAttribute('footer.footer', 'class', 'hidden');
} }
during completedTodoCount(0) { during completedTodoCount(0) {
assert Syndicate.UI.uiAttribute('button.clear-completed', 'class', 'hidden'); assert Syndicate.UI.uiAttribute('button.clear-completed', 'class', 'hidden');
} }
on message Syndicate.UI.globalEvent('button.clear-completed', 'click', _) { on message Syndicate.UI.globalEvent('button.clear-completed', 'click', _) {
:: clearCompletedTodos(); :: clearCompletedTodos();
} }
during allCompleted() { during allCompleted() {
do { :: Syndicate.UI.setProperty('.toggle-all', 'checked', true); } on start { :: Syndicate.UI.setProperty('.toggle-all', 'checked', true); }
finally { :: Syndicate.UI.setProperty('.toggle-all', 'checked', false); } on stop { :: Syndicate.UI.setProperty('.toggle-all', 'checked', false); }
} }
on message Syndicate.UI.globalEvent('.toggle-all', 'change', $e) { on message Syndicate.UI.globalEvent('.toggle-all', 'change', $e) {
:: setAllCompleted(e.target.checked); :: setAllCompleted(e.target.checked);
} }
on asserted todo($id, _, _) { on asserted todo($id, _, _) {
todoListItemView(id); todoListItemView(id);
}
} }
} }
actor { actor {
react { field this.completedCount = 0;
field this.completedCount = 0; field this.activeCount = 0;
field this.activeCount = 0; on asserted todo($id, _, $c) { if (c) this.completedCount++; else this.activeCount++; }
on asserted todo($id, _, $c) { if (c) this.completedCount++; else this.activeCount++; } on retracted 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 activeTodoCount(this.activeCount); assert completedTodoCount(this.completedCount);
assert completedTodoCount(this.completedCount); assert totalTodoCount(this.activeCount + this.completedCount);
assert totalTodoCount(this.activeCount + this.completedCount); assert allCompleted() when (this.completedCount > 0 && this.activeCount === 0);
assert allCompleted() when (this.completedCount > 0 && this.activeCount === 0);
}
} }
actor { actor {
react { during Syndicate.UI.locationHash($hash) {
during Syndicate.UI.locationHash($hash) { assert Syndicate.UI.uiAttribute('ul.filters > li > a[href="#'+hash+'"]',
assert Syndicate.UI.uiAttribute('ul.filters > li > a[href="#'+hash+'"]', 'class', 'selected');
'class', 'selected'); }
}
during Syndicate.UI.locationHash('/') { during Syndicate.UI.locationHash('/') {
assert show(true); assert show(true);
assert show(false); assert show(false);
} }
during Syndicate.UI.locationHash('/active') { during Syndicate.UI.locationHash('/active') {
assert show(false); assert show(false);
} }
during Syndicate.UI.locationHash('/completed') { during Syndicate.UI.locationHash('/completed') {
assert show(true); assert show(true);
}
} }
} }
@ -202,32 +190,32 @@ ground dataspace G {
} }
} else { } else {
db = {nextId: 0, todos: {}}; db = {nextId: 0, todos: {}};
react until { on start {
case asserted Syndicate.observe(createTodo(_)) { react {
:: createTodo('Buy milk'); stop on asserted Syndicate.observe(createTodo(_)) {
:: createTodo('Buy bread'); :: createTodo('Buy milk');
:: createTodo('Finish PhD'); :: createTodo('Buy bread');
:: createTodo('Finish PhD');
}
} }
} }
} }
react { on message createTodo($title) {
on message createTodo($title) { todoListItemModel(db.nextId++, title, false);
todoListItemModel(db.nextId++, title, false); }
}
during todo($id, _, _) { during todo($id, _, _) {
during todo(id, $title, $completed) { during todo(id, $title, $completed) {
do { on start {
db.todos[id] = {id: id, title: title, completed: completed}; db.todos[id] = {id: id, title: title, completed: completed};
localStorage['todos-syndicate'] = JSON.stringify(db);
}
}
finally {
delete db.todos[id];
localStorage['todos-syndicate'] = JSON.stringify(db); localStorage['todos-syndicate'] = JSON.stringify(db);
} }
} }
on stop {
delete db.todos[id];
localStorage['todos-syndicate'] = JSON.stringify(db);
}
} }
} }
} }

View File

@ -76,10 +76,9 @@ assertion type splitProposal(title, price, contribution, accepted);
/// core library. /// core library.
/// ///
function whileRelevantAssert(P) { function whileRelevantAssert(P) {
react { actor {
assert P; assert P;
} until { stop on retracted Syndicate.observe(P);
case retracted Syndicate.observe(P);
} }
} }
@ -117,38 +116,32 @@ function seller() {
/// The seller responds to interest in bookQuotes by asserting a /// The seller responds to interest in bookQuotes by asserting a
/// responsive record, if one exists. /// responsive record, if one exists.
react { during Syndicate.observe(bookQuote($title, _)) {
during Syndicate.observe(bookQuote($title, _)) { assert bookQuote(title, this.priceOf(title));
assert bookQuote(title, this.priceOf(title));
}
} }
/// It also responds to order requests. /// It also responds to order requests.
react { on asserted
on asserted Syndicate.observe(order($title, $offerPrice, _, _)) {
Syndicate.observe(order($title, $offerPrice, _, _)) {
/// We cannot sell a book we do not have, and we will not sell for /// We cannot sell a book we do not have, and we will not sell for
/// less than our asking price. /// less than our asking price.
var askingPrice = this.priceOf(title); var askingPrice = this.priceOf(title);
if ((askingPrice === false) || (offerPrice < askingPrice)) { if ((askingPrice === false) || (offerPrice < askingPrice)) {
whileRelevantAssert( whileRelevantAssert(
order(title, offerPrice, false, false)); order(title, offerPrice, false, false));
} else { } else {
/// But if we can sell it, we do so by allocating an order ID and /// But if we can sell it, we do so by allocating an order ID and
/// replying to the orderer. /// replying to the orderer.
var orderId = this.nextOrderId++; var orderId = this.nextOrderId++;
delete field this.books[title]; delete field this.books[title];
actor { whileRelevantAssert(
whileRelevantAssert( order(title, offerPrice, orderId, "March 9th"));
order(title, offerPrice, orderId, "March 9th"));
}
}
} }
} }
} }
@ -157,7 +150,7 @@ function seller() {
/// ### Implementation: SPLIT-PROPOSER and book-quote-requestor /// ### Implementation: SPLIT-PROPOSER and book-quote-requestor
function buyerA() { function buyerA() {
actor { actor* {
var self = this; var self = this;
/// Our actor remembers which books remain on its shopping list, and /// Our actor remembers which books remain on its shopping list, and
@ -184,8 +177,8 @@ function buyerA() {
/// First, retrieve a quote for the title, and analyze the result. /// First, retrieve a quote for the title, and analyze the result.
react until { react {
case asserted bookQuote(title, $price) { stop on asserted bookQuote(title, $price) {
if (price === false) { if (price === false) {
console.log("A learns that "+title+" is out-of-stock."); console.log("A learns that "+title+" is out-of-stock.");
buyBooks(); buyBooks();
@ -220,15 +213,15 @@ function buyerA() {
/// Make our proposal, and wait for a response. /// Make our proposal, and wait for a response.
react until { react {
case asserted stop on asserted
splitProposal(title, price, contribution, true) { splitProposal(title, price, contribution, true) {
console.log("A learns that the split-proposal for "+ console.log("A learns that the split-proposal for "+
title+" was accepted"); title+" was accepted");
buyBooks(); buyBooks();
} }
case asserted stop on asserted
splitProposal(title, price, contribution, false) { splitProposal(title, price, contribution, false) {
console.log("A learns that the split-proposal for "+ console.log("A learns that the split-proposal for "+
title+" was rejected"); title+" was rejected");
@ -255,55 +248,50 @@ function buyerB() {
/// It spends its time waiting for a SPLIT-PROPOSER to offer a /// It spends its time waiting for a SPLIT-PROPOSER to offer a
/// `splitProposal`. /// `splitProposal`.
react { on asserted
on asserted Syndicate.observe(splitProposal($title,
Syndicate.observe(splitProposal($title, $price,
$price, $theirContribution,
$theirContribution, _))
_)) {
{ var myContribution = price - theirContribution;
var myContribution = price - theirContribution; console.log("B is being asked to contribute "+myContribution+
console.log("B is being asked to contribute "+myContribution+ " toward "+title+" at price "+price);
" toward "+title+" at price "+price);
/// We may not be able to afford contributing this much. /// We may not be able to afford contributing this much.
if (myContribution > this.funds) { if (myContribution > this.funds) {
console.log("B hasn't enough funds ("+this.funds+ console.log("B hasn't enough funds ("+this.funds+
" remaining)"); " remaining)");
whileRelevantAssert( whileRelevantAssert(
splitProposal(title, price, theirContribution, false)); splitProposal(title, price, theirContribution, false));
} else { } else {
/// But if we *can* afford it, update our remaining funds and spawn a /// But if we *can* afford it, update our remaining funds and spawn a
/// small actor to handle the actual purchase now that we have agreed /// small actor to handle the actual purchase now that we have agreed
/// on a split. /// on a split.
var remainingFunds = this.funds - myContribution; var remainingFunds = this.funds - myContribution;
console.log("B accepts the offer, leaving them with "+ console.log("B accepts the offer, leaving them with "+
remainingFunds+" remaining funds"); remainingFunds+" remaining funds");
this.funds = remainingFunds; this.funds = remainingFunds;
actor { actor {
react {
/// While waiting for order confirmation, take the opportunity to /// While waiting for order confirmation, take the opportunity to
/// signal to our SPLIT-PROPOSER that we accepted their proposal. /// signal to our SPLIT-PROPOSER that we accepted their proposal.
assert splitProposal(title, assert splitProposal(title,
price, price,
theirContribution, theirContribution,
true); true);
/// When order confirmation arrives, this purchase is completed. /// When order confirmation arrives, this purchase is completed.
} until { stop on asserted order(title, price, $id, $date) {
case asserted order(title, price, $id, $date) { console.log("The order for "+title+" has id "+id+
console.log("The order for "+title+" has id "+id+ ", and will be delivered on "+date);
", and will be delivered on "+date);
}
} }
}
} }
} }
} }

View File

@ -122,10 +122,6 @@ Actor.prototype.checkForTermination = function() {
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
function createFacet() {
return new Facet(Dataspace.activeBehavior());
}
function Facet(actor) { function Facet(actor) {
this.actor = actor; this.actor = actor;
this.endpoints = Immutable.Map(); this.endpoints = Immutable.Map();
@ -141,6 +137,12 @@ function Facet(actor) {
Facet.nextFid = 0; Facet.nextFid = 0;
Facet.current = null; Facet.current = null;
Facet.build = function(f) {
var facet = new Facet(Dataspace.activeBehavior());
withCurrentFacet(facet, f);
facet.completeBuild();
};
function withCurrentFacet(facet, f) { function withCurrentFacet(facet, f) {
var previousFacet = Facet.current; var previousFacet = Facet.current;
Facet.current = facet; Facet.current = facet;
@ -373,7 +375,7 @@ function deleteField(obj, prop) {
//--------------------------------------------------------------------------- //---------------------------------------------------------------------------
module.exports.spawnActor = spawnActor; module.exports.spawnActor = spawnActor;
module.exports.createFacet = createFacet; module.exports.Facet = Facet;
module.exports.referenceField = referenceField; module.exports.referenceField = referenceField;
module.exports.declareField = declareField; module.exports.declareField = declareField;
module.exports.deleteField = deleteField; module.exports.deleteField = deleteField;