├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------