syndicate-2017/examples/two-buyer-protocol/index.expanded.js

305 lines
13 KiB
JavaScript

"use strict";
var Syndicate = require('../../src/main.js');
/// -->
/// # {{ page.title }}
///
/// This is an extended two-buyer book-purchase protocol, based
/// loosely on an example given in:
///
/// > K. Honda, N. Yoshida, and M. Carbone, “Multiparty asynchronous
/// > session types,” POPL 2008.
/// ## The Scenario
///
/// A book-seller responds to requests for book prices when asked. A
/// pair of prospective buyers run through a shopping list. For each
/// book, the first buyer offers to split the cost of the book with
/// the second. If the second has enough money left, it accepts;
/// otherwise, it rejects the offer, and the first buyer tries a
/// different split. If the second buyer agrees to a split, it then
/// negotiates the purchase of the book with the book-seller.
/// ## The Protocol
/// ### Role: SELLER
///
/// When interest in `bookQuote($title, _)` appears, asserts
/// `bookQuote(title, Maybe Float)`, `false` meaning not available,
/// and otherwise an asking-price.
///
/// When interest in `order($title, $offer-price, _, _)` appears,
/// asserts `order(title, offer-price, false, false)` for "no sale",
/// otherwise `order(title, offer-price, PositiveInteger, String)`, an
/// accepted sale.
/// ### Role: BUYER
///
/// Observes `bookQuote(title, $price)` to learn prices.
///
/// Observes `order(title, offer-price, $id, $delivery-date)` to make orders.
/// ### Role: SPLIT-PROPOSER
///
/// Observes `splitProposal(title, asking-price, contribution,
/// $accepted)` to make a split-proposal and learn whether it was
/// accepted or not.
/// ### Role: SPLIT-DISPOSER
///
/// When interest in `splitProposal($title, $asking-price,
/// $contribution, _)` appears, asserts `splitProposal(title,
/// askingPrice, contribution, true)` to indicate they are willing to
/// go through with the deal, in which case they then perform the role
/// of BUYER for title/asking-price, or asserts `splitProposal(title,
/// asking-price, contribution, false)` to indicate they are unwilling
/// to go through with the deal.
/// ## The Code
/// ### Type Declarations
/// First, we declare *assertion types* for our protocol.
var bookQuote = Syndicate.Struct.makeConstructor("bookQuote", ["title","price"]);
var order = Syndicate.Struct.makeConstructor("order", ["title","price","id","deliveryDate"]);
var splitProposal = Syndicate.Struct.makeConstructor("splitProposal", ["title","price","contribution","accepted"]);
/// ### Utilities
/// This routine is under consideration for possible addition to the
/// core library.
///
function whileRelevantAssert(P) {
Syndicate.Actor.spawnActor(function() { Syndicate.Actor.Facet.build(function () { {
Syndicate.Actor.Facet.current.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(P, 0); }));
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, true, "retracted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(Syndicate.observe(P), 0); }), (function() { var _ = Syndicate.__; return { assertion: Syndicate.observe(P), metalevel: 0 }; }), (function() {}));
} }); });
}
/// ### Implementation: SELLER
function seller() {
Syndicate.Actor.spawnActor(function() { Syndicate.Actor.Facet.build(function () { {
/// 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 = {};
Syndicate.Actor.declareField(this.books, "The Wind in the Willows", 3.95);
Syndicate.Actor.declareField(this.books, "Catch 22", 2.22);
Syndicate.Actor.declareField(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) && Syndicate.Actor.referenceField(this.books, title);
};
/// The seller responds to interest in bookQuotes by asserting a
/// responsive record, if one exists.
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, false, "asserted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(Syndicate.observe(bookQuote(_, _)), 0); }), (function() { var _ = Syndicate.__; return { assertion: Syndicate.observe(bookQuote((Syndicate._$("title")), _)), metalevel: 0 }; }), (function(title) {
var _cachedAssertion1522142580703_0 = (function() { var _ = Syndicate.__; return Syndicate.observe(bookQuote(title, _)); }).call(this);
{ Syndicate.Actor.Facet.build(function () { {
Syndicate.Actor.Facet.current.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(bookQuote(title, this.priceOf(title)), 0); }));
}
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, true, "retracted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(_cachedAssertion1522142580703_0, 0); }), (function() { var _ = Syndicate.__; return { assertion: _cachedAssertion1522142580703_0, metalevel: 0 }; }), (function() {})); }); }}));
/// It also responds to order requests.
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, false, "asserted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(Syndicate.observe(order(_, _, _, _)), 0); }), (function() { var _ = Syndicate.__; return { assertion: Syndicate.observe(order((Syndicate._$("title")), (Syndicate._$("offerPrice")), _, _)), metalevel: 0 }; }), (function(title, offerPrice) {
/// We cannot sell a book we do not have, and we will not sell for
/// less than our asking price.
var askingPrice = this.priceOf(title);
if ((askingPrice === false) || (offerPrice < askingPrice)) {
whileRelevantAssert(
order(title, offerPrice, false, false));
} else {
/// But if we can sell it, we do so by allocating an order ID and
/// replying to the orderer.
var orderId = this.nextOrderId++;
Syndicate.Actor.deleteField(this.books, title);
whileRelevantAssert(
order(title, offerPrice, orderId, "March 9th"));
}
}));
} }); });
}
/// ### Implementation: SPLIT-PROPOSER and book-quote-requestor
function buyerA() {
Syndicate.Actor.spawnActor(function() {
var self = this;
/// Our actor remembers which books remain on its shopping list, and
/// tries to buy them one at a time, sharing costs with `buyerB`.
self.titles = ["Catch 22",
"Encyclopaedia Brittannica",
"Candide",
"The Wind in the Willows"];
/// JavaScript's callback-oriented blocking means that we express our
/// loop in almost a tail-recursive style, using helper functions
/// `buyBooks` and `trySplit`.
buyBooks();
function buyBooks() {
if (self.titles.length === 0) {
console.log("A has bought everything they wanted!");
return;
}
var title = self.titles.shift();
/// First, retrieve a quote for the title, and analyze the result.
(function () { Syndicate.Actor.Facet.build(function () { {
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, true, "asserted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(bookQuote(title, _), 0); }), (function() { var _ = Syndicate.__; return { assertion: bookQuote(title, (Syndicate._$("price"))), metalevel: 0 }; }), (function(price) {
if (price === false) {
console.log("A learns that "+title+" is out-of-stock.");
buyBooks();
} else {
console.log("A learns that the price of "+title+
" is "+price);
/// Next, repeatedly make split offers to a SPLIT-DISPOSER until
/// either one is accepted, or the contribution from the
/// SPLIT-DISPOSER becomes pointlessly small. We start the process by
/// offering to split the price of the book evenly.
trySplit(title, price, price / 2);
}
}));
} }); }).call(this);
}
function trySplit(title, price, contribution) {
console.log("A makes an offer to split the price of "+title+
" contributing "+contribution);
/// If we are about to offer to split the price, but the other buyer
/// would contribute less than 10c, then it's not worth bothering; we
/// may as well buy it ourselves. Another version of the program could
/// perform the BUYER role here.
if (contribution > (price - 0.10)) {
console.log("A gives up on "+title+".");
buyBooks();
} else {
/// Make our proposal, and wait for a response.
(function () { Syndicate.Actor.Facet.build(function () { {
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, true, "asserted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(splitProposal(title, price, contribution, true), 0); }), (function() { var _ = Syndicate.__; return { assertion: splitProposal(title, price, contribution, true), metalevel: 0 }; }), (function() {
console.log("A learns that the split-proposal for "+
title+" was accepted");
buyBooks();
}));
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, true, "asserted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(splitProposal(title, price, contribution, false), 0); }), (function() { var _ = Syndicate.__; return { assertion: splitProposal(title, price, contribution, false), metalevel: 0 }; }), (function() {
console.log("A learns that the split-proposal for "+
title+" was rejected");
trySplit(title,
price,
contribution + ((price - contribution) / 2));
}));
} }); }).call(this);
}
}
});
}
/// ### Implementation: SPLIT-DISPOSER and BUYER
function buyerB() {
Syndicate.Actor.spawnActor(function() { Syndicate.Actor.Facet.build(function () { {
/// This actor maintains a record of the amount of money it has left
/// to spend.
this.funds = 5.00;
/// It spends its time waiting for a SPLIT-PROPOSER to offer a
/// `splitProposal`.
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, false, "asserted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(Syndicate.observe(splitProposal(_,
_,
_,
_)), 0); }), (function() { var _ = Syndicate.__; return { assertion: Syndicate.observe(splitProposal((Syndicate._$("title")),
(Syndicate._$("price")),
(Syndicate._$("theirContribution")),
_)), metalevel: 0 }; }), (function(title, price, theirContribution) {
var myContribution = price - theirContribution;
console.log("B is being asked to contribute "+myContribution+
" toward "+title+" at price "+price);
/// We may not be able to afford contributing this much.
if (myContribution > this.funds) {
console.log("B hasn't enough funds ("+this.funds+
" remaining)");
whileRelevantAssert(
splitProposal(title, price, theirContribution, false));
} else {
/// 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
/// on a split.
var remainingFunds = this.funds - myContribution;
console.log("B accepts the offer, leaving them with "+
remainingFunds+" remaining funds");
this.funds = remainingFunds;
Syndicate.Actor.spawnActor(function() { Syndicate.Actor.Facet.build(function () { {
/// While waiting for order confirmation, take the opportunity to
/// signal to our SPLIT-PROPOSER that we accepted their proposal.
Syndicate.Actor.Facet.current.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(splitProposal(title,
price,
theirContribution,
true), 0); }));
/// When order confirmation arrives, this purchase is completed.
Syndicate.Actor.Facet.current.onEvent(Syndicate.Actor.PRIORITY_NORMAL, true, "asserted", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(order(title, price, _, _), 0); }), (function() { var _ = Syndicate.__; return { assertion: order(title, price, (Syndicate._$("id")), (Syndicate._$("date"))), metalevel: 0 }; }), (function(id, date) {
console.log("The order for "+title+" has id "+id+
", and will be delivered on "+date);
}));
} }); });
}
}));
} }); });
}
/// ### Starting Configuration
new Syndicate.Ground(function () {
seller();
buyerA();
buyerB();
}).startStepping();