├── .gitignore ├── .travis.yml ├── HISTORY.md ├── LICENSE ├── README.md ├── dist ├── babble.js ├── babble.map └── babble.min.js ├── examples ├── ask_age.js ├── babblify.js ├── browser │ ├── browser.html │ └── pubnub │ │ ├── emma.html │ │ └── jack.html ├── guess_the_number.js ├── plan_a_meeting.js ├── pubnub.js ├── say_hi.js └── say_hi_async.js ├── img ├── plan_a_meeting.odg ├── plan_a_meeting.png ├── say_hi.odg └── say_hi.png ├── index.js ├── lib ├── Babbler.js ├── Conversation.js ├── babble.js ├── block │ ├── Block.js │ ├── Decision.js │ ├── IIf.js │ ├── Listen.js │ ├── Tell.js │ └── Then.js ├── messagebus.js └── util.js ├── package.json └── test ├── Babbler.test.js ├── Conversation.test.js ├── babble.test.js ├── block ├── Block.test.js ├── Decision.test.js ├── IIf.test.js ├── Listen.test.js ├── Tell.test.js └── Then.test.js ├── messagebus.test.js └── util.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | .idea 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # babble history 2 | https://github.com/enmasseio/babble 3 | 4 | 5 | ## 2016-02-16, version 0.11.0 6 | 7 | - Replaced `instanceof MyClass` checks with `object.isMyClass` checks. 8 | 9 | 10 | ## 2014-08-18, version 0.10.0 11 | 12 | - Implemented function `Babbler.listenOnce`. 13 | - Renamed default message listener for babblify from `onMessage` to `receive`. 14 | 15 | 16 | ## 2014-08-11, version 0.9.0 17 | 18 | - Implemented `'default'` choice in `Decision`. 19 | - Some code simplifications. 20 | - Renamed `messagers` to `messagebus`. 21 | - Documented message bus interface and protocol. 22 | 23 | 24 | ## 2014-08-07, version 0.8.0 25 | 26 | - Completely reworked the library internally to support asynchronous flows 27 | using promises. 28 | - Callbacks can now return promises to resolve callbacks for decisions, 29 | conditions, and responses asynchronously. 30 | - Implemented a new block: `IIf(condition, trueBlock, falseBlock)`. 31 | - `Babbler.listen` now has a start condition instead of a fixed string. 32 | Condition can be a function, regexp, or any value. (Uses `iif` under the hood). 33 | 34 | 35 | ## 2014-08-01, version 0.7.0 36 | 37 | - On creation, a babbler is now automatically connected to the default (local) 38 | message bus. 39 | - Changed function `Babbler.connect` to return a Promise instead of accepting 40 | a callback function as last parameter. 41 | - Added function `babble.ask`. 42 | - Added function `Block.ask`. 43 | - Changed the API for messagers: `connect` must return a token, and a messager 44 | must contain a function `disconnect(token)`. 45 | - Implemented support for babblifying actors. 46 | 47 | 48 | ## 2014-02-14, version 0.6.0 49 | 50 | - Renamed functions `publish`, `subscribe`, `unsubscribe` to `send`, `connect`, 51 | and `disconnect`. Renamed namespace `pubsub` to `messengers`. 52 | 53 | 54 | ## 2014-01-13, version 0.5.0 55 | 56 | - Messages can now be of any type, not only string. 57 | - Consistency of API improved. 58 | - Improved examples. 59 | 60 | 61 | ## 2014-01-10, version 0.4.0 62 | 63 | - API changed into a chained API. 64 | 65 | 66 | ## 2014-01-03, version 0.3.1 67 | 68 | - Documentation and examples added. 69 | - Minor bug fixes and improvements. 70 | 71 | 72 | ## 2014-01-02, version 0.3.0 73 | 74 | - Implemented customizable pubsub system. 75 | - Implemented support for pubnub. 76 | - Implemented browser support. 77 | 78 | 79 | ## 2013-12-31, version 0.2.0 80 | 81 | - Changed to flow based: Reply, Action, Decision, Trigger. 82 | 83 | 84 | ## 2013-12-24, version 0.1.0 85 | 86 | - Initial release, callback based. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Babble 2 | 3 | Dynamic communication flows between message based actors. 4 | 5 | Babble makes it easy to code communication flows between actors. A conversation 6 | is modeled as a control flow diagram containing blocks `ask`, `tell`, `listen`, 7 | `iif`, `decide`, and `then`. Each block can link to a next block in the 8 | control flow. Conversations are dynamic: a scenario is build programmatically, 9 | and the blocks can dynamically determine the next block in the scenario. 10 | During a conversation, a context is available to store the state of the 11 | conversation. 12 | 13 | Babblers communicate with each other via a message bus. Babble comes with 14 | built in support for a local message bus, and [pubnub](http://www.pubnub.com/) 15 | to connect actors distributed over multiple devices. Its easy to add support 16 | for other message buses. 17 | 18 | Babble runs in node.js and in the browser. 19 | 20 | 21 | ## Usage 22 | 23 | Install babble via npm: 24 | 25 | npm install babble 26 | 27 | Load in node.js: 28 | 29 | ```js 30 | var babble = require('babble'); 31 | ``` 32 | 33 | Load in the browser: 34 | 35 | ```html 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ``` 49 | 50 | Then, babble can be loaded and used: 51 | 52 | ```js 53 | 54 | var babble = require('babble'); 55 | 56 | var emma = babble.babbler('emma'); 57 | var jack = babble.babbler('jack'); 58 | 59 | // listen for messages containing either 'age' or 'how old' 60 | emma.listen(/age|how old/) 61 | .tell(function () { 62 | return 25; 63 | }); 64 | 65 | jack.ask('emma', 'what is your age?', function (age, context) { 66 | console.log(context.from + ' is ' + age + ' years old'); 67 | }); 68 | ``` 69 | 70 | ## Control flow 71 | 72 | TODO: describe control flow blocks 73 | 74 | 75 | ## Examples 76 | 77 | ### Say hi 78 | 79 | Babble can be used to listen for messages and send a reply. In the following 80 | example, emma listens for a message "hi", then she will listen to the next 81 | message. Depending on the contents of this second message, she determines how 82 | to respond. Jack says hi to emma, then tells his name or age, and awaits a 83 | response from emma. 84 | 85 | This scenario can be represented by the following control flow diagram: 86 | 87 | ![say hi](https://raw.github.com/enmasseio/babble/master/img/say_hi.png) 88 | 89 | The scenario can be programmed as: 90 | 91 | ```js 92 | var babble = require('babble'); 93 | 94 | var emma = babble.babbler('emma'); 95 | var jack = babble.babbler('jack'); 96 | 97 | emma.listen('hi') 98 | .listen(function (message, context) { 99 | console.log(context.from + ': ' + message); 100 | return message; 101 | }) 102 | .decide(function (message, context) { 103 | return (message.indexOf('age') != -1) ? 'age' : 'name'; 104 | }, { 105 | 'name': babble.tell('hi, my name is emma'), 106 | 'age': babble.tell('hi, my age is 27') 107 | }); 108 | 109 | jack.tell('emma', 'hi') 110 | .tell(function (message, context) { 111 | if (Math.random() > 0.5) { 112 | return 'my name is jack' 113 | } else { 114 | return 'my age is 25'; 115 | } 116 | }) 117 | .listen(function (message, context) { 118 | console.log(context.from + ': ' + message); 119 | }); 120 | ``` 121 | 122 | ### Plan a meeting 123 | 124 | The following scenario describes two peers planning a meeting in two steps: 125 | First jack asks whether emma has time for a meeting, and if so, jack will 126 | propose to meet, and await emma's response. 127 | 128 | This scenario can be represented by the following control flow diagram: 129 | 130 | ![plan a meeting](https://raw.github.com/enmasseio/babble/master/img/plan_a_meeting.png) 131 | 132 | The scenario can be coded as follows. Note that the implementations of the 133 | control flow blocks are separated from the flow itself. 134 | 135 | ```js 136 | var babble = require('babble'); 137 | 138 | var emma = babble.babbler('emma'); 139 | var jack = babble.babbler('jack'); 140 | 141 | function decideIfAvailable () { 142 | return (Math.random() > 0.4) ? 'yes' : 'no'; 143 | } 144 | 145 | function decideToAgree (response) { 146 | if (response == 'can we meet at 15:00?' && Math.random() > 0.5) { 147 | return 'ok'; 148 | } 149 | else { 150 | return 'no'; 151 | } 152 | } 153 | 154 | emma.listen('do you have time today?') 155 | .decide(decideIfAvailable, { 156 | yes: babble.tell('yes') 157 | .listen() 158 | .decide(decideToAgree, { 159 | ok: babble.tell('ok'), 160 | no: babble.tell('no') 161 | }), 162 | no: babble.tell('no') 163 | }); 164 | 165 | function noTime () { 166 | console.log('emma has no time'); 167 | } 168 | 169 | function agreesToMeet (response) { 170 | return (response == 'ok') ? 'ok': 'no'; 171 | } 172 | 173 | function agreement () { 174 | console.log('emma agreed'); 175 | } 176 | 177 | function noAgreement () { 178 | console.log('emma didn\'t agree'); 179 | } 180 | 181 | jack.ask('emma', 'do you have time today?') 182 | .decide({ 183 | yes: babble.tell('can we meet at 15:00?') 184 | .listen() 185 | .decide(agreesToMeet, { 186 | ok: babble.then(agreement), 187 | no: babble.then(noAgreement) 188 | }), 189 | no: babble.then(noTime) 190 | }); 191 | ``` 192 | 193 | 194 | ## API 195 | 196 | Babble has the following factory functions: 197 | 198 | - `babble.ask(message: String | Function [, callback: Function]) : Block` 199 | Send a question and listen for a reply. 200 | This is equivalent of doing `tell(message).listen([callback])`. 201 | 202 | - `babble.babbler(id: String) : Babbler` 203 | Factory function to create a new Babbler. 204 | 205 | - `babble.babblify(actor: Object, params: Object) : Object` 206 | Babblify an actor. The babblified actor will be extended with functions 207 | `ask`, `tell`, `listen`, and `listenOnce`. 208 | 209 | Babble expects that messages sent via `actor.send(to, message)` will be 210 | delivered by the recipient on a function `actor.receive(from, message)`. 211 | Babble replaces the original `receive` with a new one, which is used to 212 | listen for all incoming messages. Messages ignored by babble are propagated 213 | to the original `receive` function. 214 | 215 | The function accepts the following parameters: 216 | 217 | - `actor: Object` 218 | The actor to be babblified. Must be an Object containing functions 219 | `send(to, message)` and `receive(from, message)`. 220 | - `[params: Object]` 221 | Optional parameters. Can contain properties: 222 | 223 | - `id: string` 224 | The id for the babbler 225 | - `send: string` 226 | The name of an alternative send function available on the actor. 227 | - `receive: string` 228 | The name of an alternative receive function available on the actor. 229 | 230 | The function returns the babblified actor. A babblified actor can be restored 231 | in its original state using `unbabblify(actor)`. 232 | 233 | - `babble.decide([decision: Function, ] choices: Object) : Block` 234 | Create a flow starting with a `Decision` block. 235 | When a `decision` function is provided, the function is invoked as 236 | `decision(response, context)`. The function must return the id for the next 237 | block in the control flow, which must be available in the provided `options`. 238 | The function `decision` can also return a Promise resolving with an id for the 239 | next block. When `decision` is not provided, the next block will be mapped 240 | directly from the `response`, which should be a string in that case. 241 | 242 | Parameter `choices` is a map with the possible next blocks in the flow. 243 | The next block is selected by the id returned by the `decision` function. 244 | The returned block is used as next block in the control flow. 245 | 246 | When there is no matching choice, the choice `'default'` will be selected 247 | when available. 248 | 249 | - `babble.iif(condition: function | RegExp | * [, trueBlock : Block] [, falseBlock : Block]) : Block` 250 | Create a control flow starting with an `IIf` block. 251 | When the condition is a function, it can either return a boolean or a Promise 252 | resolving with a boolean value. 253 | When the condition evaluates `true`, `trueBlock` is executed. If no `trueBlock` 254 | is provided, the next block in the chain will be executed. 255 | When the condition evaluates `true`, `falseBlock` is executed. 256 | 257 | - `babble.listen([callback: Function])` 258 | Wait for a message. The provided callback function is called as 259 | `callback(response, context)`, where `response` is the just received message. 260 | When the callback returns a promise, babble will wait with execution of the 261 | next block until the promise is resolved. The result returned by the callback 262 | is passed to the next block in the chain. 263 | Providing a callback function is equivalent of doing 264 | `babble.listen().then(callback)`. 265 | 266 | - `babble.tell(message: Function | *) : Block` 267 | Create a flow starting with a `Tell` block. Message can be a static value, 268 | or a callback function returning a message dynamically. The callback function 269 | is called as `callback(response, context)`, where `response` is the latest 270 | received message, and must return a result. 271 | The returned result is send to the connected peer. 272 | When the callback returns a Promise, the value returned when the promise 273 | resolves will be send to the connected peer. 274 | 275 | - `babble.then(next: Block | function) : Block` 276 | Create a flow starting with given block. When a callback function is provided, 277 | the function is wrapped into a `Then` block. The provided callback function 278 | is called as `callback(response, context)`, where `response` is the latest 279 | received message, and must return a result. 280 | When the callback returns a promise, babble will wait with execution of the 281 | next block until the promise is resolved. The result returned by the callback 282 | is passed to the next block in the chain. 283 | 284 | 285 | - `babble.unbabblify(actor: Object) : Object` 286 | Unbabblify an actor. Returns the unbabblified actor. 287 | 288 | Babble contains the following prototypes. These prototypes are normally 289 | instantiated via the above mentioned factory functions. 290 | 291 | - `babble.Babbler` 292 | - `babble.block.Block` 293 | - `babble.block.Decision` 294 | - `babble.block.IIf` 295 | - `babble.block.Listen` 296 | - `babble.block.Tell` 297 | - `babble.block.Then` 298 | 299 | ### Babbler 300 | 301 | A babbler is created via the factory function `babble.babbler(id: String)`. 302 | After creation, a babbler is automatically connected to the default (local) 303 | message bus. The connection can replaced with another message bus using the 304 | function `Babbler.connect(bus)`. 305 | 306 | A babbler has the following functions: 307 | 308 | - `ask(to: String, message: * | Function [, callback: Function]) : Block` 309 | This is equivalent of doing `tell(to, message).listen([callback])`. 310 | Other blocks can be chained to the returned block. 311 | 312 | - `connect([bus: Object]) : Promise.` 313 | Connect to a message bus. Babble comes with interfaces to support various 314 | message buses: `pubnub`, `pubsub-js`, and `default`. These interfaces are 315 | available in the `babble.messagebus` namespace. If parameter `bus` is 316 | not provided, babble uses the `default` message bus, which works locally. 317 | A specific message bus interface can be specified like: 318 | 319 | ```js 320 | babbler.connect(babble.messagebus['pubnub']) 321 | .then(function (babbler) { 322 | // connected 323 | }); 324 | ``` 325 | 326 | The connect function returns a promise which resolves with the babbler itself 327 | when the connection is ready. 328 | 329 | See section [Message bus](#message-bus) for documentation on the interface 330 | of a message bus. 331 | 332 | - `disconnect()` 333 | Disconnect from the connected message bus. 334 | 335 | - `listen([condition: Function | RegExp | * [, callback: Function]]) : Block` 336 | Listen for incoming messages and start the conversation flow. 337 | Other blocks can be chained to the returned block. 338 | 339 | Providing a condition will only start the flow when condition is met, 340 | this is equivalent of doing `listen().iif(condition)`. 341 | 342 | Providing a callback function is equivalent of doing either 343 | `listen(message).then(callback)` or `listen().iif(message).then(callback)`. 344 | The callback is invoked as `callback(message, context)`, and must return 345 | either a result or a Promise resolving with a result. The result will be 346 | passed to the next block in the chain. 347 | 348 | - `listenOnce([condition: Function | RegExp | * [, callback: Function]]) : Block` 349 | Equal to `listen`, except that the listener is removed as soon as a message 350 | is received matching listeners condition, i.e. the listener is executed only 351 | once. 352 | 353 | - `send(to: String, message: *)` 354 | Send a message to another peer. 355 | 356 | - `tell(to: String, message: Function | *)` 357 | Send a notification to another peer. 358 | `message` can be a static value or a callback function. When `message` is 359 | a function, it is invoked as `callback(message, context)`, and must return 360 | either a result or a Promise resolving with a result. The result will be 361 | sent to the other peer, and will be passed to the next block in the chain. 362 | 363 | ### Block 364 | 365 | Blocks can be created via the factory functions available in `babble` 366 | (`tell`, `iif`, `decide`, `then`, `listen`), or in a Babbler (`listen`, `tell`, 367 | `ask`). Blocks can be chained together, resulting in a control flow. The results 368 | returned by blocks are used as input argument for the next block in the chain. 369 | 370 | A `Block` has the following functions: 371 | 372 | - `ask(message: * [, callback]) : Block` 373 | Append a `Tell` and `Listen` block to the control flow. 374 | Parameter `message` can be a callback function or an object or value. 375 | 376 | - `decide([decision: function, ] choices: Object) : Block` 377 | Append a `Decision` block to the control flow. 378 | 379 | - `iif(condition: function | RegExp | * [, trueBlock : Block] [, falseBlock : Block]) : Block` 380 | Append an `IIf` block to the control flow. 381 | When the condition evaluates `true`, `trueBlock` is executed. 382 | If no `trueBlock` is provided, the next block in the chain will be executed. 383 | When the condition evaluates `true`, `falseBlock` is executed. 384 | 385 | - `listen([callback: Function]) : Block` 386 | Append a `Listen` block to the control flow. Providing a callback function is 387 | equivalent of doing `listen().then(callback)`. 388 | 389 | - `tell(message: * | Function) : Block` 390 | Append a `Tell` block to the control flow. Parameter `message` can be callback 391 | function or an object or value. 392 | 393 | - `then(block : Block | function) : Block` 394 | Append an arbitrary block to the control flow. When a callback function is 395 | provided, it is wrapped into a `Then` block and added to the chain. 396 | 397 | 398 | ## Message bus 399 | 400 | Babblers talk to each other via a message bus. This can be any message bus 401 | implementation. Babble comes with support for two message buses: a local 402 | message bus and [pubnub](http://www.pubnub.com/). 403 | 404 | ### Interface 405 | 406 | The function `Babbler.connect(bus)` accepts a message bus interface. This 407 | interface must be an Object with the following functions: 408 | 409 | - `connect(params: Object) : string` 410 | The function `connect` will be called by the Babbler with an object having 411 | the following parameters: 412 | 413 | - `id` the id of the babbler itself. 414 | - `message` the callback function to deliver messages for this babbler. 415 | This function must be invoked as `message(msg : *)`. 416 | - `callback` an optional callback function which is invoked when the 417 | connection is established. 418 | 419 | The `connect` function must return a token which can be used to disconnect 420 | again. 421 | 422 | - `disconnect(token: string)` 423 | Disconnect from a message bus. `token` is the token returned by the `connect` 424 | function. 425 | 426 | - `send(id: string, message: *)` 427 | Send a message to a babbler. 428 | 429 | ### Protocol 430 | 431 | The messages sent between babblers are JSON objects having the following properties: 432 | 433 | - `id: string` 434 | A unique identifier for the conversation, typically a uuid. 435 | This id is generated by the initiator of a conversation, and is sent with 436 | every message between the two babblers during the conversation. 437 | - `from: string` 438 | The id of the sender. 439 | - `to: string` 440 | The id of the receiver. 441 | - `message: *` 442 | The message contents. This is a (serializable) JSON object (often a string). 443 | 444 | Example: 445 | 446 | ```json 447 | { 448 | "id": "547d1840-2142-11e4-8c21-0800200c9a66", 449 | "from": "babbler1", 450 | "to": "babbler2", 451 | "message": "Hello babbler!" 452 | } 453 | ``` 454 | 455 | 456 | ## Build 457 | 458 | Babble can be build for use in the browser. This is done using the tools 459 | browserify and uglify. First install all project dependencies: 460 | 461 | npm install 462 | 463 | To build the library, run: 464 | 465 | npm run build 466 | 467 | This generates the files `./dist/babble.js`, `./dist/babble.min.js`, and 468 | `./dist/babble.min.map`. 469 | 470 | 471 | ## Test 472 | 473 | To execute tests for the library, install the project dependencies once: 474 | 475 | npm install 476 | 477 | Then, the tests can be executed: 478 | 479 | npm test 480 | 481 | 482 | # Roadmap 483 | 484 | - Implement error handling and timeout conditions. 485 | - Store message history in the context. 486 | - Implement conversations with multiple peers at the same time. 487 | -------------------------------------------------------------------------------- /examples/ask_age.js: -------------------------------------------------------------------------------- 1 | var babble = require('../index'); 2 | 3 | var emma = babble.babbler('emma'); 4 | var jack = babble.babbler('jack'); 5 | 6 | // listen for messages containing either 'age' or 'how old' 7 | emma.listen(/age|how old/) 8 | .tell(function () { 9 | return 25; 10 | }); 11 | 12 | jack.ask('emma', 'what is your age?') 13 | .then(function (age, context) { 14 | console.log(context.from + ' is ' + age + ' years old'); 15 | }); 16 | -------------------------------------------------------------------------------- /examples/babblify.js: -------------------------------------------------------------------------------- 1 | var babble = require('../index'); 2 | 3 | /****************************** 4 | * 5 | * Create a simple actor system 6 | * 7 | ******************************/ 8 | 9 | function Actor(id) { 10 | this.id = id; 11 | Actor.actors[id] = this; 12 | } 13 | 14 | Actor.actors = {}; // map with all actors by their id 15 | 16 | Actor.prototype.send = function (to, message) { 17 | var actor = Actor.actors[to]; 18 | if (!actor) { 19 | throw new Error('Not found'); 20 | } 21 | actor.receive(this.id, message); 22 | }; 23 | 24 | Actor.prototype.receive = function (from, message) { 25 | // ... to be overwritten by the actor 26 | }; 27 | 28 | 29 | 30 | /****************************** 31 | * 32 | * Regular usage 33 | * 34 | ******************************/ 35 | 36 | var emma = new Actor('emma'); 37 | var jack = new Actor('jack'); 38 | 39 | emma.receive = function (from, message) { 40 | console.log('Received a message from ' + from + ': "' + message + '"') 41 | }; 42 | 43 | jack.send('emma', 'hello emma!'); 44 | 45 | 46 | 47 | 48 | /****************************** 49 | * 50 | * Babblified usage 51 | * 52 | ******************************/ 53 | 54 | // create two actors and babblify them 55 | var susan = babble.babblify(new Actor('susan')); 56 | var john = babble.babblify(new Actor('john')); 57 | 58 | susan.listen('hi') 59 | .listen(printMessage) 60 | .decide(function (message, context) { 61 | return (message.indexOf('age') != -1) ? 'age' : 'name'; 62 | }, { 63 | 'name': babble.tell('hi, my name is susan'), 64 | 'age': babble.tell('hi, my age is 27') 65 | }); 66 | 67 | john.tell('susan', 'hi') 68 | .tell(function (message, context) { 69 | if (Math.random() > 0.5) { 70 | return 'my name is john' 71 | } else { 72 | return 'my age is 25'; 73 | } 74 | }) 75 | .listen(printMessage); 76 | 77 | function printMessage (message, context) { 78 | console.log(context.from + ': ' + message); 79 | return message; 80 | } 81 | -------------------------------------------------------------------------------- /examples/browser/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | babble in the browser 5 | 6 | 7 | 8 | 9 |
10 | 11 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/browser/pubnub/emma.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | emma | babble in the browser using pubnub 5 | 6 | 7 | 8 | 9 | 10 |
First open emma.html in a browser, then open jack.html in a browser.
11 |
12 | 13 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/browser/pubnub/jack.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | jack | babble in the browser using pubnub 5 | 6 | 7 | 8 | 9 | 10 |
First open emma.html in a browser, then open jack.html in a browser.
11 |
12 | 13 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/guess_the_number.js: -------------------------------------------------------------------------------- 1 | var babble = require('../index'); 2 | 3 | var MIN = 0; 4 | var MAX = 50; 5 | 6 | /* -------------------------------------------------------------------------- */ 7 | 8 | (function () { 9 | var emma = babble.babbler('emma'); 10 | 11 | function decideToPlay () { 12 | return (Math.random() > 0.2) ? 'start': 'deny'; 13 | } 14 | 15 | function decideIfCorrect (guess, context) { 16 | if (guess < context.number) { 17 | return 'higher'; 18 | } 19 | else if (guess > context.number) { 20 | return 'lower'; 21 | } 22 | else { 23 | return 'right'; 24 | } 25 | } 26 | 27 | var start = function (response, context) { 28 | // choose a random value 29 | context.number = randomInt(MIN, MAX); 30 | 31 | console.log('emma: ok I have a number in mind between ' + MIN + ' and ' + MAX); 32 | return 'ok'; 33 | }; 34 | 35 | var deny = function () { 36 | return 'no thanks'; 37 | }; 38 | 39 | function higher () { 40 | console.log('emma: higher'); 41 | return 'higher'; 42 | } 43 | 44 | function lower () { 45 | console.log('emma: lower'); 46 | return 'lower'; 47 | } 48 | 49 | function right () { 50 | console.log('emma: right!'); 51 | return 'right'; 52 | } 53 | 54 | var check = babble.decide(decideIfCorrect); 55 | check.addChoice('higher', babble.tell(higher).listen().then(check)); 56 | check.addChoice('lower', babble.tell(lower).listen().then(check)); 57 | check.addChoice('right', babble.tell(right)); 58 | 59 | emma.listen('lets play guess the number') 60 | .decide(decideToPlay, { 61 | start: babble.tell(start).listen().then(check), 62 | deny: babble.tell(deny) 63 | }); 64 | 65 | })(); 66 | 67 | /* -------------------------------------------------------------------------- */ 68 | 69 | (function () { 70 | var jack = babble.babbler('jack'); 71 | 72 | function canStart (response) { 73 | return (response == 'ok'); 74 | } 75 | 76 | function decideIfCorrect (response) { 77 | return (response == 'right') ? 'right': 'wrong'; 78 | } 79 | 80 | function start(response, context) { 81 | context.lower = MIN; 82 | context.upper = MAX; 83 | } 84 | 85 | function whine() { 86 | console.log('emma doesn\'t want to play guess the number :('); 87 | } 88 | 89 | function triumph (response, context) { 90 | console.log('jack: I found it! The correct number is: ' + context.number); 91 | } 92 | 93 | function guess (response, context) { 94 | if (response == 'higher') { 95 | context.lower = context.number + 1; 96 | } 97 | else if (response == 'lower') { 98 | context.upper = context.number - 1; 99 | } 100 | 101 | context.number = randomInt(context.lower, context.upper); 102 | console.log('jack: guessing ' + context.number + '...'); 103 | return context.number; 104 | } 105 | 106 | var checkGuess = babble.decide(decideIfCorrect); 107 | checkGuess.addChoice('right', babble.then(triumph)); 108 | checkGuess.addChoice('wrong', babble.tell(guess).listen().then(checkGuess)); 109 | 110 | jack.ask('emma', 'lets play guess the number') 111 | .iif( 112 | canStart, // condition 113 | babble.then(start).tell(guess).listen().then(checkGuess), // trueBlock 114 | babble.then(whine) // falseBlock 115 | ); 116 | 117 | })(); 118 | 119 | function randomInt(min, max) { 120 | return Math.floor(min + Math.random() * (max - min)); 121 | } 122 | -------------------------------------------------------------------------------- /examples/plan_a_meeting.js: -------------------------------------------------------------------------------- 1 | var babble = require('../index'); 2 | 3 | var emma = babble.babbler('emma'); 4 | var jack = babble.babbler('jack'); 5 | 6 | function decideIfAvailable () { 7 | return (Math.random() > 0.4) ? 'yes' : 'no'; 8 | } 9 | 10 | function decideToAgree (response) { 11 | if (response == 'can we meet at 15:00?' && Math.random() > 0.5) { 12 | return 'ok'; 13 | } 14 | else { 15 | return 'no'; 16 | } 17 | } 18 | 19 | emma.listen('do you have time today?') 20 | .decide(decideIfAvailable, { 21 | yes: babble.tell('yes') 22 | .listen() 23 | .decide(decideToAgree, { 24 | ok: babble.tell('ok'), 25 | no: babble.tell('no') 26 | }), 27 | no: babble.tell('no') 28 | }); 29 | 30 | function noTime () { 31 | console.log('emma has no time'); 32 | } 33 | 34 | function agreesToMeet (response) { 35 | return (response == 'ok') ? 'ok': 'no'; 36 | } 37 | 38 | function agreement () { 39 | console.log('emma agreed'); 40 | } 41 | 42 | function noAgreement () { 43 | console.log('emma didn\'t agree'); 44 | } 45 | 46 | jack.ask('emma', 'do you have time today?') 47 | .decide({ 48 | yes: babble.tell('can we meet at 15:00?') 49 | .listen() 50 | .decide(agreesToMeet, { 51 | ok: babble.then(agreement), 52 | no: babble.then(noAgreement) 53 | }), 54 | no: babble.then(noTime) 55 | }); 56 | -------------------------------------------------------------------------------- /examples/pubnub.js: -------------------------------------------------------------------------------- 1 | var babble = require('../index'); 2 | var Promise = require('es6-promise').Promise; 3 | 4 | // initialize pubnub messaging 5 | var pubnub = babble.messagebus.pubnub({ 6 | publish_key: 'demo', // REPLACE THIS WITH YOUR PUBNUB PUBLISH KEY 7 | subscribe_key: 'demo' // REPLACE THIS WITH YOUR PUBNUB SUBSCRIBE KEY 8 | }); 9 | 10 | var emma = babble.babbler('emma'); 11 | var jack = babble.babbler('jack'); 12 | 13 | Promise.all([emma.connect(pubnub), jack.connect(pubnub)]) 14 | .then(function () { 15 | emma.listen('hi') 16 | .listen(printMessage) 17 | .decide(function (message, context) { 18 | return (message.indexOf('age') != -1) ? 'age' : 'name'; 19 | }, { 20 | 'name': babble.tell('hi, my name is emma'), 21 | 'age': babble.tell('hi, my age is 27') 22 | }); 23 | 24 | jack.tell('emma', 'hi') 25 | .tell(function (message, context) { 26 | if (Math.random() > 0.5) { 27 | return 'my name is jack' 28 | } else { 29 | return 'my age is 25'; 30 | } 31 | }) 32 | .listen(printMessage); 33 | }); 34 | 35 | function printMessage (message, context) { 36 | console.log(context.from + ': ' + message); 37 | return message; 38 | } 39 | -------------------------------------------------------------------------------- /examples/say_hi.js: -------------------------------------------------------------------------------- 1 | var babble = require('../index'); 2 | 3 | var emma = babble.babbler('emma'); 4 | var jack = babble.babbler('jack'); 5 | 6 | emma.listen('hi') 7 | .listen(function (message, context) { 8 | console.log(context.from + ': ' + message); 9 | return message; 10 | }) 11 | .decide(function (message, context) { 12 | return (message.indexOf('age') != -1) ? 'age' : 'name'; 13 | }, { 14 | 'name': babble.tell('hi, my name is emma'), 15 | 'age': babble.tell('hi, my age is 27') 16 | }); 17 | 18 | jack.tell('emma', 'hi') 19 | .tell(function (message, context) { 20 | if (Math.random() > 0.5) { 21 | return 'my name is jack' 22 | } else { 23 | return 'my age is 25'; 24 | } 25 | }) 26 | .listen(function (message, context) { 27 | console.log(context.from + ': ' + message); 28 | }); 29 | -------------------------------------------------------------------------------- /examples/say_hi_async.js: -------------------------------------------------------------------------------- 1 | // All callbacks in babble can return a Promise in order to allow 2 | // asynchronous tasks. 3 | 4 | var Promise = require('es6-promise').Promise; // pick your favorite Promise library 5 | var babble = require('../index'); 6 | 7 | var emma = babble.babbler('emma'); 8 | var jack = babble.babbler('jack'); 9 | 10 | emma.listen('hi') 11 | .listen(function (message, context) { 12 | console.log(context.from + ': ' + message); 13 | return message; 14 | }) 15 | .decide(function (message, context) { 16 | // return a promise which we will resolve later on 17 | return new Promise(function (resolve, reject) { 18 | console.log('emma is thinking...'); 19 | 20 | // take some time to think... 21 | setTimeout(function () { 22 | // ok make a decision and resolve the promise 23 | var decision = (message.indexOf('age') != -1) ? 'age' : 'name'; 24 | resolve(decision); 25 | }, 1000); 26 | }); 27 | }, { 28 | 'name': babble.tell('hi, my name is emma'), 29 | 'age': babble.tell('hi, my age is 27') 30 | }); 31 | 32 | jack.tell('emma', 'hi') 33 | .tell(function (message, context) { 34 | // return a promise which we will resolve later on 35 | return new Promise(function (resolve, reject) { 36 | console.log('jack is typing a message...'); 37 | 38 | // pretend it takes some time to type a message 39 | setTimeout(function () { 40 | if (Math.random() > 0.5) { 41 | resolve('my name is jack'); 42 | } else { 43 | resolve('my age is 25'); 44 | } 45 | }, 1000); 46 | }); 47 | 48 | }) 49 | .listen(function (message, context) { 50 | console.log(context.from + ': ' + message); 51 | }); 52 | -------------------------------------------------------------------------------- /img/plan_a_meeting.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enmasseio/babble/6b7e84d7fb0df2d1129f0646b37a9d89d3da2539/img/plan_a_meeting.odg -------------------------------------------------------------------------------- /img/plan_a_meeting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enmasseio/babble/6b7e84d7fb0df2d1129f0646b37a9d89d3da2539/img/plan_a_meeting.png -------------------------------------------------------------------------------- /img/say_hi.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enmasseio/babble/6b7e84d7fb0df2d1129f0646b37a9d89d3da2539/img/say_hi.odg -------------------------------------------------------------------------------- /img/say_hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enmasseio/babble/6b7e84d7fb0df2d1129f0646b37a9d89d3da2539/img/say_hi.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/babble'); 4 | -------------------------------------------------------------------------------- /lib/Babbler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var uuid = require('node-uuid'); 4 | var Promise = require('es6-promise').Promise; 5 | 6 | var messagebus = require('./messagebus'); 7 | var Conversation = require('./Conversation'); 8 | var Block = require('./block/Block'); 9 | var Then = require('./block/Then'); 10 | var Tell = require('./block/Tell'); 11 | var Listen = require('./block/Listen'); 12 | 13 | require('./block/IIf'); // append iif function to Block 14 | 15 | /** 16 | * Babbler 17 | * @param {String} id 18 | * @constructor 19 | */ 20 | function Babbler (id) { 21 | if (!(this instanceof Babbler)) { 22 | throw new SyntaxError('Constructor must be called with the new operator'); 23 | } 24 | 25 | if (!id) { 26 | throw new Error('id required'); 27 | } 28 | 29 | this.id = id; 30 | this.listeners = []; // Array. 31 | this.conversations = {}; // Array.> all open conversations 32 | 33 | this.connect(); // automatically connect to the local message bus 34 | } 35 | 36 | // type information 37 | Babbler.prototype.isBabbler = true; 38 | 39 | /** 40 | * Connect to a message bus 41 | * @param {{connect: function, disconnect: function, send: function}} [bus] 42 | * A messaging interface. Must have the following functions: 43 | * - connect(params: {id: string, 44 | * message: function, callback: function}) : string 45 | * must return a token to disconnects again. 46 | * parameter callback is optional. 47 | * - disconnect(token: string) 48 | * disconnect from a message bus. 49 | * - send(id: string, message: *) 50 | * send a message 51 | * A number of interfaces is provided under babble.messagebus. 52 | * Default interface is babble.messagebus['default'] 53 | * @return {Promise.} Returns a Promise which resolves when the 54 | * babbler is connected. 55 | */ 56 | Babbler.prototype.connect = function (bus) { 57 | // disconnect (in case we are already connected) 58 | this.disconnect(); 59 | 60 | if (!bus) { 61 | bus = messagebus['default'](); 62 | } 63 | 64 | // validate the message bus functions 65 | if (typeof bus.connect !== 'function') { 66 | throw new Error('message bus must contain a function ' + 67 | 'connect(params: {id: string, callback: function}) : string'); 68 | } 69 | if (typeof bus.disconnect !== 'function') { 70 | throw new Error('message bus must contain a function ' + 71 | 'disconnect(token: string)'); 72 | } 73 | if (typeof bus.send !== 'function') { 74 | throw new Error('message bus must contain a function ' + 75 | 'send(params: {id: string, message: *})'); 76 | } 77 | 78 | // we return a promise, but we run the message.connect function immediately 79 | // (outside of the Promise), so that synchronous connects are done without 80 | // the need to await the promise to resolve on the next tick. 81 | var _resolve = null; 82 | var connected = new Promise(function (resolve, reject) { 83 | _resolve = resolve; 84 | }); 85 | 86 | var token = bus.connect({ 87 | id: this.id, 88 | message: this._receive.bind(this), 89 | callback: _resolve 90 | }); 91 | 92 | // link functions to disconnect and send 93 | this.disconnect = function () { 94 | bus.disconnect(token); 95 | }; 96 | this.send = bus.send; 97 | 98 | // return a promise 99 | return connected; 100 | }; 101 | 102 | /** 103 | * Handle an incoming message 104 | * @param {{id: string, from: string, to: string, message: string}} envelope 105 | * @private 106 | */ 107 | Babbler.prototype._receive = function (envelope) { 108 | // ignore when envelope does not contain an id and message 109 | if (!envelope || !('id' in envelope) || !('message' in envelope)) { 110 | return; 111 | } 112 | 113 | // console.log('_receive', envelope) // TODO: cleanup 114 | 115 | var me = this; 116 | var id = envelope.id; 117 | var conversations = this.conversations[id]; 118 | if (conversations && conversations.length) { 119 | // directly deliver to all open conversations with this id 120 | conversations.forEach(function (conversation) { 121 | conversation.deliver(envelope); 122 | }) 123 | } 124 | else { 125 | // start new conversations at each of the listeners 126 | if (!conversations) { 127 | conversations = []; 128 | } 129 | this.conversations[id] = conversations; 130 | 131 | this.listeners.forEach(function (block) { 132 | // create a new conversation 133 | var conversation = new Conversation({ 134 | id: id, 135 | self: me.id, 136 | other: envelope.from, 137 | context: { 138 | from: envelope.from 139 | }, 140 | send: me.send 141 | }); 142 | 143 | // append this conversation to the list with conversations 144 | conversations.push(conversation); 145 | 146 | // deliver the first message to the new conversation 147 | conversation.deliver(envelope); 148 | 149 | // process the conversation 150 | return me._process(block, conversation) 151 | .then(function() { 152 | // remove the conversation from the list again 153 | var index = conversations.indexOf(conversation); 154 | if (index !== -1) { 155 | conversations.splice(index, 1); 156 | } 157 | if (conversations.length === 0) { 158 | delete me.conversations[id]; 159 | } 160 | }); 161 | }); 162 | } 163 | }; 164 | 165 | /** 166 | * Disconnect from the babblebox 167 | */ 168 | Babbler.prototype.disconnect = function () { 169 | // by default, do nothing. The disconnect function will be overwritten 170 | // when the Babbler is connected to a message bus. 171 | }; 172 | 173 | /** 174 | * Send a message 175 | * @param {String} to Id of a babbler 176 | * @param {*} message Any message. Message must be a stringifiable JSON object. 177 | */ 178 | Babbler.prototype.send = function (to, message) { 179 | // send is overridden when running connect 180 | throw new Error('Cannot send: not connected'); 181 | }; 182 | 183 | /** 184 | * Listen for a specific event 185 | * 186 | * Providing a condition will only start the flow when condition is met, 187 | * this is equivalent of doing `listen().iif(condition)` 188 | * 189 | * Providing a callback function is equivalent of doing either 190 | * `listen(message).then(callback)` or `listen().iif(message).then(callback)`. 191 | * 192 | * @param {function | RegExp | String | *} [condition] 193 | * @param {Function} [callback] Invoked as callback(message, context), 194 | * where `message` is the just received message, 195 | * and `context` is an object where state can be 196 | * stored during a conversation. This is equivalent 197 | * of doing `listen().then(callback)` 198 | * @return {Block} block Start block of a control flow. 199 | */ 200 | Babbler.prototype.listen = function (condition, callback) { 201 | var listen = new Listen(); 202 | this.listeners.push(listen); 203 | 204 | var block = listen; 205 | if (condition) { 206 | block = block.iif(condition); 207 | } 208 | if (callback) { 209 | block = block.then(callback); 210 | } 211 | return block; 212 | }; 213 | 214 | /** 215 | * Listen for a specific event, and execute the flow once. 216 | * 217 | * Providing a condition will only start the flow when condition is met, 218 | * this is equivalent of doing `listen().iif(condition)` 219 | * 220 | * Providing a callback function is equivalent of doing either 221 | * `listen(message).then(callback)` or `listen().iif(message).then(callback)`. 222 | * 223 | * @param {function | RegExp | String | *} [condition] 224 | * @param {Function} [callback] Invoked as callback(message, context), 225 | * where `message` is the just received message, 226 | * and `context` is an object where state can be 227 | * stored during a conversation. This is equivalent 228 | * of doing `listen().then(callback)` 229 | * @return {Block} block Start block of a control flow. 230 | */ 231 | Babbler.prototype.listenOnce = function (condition, callback) { 232 | var listen = new Listen(); 233 | this.listeners.push(listen); 234 | 235 | var me = this; 236 | var block = listen; 237 | 238 | if (condition) { 239 | block = block.iif(condition); 240 | } 241 | 242 | block = block.then(function (message) { 243 | // remove the flow from the listeners after fired once 244 | var index = me.listeners.indexOf(listen); 245 | if (index !== -1) { 246 | me.listeners.splice(index, 1); 247 | } 248 | return message; 249 | }); 250 | 251 | if (callback) { 252 | block = block.then(callback); 253 | } 254 | 255 | return block; 256 | }; 257 | 258 | /** 259 | * Send a message to the other peer 260 | * Creates a block Tell, and runs the block immediately. 261 | * @param {String} to Babbler id 262 | * @param {Function | *} message 263 | * @return {Block} block Last block in the created control flow 264 | */ 265 | Babbler.prototype.tell = function (to, message) { 266 | var me = this; 267 | var cid = uuid.v4(); // create an id for this conversation 268 | 269 | // create a new conversation 270 | var conversation = new Conversation({ 271 | id: cid, 272 | self: this.id, 273 | other: to, 274 | context: { 275 | from: to 276 | }, 277 | send: me.send 278 | }); 279 | this.conversations[cid] = [conversation]; 280 | 281 | var block = new Tell(message); 282 | 283 | // run the Tell block on the next tick, when the conversation flow is created 284 | setTimeout(function () { 285 | me._process(block, conversation) 286 | .then(function () { 287 | // cleanup the conversation 288 | delete me.conversations[cid]; 289 | }) 290 | }, 0); 291 | 292 | return block; 293 | }; 294 | 295 | /** 296 | * Send a question, listen for a response. 297 | * Creates two blocks: Tell and Listen, and runs them immediately. 298 | * This is equivalent of doing `Babbler.tell(to, message).listen(callback)` 299 | * @param {String} to Babbler id 300 | * @param {* | Function} message A message or a callback returning a message. 301 | * @param {Function} [callback] Invoked as callback(message, context), 302 | * where `message` is the just received message, 303 | * and `context` is an object where state can be 304 | * stored during a conversation. This is equivalent 305 | * of doing `listen().then(callback)` 306 | * @return {Block} block Last block in the created control flow 307 | */ 308 | Babbler.prototype.ask = function (to, message, callback) { 309 | return this 310 | .tell(to, message) 311 | .listen(callback); 312 | }; 313 | 314 | /** 315 | * Process a flow starting with `block`, given a conversation 316 | * @param {Block} block 317 | * @param {Conversation} conversation 318 | * @return {Promise.} Resolves when the conversation is finished 319 | * @private 320 | */ 321 | Babbler.prototype._process = function (block, conversation) { 322 | return new Promise(function (resolve, reject) { 323 | /** 324 | * Process a block, given the conversation and a message which is chained 325 | * from block to block. 326 | * @param {Block} block 327 | * @param {*} [message] 328 | */ 329 | function process(block, message) { 330 | //console.log('process', conversation.self, conversation.id, block.constructor.name, message) // TODO: cleanup 331 | 332 | block.execute(conversation, message) 333 | .then(function (next) { 334 | if (next.block) { 335 | // recursively evaluate the next block in the conversation flow 336 | process(next.block, next.result); 337 | } 338 | else { 339 | // we are done, this is the end of the conversation 340 | resolve(conversation); 341 | } 342 | }); 343 | } 344 | 345 | // process the first block 346 | process(block); 347 | }); 348 | }; 349 | 350 | module.exports = Babbler; 351 | -------------------------------------------------------------------------------- /lib/Conversation.js: -------------------------------------------------------------------------------- 1 | var uuid = require('node-uuid'); 2 | var Promise = require('es6-promise').Promise; 3 | 4 | /** 5 | * A conversation 6 | * Holds meta data for a conversation between two peers 7 | * @param {Object} [config] Configuration options: 8 | * {string} [id] A unique id for the conversation. If not provided, a uuid is generated 9 | * {string} self Id of the peer on this side of the conversation 10 | * {string} other Id of the peer on the other side of the conversation 11 | * {Object} [context] Context passed with all callbacks of the conversation 12 | * {function(to: string, message: *): Promise} send Function to send a message 13 | * @constructor 14 | */ 15 | function Conversation (config) { 16 | if (!(this instanceof Conversation)) { 17 | throw new SyntaxError('Constructor must be called with the new operator'); 18 | } 19 | 20 | // public properties 21 | this.id = config && config.id || uuid.v4(); 22 | this.self = config && config.self || null; 23 | this.other = config && config.other || null; 24 | this.context = config && config.context || {}; 25 | 26 | // private properties 27 | this._send = config && config.send || null; 28 | this._inbox = []; // queue with received but not yet picked messages 29 | this._receivers = []; // queue with handlers waiting for a new message 30 | } 31 | 32 | // type information 33 | Conversation.prototype.isConversation = true; 34 | 35 | /** 36 | * Send a message 37 | * @param {*} message 38 | * @return {Promise.} Resolves when the message has been sent 39 | */ 40 | Conversation.prototype.send = function (message) { 41 | return this._send(this.other, { 42 | id: this.id, 43 | from: this.self, 44 | to: this.other, 45 | message: message 46 | }); 47 | }; 48 | 49 | /** 50 | * Deliver a message 51 | * @param {{id: string, from: string, to: string, message: string}} envelope 52 | */ 53 | Conversation.prototype.deliver = function (envelope) { 54 | if (this._receivers.length) { 55 | var receiver = this._receivers.shift(); 56 | receiver(envelope.message); 57 | } 58 | else { 59 | this._inbox.push(envelope.message); 60 | } 61 | }; 62 | 63 | /** 64 | * Receive a message. 65 | * @returns {Promise.<*>} Resolves with a message as soon as a message 66 | * is delivered. 67 | */ 68 | Conversation.prototype.receive = function () { 69 | var me = this; 70 | 71 | if (this._inbox.length) { 72 | return Promise.resolve(this._inbox.shift()); 73 | } 74 | else { 75 | return new Promise(function (resolve) { 76 | me._receivers.push(resolve); 77 | }) 78 | } 79 | }; 80 | 81 | module.exports = Conversation; 82 | -------------------------------------------------------------------------------- /lib/babble.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Babbler = require('./Babbler'); 4 | 5 | var Tell = require('./block/Tell'); 6 | var Listen = require('./block/Listen'); 7 | var Then = require('./block/Then'); 8 | var Decision = require('./block/Decision'); 9 | var IIf = require('./block/IIf'); 10 | 11 | /** 12 | * Create a new babbler 13 | * @param {String} id 14 | * @return {Babbler} babbler 15 | */ 16 | exports.babbler = function (id) { 17 | return new Babbler(id); 18 | }; 19 | 20 | /** 21 | * Create a control flow starting with a tell block 22 | * @param {* | Function} [message] A static message or callback function 23 | * returning a message dynamically. 24 | * When `message` is a function, it will be 25 | * invoked as callback(message, context), 26 | * where `message` is the output from the 27 | * previous block in the chain, and `context` is 28 | * an object where state can be stored during a 29 | * conversation. 30 | * @return {Tell} tell 31 | */ 32 | exports.tell = function (message) { 33 | return new Tell(message); 34 | }; 35 | 36 | /** 37 | * Send a question, listen for a response. 38 | * Creates two blocks: Tell and Listen. 39 | * This is equivalent of doing `babble.tell(message).listen(callback)` 40 | * @param {* | Function} message 41 | * @param {Function} [callback] Invoked as callback(message, context), 42 | * where `message` is the just received message, 43 | * and `context` is an object where state can be 44 | * stored during a conversation. This is equivalent 45 | * of doing `listen().then(callback)` 46 | * @return {Block} block Last block in the created control flow 47 | */ 48 | exports.ask = function (message, callback) { 49 | return exports 50 | .tell(message) 51 | .listen(callback); 52 | }; 53 | 54 | /** 55 | * Create a decision block and chain it to the current block. 56 | * 57 | * Syntax: 58 | * 59 | * decide(choices) 60 | * decide(decision, choices) 61 | * 62 | * Where: 63 | * 64 | * {Function | Object} [decision] 65 | * When a `decision` function is provided, the 66 | * function is invoked as decision(message, context), 67 | * where `message` is the output from the previous 68 | * block in the chain, and `context` is an object 69 | * where state can be stored during a conversation. 70 | * The function must return the id for the next 71 | * block in the control flow, which must be 72 | * available in the provided `choices`. 73 | * If `decision` is not provided, the next block 74 | * will be mapped directly from the message. 75 | * {Object.} choices 76 | * A map with the possible next blocks in the flow 77 | * The next block is selected by the id returned 78 | * by the decision function. 79 | * 80 | * There is one special id for choices: 'default'. This id is called when either 81 | * the decision function returns an id which does not match any of the available 82 | * choices. 83 | * 84 | * @param {Function | Object} arg1 Can be {function} decision or {Object} choices 85 | * @param {Object} [arg2] choices 86 | * @return {Block} decision The created decision block 87 | */ 88 | exports.decide = function (arg1, arg2) { 89 | // TODO: test arguments.length > 2 90 | return new Decision(arg1, arg2); 91 | }; 92 | 93 | /** 94 | * Listen for a message. 95 | * 96 | * Optionally a callback function can be provided, which is equivalent of 97 | * doing `listen().then(callback)`. 98 | * 99 | * @param {Function} [callback] Invoked as callback(message, context), 100 | * where `message` is the just received message, 101 | * and `context` is an object where state can be 102 | * stored during a conversation. This is equivalent 103 | * of doing `listen().then(callback)` 104 | * @return {Block} Returns the created Listen block 105 | */ 106 | exports.listen = function(callback) { 107 | var block = new Listen(); 108 | if (callback) { 109 | return block.then(callback); 110 | } 111 | return block; 112 | }; 113 | 114 | /** 115 | * Create a control flow starting with a Then block 116 | * @param {Function} callback Invoked as callback(message, context), 117 | * where `message` is the output from the previous 118 | * block in the chain, and `context` is an object 119 | * where state can be stored during a conversation. 120 | * @return {Then} then 121 | */ 122 | exports.then = function (callback) { 123 | return new Then(callback); 124 | }; 125 | 126 | /** 127 | * IIf 128 | * Create an iif block, which checks a condition and continues either with 129 | * the trueBlock or the falseBlock. The input message is passed to the next 130 | * block in the flow. 131 | * 132 | * Can be used as follows: 133 | * - When `condition` evaluates true: 134 | * - when `trueBlock` is provided, the flow continues with `trueBlock` 135 | * - else, when there is a block connected to the IIf block, the flow continues 136 | * with that block. 137 | * - When `condition` evaluates false: 138 | * - when `falseBlock` is provided, the flow continues with `falseBlock` 139 | * 140 | * Syntax: 141 | * 142 | * new IIf(condition, trueBlock) 143 | * new IIf(condition, trueBlock [, falseBlock]) 144 | * new IIf(condition).then(...) 145 | * 146 | * @param {Function | RegExp | *} condition A condition returning true or false 147 | * In case of a function, 148 | * the function is invoked as 149 | * `condition(message, context)` and 150 | * must return a boolean. In case of 151 | * a RegExp, condition will be tested 152 | * to return true. In other cases, 153 | * non-strict equality is tested on 154 | * the input. 155 | * @param {Block} [trueBlock] 156 | * @param {Block} [falseBlock] 157 | * @returns {Block} 158 | */ 159 | exports.iif = function (condition, trueBlock, falseBlock) { 160 | return new IIf(condition, trueBlock, falseBlock); 161 | }; 162 | 163 | // export the babbler prototype 164 | exports.Babbler = Babbler; 165 | 166 | // export all flow blocks 167 | exports.block = { 168 | Block: require('./block/Block'), 169 | Then: require('./block/Then'), 170 | Decision: require('./block/Decision'), 171 | IIf: require('./block/IIf'), 172 | Listen: require('./block/Listen'), 173 | Tell: require('./block/Tell') 174 | }; 175 | 176 | // export messagebus interfaces 177 | exports.messagebus = require('./messagebus'); 178 | 179 | /** 180 | * Babblify an actor. The babblified actor will be extended with functions 181 | * `ask`, `tell`, and `listen`. 182 | * 183 | * Babble expects that messages sent via `actor.send(to, message)` will be 184 | * delivered by the recipient on a function `actor.receive(from, message)`. 185 | * Babble replaces the original `receive` with a new one, which is used to 186 | * listen for all incoming messages. Messages ignored by babble are propagated 187 | * to the original `receive` function. 188 | * 189 | * The actor can be restored in its original state using `unbabblify(actor)`. 190 | * 191 | * @param {Object} actor The actor to be babblified. Must be an object 192 | * containing functions `send(to, message)` and 193 | * `receive(from, message)`. 194 | * @param {Object} [params] Optional parameters. Can contain properties: 195 | * - id: string The id for the babbler 196 | * - send: string The name of an alternative 197 | * send function available on 198 | * the actor. 199 | * - receive: string The name of an alternative 200 | * receive function available 201 | * on the actor. 202 | * @returns {Object} Returns the babblified actor. 203 | */ 204 | exports.babblify = function (actor, params) { 205 | var babblerId; 206 | if (params && params.id !== undefined) { 207 | babblerId = params.id; 208 | } 209 | else if (actor.id !== undefined) { 210 | babblerId = actor.id 211 | } 212 | else { 213 | throw new Error('Id missing. Ensure that either actor has a property "id", ' + 214 | 'or provide an id as a property in second argument params') 215 | } 216 | 217 | // validate actor 218 | ['ask', 'tell', 'listen', 'listenOnce'].forEach(function (prop) { 219 | if (actor[prop] !== undefined) { 220 | throw new Error('Conflict: actor already has a property "' + prop + '"'); 221 | } 222 | }); 223 | 224 | var sendName = params && params.send || 'send'; 225 | if (typeof actor[sendName] !== 'function') { 226 | throw new Error('Missing function. ' + 227 | 'Function "' + sendName + '(to, message)" expected on actor or on params'); 228 | } 229 | 230 | // create a new babbler 231 | var babbler = exports.babbler(babblerId); 232 | 233 | // attach receive function to the babbler 234 | var receiveName = params && params.receive || 'receive'; 235 | var receiveOriginal = actor.hasOwnProperty(receiveName) ? actor[receiveName] : null; 236 | if (receiveOriginal) { 237 | actor[receiveName] = function (from, message) { 238 | babbler._receive(message); 239 | // TODO: only propagate to receiveOriginal if the message is not handled by the babbler 240 | return receiveOriginal.call(actor, from, message); 241 | }; 242 | } 243 | else { 244 | actor[receiveName] = function (from, message) { 245 | return babbler._receive(message); 246 | }; 247 | } 248 | 249 | // attach send function to the babbler 250 | babbler.send = function (to, message) { 251 | // FIXME: there should be no need to send a message on next tick 252 | setTimeout(function () { 253 | actor[sendName](to, message) 254 | }, 0) 255 | }; 256 | 257 | // attach babbler functions and properties to the actor 258 | actor.__babbler__ = { 259 | babbler: babbler, 260 | receiveOriginal: receiveOriginal, 261 | receiveName: receiveName 262 | }; 263 | actor.ask = babbler.ask.bind(babbler); 264 | actor.tell = babbler.tell.bind(babbler); 265 | actor.listen = babbler.listen.bind(babbler); 266 | actor.listenOnce = babbler.listenOnce.bind(babbler); 267 | 268 | return actor; 269 | }; 270 | 271 | /** 272 | * Unbabblify an actor. 273 | * @param {Object} actor 274 | * @return {Object} Returns the unbabblified actor. 275 | */ 276 | exports.unbabblify = function (actor) { 277 | var __babbler__ = actor.__babbler__; 278 | if (__babbler__) { 279 | delete actor.__babbler__; 280 | delete actor.ask; 281 | delete actor.tell; 282 | delete actor.listen; 283 | delete actor.listenOnce; 284 | delete actor[__babbler__.receiveName]; 285 | 286 | // restore any original receiveOriginal method 287 | if (__babbler__.receiveOriginal) { 288 | actor[__babbler__.receiveName] = __babbler__.receiveOriginal; 289 | } 290 | } 291 | 292 | return actor; 293 | }; 294 | -------------------------------------------------------------------------------- /lib/block/Block.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Abstract control flow diagram block 5 | * @constructor 6 | */ 7 | function Block() { 8 | this.next = null; 9 | this.previous = null; 10 | } 11 | 12 | // type information 13 | Block.prototype.isBlock = true; 14 | 15 | /** 16 | * Execute the block 17 | * @param {Conversation} conversation 18 | * @param {*} message 19 | * @return {Promise.<{result: *, block: Block}, Error>} next 20 | */ 21 | Block.prototype.execute = function (conversation, message) { 22 | throw new Error('Cannot run an abstract Block'); 23 | }; 24 | 25 | module.exports = Block; 26 | -------------------------------------------------------------------------------- /lib/block/Decision.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('es6-promise').Promise; 4 | var Block = require('./Block'); 5 | var isPromise =require('../util').isPromise; 6 | 7 | require('./Then'); // extend Block with function then 8 | 9 | /** 10 | * Decision 11 | * A decision is made by executing the provided callback function, which returns 12 | * a next control flow block. 13 | * 14 | * Syntax: 15 | * 16 | * new Decision(choices) 17 | * new Decision(decision, choices) 18 | * 19 | * Where: 20 | * 21 | * {Function | Object} [decision] 22 | * When a `decision` function is provided, the 23 | * function is invoked as decision(message, context), 24 | * where `message` is the output from the previous 25 | * block in the chain, and `context` is an object 26 | * where state can be stored during a conversation. 27 | * The function must return the id for the next 28 | * block in the control flow, which must be 29 | * available in the provided `choices`. 30 | * If `decision` is not provided, the next block 31 | * will be mapped directly from the message. 32 | * {Object.} choices 33 | * A map with the possible next blocks in the flow 34 | * The next block is selected by the id returned 35 | * by the decision function. 36 | * 37 | * There is one special id for choices: 'default'. This id is called when either 38 | * the decision function returns an id which does not match any of the available 39 | * choices. 40 | * 41 | * @param arg1 42 | * @param arg2 43 | * @constructor 44 | * @extends {Block} 45 | */ 46 | function Decision (arg1, arg2) { 47 | var decision, choices; 48 | 49 | if (!(this instanceof Decision)) { 50 | throw new SyntaxError('Constructor must be called with the new operator'); 51 | } 52 | 53 | if (typeof arg1 === 'function') { 54 | decision = arg1; 55 | choices = arg2; 56 | } 57 | else { 58 | decision = null; 59 | choices = arg1; 60 | } 61 | 62 | if (decision) { 63 | if (typeof decision !== 'function') { 64 | throw new TypeError('Parameter decision must be a function'); 65 | } 66 | } 67 | else { 68 | decision = function (message, context) { 69 | return message; 70 | } 71 | } 72 | 73 | if (choices && (typeof choices === 'function')) { 74 | throw new TypeError('Parameter choices must be an object'); 75 | } 76 | 77 | this.decision = decision; 78 | this.choices = {}; 79 | 80 | // append all choices 81 | if (choices) { 82 | var me = this; 83 | Object.keys(choices).forEach(function (id) { 84 | me.addChoice(id, choices[id]); 85 | }); 86 | } 87 | } 88 | 89 | Decision.prototype = Object.create(Block.prototype); 90 | Decision.prototype.constructor = Decision; 91 | 92 | // type information 93 | Decision.prototype.isDecision = true; 94 | 95 | /** 96 | * Execute the block 97 | * @param {Conversation} conversation 98 | * @param {*} message 99 | * @return {Promise.<{result: *, block: Block}, Error>} next 100 | */ 101 | Decision.prototype.execute = function (conversation, message) { 102 | var me = this; 103 | var id = this.decision(message, conversation.context); 104 | 105 | var resolve = isPromise(id) ? id : Promise.resolve(id); 106 | return resolve.then(function (id) { 107 | var next = me.choices[id]; 108 | 109 | if (!next) { 110 | // there is no match, fall back on the default choice 111 | next = me.choices['default']; 112 | } 113 | 114 | if (!next) { 115 | throw new Error('Block with id "' + id + '" not found'); 116 | } 117 | 118 | return { 119 | result: message, 120 | block: next 121 | }; 122 | }); 123 | }; 124 | 125 | /** 126 | * Add a choice to the decision block. 127 | * The choice can be a new chain of blocks. The first block of the chain 128 | * will be triggered when the this id comes out of the decision function. 129 | * @param {String | 'default'} id 130 | * @param {Block} block 131 | * @return {Decision} self 132 | */ 133 | Decision.prototype.addChoice = function (id, block) { 134 | if (typeof id !== 'string') { 135 | throw new TypeError('String expected as choice id'); 136 | } 137 | 138 | if (!block || !block.isBlock) { 139 | throw new TypeError('Block expected as choice'); 140 | } 141 | 142 | if (id in this.choices) { 143 | throw new Error('Choice with id "' + id + '" already exists'); 144 | } 145 | 146 | // find the first block of the chain 147 | var first = block; 148 | while (first && first.previous) { 149 | first = first.previous; 150 | } 151 | 152 | this.choices[id] = first; 153 | 154 | return this; 155 | }; 156 | 157 | /** 158 | * Create a decision block and chain it to the current block. 159 | * Returns the first block in the chain. 160 | * 161 | * Syntax: 162 | * 163 | * decide(choices) 164 | * decide(decision, choices) 165 | * 166 | * Where: 167 | * 168 | * {Function | Object} [decision] 169 | * When a `decision` function is provided, the 170 | * function is invoked as decision(message, context), 171 | * where `message` is the output from the previous 172 | * block in the chain, and `context` is an object 173 | * where state can be stored during a conversation. 174 | * The function must return the id for the next 175 | * block in the control flow, which must be 176 | * available in the provided `choices`. 177 | * If `decision` is not provided, the next block 178 | * will be mapped directly from the message. 179 | * {Object.} choices 180 | * A map with the possible next blocks in the flow 181 | * The next block is selected by the id returned 182 | * by the decision function. 183 | * 184 | * There is one special id for choices: 'default'. This id is called when either 185 | * the decision function returns an id which does not match any of the available 186 | * choices. 187 | * 188 | * @param {Function | Object} arg1 Can be {function} decision or {Object} choices 189 | * @param {Object} [arg2] choices 190 | * @return {Block} first First block in the chain 191 | */ 192 | Block.prototype.decide = function (arg1, arg2) { 193 | var decision = new Decision(arg1, arg2); 194 | 195 | return this.then(decision); 196 | }; 197 | 198 | module.exports = Decision; 199 | -------------------------------------------------------------------------------- /lib/block/IIf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('es6-promise').Promise; 4 | var Block = require('./Block'); 5 | var isPromise = require('../util').isPromise; 6 | 7 | require('./Then'); // extend Block with function then 8 | 9 | /** 10 | * IIf 11 | * Create an iif block, which checks a condition and continues either with 12 | * the trueBlock or the falseBlock. The input message is passed to the next 13 | * block in the flow. 14 | * 15 | * Can be used as follows: 16 | * - When `condition` evaluates true: 17 | * - when `trueBlock` is provided, the flow continues with `trueBlock` 18 | * - else, when there is a block connected to the IIf block, the flow continues 19 | * with that block. 20 | * - When `condition` evaluates false: 21 | * - when `falseBlock` is provided, the flow continues with `falseBlock` 22 | * 23 | * Syntax: 24 | * 25 | * new IIf(condition, trueBlock) 26 | * new IIf(condition, trueBlock [, falseBlock]) 27 | * new IIf(condition).then(...) 28 | * 29 | * @param {Function | RegExp | *} condition A condition returning true or false 30 | * In case of a function, 31 | * the function is invoked as 32 | * `condition(message, context)` and 33 | * must return a boolean. In case of 34 | * a RegExp, condition will be tested 35 | * to return true. In other cases, 36 | * non-strict equality is tested on 37 | * the input. 38 | * @param {Block} [trueBlock] 39 | * @param {Block} [falseBlock] 40 | * @constructor 41 | * @extends {Block} 42 | */ 43 | function IIf (condition, trueBlock, falseBlock) { 44 | if (!(this instanceof IIf)) { 45 | throw new SyntaxError('Constructor must be called with the new operator'); 46 | } 47 | 48 | if (typeof condition === 'function') { 49 | this.condition = condition; 50 | } 51 | else if (condition instanceof RegExp) { 52 | this.condition = function (message, context) { 53 | return condition.test(message); 54 | } 55 | } 56 | else { 57 | this.condition = function (message, context) { 58 | return message == condition; 59 | } 60 | } 61 | 62 | if (trueBlock && !trueBlock.isBlock) { 63 | throw new TypeError('Parameter trueBlock must be a Block'); 64 | } 65 | 66 | if (falseBlock && !falseBlock.isBlock) { 67 | throw new TypeError('Parameter falseBlock must be a Block'); 68 | } 69 | 70 | this.trueBlock = trueBlock || null; 71 | this.falseBlock = falseBlock || null; 72 | } 73 | 74 | IIf.prototype = Object.create(Block.prototype); 75 | IIf.prototype.constructor = IIf; 76 | 77 | // type information 78 | IIf.prototype.isIIf = true; 79 | 80 | /** 81 | * Execute the block 82 | * @param {Conversation} conversation 83 | * @param {*} message 84 | * @return {Promise.<{result: *, block: Block}, Error>} next 85 | */ 86 | IIf.prototype.execute = function (conversation, message) { 87 | var me = this; 88 | var condition = this.condition(message, conversation.context); 89 | 90 | var resolve = isPromise(condition) ? condition : Promise.resolve(condition); 91 | 92 | return resolve.then(function (condition) { 93 | var next = condition ? (me.trueBlock || me.next) : me.falseBlock; 94 | 95 | return { 96 | result: message, 97 | block: next 98 | }; 99 | }); 100 | }; 101 | 102 | /** 103 | * IIf 104 | * Create an iif block, which checks a condition and continues either with 105 | * the trueBlock or the falseBlock. The input message is passed to the next 106 | * block in the flow. 107 | * 108 | * Can be used as follows: 109 | * - When `condition` evaluates true: 110 | * - when `trueBlock` is provided, the flow continues with `trueBlock` 111 | * - else, when there is a block connected to the IIf block, the flow continues 112 | * with that block. 113 | * - When `condition` evaluates false: 114 | * - when `falseBlock` is provided, the flow continues with `falseBlock` 115 | * 116 | * Syntax: 117 | * 118 | * new IIf(condition, trueBlock) 119 | * new IIf(condition, trueBlock [, falseBlock]) 120 | * new IIf(condition).then(...) 121 | * 122 | * @param {Function | RegExp | *} condition A condition returning true or false 123 | * In case of a function, 124 | * the function is invoked as 125 | * `condition(message, context)` and 126 | * must return a boolean. In case of 127 | * a RegExp, condition will be tested 128 | * to return true. In other cases, 129 | * non-strict equality is tested on 130 | * the input. 131 | * @param {Block} [trueBlock] 132 | * @param {Block} [falseBlock] 133 | * @returns {Block} Returns the created IIf block 134 | */ 135 | Block.prototype.iif = function (condition, trueBlock, falseBlock) { 136 | var iif = new IIf(condition, trueBlock, falseBlock); 137 | 138 | return this.then(iif); 139 | }; 140 | 141 | module.exports = IIf; 142 | -------------------------------------------------------------------------------- /lib/block/Listen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('es6-promise').Promise; 4 | var Block = require('./Block'); 5 | var Then = require('./Then'); 6 | 7 | /** 8 | * Listen 9 | * Wait until a message comes in from the connected peer, then continue 10 | * with the next block in the control flow. 11 | * 12 | * @constructor 13 | * @extends {Block} 14 | */ 15 | function Listen () { 16 | if (!(this instanceof Listen)) { 17 | throw new SyntaxError('Constructor must be called with the new operator'); 18 | } 19 | } 20 | 21 | Listen.prototype = Object.create(Block.prototype); 22 | Listen.prototype.constructor = Listen; 23 | 24 | // type information 25 | Listen.prototype.isListen = true; 26 | 27 | /** 28 | * Execute the block 29 | * @param {Conversation} conversation 30 | * @param {*} [message] Message is ignored by Listen blocks 31 | * @return {Promise.<{result: *, block: Block}, Error>} next 32 | */ 33 | Listen.prototype.execute = function (conversation, message) { 34 | var me = this; 35 | 36 | // wait until a message is received 37 | return conversation.receive() 38 | .then(function (message) { 39 | return { 40 | result: message, 41 | block: me.next 42 | } 43 | }); 44 | }; 45 | 46 | /** 47 | * Create a Listen block and chain it to the current block 48 | * 49 | * Optionally a callback function can be provided, which is equivalent of 50 | * doing `listen().then(callback)`. 51 | * 52 | * @param {Function} [callback] Executed as callback(message: *, context: Object) 53 | * Must return a result 54 | * @return {Block} Returns the appended block 55 | */ 56 | Block.prototype.listen = function (callback) { 57 | var listen = new Listen(); 58 | var block = this.then(listen); 59 | if (callback) { 60 | block = block.then(callback); 61 | } 62 | return block; 63 | }; 64 | 65 | module.exports = Listen; 66 | -------------------------------------------------------------------------------- /lib/block/Tell.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('es6-promise').Promise; 4 | var Block = require('./Block'); 5 | var isPromise = require('../util').isPromise; 6 | 7 | require('./Then'); // extend Block with function then 8 | require('./Listen'); // extend Block with function listen 9 | 10 | /** 11 | * Tell 12 | * Send a message to the other peer. 13 | * @param {* | Function} message A static message or callback function 14 | * returning a message dynamically. 15 | * When `message` is a function, it will be 16 | * invoked as callback(message, context), 17 | * where `message` is the output from the 18 | * previous block in the chain, and `context` is 19 | * an object where state can be stored during a 20 | * conversation. 21 | * @constructor 22 | * @extends {Block} 23 | */ 24 | function Tell (message) { 25 | if (!(this instanceof Tell)) { 26 | throw new SyntaxError('Constructor must be called with the new operator'); 27 | } 28 | 29 | this.message = message; 30 | } 31 | 32 | Tell.prototype = Object.create(Block.prototype); 33 | Tell.prototype.constructor = Tell; 34 | 35 | // type information 36 | Tell.prototype.isTell = true; 37 | 38 | /** 39 | * Execute the block 40 | * @param {Conversation} conversation 41 | * @param {*} [message] A message is ignored by the Tell block 42 | * @return {Promise.<{result: *, block: Block}, Error>} next 43 | */ 44 | Tell.prototype.execute = function (conversation, message) { 45 | // resolve the message 46 | var me = this; 47 | var resolve; 48 | if (typeof this.message === 'function') { 49 | var result = this.message(message, conversation.context); 50 | resolve = isPromise(result) ? result : Promise.resolve(result); 51 | } 52 | else { 53 | resolve = Promise.resolve(this.message); // static string or value 54 | } 55 | 56 | return resolve 57 | .then(function (result) { 58 | var res = conversation.send(result); 59 | var done = isPromise(res) ? res : Promise.resolve(res); 60 | 61 | return done.then(function () { 62 | return { 63 | result: result, 64 | block: me.next 65 | }; 66 | }); 67 | }); 68 | }; 69 | 70 | /** 71 | * Create a Tell block and chain it to the current block 72 | * @param {* | Function} [message] A static message or callback function 73 | * returning a message dynamically. 74 | * When `message` is a function, it will be 75 | * invoked as callback(message, context), 76 | * where `message` is the output from the 77 | * previous block in the chain, and `context` is 78 | * an object where state can be stored during a 79 | * conversation. 80 | * @return {Block} Returns the appended block 81 | */ 82 | Block.prototype.tell = function (message) { 83 | var block = new Tell(message); 84 | 85 | return this.then(block); 86 | }; 87 | 88 | /** 89 | * Send a question, listen for a response. 90 | * Creates two blocks: Tell and Listen. 91 | * This is equivalent of doing `babble.tell(message).listen(callback)` 92 | * @param {* | Function} message 93 | * @param {Function} [callback] Invoked as callback(message, context), 94 | * where `message` is the just received message, 95 | * and `context` is an object where state can be 96 | * stored during a conversation. This is equivalent 97 | * of doing `listen().then(callback)` 98 | * @return {Block} Returns the appended block 99 | */ 100 | Block.prototype.ask = function (message, callback) { 101 | // FIXME: this doesn't work 102 | return this 103 | .tell(message) 104 | .listen(callback); 105 | }; 106 | 107 | module.exports = Tell; 108 | -------------------------------------------------------------------------------- /lib/block/Then.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('es6-promise').Promise; 4 | var Block = require('./Block'); 5 | var isPromise = require('../util').isPromise; 6 | 7 | /** 8 | * Then 9 | * Execute a callback function or a next block in the chain. 10 | * @param {Function} callback Invoked as callback(message, context), 11 | * where `message` is the output from the previous 12 | * block in the chain, and `context` is an object 13 | * where state can be stored during a conversation. 14 | * @constructor 15 | * @extends {Block} 16 | */ 17 | function Then (callback) { 18 | if (!(this instanceof Then)) { 19 | throw new SyntaxError('Constructor must be called with the new operator'); 20 | } 21 | 22 | if (!(typeof callback === 'function')) { 23 | throw new TypeError('Parameter callback must be a Function'); 24 | } 25 | 26 | this.callback = callback; 27 | } 28 | 29 | Then.prototype = Object.create(Block.prototype); 30 | Then.prototype.constructor = Then; 31 | 32 | // type information 33 | Then.prototype.isThen = true; 34 | 35 | /** 36 | * Execute the block 37 | * @param {Conversation} conversation 38 | * @param {*} message 39 | * @return {Promise.<{result: *, block: Block}, Error>} next 40 | */ 41 | Then.prototype.execute = function (conversation, message) { 42 | var me = this; 43 | var result = this.callback(message, conversation.context); 44 | 45 | var resolve = isPromise(result) ? result : Promise.resolve(result); 46 | 47 | return resolve.then(function (result) { 48 | return { 49 | result: result, 50 | block: me.next 51 | } 52 | }); 53 | }; 54 | 55 | /** 56 | * Chain a block to the current block. 57 | * 58 | * When a function is provided, a Then block will be generated which 59 | * executes the function. The function is invoked as callback(message, context), 60 | * where `message` is the output from the previous block in the chain, 61 | * and `context` is an object where state can be stored during a conversation. 62 | * 63 | * @param {Block | function} next A callback function or Block. 64 | * @return {Block} Returns the appended block 65 | */ 66 | Block.prototype.then = function (next) { 67 | // turn a callback function into a Then block 68 | if (typeof next === 'function') { 69 | next = new Then(next); 70 | } 71 | 72 | if (!next || !next.isBlock) { 73 | throw new TypeError('Parameter next must be a Block or function'); 74 | } 75 | 76 | // append after the last block 77 | next.previous = this; 78 | this.next = next; 79 | 80 | // return the appended block 81 | return next; 82 | }; 83 | 84 | module.exports = Then; 85 | -------------------------------------------------------------------------------- /lib/messagebus.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = require('es6-promise').Promise; 4 | 5 | // built-in messaging interfaces 6 | 7 | /** 8 | * pubsub-js messaging interface 9 | * @returns {{connect: function, disconnect: function, send: function}} 10 | */ 11 | exports['pubsub-js'] = function () { 12 | var PubSub = require('pubsub-js'); 13 | 14 | return { 15 | connect: function (params) { 16 | var token = PubSub.subscribe(params.id, function (id, message) { 17 | params.message(message); 18 | }); 19 | 20 | if (typeof params.callback === 'function') { 21 | params.callback(); 22 | } 23 | 24 | return token; 25 | }, 26 | 27 | disconnect: function(token) { 28 | PubSub.unsubscribe(token); 29 | }, 30 | 31 | send: function (to, message) { 32 | PubSub.publish(to, message); 33 | } 34 | } 35 | }; 36 | 37 | /** 38 | * // pubnub messaging interface 39 | * @param {{publish_key: string, subscribe_key: string}} params 40 | * @returns {{connect: function, disconnect: function, send: function}} 41 | */ 42 | exports['pubnub'] = function (params) { 43 | var PUBNUB; 44 | if (typeof window !== 'undefined') { 45 | // browser 46 | if (typeof window['PUBNUB'] === 'undefined') { 47 | throw new Error('Please load pubnub first in the browser'); 48 | } 49 | PUBNUB = window['PUBNUB']; 50 | } 51 | else { 52 | // node.js 53 | PUBNUB = require('pubnub'); 54 | } 55 | 56 | var pubnub = PUBNUB.init(params); 57 | 58 | return { 59 | connect: function (params) { 60 | pubnub.subscribe({ 61 | channel: params.id, 62 | message: params.message, 63 | connect: params.callback 64 | }); 65 | 66 | return params.id; 67 | }, 68 | 69 | disconnect: function (id) { 70 | pubnub.unsubscribe(id); 71 | }, 72 | 73 | send: function (to, message) { 74 | return new Promise(function (resolve, reject) { 75 | pubnub.publish({ 76 | channel: to, 77 | message: message, 78 | callback: resolve 79 | }); 80 | }) 81 | } 82 | } 83 | }; 84 | 85 | // default interface 86 | exports['default'] = exports['pubsub-js']; 87 | -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test whether the provided value is a Promise. 3 | * A value is marked as a Promise when it is an object containing functions 4 | * `then` and `catch`. 5 | * @param {*} value 6 | * @return {boolean} Returns true when `value` is a Promise 7 | */ 8 | exports.isPromise = function (value) { 9 | return value && 10 | typeof value['then'] === 'function' && 11 | typeof value['catch'] === 'function' 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babble", 3 | "version": "0.11.0", 4 | "description": "Dynamic communication flows between message based actors.", 5 | "author": "Jos de Jong (https://github.com/josdejong)", 6 | "keywords": [ 7 | "pubsub", 8 | "conversation", 9 | "talk" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/josdejong/babble.git" 14 | }, 15 | "dependencies": { 16 | "es6-promise": "^3.1.2", 17 | "node-uuid": "~1.4.7", 18 | "pubnub": "~3.9.2", 19 | "pubsub-js": "~1.5.3" 20 | }, 21 | "devDependencies": { 22 | "mocha": "latest", 23 | "browserify": "latest", 24 | "uglify-js": "latest" 25 | }, 26 | "scripts": { 27 | "build": "browserify ./index.js -o ./dist/babble.js -s babble -x pubnub; uglifyjs ./dist/babble.js --output ./dist/babble.min.js --source-map ./dist/babble.map --source-map-url ./babble.min.map --compress --mangle --comments", 28 | "test": "mocha test --recursive --reporter spec" 29 | }, 30 | "main": "./index.js" 31 | } 32 | -------------------------------------------------------------------------------- /test/Babbler.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Babbler = require('../lib/Babbler'); 3 | var Block = require('../lib/block/Block'); 4 | var Tell = require('../lib/block/Tell'); 5 | var Then = require('../lib/block/Then'); 6 | var Decision = require('../lib/block/Decision'); 7 | 8 | describe('Babbler', function() { 9 | var emma, jack; 10 | 11 | beforeEach(function () { 12 | emma = new Babbler('emma'); 13 | jack = new Babbler('jack'); 14 | 15 | emma.connect(); 16 | jack.connect(); 17 | }); 18 | 19 | afterEach(function () { 20 | // there shouldn't be any open conversations left 21 | assert.equal(Object.keys(emma.conversations).length, 0); 22 | assert.equal(Object.keys(jack.conversations).length, 0); 23 | 24 | emma.disconnect(); 25 | jack.disconnect(); 26 | 27 | emma = null; 28 | jack = null; 29 | }); 30 | 31 | it('should create and destroy a babbler', function() { 32 | new Babbler('susan').connect() 33 | .then(function (susan) { 34 | assert.ok(susan instanceof Babbler); 35 | susan.disconnect(); 36 | }); 37 | }); 38 | 39 | it('should throw an error when creating a babbler with wrong syntax', function() { 40 | assert.throws (function () {new Babbler(); }); 41 | assert.throws (function () {Babbler('whoops'); }); 42 | }); 43 | 44 | describe ('listen', function () { 45 | 46 | it ('should listen to a message', function (done) { 47 | emma.listen('test', function (response) { 48 | assert.equal(response, 'test'); 49 | assert.equal(Object.keys(emma.listeners).length, 1); 50 | done(); 51 | }); 52 | 53 | assert.equal(Object.keys(emma.listeners).length, 1); 54 | 55 | emma._receive({ 56 | id: '1', 57 | from: 'jack', 58 | to: 'emma', 59 | message: 'test' 60 | }); 61 | }); 62 | 63 | it ('should listen to a message once', function (done) { 64 | emma.listenOnce('test', function (response) { 65 | try { 66 | assert.equal(response, 'test'); 67 | assert.equal(Object.keys(emma.listeners).length, 0); 68 | done(); 69 | } 70 | catch(err) { 71 | done(err); 72 | } 73 | }); 74 | 75 | assert.equal(Object.keys(emma.listeners).length, 1); 76 | 77 | emma._receive({ 78 | id: '1', 79 | from: 'jack', 80 | to: 'emma', 81 | message: 'test' 82 | }); 83 | }); 84 | 85 | }); 86 | 87 | describe ('tell', function () { 88 | 89 | it('should tell a message', function(done) { 90 | emma.listen('test', function (response) { 91 | assert.equal(response, 'test'); 92 | done(); 93 | }); 94 | 95 | jack.tell('emma', 'test'); 96 | }); 97 | 98 | it('should tell a function as message', function(done) { 99 | emma.listen('test', function (response) { 100 | assert.equal(response, 'test'); 101 | done(); 102 | }); 103 | 104 | jack.tell('emma', function () { 105 | return 'test'; 106 | }); 107 | }); 108 | 109 | it('should tell two messages subsequently', function(done) { 110 | emma.listen('foo') 111 | .listen(function (response) { 112 | assert.deepEqual(response, 'bar'); 113 | done(); 114 | }); 115 | 116 | jack.tell('emma', 'foo') 117 | .tell('bar'); 118 | }); 119 | 120 | it('should chain some blocks to a Tell block', function(done) { 121 | emma.listen('foo') 122 | .listen(function (response) { 123 | assert.equal(response, 'bar'); 124 | }) 125 | .tell('bye'); 126 | 127 | jack.tell('emma', 'foo') 128 | .then(function (response) { 129 | assert.equal(response, 'foo'); 130 | return 'bar'; 131 | }) 132 | .tell(function (response) { 133 | return response; 134 | }) 135 | .listen(function (response) { 136 | assert.equal(response, 'bye'); 137 | done(); 138 | }); 139 | }); 140 | 141 | }); 142 | 143 | describe ('ask', function () { 144 | 145 | it('should send a message and listen for a reply', function(done) { 146 | emma.listen('what is your name?') 147 | .tell(function (message) { 148 | return 'emma'; 149 | }); 150 | 151 | jack.ask('emma', 'what is your name?', function (result) { 152 | assert.equal(result, 'emma'); 153 | done(); 154 | }); 155 | }); 156 | 157 | it('should send a message, listen, and send a reply', function(done) { 158 | emma.listen('count', function () { 159 | return 0; 160 | }) 161 | .tell(function (count) { 162 | return count + 1; 163 | }) 164 | .listen(function (count) { 165 | assert.equal(count, 3); 166 | done(); 167 | }); 168 | 169 | jack.ask('emma', 'count') 170 | .tell(function (count) { 171 | return count + 2; 172 | }); 173 | }); 174 | 175 | it('should send an object as reply', function(done) { 176 | emma.listen('test') 177 | .tell(function (response) { 178 | return {a: 2, b: 3} 179 | }); 180 | 181 | jack.ask('emma', 'test', function (response) { 182 | assert.deepEqual(response, {a: 2, b: 3}); 183 | done(); 184 | }); 185 | }); 186 | 187 | it('should send an object as reply (2)', function(done) { 188 | emma.listen('test') 189 | .tell({a: 2, b: 3}); 190 | 191 | jack.ask('emma', 'test', function (response) { 192 | assert.deepEqual(response, {a: 2, b: 3}); 193 | done(); 194 | }); 195 | }); 196 | 197 | it('should invoke the callback provided with listener', function(done) { 198 | emma.listen('what is you age?', function (response) { 199 | assert.equal(response, 'what is you age?'); 200 | done(); 201 | }); 202 | 203 | jack.tell('emma', 'what is you age?'); 204 | }); 205 | 206 | it('should invoke the callback provided with ask', function(done) { 207 | emma.listen('age') 208 | .tell(function () { 209 | return 32; 210 | }); 211 | 212 | jack.ask('emma', 'age', function (response) { 213 | assert.equal(response, 32); 214 | done(); 215 | }); 216 | }); 217 | 218 | it('should invoke an ask in a chain', function(done) { 219 | emma.listen('hi') 220 | .ask('what is your age?', function (response) { 221 | assert.equal(response, 32); 222 | setTimeout(done, 0); 223 | }); 224 | 225 | jack.tell('emma', 'hi') 226 | .listen(function (response) { 227 | assert.equal(response, 'what is your age?'); 228 | }) 229 | .tell(32); 230 | }); 231 | 232 | it('should make a decision during a conversation', function(done) { 233 | emma.listen('are you available?') 234 | .tell(function (response) { 235 | return 'yes'; 236 | }); 237 | 238 | jack.ask('emma', 'are you available?') 239 | .decide(function (response) { 240 | assert.equal(response, 'yes'); 241 | return response; 242 | }, { 243 | yes: new Then(function (response) { 244 | assert.equal(response, 'yes'); 245 | done(); 246 | }) 247 | }); 248 | }); 249 | 250 | it('should make a decision with an iif block', function(done) { 251 | emma.listen('are you available?') 252 | .tell(function (response) { 253 | return 'yes'; 254 | }); 255 | 256 | jack.ask('emma', 'are you available?') 257 | .iif('yes', new Then(function (message) { 258 | assert.equal(message, 'yes'); 259 | done(); 260 | }), new Then(function (message) { 261 | assert.ok(false, 'should not execute falseBlock') 262 | })); 263 | }); 264 | 265 | it('should make a decision with an inline iif block', function(done) { 266 | emma.listen('are you available?') 267 | .tell(function (response) { 268 | return 'yes'; 269 | }); 270 | 271 | jack.ask('emma', 'are you available?') 272 | .iif('yes') 273 | .then(function (message) { 274 | assert.equal(message, 'yes'); 275 | done(); 276 | }); 277 | }); 278 | 279 | it('should run then blocks', function(done) { 280 | var logs = []; 281 | 282 | emma.listen('are you available?') 283 | .then(function (response) { 284 | logs.push('log 1'); 285 | }) 286 | .tell(function (response) { 287 | logs.push('log 2'); 288 | assert.strictEqual(response, undefined); 289 | return 'yes'; 290 | }); 291 | 292 | jack.ask('emma', 'are you available?', function (response) { 293 | assert.equal(response, 'yes'); 294 | logs.push('log 3'); 295 | }) 296 | .then(function () { 297 | logs.push('log 4'); 298 | 299 | assert.deepEqual(logs, ['log 1', 'log 2', 'log 3', 'log 4']); 300 | 301 | done(); 302 | }); 303 | }); 304 | 305 | it('should keep state in the context during the conversation', function(done) { 306 | emma.listen('question', function () { 307 | return 'a'; 308 | }) 309 | .then(function (response, context) { 310 | context.a = 1; 311 | return response; 312 | }) 313 | .decide(function (response, context) { 314 | context.b = 2; 315 | assert.equal(context.a, 1); 316 | 317 | return 'first'; 318 | }, { 319 | first: new Tell(function (response, context) { 320 | assert.equal(response, 'a'); 321 | assert.equal(context.a, 1); 322 | assert.equal(context.b, 2); 323 | context.c = 3; 324 | 325 | return 'b'; 326 | }) 327 | .listen(function (response, context) { 328 | assert.equal(response, 'c'); 329 | assert.equal(context.a, 1); 330 | assert.equal(context.b, 2); 331 | assert.equal(context.c, 3); 332 | done(); 333 | }) 334 | }); 335 | 336 | jack.ask('emma', 'question') 337 | .then(function (response, context) { 338 | context.a = 1; 339 | return response; 340 | }) 341 | .tell(function (response, context) { 342 | assert.equal(response, 'b'); 343 | assert.equal(context.a, 1); 344 | 345 | return 'c'; 346 | }); 347 | }); 348 | 349 | }); 350 | 351 | }); 352 | -------------------------------------------------------------------------------- /test/Conversation.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Promise = require('es6-promise').Promise; 3 | var Conversation = require('../lib/Conversation'); 4 | 5 | describe('Conversation', function() { 6 | 7 | it('should create a conversation', function () { 8 | var send = function () {}; 9 | var conversation = new Conversation({ 10 | id: 1, 11 | self: 'peer1', 12 | other: 'peer2', 13 | send: send 14 | }); 15 | 16 | assert(conversation instanceof Conversation); 17 | assert.strictEqual(conversation.id, 1); 18 | assert.strictEqual(conversation.self, 'peer1'); 19 | assert.strictEqual(conversation.other, 'peer2'); 20 | assert.strictEqual(conversation._send, send); 21 | }); 22 | 23 | it('should throw an error when created without new operator', function () { 24 | assert.throws(function () {Conversation({})}, /new operator/); 25 | }); 26 | 27 | it('should send a message', function (done) { 28 | var send = function (to, message) { 29 | assert.equal(to, 'peer2'); 30 | assert.deepEqual(message, { 31 | 'id': 1, 32 | 'from': 'peer1', 33 | 'to': 'peer2', 34 | 'message': 'hi' 35 | }); 36 | done(); 37 | }; 38 | 39 | var conversation = new Conversation({ 40 | id: 1, 41 | self: 'peer1', 42 | other: 'peer2', 43 | send: send 44 | }); 45 | conversation.send('hi'); 46 | }); 47 | 48 | it('should wait for a message delivery', function () { 49 | var conversation = new Conversation({ 50 | id: 1, 51 | self: 'peer1', 52 | other: 'peer2' 53 | }); 54 | 55 | var promise = conversation.receive().then(function (message) { 56 | assert.equal(message, 'hi'); 57 | }); 58 | 59 | assert.equal(conversation._receivers.length, 1); 60 | 61 | conversation.deliver({message: 'hi'}); 62 | 63 | assert.equal(conversation._receivers.length, 0); 64 | 65 | return promise; 66 | }); 67 | 68 | it('should wait for multiple message deliveries', function () { 69 | var log = []; 70 | var conversation = new Conversation({ 71 | id: 1, 72 | self: 'peer1', 73 | other: 'peer2' 74 | }); 75 | 76 | var promise1 = conversation.receive().then(function (message) { 77 | assert.equal(message, 'foo'); 78 | log.push(message); 79 | }); 80 | 81 | var promise2 = conversation.receive().then(function (message) { 82 | assert.equal(message, 'bar'); 83 | log.push(message); 84 | }); 85 | 86 | assert.equal(conversation._receivers.length, 2); 87 | 88 | conversation.deliver({message: 'foo'}); 89 | conversation.deliver({message: 'bar'}); 90 | 91 | assert.equal(conversation._receivers.length, 0); 92 | 93 | return Promise.all([promise1, promise2]) 94 | .then(function () { 95 | assert.equal(conversation._receivers.length, 0); 96 | assert.deepEqual(log, ['foo', 'bar']) 97 | }); 98 | }); 99 | 100 | it('should receive a message from the queue', function () { 101 | var conversation = new Conversation({ 102 | id: 1, 103 | self: 'peer1', 104 | other: 'peer2' 105 | }); 106 | 107 | conversation.deliver({message: 'hi'}); 108 | 109 | assert.deepEqual(conversation._inbox, ['hi']); 110 | 111 | return conversation.receive().then(function (message) { 112 | assert.equal(message, 'hi'); 113 | 114 | assert.deepEqual(conversation._inbox, []); 115 | }); 116 | }); 117 | 118 | it('should receive multiple messages from the queue', function () { 119 | var conversation = new Conversation({ 120 | id: 1, 121 | self: 'peer1', 122 | other: 'peer2' 123 | }); 124 | 125 | conversation.deliver({message: 'hi'}); 126 | conversation.deliver({message: 'there'}); 127 | 128 | assert.deepEqual(conversation._inbox, ['hi', 'there']); 129 | 130 | return conversation.receive() 131 | .then(function (message) { 132 | assert.equal(message, 'hi'); 133 | 134 | assert.deepEqual(conversation._inbox, ['there']); 135 | 136 | return conversation.receive(); 137 | }) 138 | .then(function (message) { 139 | assert.equal(message, 'there'); 140 | 141 | assert.deepEqual(conversation._inbox, []); 142 | }); 143 | }); 144 | 145 | }); -------------------------------------------------------------------------------- /test/babble.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var babble = require('../index'); 3 | var Babbler = require('../lib/Babbler'); 4 | 5 | var Tell = require('../lib/block/Tell'); 6 | var Listen = require('../lib/block/Listen'); 7 | var Decision = require('../lib/block/Decision'); 8 | var IIf = require('../lib/block/IIf'); 9 | var Then = require('../lib/block/Then'); 10 | 11 | describe('babble', function() { 12 | 13 | it('should create a babbler', function() { 14 | var emma = babble.babbler('emma0'); 15 | assert.ok(emma instanceof Babbler); 16 | }); 17 | 18 | it('should throw an error when creating a Babbler without id', function() { 19 | assert.throws(function () {babble.babbler(); }); 20 | }); 21 | 22 | it('should create a flow starting with a tell block', function() { 23 | var block = babble.tell(function () {}); 24 | assert.ok(block instanceof Tell); 25 | }); 26 | 27 | it('should create a flow starting with ask', function() { 28 | var block = babble.ask('what is your name'); 29 | assert.ok(block instanceof Listen); 30 | assert.ok(block.previous instanceof Tell); 31 | }); 32 | 33 | it('should create a flow starting with listen', function() { 34 | var block = babble.listen(); 35 | assert.ok(block instanceof Listen); 36 | }); 37 | 38 | it('should create a flow starting with a decision block', function() { 39 | var block = babble.decide(function () {}, {}); 40 | assert.ok(block instanceof Decision); 41 | }); 42 | 43 | it('should create a flow starting with an iif block', function() { 44 | var block = babble.iif(function () {}); 45 | assert.ok(block instanceof IIf); 46 | }); 47 | 48 | it('should create a flow starting with a Then block', function() { 49 | var block = babble.then(function () {}); 50 | assert.ok(block instanceof Then); 51 | }); 52 | 53 | describe('babblify', function() { 54 | 55 | // create a simple actor system 56 | function Actor(id) { 57 | this.id = id; 58 | Actor.actors[id] = this; 59 | } 60 | Actor.actors = {}; // map with all actors by their id 61 | Actor.prototype.send = function (to, message) { 62 | var actor = Actor.actors[to]; 63 | if (!actor) { 64 | throw new Error('Not found'); 65 | } 66 | actor.receive(this.id, message); 67 | }; 68 | Actor.prototype.receive = function (from, message) { 69 | // ... to be overwritten by the actor 70 | }; 71 | 72 | beforeEach(function () { 73 | Actor.actors = {}; 74 | }); 75 | 76 | it('should babblify an object', function() { 77 | var actor1 = babble.babblify(new Actor('actor1')); 78 | 79 | assert.equal(typeof actor1.ask, 'function'); 80 | assert.equal(typeof actor1.tell, 'function'); 81 | assert.equal(typeof actor1.listen, 'function'); 82 | }); 83 | 84 | it('should have a conversation with babblified objects', function(done) { 85 | var actor1 = babble.babblify(new Actor('actor1')); 86 | var actor2 = babble.babblify(new Actor('actor2')); 87 | 88 | actor1.listen('test') 89 | .tell(function () { 90 | return 'hi'; 91 | }); 92 | 93 | actor2.ask('actor1', 'test') 94 | .then(function (response, context) { 95 | assert.equal(response, 'hi'); 96 | 97 | done(); 98 | }); 99 | }); 100 | 101 | it('should create babblified objects with custom functions', function(done) { 102 | // create a simple actor system 103 | function Actor2(id) { 104 | this.id = id; 105 | Actor2.actors[id] = this; 106 | } 107 | Actor2.actors = {}; // map with all actors by their id 108 | Actor2.prototype.sendIt = function (to, message) { 109 | var actor = Actor2.actors[to]; 110 | if (!actor) { 111 | throw new Error('Not found'); 112 | } 113 | actor.receiveIt(this.id, message); 114 | }; 115 | Actor2.prototype.receiveIt = function (from, message) { 116 | // ... to be overwritten by the actor 117 | }; 118 | 119 | var actor1 = babble.babblify(new Actor2('actor1'), {send: 'sendIt', receive: 'receiveIt'}); 120 | var actor2 = babble.babblify(new Actor2('actor2'), {send: 'sendIt', receive: 'receiveIt'}); 121 | 122 | actor1.listen('test') 123 | .tell(function () { 124 | return 'hi'; 125 | }); 126 | 127 | actor2.ask('actor1', 'test') 128 | .then(function (response, context) { 129 | assert.equal(response, 'hi'); 130 | 131 | done(); 132 | }); 133 | }); 134 | 135 | it('should unbabblify a babblified object', function() { 136 | var orig = new Actor('actor1'); 137 | 138 | // copy the original properties 139 | var original = {}; 140 | Object.keys(orig).forEach(function (prop) { 141 | original[prop] = orig[prop]; 142 | }); 143 | 144 | var babblified = babble.babblify(orig); 145 | var unbabblified = babble.unbabblify(babblified); 146 | 147 | // compare the properties 148 | assert.deepEqual(Object.keys(original), Object.keys(unbabblified)); 149 | Object.keys(unbabblified).forEach(function (prop) { 150 | assert.strictEqual(unbabblified[prop], original[prop]); 151 | assert.strictEqual(orig[prop], original[prop]); 152 | }); 153 | }); 154 | 155 | }); 156 | 157 | }); 158 | -------------------------------------------------------------------------------- /test/block/Block.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Block = require('../../lib/block/Block'); 3 | 4 | describe('Block', function() { 5 | 6 | it('should create a block', function () { 7 | var block = new Block(); 8 | assert.ok(block instanceof Block); 9 | }); 10 | 11 | it('should refuse to run an (abstract) block', function () { 12 | var block = new Block(); 13 | assert.throws(function () {block.execute()}); 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /test/block/Decision.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Promise = require('es6-promise').Promise; 3 | var Conversation = require('../../lib/Conversation'); 4 | var Block = require('../../lib/block/Block'); 5 | var Decision = require('../../lib/block/Decision'); 6 | 7 | describe('Decision', function() { 8 | 9 | it('should create a decision', function () { 10 | var decision1 = new Decision(function () {}, {}); 11 | assert.ok(decision1 instanceof Decision); 12 | 13 | var decision2 = new Decision({ 14 | yes: new Block(), 15 | no: new Block() 16 | }); 17 | assert.ok(decision2 instanceof Decision); 18 | assert.deepEqual(decision2.choices, { 19 | yes: new Block(), 20 | no: new Block() 21 | }); 22 | }); 23 | 24 | it('should add choices to a decision', function () { 25 | var decision = new Decision(); 26 | assert.deepEqual(decision.choices, {}); 27 | decision.addChoice('yes', new Block()); 28 | decision.addChoice('no', new Block()); 29 | 30 | assert.deepEqual(decision.choices, { 31 | yes: new Block(), 32 | no: new Block() 33 | }); 34 | }); 35 | 36 | it('should throw an error when adding invalid choices to a decision', function () { 37 | var decision = new Decision(); 38 | assert.deepEqual(decision.choices, {}); 39 | 40 | assert.throws(function () { 41 | decision.addChoice(); 42 | }); 43 | assert.throws(function () { 44 | decision.addChoice(123, new Block()); 45 | }); 46 | assert.throws(function () { 47 | decision.addChoice('id', function() {}); 48 | }); 49 | }); 50 | 51 | it('should throw an error when wrongly creating a decision', function () { 52 | assert.throws(function () { Decision({ 53 | yes: new Block(), 54 | no: 'no block' 55 | })}, SyntaxError); 56 | assert.throws(function () { Decision('no function', {}) }, SyntaxError); 57 | }); 58 | 59 | it('should throw an error when no matching choice', function () { 60 | var decision = new Decision(function () { 61 | return 'non existing id' 62 | }, { 63 | yes: new Block(), 64 | no: new Block() 65 | }); 66 | 67 | var conversation = new Conversation(); 68 | return decision.execute(conversation) 69 | .then(function (next) { 70 | assert.ok(false, 'should not succeed') 71 | }) 72 | .catch(function (err) { 73 | assert.equal(err.toString(), 'Error: Block with id "non existing id" not found'); 74 | }) 75 | }); 76 | 77 | it('should fallback to default when no matching choice', function () { 78 | var def = new Block(); 79 | var decision = new Decision(function () { 80 | return 'non existing id' 81 | }, { 82 | yes: new Block(), 83 | no: new Block(), 84 | 'default': def 85 | }); 86 | 87 | var conversation = new Conversation(); 88 | return decision.execute(conversation) 89 | .then(function (next) { 90 | assert.strictEqual(next.result, undefined); 91 | assert.strictEqual(next.block, def); 92 | }); 93 | }); 94 | 95 | it('should execute a decision without decision function', function () { 96 | var yes = new Block(); 97 | var no = new Block(); 98 | var decision = new Decision({ 99 | yes: yes, 100 | no: no 101 | }); 102 | 103 | var conversation = new Conversation(); 104 | return decision.execute(conversation, 'yes').then(function (next) { 105 | assert.deepEqual(next, { 106 | result: 'yes', 107 | block: yes 108 | }) 109 | }) 110 | }); 111 | 112 | it('should execute a decision function returning a Promise', function () { 113 | var yes = new Block(); 114 | var no = new Block(); 115 | var fn = function () { 116 | return new Promise(function (resolve, reject) { 117 | setTimeout(function () { 118 | resolve('yes'); 119 | }, 10); 120 | }); 121 | }; 122 | var decision = new Decision(fn, { 123 | yes: yes, 124 | no: no 125 | }); 126 | 127 | var conversation = new Conversation(); 128 | return decision.execute(conversation, 'yes').then(function (next) { 129 | assert.deepEqual(next, { 130 | result: 'yes', 131 | block: yes 132 | }) 133 | }) 134 | }); 135 | 136 | it('should execute a decision with context', function () { 137 | var yes = new Block(); 138 | var decision = new Decision(function (response, context) { 139 | assert.strictEqual(response, 'message'); 140 | assert.deepEqual(context, {a: 2}); 141 | return 'yes'; 142 | }, { 143 | yes: yes 144 | }); 145 | 146 | var conversation = new Conversation({ 147 | context: {a: 2} 148 | }); 149 | return decision.execute(conversation, 'message').then(function (next) { 150 | assert.deepEqual(next, { 151 | result: 'message', 152 | block: yes 153 | }) 154 | }); 155 | }); 156 | 157 | it('should execute a decision with context and argument', function () { 158 | var yes = new Block(); 159 | var decision = new Decision(function (response, context) { 160 | assert.strictEqual(response, 'hello world'); 161 | assert.deepEqual(context, {a: 2}); 162 | return 'yes'; 163 | }, { 164 | yes: yes 165 | }); 166 | 167 | var conversation = new Conversation({ 168 | context: {a: 2} 169 | }); 170 | return decision.execute(conversation, 'hello world').then(function (next) { 171 | assert.deepEqual(next, { 172 | result: 'hello world', 173 | block: yes 174 | }) 175 | }); 176 | }); 177 | 178 | }); 179 | -------------------------------------------------------------------------------- /test/block/IIf.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Promise = require('es6-promise').Promise; 3 | var Conversation = require('../../lib/Conversation'); 4 | var Block = require('../../lib/block/Block'); 5 | var IIf = require('../../lib/block/IIf'); 6 | var Then = require('../../lib/block/Then'); 7 | 8 | describe('IIf', function() { 9 | 10 | it('should create an iif without trueBlock and falseBlock', function () { 11 | var conversation = new Conversation(); 12 | var condition = function (message, context) { 13 | assert.equal(message, 'message'); 14 | assert.strictEqual(context, conversation.context); 15 | return true; 16 | }; 17 | var iif = new IIf(condition); 18 | iif.then(function () {}); 19 | 20 | assert.ok(iif instanceof IIf); 21 | 22 | 23 | return iif.execute(conversation, 'message').then(function (next) { 24 | assert.equal(next.result, 'message'); 25 | assert.ok(next.block instanceof Then); 26 | }); 27 | }); 28 | 29 | it('should create an iif with trueBlock, falseBlock, and next block', function () { 30 | var conversation = new Conversation(); 31 | var condition = function (message, context) { 32 | assert.strictEqual(context, conversation.context); 33 | return message === 'yes'; 34 | }; 35 | var trueBlock = new Then(function () {}); 36 | var falseBlock = new Then(function () {}); 37 | var iif = new IIf(condition, trueBlock, falseBlock); 38 | iif.then(function () {}); 39 | 40 | assert.ok(iif instanceof IIf); 41 | 42 | return iif.execute(conversation, 'yes') 43 | .then(function (next) { 44 | assert.equal(next.result, 'yes'); 45 | assert.strictEqual(next.block, trueBlock); 46 | 47 | return iif.execute(conversation, 'no'); 48 | }) 49 | .then(function (next) { 50 | assert.equal(next.result, 'no'); 51 | assert.strictEqual(next.block, falseBlock); 52 | }); 53 | 54 | }); 55 | 56 | it('should create an iif with a RegExp condition', function () { 57 | var conversation = new Conversation(); 58 | var condition = /yes/; 59 | var trueBlock = new Then(function () {}); 60 | var falseBlock = new Then(function () {}); 61 | var iif = new IIf(condition, trueBlock, falseBlock); 62 | 63 | assert.ok(iif instanceof IIf); 64 | 65 | return iif.execute(conversation, 'yes') 66 | .then(function (next) { 67 | assert.equal(next.result, 'yes'); 68 | assert.strictEqual(next.block, trueBlock); 69 | 70 | return iif.execute(conversation, 'no'); 71 | }) 72 | .then(function (next) { 73 | assert.equal(next.result, 'no'); 74 | assert.strictEqual(next.block, falseBlock); 75 | }); 76 | }); 77 | 78 | it('should create an iif with a string condition', function () { 79 | var conversation = new Conversation(); 80 | var condition = 'yes'; 81 | var trueBlock = new Then(function () {}); 82 | var falseBlock = new Then(function () {}); 83 | var iif = new IIf(condition, trueBlock, falseBlock); 84 | 85 | assert.ok(iif instanceof IIf); 86 | 87 | return iif.execute(conversation, 'yes') 88 | .then(function (next) { 89 | assert.equal(next.result, 'yes'); 90 | assert.strictEqual(next.block, trueBlock); 91 | 92 | return iif.execute(conversation, 'no'); 93 | }) 94 | .then(function (next) { 95 | assert.equal(next.result, 'no'); 96 | assert.strictEqual(next.block, falseBlock); 97 | }); 98 | }); 99 | 100 | it('should create an iif with a condition returning a Promise', function () { 101 | var conversation = new Conversation(); 102 | var condition = function (message) { 103 | return new Promise(function (resolve, reject) { 104 | setTimeout(function () { 105 | resolve(message == 'yes'); 106 | }, 10) 107 | }) 108 | }; 109 | var trueBlock = new Then(function () {}); 110 | var falseBlock = new Then(function () {}); 111 | var iif = new IIf(condition, trueBlock, falseBlock); 112 | 113 | assert.ok(iif instanceof IIf); 114 | 115 | return iif.execute(conversation, 'yes') 116 | .then(function (next) { 117 | assert.equal(next.result, 'yes'); 118 | assert.strictEqual(next.block, trueBlock); 119 | 120 | return iif.execute(conversation, 'no'); 121 | }) 122 | .then(function (next) { 123 | assert.equal(next.result, 'no'); 124 | assert.strictEqual(next.block, falseBlock); 125 | }); 126 | }); 127 | 128 | it('should create an iif with a number condition', function () { 129 | var conversation = new Conversation(); 130 | var condition = 42; 131 | var trueBlock = new Then(function () {}); 132 | var falseBlock = new Then(function () {}); 133 | var iif = new IIf(condition, trueBlock, falseBlock); 134 | 135 | assert.ok(iif instanceof IIf); 136 | 137 | return iif.execute(conversation, 42) 138 | .then(function (next) { 139 | assert.equal(next.result, 42); 140 | assert.strictEqual(next.block, trueBlock); 141 | 142 | return iif.execute(conversation, 12); 143 | }) 144 | .then(function (next) { 145 | assert.equal(next.result, 12); 146 | assert.strictEqual(next.block, falseBlock); 147 | }); 148 | }); 149 | 150 | it('should throw an error on invalid input arguments', function () { 151 | assert.throws(function () {new IIf(function () {}, function () {});}, /trueBlock must be a Block/); 152 | assert.throws(function () {new IIf(function () {}, new Block(), function () {})}, /falseBlock must be a Block/); 153 | }); 154 | 155 | it('should throw an error when calling without new operator', function () { 156 | assert.throws(function () {IIf();}); 157 | }); 158 | 159 | }); 160 | -------------------------------------------------------------------------------- /test/block/Listen.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Promise = require('es6-promise').Promise; 3 | var Conversation = require('../../lib/Conversation'); 4 | var Block = require('../../lib/block/Block'); 5 | var Listen = require('../../lib/block/Listen'); 6 | var IIf = require('../../lib/block/IIf'); 7 | var Then = require('../../lib/block/Then'); 8 | 9 | describe('Listen', function() { 10 | 11 | it('should create a listener', function () { 12 | var listener1 = new Listen(); 13 | assert.ok(listener1 instanceof Listen); 14 | 15 | var listener2 = new Listen(function () {}); 16 | assert.ok(listener2 instanceof Listen); 17 | }); 18 | 19 | it('should throw an error when wrongly creating a listener', function () { 20 | assert.throws(function () { Listen(function () {}) }, SyntaxError); 21 | }); 22 | 23 | it('should execute a listener', function () { 24 | var listener = new Listen(); 25 | 26 | var conversation = new Conversation(); 27 | conversation.deliver({message: 'foo'}); 28 | 29 | return listener.execute(conversation).then(function(next) { 30 | assert.deepEqual(next, { 31 | result: 'foo', 32 | block: undefined 33 | }) 34 | }); 35 | }); 36 | 37 | it('should execute a listener with delay in receiving a message', function () { 38 | var listener = new Listen(); 39 | 40 | var conversation = new Conversation(); 41 | setTimeout(function () { 42 | conversation.deliver({message: 'foo'}); 43 | }, 10); 44 | 45 | return listener.execute(conversation).then(function(next) { 46 | assert.deepEqual(next, { 47 | result: 'foo', 48 | block: undefined 49 | }) 50 | }); 51 | }); 52 | 53 | it('should execute a listener with next block', function () { 54 | var listener = new Listen(); 55 | var nextListener = new Listen (); 56 | listener.then(nextListener); 57 | 58 | 59 | var conversation = new Conversation(); 60 | conversation.deliver({message: 'foo'}); 61 | 62 | return listener.execute(conversation).then(function(next) { 63 | assert.strictEqual(next.result, 'foo'); 64 | assert.strictEqual(next.block, nextListener); 65 | }); 66 | }); 67 | 68 | it('should create a listen+iif block from function .listen', function () { 69 | var block = new Block(); 70 | var callback = function (message) { 71 | return message + 'bar'; 72 | }; 73 | var b = block.listen(callback); 74 | 75 | var listen = block.next; 76 | var then = listen.next; 77 | 78 | assert.strictEqual(b, then); 79 | assert(listen instanceof Listen); 80 | assert(then instanceof Then); 81 | assert.strictEqual(then.next, undefined); // TODO: should be null 82 | 83 | var conversation = new Conversation(); 84 | conversation.deliver({message: 'foo'}); 85 | 86 | return block.next.execute(conversation).then(function(next) { 87 | assert(next.block instanceof Then); 88 | assert.strictEqual(next.result, 'foo'); 89 | 90 | return next.block.execute(conversation, next.result); 91 | }).then(function(next) { 92 | assert.equal(next.block, null); 93 | assert.strictEqual(next.result, 'foobar'); 94 | }); 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /test/block/Tell.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Promise = require('es6-promise').Promise; 3 | var Conversation = require('../../lib/Conversation'); 4 | var Tell = require('../../lib/block/Tell'); 5 | 6 | describe('Tell', function() { 7 | 8 | var sent = []; 9 | var send = function (from, message) { 10 | sent.push([from, message]); 11 | return Promise.resolve(); 12 | }; 13 | 14 | beforeEach(function () { 15 | sent = []; 16 | }); 17 | 18 | it('should create a tell', function () { 19 | var tell1 = new Tell(); 20 | assert.ok(tell1 instanceof Tell); 21 | 22 | var tell2 = new Tell(function () {}); 23 | assert.ok(tell2 instanceof Tell); 24 | }); 25 | 26 | it('should throw an error when wrongly creating a tell', function () { 27 | assert.throws(function () { Tell(function () {}) }, SyntaxError); 28 | }); 29 | 30 | it('should execute a tell with static message', function () { 31 | var tell = new Tell('foo'); 32 | 33 | var conversation = new Conversation({ 34 | id: 1, 35 | self: 'peer1', 36 | other: 'peer2', 37 | send: send 38 | }); 39 | return tell.execute(conversation).then(function(next) { 40 | assert.deepEqual(next, { 41 | result: 'foo', 42 | block: undefined 43 | }); 44 | assert.deepEqual(sent, [['peer2', {id:1, from: 'peer1', to: 'peer2', message: 'foo'}]]); 45 | }); 46 | }); 47 | 48 | it('should execute a tell with callback function', function () { 49 | var tell = new Tell(function (response, context) { 50 | assert.strictEqual(response, 'bar'); 51 | assert.strictEqual(context, conversation.context); 52 | return 'foo'; 53 | }); 54 | 55 | var conversation = new Conversation({ 56 | id: 1, 57 | self: 'peer1', 58 | other: 'peer2', 59 | send: send 60 | }); 61 | return tell.execute(conversation, 'bar').then(function(next) { 62 | assert.deepEqual(next, { 63 | result: 'foo', 64 | block: undefined 65 | }); 66 | assert.deepEqual(sent, [['peer2', {id:1, from: 'peer1', to: 'peer2', message: 'foo'}]]); 67 | }); 68 | }); 69 | 70 | it('should execute a tell with callback function returning a Promise', function () { 71 | var tell = new Tell(function (response, context) { 72 | assert.strictEqual(response, undefined); 73 | assert.strictEqual(context, conversation.context); 74 | return new Promise(function (resolve, reject) { 75 | setTimeout(function () { 76 | resolve('foo'); 77 | }, 10) 78 | }); 79 | }); 80 | 81 | var conversation = new Conversation({ 82 | id: 1, 83 | self: 'peer1', 84 | other: 'peer2', 85 | send: send 86 | }); 87 | return tell.execute(conversation).then(function(next) { 88 | assert.deepEqual(next, { 89 | result: 'foo', 90 | block: undefined 91 | }); 92 | assert.deepEqual(sent, [['peer2', {id:1, from: 'peer1', to: 'peer2', message: 'foo'}]]); 93 | }); 94 | }); 95 | 96 | it('should execute a tell with context', function () { 97 | var tell = new Tell(function (response, context) { 98 | assert.strictEqual(response, undefined); 99 | assert.deepEqual(context, {a: 2}); 100 | return 'foo'; 101 | }); 102 | 103 | var conversation = new Conversation({ 104 | id: 1, 105 | self: 'peer1', 106 | other: 'peer2', 107 | send: send, 108 | context: {a: 2} 109 | }); 110 | return tell.execute(conversation, undefined).then(function(next) { 111 | assert.deepEqual(next, { 112 | result: 'foo', 113 | block: undefined 114 | }); 115 | assert.deepEqual(sent, [['peer2', {id:1, from: 'peer1', to: 'peer2', message: 'foo'}]]); 116 | }); 117 | }); 118 | 119 | it('should execute a tell with context and argument', function () { 120 | var tell = new Tell(function (response, context) { 121 | assert.strictEqual(response, 'hello world'); 122 | assert.deepEqual(context, {a: 2}); 123 | return 'foo' 124 | }); 125 | 126 | var conversation = new Conversation({ 127 | id: 1, 128 | self: 'peer1', 129 | other: 'peer2', 130 | send: send, 131 | context: {a: 2} 132 | }); 133 | return tell.execute(conversation, 'hello world').then(function(next) { 134 | assert.deepEqual(next, { 135 | result: 'foo', 136 | block: undefined 137 | }); 138 | assert.deepEqual(sent, [['peer2', {id:1, from: 'peer1', to: 'peer2', message: 'foo'}]]); 139 | }); 140 | }); 141 | 142 | it('should execute a tell with next block', function () { 143 | var tell = new Tell(function () {return 'foo'}); 144 | var nextTell = new Tell (function () {return 'foo'}); 145 | tell.then(nextTell); 146 | 147 | var conversation = new Conversation({ 148 | id: 1, 149 | self: 'peer1', 150 | other: 'peer2', 151 | send: send 152 | }); 153 | return tell.execute(conversation).then(function(next) { 154 | assert.strictEqual(next.result, 'foo'); 155 | assert.strictEqual(next.block, nextTell); 156 | 157 | assert.deepEqual(sent, [['peer2', {id:1, from: 'peer1', to: 'peer2', message: 'foo'}]]); 158 | }); 159 | }); 160 | 161 | it('should execute a tell with an async send method', function () { 162 | var action = new Tell('foo'); 163 | 164 | var conversation = new Conversation({ 165 | id: 1, 166 | self: 'peer1', 167 | other: 'peer2', 168 | send: function (from, message) { 169 | return new Promise(function (resolve, reject) { 170 | setTimeout(function () { 171 | sent.push([from, message]); 172 | resolve(); 173 | }, 10); 174 | }); 175 | } 176 | }); 177 | 178 | return action.execute(conversation, 'in').then(function(next) { 179 | assert.strictEqual(next.result, 'foo'); 180 | assert.strictEqual(next.block, undefined); 181 | 182 | assert.deepEqual(sent, [['peer2', {id:1, from: 'peer1', to: 'peer2', message: 'foo'}]]); 183 | }); 184 | }); 185 | 186 | it('should pass the result from and to callback when executing', function () { 187 | var action = new Tell(function (response, context) { 188 | assert.equal(response, 'in'); 189 | return 'out'; 190 | }); 191 | 192 | var conversation = new Conversation({ 193 | id: 1, 194 | self: 'peer1', 195 | other: 'peer2', 196 | send: send 197 | }); 198 | return action.execute(conversation, 'in').then(function(next) { 199 | assert.strictEqual(next.result, 'out'); 200 | assert.strictEqual(next.block, undefined); 201 | 202 | assert.deepEqual(sent, [['peer2', {id:1, from: 'peer1', to: 'peer2', message: 'out'}]]); 203 | }); 204 | }); 205 | 206 | }); 207 | -------------------------------------------------------------------------------- /test/block/Then.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Conversation = require('../../lib/Conversation'); 3 | var Then = require('../../lib/block/Then'); 4 | 5 | describe('Then', function() { 6 | 7 | it('should create a Then block', function () { 8 | var action1 = new Then(function () {}); 9 | assert.ok(action1 instanceof Then); 10 | }); 11 | 12 | it('should throw an error when wrongly creating a Then block', function () { 13 | assert.throws(function () { Then(function () {}) }, SyntaxError); 14 | assert.throws(function () { new Then()}, TypeError); 15 | assert.throws(function () { new Then('bla')}, TypeError); 16 | }); 17 | 18 | it('should execute a Then block without message', function () { 19 | var action = new Then(function (response, context) { 20 | assert.strictEqual(response, undefined); 21 | assert.strictEqual(context, conversation.context); 22 | }); 23 | 24 | var conversation = new Conversation(); 25 | return action.execute(conversation).then(function(next) { 26 | assert.deepEqual(next, { 27 | result: undefined, 28 | block: undefined 29 | }) 30 | }); 31 | }); 32 | 33 | it('should execute a Then block with context', function () { 34 | var conversation = new Conversation({ 35 | context: {a: 2} 36 | }); 37 | var action = new Then(function (response, context) { 38 | assert.strictEqual(response, undefined); 39 | assert.deepEqual(context, {a: 2}); 40 | }); 41 | 42 | return action.execute(conversation, undefined).then(function(next) { 43 | assert.deepEqual(next, { 44 | result: undefined, 45 | block: undefined 46 | }) 47 | }); 48 | }); 49 | 50 | it('should execute a Then block with context and argument', function () { 51 | var conversation = new Conversation({ 52 | context: {a: 2} 53 | }); 54 | var action = new Then(function (response, context) { 55 | assert.strictEqual(response, 'hello world'); 56 | assert.deepEqual(context, {a: 2}); 57 | }); 58 | 59 | return action.execute(conversation, 'hello world').then(function(next) { 60 | assert.deepEqual(next, { 61 | result: undefined, 62 | block: undefined 63 | }) 64 | }); 65 | }); 66 | 67 | it('should execute a Then block with next block', function () { 68 | var action = new Then(function () {}); 69 | var nextThen = new Then (function () {}); 70 | action.then(nextThen); 71 | 72 | var conversation = new Conversation(); 73 | return action.execute(conversation).then(function(next) { 74 | assert.strictEqual(next.result, undefined); 75 | assert.strictEqual(next.block, nextThen); 76 | }); 77 | }); 78 | 79 | it('should pass the result from and to callback when executing', function () { 80 | var action = new Then(function (response, context) { 81 | assert.equal(response, 'in'); 82 | return 'out'; 83 | }); 84 | 85 | var conversation = new Conversation(); 86 | return action.execute(conversation, 'in').then(function(next) { 87 | assert.strictEqual(next.result, 'out'); 88 | assert.strictEqual(next.block, undefined); 89 | }); 90 | }); 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /test/messagebus.test.js: -------------------------------------------------------------------------------- 1 | // TODO: test message bus interfaces. 2 | -------------------------------------------------------------------------------- /test/util.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Promise = require('es6-promise').Promise; 3 | var util = require('../lib/util'); 4 | 5 | describe('util', function() { 6 | 7 | it('should check whether an object is a Promise using ducktyping', function () { 8 | assert.equal(util.isPromise({}), false); 9 | assert.equal(util.isPromise('promise'), false); 10 | var obj = { 11 | 'then': 'foo', 12 | 'catch': 'bar' 13 | }; 14 | assert.equal(util.isPromise(obj), false); 15 | 16 | assert.equal(util.isPromise(new Promise(function() {})), true); 17 | assert.equal(util.isPromise(Promise.resolve()), true); 18 | assert.equal(util.isPromise(Promise.reject()), true); 19 | 20 | var myPromise = { 21 | 'then': function() {}, 22 | 'catch': function() {} 23 | }; 24 | assert.equal(util.isPromise(myPromise), true); 25 | }) 26 | }); 27 | 28 | --------------------------------------------------------------------------------