/// --- /// title: Two-Party Buyer Protocol /// --- /// /// 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. /// # A Sample Run /// /// Run the program with `../../bin/syndicatec index.js | node` from a /// checkout of the Syndicate repository. /// /// A learns that the price of Catch 22 is 2.22 /// A makes an offer to split the price of Catch 22 contributing 1.11 /// B is being asked to contribute 1.11 toward Catch 22 at price 2.22 /// B accepts the offer, leaving them with 3.8899999999999997 remaining funds /// A learns that the split-proposal for Catch 22 was accepted /// A learns that Encyclopaedia Brittannica is out-of-stock. /// The order for Catch 22 has id 10001483, and will be delivered on March 9th /// A learns that the price of Candide is 34.95 /// A makes an offer to split the price of Candide contributing 17.475 /// B is being asked to contribute 17.475 toward Candide at price 34.95 /// B hasn't enough funds (3.8899999999999997 remaining) /// A learns that the split-proposal for Candide was rejected /// A makes an offer to split the price of Candide contributing 26.212500000000002 /// B is being asked to contribute 8.7375 toward Candide at price 34.95 /// B hasn't enough funds (3.8899999999999997 remaining) /// A learns that the split-proposal for Candide was rejected /// A makes an offer to split the price of Candide contributing 30.581250000000004 /// B is being asked to contribute 4.368749999999999 toward Candide at price 34.95 /// B hasn't enough funds (3.8899999999999997 remaining) /// A learns that the split-proposal for Candide was rejected /// A makes an offer to split the price of Candide contributing 32.765625 /// B is being asked to contribute 2.184375000000003 toward Candide at price 34.95 /// B accepts the offer, leaving them with 1.7056249999999968 remaining funds /// A learns that the split-proposal for Candide was accepted /// A learns that the price of The Wind in the Willows is 3.95 /// A makes an offer to split the price of The Wind in the Willows contributing 1.975 /// The order for Candide has id 10001484, and will be delivered on March 9th /// B is being asked to contribute 1.975 toward The Wind in the Willows at price 3.95 /// B hasn't enough funds (1.7056249999999968 remaining) /// A learns that the split-proposal for The Wind in the Willows was rejected /// A makes an offer to split the price of The Wind in the Willows contributing 2.9625000000000004 /// B is being asked to contribute 0.9874999999999998 toward The Wind in the Willows at price 3.95 /// B accepts the offer, leaving them with 0.718124999999997 remaining funds /// A learns that the split-proposal for The Wind in the Willows was accepted /// A has bought everything they wanted! /// The order for The Wind in the Willows has id 10001485, and will be delivered on March 9th /// # The Code /// ## Type Declarations /// First, we declare *assertion types* for our protocol. assertion type bookQuote(title, price); assertion type order(title, price, id, deliveryDate); assertion type splitProposal(title, price, contribution, accepted); /// ## Utilities /// This routine is under consideration for possible addition to the /// core library. /// function whileRelevantAssert(P) { react { assert P; } until { case retracted Syndicate.observe(P); } } /// ## Implementation: SELLER function seller() { actor { /// 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. this.books = { "The Wind in the Willows": 3.95, "Catch 22": 2.22, "Candide": 34.95 }; this.nextOrderId = 10001483; /// The seller responds to interest in bookQuotes by asserting a /// responsive record, if one exists. react { during Syndicate.observe(bookQuote($title, _)) { assert bookQuote(title, title in this.books ? this.books[title] : false); } } /// It also responds to order requests. react { on asserted Syndicate.observe(order($title, $offerPrice, _, _)) { /// We cannot sell a book we do not have, and we will not sell for /// less than our asking price. var askingPrice = title in this.books ? this.books[title] : false; 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++; delete this.books[title]; actor { whileRelevantAssert(order(title, offerPrice, orderId, "March 9th")); } } } } } } /// ## Implementation: SPLIT-PROPOSER and book-quote-requestor function buyerA() { actor { 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. react until { case asserted bookQuote(title, $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); } } } } 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. react until { case asserted splitProposal(title, price, contribution, true) { console.log("A learns that the split-proposal for "+title+" was accepted"); buyBooks(); } case asserted splitProposal(title, price, contribution, false) { console.log("A learns that the split-proposal for "+title+" was rejected"); trySplit(title, price, contribution + ((price - contribution) / 2)); } } } } } } /// ## Implementation: SPLIT-DISPOSER and BUYER function buyerB() { actor { /// 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`. react { on asserted Syndicate.observe(splitProposal($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; actor { react { /// While waiting for order confirmation, take the opportunity to /// signal to our SPLIT-PROPOSER that we accepted their proposal. assert splitProposal(title, price, theirContribution, true); /// When order confirmation arrives, this purchase is completed. } until { case asserted order(title, price, $id, $date) { console.log("The order for "+title+" has id "+id+ ", and will be delivered on "+date); } } } } } } } } /// ## Starting Configuration ground dataspace { seller(); buyerA(); buyerB(); }