├── LICENSE ├── Makefile ├── README.md ├── benchmark ├── doxbee-sequential-errors.sjs ├── doxbee-sequential.sjs └── madeup-parallel.sjs ├── channel.js ├── docs ├── channel.html ├── docco.css ├── public │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ └── novecento-bold.woff │ └── stylesheets │ │ └── normalize.css ├── state_machine.html ├── stream.html └── task.html ├── package.json ├── src ├── channel.js ├── state_machine.js ├── stream.js └── task.js ├── stream.js └── test ├── channel.sjs └── control.sjs /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Srikumar K. S. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions:

9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test : $(patsubst %.sjs, %.js, $(wildcard test/*.sjs)) 3 | -./node_modules/mocha/bin/mocha --reporter progress 4 | 5 | %.js : %.sjs src/task.js 6 | sjs --module ./src/task.js -c -r $< -o $@ 7 | 8 | .PHONY : test 9 | .SILENT : test 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # task - a sweetjs macro for CSP in Javascript 2 | 3 | The [task] macro, in conjunction with [Channel] objects lets you write [CSP]-ish 4 | code in Javascript that can interop with [Node.js]'s callback mechanism. This 5 | came out of a need for a better way to deal with async activities than offered 6 | by [Promises/A+][] or even generators. 7 | 8 | Apart from tasks and channels, cspjs attempts at a saner and more expressive 9 | error handling mechanism than the traditional try-catch-finally model. See 10 | [blog post][errman] and [follow up][errman2] describing the error management 11 | scheme in detail. 12 | 13 | [CSP]: https://en.wikipedia.org/wiki/Communicating_sequential_processes 14 | 15 | ### Contents 16 | 17 | 1. [Installation](#installation) 18 | 2. [Using the task macro](#using-the-task-macro) 19 | 3. [Guarantees provided by tasks](#guarantees-provided-by-tasks) 20 | 4. [Sample code illustrating various features](#sample-code-illustrating-various-features) 21 | 5. [Error tracing](#error-tracing) 22 | 6. [Performance](#performance) 23 | 1. [doxbee-sequential](#doxbee-sequential) 24 | 2. [doxbee-sequential-errors](#doxbee-sequential-errors) 25 | 3. [madeup-parallel](#madeup-parallel) 26 | 7. [History](#history) 27 | 28 | ## Installation 29 | 30 | 1. You need to have [sweetjs][] installed with `npm install -g sweet.js@0.7.8` 31 | 2. Install cspjs using npm like this - `npm install cspjs` to get it into your `node_modules` directory. 32 | 3. To compile a `.sjs` file that uses the `task` macro, do - 33 | 34 | sjs -m cspjs my-task-source.sjs > my-task-source.js 35 | 36 | 4. To use the `Channel` module, require it like this - 37 | 38 | var Channel = require('cspjs/channel'); 39 | 40 | 5. Or if you want to use channels with nodejs stream support, like this - 41 | 42 | // WARNING: EXPERIMENTAL. Interface may change. 43 | var Channel = require('cspjs/stream'); 44 | 45 | For complete documentation, see the docco generated docs in `docs/*.html`. 46 | 47 | ## Using the task macro 48 | 49 | `cspjs` provides a single macro called `task` that is similar to `function` in 50 | form, but interprets certain statements as asynchronous operations. Different 51 | tasks may communicate with each other via `Channel` objects provided by 52 | `cspjs/channel`. 53 | 54 | Any NodeJS style async operation with a `function (err, result) {..}` callback 55 | as its last argument can be conveniently used within `task`, which itself 56 | compiles into a function of that form. 57 | 58 | Below is a simple hello world - 59 | 60 | ```js 61 | task greet(name) { 62 | console.log("Hello", name); 63 | return 42; 64 | } 65 | ``` 66 | 67 | The above task is equivalent to the following and compiles into a function with 68 | exactly the same signature as the below function - 69 | 70 | ```js 71 | function greet(name, callback) { 72 | console.log("Hello", name); 73 | callback(null, 42); 74 | } 75 | ``` 76 | 77 | .. except that upon calling, `greet` will execute on the next IO turn instead 78 | of i]mediately. If `greet` did a `throw 42;` instead of `return 42;`, then the 79 | callback's first "error" argument will be the one set to `42`. 80 | 81 | ### Guarantees provided by tasks 82 | 83 | 1. A `task`, after compilation by cspjs, becomes an ordinary function which 84 | accepts an extra final argument that is expected to be a callback following 85 | the NodeJS convention of `function (err, result) {...}`. 86 | 87 | 2. A task will communicate normal or error return only via the `callback` 88 | argument. In particular, it is guaranteed to never throw .. in the normal 89 | Javascript sense. 90 | 91 | 3. When a task function is called, it will begin executing only on the next IO 92 | turn. 93 | 94 | 4. A task will always call the passed callback once and once only. 95 | 96 | ### Sample code illustrating various features 97 | 98 | ```js 99 | task sampleTask(x, y, z) { 100 | // "sampleTask" will compile into a function with the signature - 101 | // function sampleTask(x, y, z, callback) { ... } 102 | 103 | var dist = Math.sqrt(x * x + y * y + z * z); 104 | // Regular state variable declarations. Note that uninitialized var statements 105 | // are illegal. 106 | 107 | console.log("hello from cspjs!"); 108 | // Normal JS statements ending with a semicolon are treated as synchronous. 109 | // Note that the semicolon is not optional. 110 | 111 | handle <- fs.open("some_file.txt", {encoding: 'utf8'}); 112 | // `handle` is automatically declared to be a state variable and will be bound 113 | // to the result of the asynchronous file open call. All following statements will 114 | // execute only after this async open succeeds. You can use all of NodeJS's 115 | // async APIs with cspjs, without any wrapper code. 116 | // 117 | // If fs.open failed for some reason, the error will "bubble up" and the 118 | // following statements won't be executed at all. Read on to find out 119 | // more about error handling. 120 | 121 | err, json <<- readJSON(handle); 122 | // You can use <<- instead of <- to explicitly get at the error value 123 | // instead of "bubbling up" errors. 124 | 125 | if (!err && json) { 126 | // "if" statements work as usual. Bodies can themselves 127 | // contain async statements. 128 | } else { 129 | // ... and so does `else`. Note that as of this writing, 130 | // the if-then-else-if cascade isn't supported. 131 | } 132 | 133 | switch (json.type) { 134 | // Switch also just works, except that there is no fall through 135 | // and the braces after the case parts are mandatory .. and you 136 | // don't need break statements (which don't exist in cspjs). 137 | case "number": { 138 | // Async statements permitted here too. 139 | } 140 | case "string", "object": { 141 | // You can match against multiple values. 142 | } 143 | } 144 | 145 | // (If none of the switch cases match, that's treated as an error.) 146 | 147 | while (someCondition(x,y)) { 148 | // While loops are also supported, with async statements 149 | // permitted in the block. 150 | // 151 | // TODO: No "break;" statement as of this version. 152 | } 153 | 154 | var arr = ["one", "two", "three"]; 155 | for (var i = 0; i < arr.length; ++i) { 156 | // For loops are also supported and they expand 157 | // into the `while` form. 158 | 159 | content <- fs.readFile(arr[i] + '.txt', {encoding: 'utf8'}); 160 | // .. so yes you can write loops with async code too. 161 | } 162 | 163 | chan ch, in, out; 164 | // Declares and initializes channel objects. 165 | // This is equivalent to - 166 | // var ch = new Channel(), in = new Channel(), out = new Channel(); 167 | // where 168 | // var Channel = require("cspjs/channel"); 169 | 170 | chval <- ch.take(); 171 | // This is an explicit way to wait for and take the next value coming 172 | // on the channel. 173 | 174 | chval <- chan ch; 175 | // This is syntactic sugar for the previous explicit take(). 176 | 177 | await out.put(42); 178 | // Puts the given value on to the channel and waits until it is 179 | // processed by some reader. You can omit `await`, in which case 180 | // this task won't wait for the put value to be processed. 181 | // 182 | // This form of "await" is syntactic sugar for async steps which 183 | // you need to perform for their side effects - i.e. you don't 184 | // need any result from the async step. The above await is 185 | // equivalent to - 186 | // <- out.put(42); 187 | // .. where there is no variable to the left of the "<-". 188 | 189 | ch := readJSON(handle); 190 | in := someAsyncOp(x, y); 191 | // This is a "data flow variable bind", which sends the result of the 192 | // readJSON operation to the channel. Once the operation completes, 193 | // the channel will perpetually yield the result value no matter how 194 | // many times you `.take()` values from it. 195 | // 196 | // The above binding statement is non-blocking and will result in the 197 | // async tasks being "spawned". 198 | 199 | await ch in; 200 | // Prior to this "await", `ch` and `in` are channels. After this 201 | // await, they become bound to the actual value received on those 202 | // channels. This works no matter which tasks these "channel variables" 203 | // occur in and in which tasks the fulfillment of the channels 204 | // occurs. In effect, this facility mimics promises. (TODO: also 205 | // interop with promise APIs using this mechanism). 206 | // 207 | // In particular, you can spawn a task passing in these channels 208 | // as arguments. If the task binds the channels using `:=`, then 209 | // such an await in this task will receive the fulfilled values. 210 | // 211 | // If some error occurs, then it is bubbled up from this await point 212 | // and not from the original bind point. This is because if you don't 213 | // need the value on the channel, there is no reason for you to 214 | // bother with errors in that process as well (as far as I can think 215 | // of it). 216 | 217 | throw new Error("harumph!"); 218 | // throwing an error that isnt caught within the task will result in 219 | // the error propagating asynchronously to the task initiator via the 220 | // provided callback function. The throw ends up being a no-op if the 221 | // thrown value is null or undefined, since the convention with the 222 | // callback signature is that err === null means no error. 223 | 224 | catch (e) { 225 | // You can handle all errors thrown by statements following this 226 | // catch block here. If you do nothing, the error gets automatically 227 | // rethrown. If you handle it successfully, you either `return` a 228 | // value from here, or `retry;`, which results in the statements 229 | // immediately following this catch block. 230 | 231 | // As always, all blocks, including catch blocks, support async statements. 232 | // A catch block is scoped to the block that contains it. 233 | } 234 | 235 | finally { 236 | // Finally blocks perform cleanup operations on error or normal returns. 237 | // A finally block (as is its statement forms) is scoped to the 238 | // block that contains it. 239 | // 240 | // WARNING: Though you can return or throw here, you really shouldn't. 241 | // If your cleanup code raises errors, then you cannot reason about 242 | // error behaviour. 243 | handle.close(); 244 | } 245 | 246 | finally handle.close(); // This statement form of finally is also supported. 247 | 248 | return x * x, y * y, z * z; 249 | // Return statements can return multiple values, unlike throw. 250 | // If no return statement is included in a task, it is equivalent to 251 | // placing a `return true;` at the end. 252 | } 253 | ``` 254 | 255 | ### Error tracing 256 | 257 | If an error is raised deep within an async sequence of operations and the error 258 | is allowed to bubble up to one of the originating tasks, then the error object 259 | will contain a `.cspjsStack` property which will contain a trace of all the 260 | async steps that led to the error ... much like a stack trace. 261 | 262 | Note that this tracing is always turned ON in the system and isn't optional, 263 | since there is no penalty for normal operation when such an error doesn't 264 | occur. 265 | 266 | 267 | ## Performance 268 | 269 | The macro and libraries are not feature complete and, especially I'd like to 270 | add more tracing. However, it mostly works and seems to marginally beat 271 | bluebird in performance while having the same degree of brevity as the 272 | generator based code. The caveat is that the code is evolving and performance 273 | may fluctuate a bit as some features are added. (I'll try my best to not 274 | compromise.) 275 | 276 | Here are some sample results (as of 7 Feb 2014, on my MacBook Air 1.7GHz Core 277 | i5, 4GB RAM, node v0.11.10) - 278 | 279 | ### doxbee-sequential 280 | 281 | Using [doxbee-sequential.sjs](https://github.com/srikumarks/cspjs/blob/master/benchmark/doxbee-sequential.sjs). 282 | 283 | ``` 284 | results for 10000 parallel executions, 1 ms per I/O op 285 | 286 | file time(ms) memory(MB) 287 | callbacks-baseline.js 385 38.61 288 | sweetjs-task.js 672 46.71 289 | promises-bluebird-generator.js 734 38.81 290 | promises-bluebird.js 744 51.07 291 | callbacks-caolan-async-waterfall.js 1211 75.30 292 | promises-obvious-kew.js 1547 115.41 293 | promises-tildeio-rsvp.js 2280 111.19 294 | promises-medikoo-deferred.js 4084 311.98 295 | promises-dfilatov-vow.js 4655 243.75 296 | promises-cujojs-when.js 7899 263.96 297 | promises-calvinmetcalf-liar.js 9655 237.90 298 | promises-kriskowal-q.js 47652 700.61 299 | ``` 300 | 301 | ### doxbee-sequential-errors 302 | 303 | Using [doxbee-sequential-errors.sjs](https://github.com/srikumarks/cspjs/blob/master/benchmark/doxbee-sequential-errors.sjs). 304 | 305 | ``` 306 | results for 10000 parallel executions, 1 ms per I/O op 307 | Likelihood of rejection: 0.1 308 | 309 | file time(ms) memory(MB) 310 | callbacks-baseline.js 490 39.61 311 | sweetjs-task.js 690 57.55 312 | promises-bluebird-generator.js 861 41.52 313 | promises-bluebird.js 985 66.33 314 | callbacks-caolan-async-waterfall.js 1278 76.50 315 | promises-obvious-kew.js 1690 138.42 316 | promises-tildeio-rsvp.js 2579 179.89 317 | promises-dfilatov-vow.js 5249 345.24 318 | promises-cujojs-when.js 8938 421.38 319 | promises-calvinmetcalf-liar.js 9228 299.89 320 | promises-kriskowal-q.js 48887 705.21 321 | promises-medikoo-deferred.js OOM OOM 322 | ``` 323 | 324 | ### madeup-parallel 325 | 326 | Using [madeup-parallel.sjs](https://github.com/srikumarks/cspjs/blob/master/benchmark/madeup-parallel.sjs). 327 | 328 | Some libraries were disabled for this benchmark because I didn't have the 329 | patience to wait for them to complete ;P 330 | 331 | ``` 332 | file time(ms) memory(MB) 333 | callbacks-baseline.js 641 46.52 334 | sweetjs-task.js 1930 140.13 335 | promises-bluebird.js 2207 167.87 336 | promises-bluebird-generator.js 2301 170.73 337 | callbacks-caolan-async-parallel.js 4214 216.52 338 | promises-obvious-kew.js 5611 739.51 339 | promises-tildeio-rsvp.js 8857 872.50 340 | ``` 341 | 342 | # History 343 | 344 | **Note:** I'd placed this part at the top initially because I first wrote 345 | cspjs out of a desperate need to find a way to work with async code that was 346 | compatible with my brain. Now that cspjs has had some time in my projects, this 347 | can take a back seat. 348 | 349 | My brain doesn't think well with promises. Despite that, [bluebird] is a 350 | fantastic implementation of the [Promises/A+] spec and then some, that many in 351 | the community are switching to promises wholesale. 352 | 353 | So what *does* my brain think well with? The kind of "communicating sequential 354 | processes" model used in [Haskell], [Erlang] and [Go] works very well with my 355 | brain. Also [clojure]'s [core.async] module uses this approach. Given this 356 | prominence of the CSP model, I'm quite sure there are many like me who want to 357 | use the CSP model with Javascript without having to switch to another language 358 | entirely. 359 | 360 | 361 | [Haskell]: http://www.haskell.org 362 | [Erlang]: http://erlang.org 363 | [Go]: http://golang.org 364 | [clojure]: http://clojure.org 365 | [core.async]: https://github.com/clojure/core.async 366 | [Promises/A+]: http://promises-aplus.github.io/promises-spec/ 367 | [bluebird]: https://github.com/petkaantonov/bluebird 368 | [task]: https://github.com/srikumarks/cspjs/blob/master/src/task.js 369 | [Channel]: https://github.com/srikumarks/cspjs/blob/master/src/channel.js 370 | [errman]: http://sriku.org/blog/2014/02/11/bye-bye-js-promises/ 371 | [errman2]: http://sriku.org/blog/2014/10/11/errors-recovery-and-async-code-flow/ 372 | [Node.js]: http://nodejs.org 373 | 374 | 375 | So, what did I do? I wrote a [sweetjs] macro named [task] and a support library 376 | for channels that provides this facility using as close to JS syntax as 377 | possible. It compiles CSP-style code into a pure-JS (ES5) state machine. The 378 | code looks similar to generators and when generator support is ubiquitous the 379 | macro can easily be implemented to write code using them. However, for the 380 | moment, generators are not ubiquitous on the browser side and it helps to have 381 | good async facilities there too. 382 | 383 | No additional wrappers are needed to work with NodeJS-style callbacks since a 384 | "task" compiles down to a pure-JS function which takes a NodeJS-style callback 385 | as the final argument. 386 | 387 | [sweetjs]: http://sweetjs.org/ 388 | 389 | ## Show me the code already! 390 | 391 | 1. Compare [task/doxbee-sequential] and [bluebird/doxbee-sequential] for the 392 | `doxbee-sequential` benchmark. 393 | 2. Compare [task/doxbee-sequential-errors] and 394 | [bluebird/doxbee-sequential-errors] for the `doxbee-sequential-errors` 395 | benchmark. 396 | 3. Compare [task/madeup-parallel] and [bluebird/madeup-parallel] for the 397 | `madeup-parallel` benchmark. 398 | 399 | [task/doxbee-sequential]: https://github.com/srikumarks/cspjs/blob/master/benchmark/doxbee-sequential.sjs 400 | [bluebird/doxbee-sequential]: https://github.com/petkaantonov/bluebird/blob/master/benchmark/doxbee-sequential/promises-bluebird-generator.js 401 | [task/doxbee-sequential-errors]: https://github.com/srikumarks/cspjs/blob/master/benchmark/doxbee-sequential-errors.sjs 402 | [bluebird/doxbee-sequential-errors]: https://github.com/petkaantonov/bluebird/blob/master/benchmark/doxbee-sequential-errors/promises-bluebird-generator.js 403 | [task/madeup-parallel]: https://github.com/srikumarks/cspjs/blob/master/benchmark/madeup-parallel.sjs 404 | [bluebird/madeup-parallel]: https://github.com/petkaantonov/bluebird/blob/master/benchmark/madeup-parallel/promises-bluebird-generator.js 405 | 406 | ## So what's different from ES6 generators? 407 | 408 | There are a lot of similarities with generators, but some significant 409 | differences exist too. 410 | 411 | In two words, the difference is "error management". I think the traditional 412 | `try {} catch (e) {} finally {}` blocks promote sloppy thinking about error 413 | conditions. I want to place function-scoped `catch` and `finally` clauses up 414 | front or anywhere I want, near the code where I should be thinking about error 415 | conditions. Also "throw-ing" an error should not mean "dropping" it to 416 | catch/finally clauses below, should it? ;) 417 | 418 | -------------------------------------------------------------------------------- /benchmark/doxbee-sequential-errors.sjs: -------------------------------------------------------------------------------- 1 | // This file is a port of the benchmark written for bluebird 2 | // https://github.com/petkaantonov/bluebird/tree/master/benchmark/doxbee-sequential-errors 3 | // It requires 'state_machine.js' to be in ../node_modules 4 | 5 | require('../lib/fakes'); 6 | 7 | task upload(stream, idOrPath, tag) { 8 | var blob = blobManager.create(account), 9 | tx = db.begin(); 10 | catch (err) { 11 | tx.rollback(); 12 | } 13 | blobId <- blob.put(stream); 14 | file <- self.byUuidOrPath(idOrPath).get(); 15 | var previousId = file ? file.version : null; 16 | version = { 17 | userAccountId: userAccount.id, 18 | date: new Date(), 19 | blobId: blobId, 20 | creatorId: userAccount.id, 21 | previousId: previousId, 22 | }; 23 | version.id = Version.createHash(version); 24 | await Version.insert(version).execWithin(tx); 25 | triggerIntentionalError(); 26 | if (!file) { 27 | var splitPath = idOrPath.split('/'); 28 | var fileName = splitPath[splitPath.length - 1]; 29 | file = { 30 | id: uuid.v1(), 31 | userAccountId: userAccount.id, 32 | name: fileName, 33 | version: version.id 34 | }; 35 | query <- self.createQuery(idOrPath, file); 36 | await query.execWithin(tx); 37 | triggerIntentionalError(); 38 | } 39 | await FileVersion.insert({fileId: file.id, versionId: version.id}) 40 | .execWithin(tx); 41 | triggerIntentionalError(); 42 | await File.whereUpdate({id: file.id}, {version: version.id}) 43 | .execWithin(tx); 44 | triggerIntentionalError(); 45 | tx.commit(); 46 | } 47 | 48 | module.exports = upload; 49 | -------------------------------------------------------------------------------- /benchmark/doxbee-sequential.sjs: -------------------------------------------------------------------------------- 1 | // This file is a port of the benchmark written for bluebird 2 | // https://github.com/petkaantonov/bluebird/tree/master/benchmark/doxbee-sequential 3 | // It requires 'state_machine.js' to be in ../node_modules 4 | 5 | require('../lib/fakes'); 6 | 7 | task upload(stream, idOrPath, tag) { 8 | var blob = blobManager.create(account); 9 | var tx = db.begin(); 10 | catch (err) { 11 | tx.rollback(); 12 | } 13 | blobId <- blob.put(stream); 14 | file <- self.byUuidOrPath(idOrPath).get(); 15 | var previousId = file ? file.version : null; 16 | var version = { 17 | userAccountId: userAccount.id, 18 | date: new Date(), 19 | blobId: blobId, 20 | creatorId: userAccount.id, 21 | previousId: previousId 22 | }; 23 | version.id = Version.createHash(version); 24 | await Version.insert(version).execWithin(tx); 25 | if (!file) { 26 | var splitPath = idOrPath.split('/'); 27 | var fileName = splitPath[splitPath.length - 1]; 28 | file = { 29 | id: uuid.v1(), 30 | userAccountId: userAccount.id, 31 | name: fileName, 32 | version: version.id 33 | }; 34 | query <- self.createQuery(idOrPath, file); 35 | await query.execWithin(tx); 36 | } 37 | await FileVersion.insert({fileId: file.id, versionId: version.id}).execWithin(tx); 38 | await File.whereUpdate({id: file.id}, { 39 | version: version.id 40 | }).execWithin(tx); 41 | tx.commit(); 42 | } 43 | 44 | module.exports = upload; 45 | -------------------------------------------------------------------------------- /benchmark/madeup-parallel.sjs: -------------------------------------------------------------------------------- 1 | // This file is a port of the benchmark written for bluebird 2 | // https://github.com/petkaantonov/bluebird/tree/master/benchmark/madeup-parallel 3 | // It requires 'state_machine.js' and 'channel.js' to be in ../node_modules 4 | 5 | require('../lib/fakes'); 6 | var Channel = require('channel'); 7 | 8 | module.exports = task upload(stream, idOrPath, tag) { 9 | var queries = new Array(global.parallelQueries), 10 | tx = db.begin(); 11 | 12 | catch (e) { 13 | tx.rollback(); 14 | } 15 | 16 | var ch = new Channel(); 17 | 18 | for (var i = 0, len = queries.length; i < len; ++i) { 19 | FileVersion.insert({index: i}).execWithin(tx, ch.receive(i)); 20 | } 21 | 22 | // Note that the error handling in this case isn't the same 23 | // as the others, where one error occuring results in the whole 24 | // upload operation aborting with that error. With channels, 25 | // the error is passed on to you via an object that you can 26 | // examine and, depending on the channel that failed, decide 27 | // whether you want to abort or not. I keep the code below 28 | // simple just for benchmarking purposes .. since the other 29 | // benchmarks are not setup to fail in this case. 30 | // 31 | // The following would do as a strategy for handling specific 32 | // errors that crop up in the parallel operations. 33 | // 34 | // for (i = 0; i < len; ++i) { 35 | // result <- chan ch; 36 | // if (result.err) { 37 | // throw err; 38 | // } 39 | // } 40 | // 41 | // 42 | result <- ch.takeN(queries.length); 43 | 44 | tx.commit(); 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /channel.js: -------------------------------------------------------------------------------- 1 | module.exports = require('cspjs/src/channel.js'); // Redirect 2 | -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Typography ----------------------------*/ 2 | 3 | @font-face { 4 | font-family: 'aller-light'; 5 | src: url('public/fonts/aller-light.eot'); 6 | src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), 7 | url('public/fonts/aller-light.woff') format('woff'), 8 | url('public/fonts/aller-light.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'aller-bold'; 15 | src: url('public/fonts/aller-bold.eot'); 16 | src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), 17 | url('public/fonts/aller-bold.woff') format('woff'), 18 | url('public/fonts/aller-bold.ttf') format('truetype'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'novecento-bold'; 25 | src: url('public/fonts/novecento-bold.eot'); 26 | src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'), 27 | url('public/fonts/novecento-bold.woff') format('woff'), 28 | url('public/fonts/novecento-bold.ttf') format('truetype'); 29 | font-weight: normal; 30 | font-style: normal; 31 | } 32 | 33 | /*--------------------- Layout ----------------------------*/ 34 | html { height: 100%; } 35 | body { 36 | font-family: "aller-light"; 37 | font-size: 14px; 38 | line-height: 18px; 39 | color: #30404f; 40 | margin: 0; padding: 0; 41 | height:100%; 42 | } 43 | #container { min-height: 100%; } 44 | 45 | a { 46 | color: #000; 47 | } 48 | 49 | b, strong { 50 | font-weight: normal; 51 | font-family: "aller-bold"; 52 | } 53 | 54 | p { 55 | margin: 15px 0 0px; 56 | } 57 | .annotation ul, .annotation ol { 58 | margin: 25px 0; 59 | } 60 | .annotation ul li, .annotation ol li { 61 | font-size: 14px; 62 | line-height: 18px; 63 | margin: 10px 0; 64 | } 65 | 66 | h1, h2, h3, h4, h5, h6 { 67 | color: #112233; 68 | line-height: 1em; 69 | font-weight: normal; 70 | font-family: "novecento-bold"; 71 | text-transform: uppercase; 72 | margin: 30px 0 15px 0; 73 | } 74 | 75 | h1 { 76 | margin-top: 40px; 77 | } 78 | 79 | hr { 80 | border: 0; 81 | background: 1px #ddd; 82 | height: 1px; 83 | margin: 20px 0; 84 | } 85 | 86 | pre, tt, code { 87 | font-size: 12px; line-height: 16px; 88 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 89 | margin: 0; padding: 0; 90 | } 91 | .annotation pre { 92 | display: block; 93 | margin: 0; 94 | padding: 7px 10px; 95 | background: #fcfcfc; 96 | -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 97 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 98 | box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 99 | overflow-x: auto; 100 | } 101 | .annotation pre code { 102 | border: 0; 103 | padding: 0; 104 | background: transparent; 105 | } 106 | 107 | 108 | blockquote { 109 | border-left: 5px solid #ccc; 110 | margin: 0; 111 | padding: 1px 0 1px 1em; 112 | } 113 | .sections blockquote p { 114 | font-family: Menlo, Consolas, Monaco, monospace; 115 | font-size: 12px; line-height: 16px; 116 | color: #999; 117 | margin: 10px 0 0; 118 | white-space: pre-wrap; 119 | } 120 | 121 | ul.sections { 122 | list-style: none; 123 | padding:0 0 5px 0;; 124 | margin:0; 125 | } 126 | 127 | /* 128 | Force border-box so that % widths fit the parent 129 | container without overlap because of margin/padding. 130 | 131 | More Info : http://www.quirksmode.org/css/box.html 132 | */ 133 | ul.sections > li > div { 134 | -moz-box-sizing: border-box; /* firefox */ 135 | -ms-box-sizing: border-box; /* ie */ 136 | -webkit-box-sizing: border-box; /* webkit */ 137 | -khtml-box-sizing: border-box; /* konqueror */ 138 | box-sizing: border-box; /* css3 */ 139 | } 140 | 141 | 142 | /*---------------------- Jump Page -----------------------------*/ 143 | #jump_to, #jump_page { 144 | margin: 0; 145 | background: white; 146 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 147 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 148 | font: 16px Arial; 149 | cursor: pointer; 150 | text-align: right; 151 | list-style: none; 152 | } 153 | 154 | #jump_to a { 155 | text-decoration: none; 156 | } 157 | 158 | #jump_to a.large { 159 | display: none; 160 | } 161 | #jump_to a.small { 162 | font-size: 22px; 163 | font-weight: bold; 164 | color: #676767; 165 | } 166 | 167 | #jump_to, #jump_wrapper { 168 | position: fixed; 169 | right: 0; top: 0; 170 | padding: 10px 15px; 171 | margin:0; 172 | } 173 | 174 | #jump_wrapper { 175 | display: none; 176 | padding:0; 177 | } 178 | 179 | #jump_to:hover #jump_wrapper { 180 | display: block; 181 | } 182 | 183 | #jump_page { 184 | padding: 5px 0 3px; 185 | margin: 0 0 25px 25px; 186 | } 187 | 188 | #jump_page .source { 189 | display: block; 190 | padding: 15px; 191 | text-decoration: none; 192 | border-top: 1px solid #eee; 193 | } 194 | 195 | #jump_page .source:hover { 196 | background: #f5f5ff; 197 | } 198 | 199 | #jump_page .source:first-child { 200 | } 201 | 202 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 203 | @media only screen and (min-width: 320px) { 204 | .pilwrap { display: none; } 205 | 206 | ul.sections > li > div { 207 | display: block; 208 | padding:5px 10px 0 10px; 209 | } 210 | 211 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 212 | padding-left: 30px; 213 | } 214 | 215 | ul.sections > li > div.content { 216 | overflow-x:auto; 217 | -webkit-box-shadow: inset 0 0 5px #e5e5ee; 218 | box-shadow: inset 0 0 5px #e5e5ee; 219 | border: 1px solid #dedede; 220 | margin:5px 10px 5px 10px; 221 | padding-bottom: 5px; 222 | } 223 | 224 | ul.sections > li > div.annotation pre { 225 | margin: 7px 0 7px; 226 | padding-left: 15px; 227 | } 228 | 229 | ul.sections > li > div.annotation p tt, .annotation code { 230 | background: #f8f8ff; 231 | border: 1px solid #dedede; 232 | font-size: 12px; 233 | padding: 0 0.2em; 234 | } 235 | } 236 | 237 | /*---------------------- (> 481px) ---------------------*/ 238 | @media only screen and (min-width: 481px) { 239 | #container { 240 | position: relative; 241 | } 242 | body { 243 | background-color: #F5F5FF; 244 | font-size: 15px; 245 | line-height: 21px; 246 | } 247 | pre, tt, code { 248 | line-height: 18px; 249 | } 250 | p, ul, ol { 251 | margin: 0 0 15px; 252 | } 253 | 254 | 255 | #jump_to { 256 | padding: 5px 10px; 257 | } 258 | #jump_wrapper { 259 | padding: 0; 260 | } 261 | #jump_to, #jump_page { 262 | font: 10px Arial; 263 | text-transform: uppercase; 264 | } 265 | #jump_page .source { 266 | padding: 5px 10px; 267 | } 268 | #jump_to a.large { 269 | display: inline-block; 270 | } 271 | #jump_to a.small { 272 | display: none; 273 | } 274 | 275 | 276 | 277 | #background { 278 | position: absolute; 279 | top: 0; bottom: 0; 280 | width: 350px; 281 | background: #fff; 282 | border-right: 1px solid #e5e5ee; 283 | z-index: -1; 284 | } 285 | 286 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 287 | padding-left: 40px; 288 | } 289 | 290 | ul.sections > li { 291 | white-space: nowrap; 292 | } 293 | 294 | ul.sections > li > div { 295 | display: inline-block; 296 | } 297 | 298 | ul.sections > li > div.annotation { 299 | max-width: 350px; 300 | min-width: 350px; 301 | min-height: 5px; 302 | padding: 13px; 303 | overflow-x: hidden; 304 | white-space: normal; 305 | vertical-align: top; 306 | text-align: left; 307 | } 308 | ul.sections > li > div.annotation pre { 309 | margin: 15px 0 15px; 310 | padding-left: 15px; 311 | } 312 | 313 | ul.sections > li > div.content { 314 | padding: 13px; 315 | vertical-align: top; 316 | border: none; 317 | -webkit-box-shadow: none; 318 | box-shadow: none; 319 | } 320 | 321 | .pilwrap { 322 | position: relative; 323 | display: inline; 324 | } 325 | 326 | .pilcrow { 327 | font: 12px Arial; 328 | text-decoration: none; 329 | color: #454545; 330 | position: absolute; 331 | top: 3px; left: -20px; 332 | padding: 1px 2px; 333 | opacity: 0; 334 | -webkit-transition: opacity 0.2s linear; 335 | } 336 | .for-h1 .pilcrow { 337 | top: 47px; 338 | } 339 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 340 | top: 35px; 341 | } 342 | 343 | ul.sections > li > div.annotation:hover .pilcrow { 344 | opacity: 1; 345 | } 346 | } 347 | 348 | /*---------------------- (> 1025px) ---------------------*/ 349 | @media only screen and (min-width: 1025px) { 350 | 351 | body { 352 | font-size: 16px; 353 | line-height: 24px; 354 | } 355 | 356 | #background { 357 | width: 525px; 358 | } 359 | ul.sections > li > div.annotation { 360 | max-width: 525px; 361 | min-width: 525px; 362 | padding: 10px 25px 1px 50px; 363 | } 364 | ul.sections > li > div.content { 365 | padding: 9px 15px 16px 25px; 366 | } 367 | } 368 | 369 | /*---------------------- Syntax Highlighting -----------------------------*/ 370 | 371 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 372 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 373 | /* 374 | 375 | github.com style (c) Vasily Polovnyov 376 | 377 | */ 378 | 379 | pre code { 380 | display: block; padding: 0.5em; 381 | color: #000; 382 | background: #f8f8ff 383 | } 384 | 385 | pre .hljs-comment, 386 | pre .hljs-template_comment, 387 | pre .hljs-diff .hljs-header, 388 | pre .hljs-javadoc { 389 | color: #408080; 390 | font-style: italic 391 | } 392 | 393 | pre .hljs-keyword, 394 | pre .hljs-assignment, 395 | pre .hljs-literal, 396 | pre .hljs-css .hljs-rule .hljs-keyword, 397 | pre .hljs-winutils, 398 | pre .hljs-javascript .hljs-title, 399 | pre .hljs-lisp .hljs-title, 400 | pre .hljs-subst { 401 | color: #954121; 402 | /*font-weight: bold*/ 403 | } 404 | 405 | pre .hljs-number, 406 | pre .hljs-hexcolor { 407 | color: #40a070 408 | } 409 | 410 | pre .hljs-string, 411 | pre .hljs-tag .hljs-value, 412 | pre .hljs-phpdoc, 413 | pre .hljs-tex .hljs-formula { 414 | color: #219161; 415 | } 416 | 417 | pre .hljs-title, 418 | pre .hljs-id { 419 | color: #19469D; 420 | } 421 | pre .hljs-params { 422 | color: #00F; 423 | } 424 | 425 | pre .hljs-javascript .hljs-title, 426 | pre .hljs-lisp .hljs-title, 427 | pre .hljs-subst { 428 | font-weight: normal 429 | } 430 | 431 | pre .hljs-class .hljs-title, 432 | pre .hljs-haskell .hljs-label, 433 | pre .hljs-tex .hljs-command { 434 | color: #458; 435 | font-weight: bold 436 | } 437 | 438 | pre .hljs-tag, 439 | pre .hljs-tag .hljs-title, 440 | pre .hljs-rules .hljs-property, 441 | pre .hljs-django .hljs-tag .hljs-keyword { 442 | color: #000080; 443 | font-weight: normal 444 | } 445 | 446 | pre .hljs-attribute, 447 | pre .hljs-variable, 448 | pre .hljs-instancevar, 449 | pre .hljs-lisp .hljs-body { 450 | color: #008080 451 | } 452 | 453 | pre .hljs-regexp { 454 | color: #B68 455 | } 456 | 457 | pre .hljs-class { 458 | color: #458; 459 | font-weight: bold 460 | } 461 | 462 | pre .hljs-symbol, 463 | pre .hljs-ruby .hljs-symbol .hljs-string, 464 | pre .hljs-ruby .hljs-symbol .hljs-keyword, 465 | pre .hljs-ruby .hljs-symbol .hljs-keymethods, 466 | pre .hljs-lisp .hljs-keyword, 467 | pre .hljs-tex .hljs-special, 468 | pre .hljs-input_number { 469 | color: #990073 470 | } 471 | 472 | pre .hljs-builtin, 473 | pre .hljs-constructor, 474 | pre .hljs-built_in, 475 | pre .hljs-lisp .hljs-title { 476 | color: #0086b3 477 | } 478 | 479 | pre .hljs-preprocessor, 480 | pre .hljs-pi, 481 | pre .hljs-doctype, 482 | pre .hljs-shebang, 483 | pre .hljs-cdata { 484 | color: #999; 485 | font-weight: bold 486 | } 487 | 488 | pre .hljs-deletion { 489 | background: #fdd 490 | } 491 | 492 | pre .hljs-addition { 493 | background: #dfd 494 | } 495 | 496 | pre .hljs-diff .hljs-change { 497 | background: #0086b3 498 | } 499 | 500 | pre .hljs-chunk { 501 | color: #aaa 502 | } 503 | 504 | pre .hljs-tex .hljs-formula { 505 | opacity: 0.5; 506 | } 507 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srikumarks/cspjs/434806b73ca8ef99e2eb11c99054836f6a901293/docs/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /docs/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } -------------------------------------------------------------------------------- /docs/state_machine.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | State machine support for task.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | 45 | 573 |
574 | 575 | 576 | -------------------------------------------------------------------------------- /docs/stream.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | stream.js 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 44 | 45 | 180 |
181 | 182 | 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cspjs", 3 | "description": "A macro for easing programming with async Javascript APIs", 4 | "author": "Srikumar", 5 | "version": "0.7.2", 6 | "main" : "src/task.js", 7 | "dependencies": { 8 | "sweet.js" : "0.7.8" 9 | }, 10 | "devDependencies": { 11 | "mocha": "~1.3.2" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/srikumarks/cspjs.git" 16 | }, 17 | "keywords": [ 18 | "sweet-macros", 19 | "macros", 20 | "javascript", 21 | "csp", 22 | "asynchronous" 23 | ], 24 | "license": "MIT" 25 | } 26 | -------------------------------------------------------------------------------- /src/channel.js: -------------------------------------------------------------------------------- 1 | // A channel is a queue with a read-end and a write-end. 2 | // Values are written and read asynchronously via callbacks. 3 | // The basic channel is such that the callback associated 4 | // with a value put into it will be called when the value 5 | // is consumed from the read end. 6 | 7 | var nextTick = (function () { 8 | return this.setImmediate || process.nextTick; 9 | }()); 10 | 11 | function Channel() { 12 | this._queue = new Array; 13 | this._pending = new Array; 14 | return this; 15 | } 16 | 17 | // Convenience class method to instantiate a channel. 18 | Channel.new = function () { 19 | return new Channel(); 20 | }; 21 | 22 | function sendValue(value, callback) { 23 | callback && nextTick(function () { callback(null, value); }); 24 | } 25 | 26 | function sendError(err, callback) { 27 | callback && nextTick(function () { callback(err, null); }); 28 | } 29 | 30 | function sendValueS(value, callback) { 31 | callback && callback(null, value); 32 | } 33 | 34 | function sendErrorS(err, callback) { 35 | callback && callback(err, null); 36 | } 37 | 38 | function CBV(callback, value) { 39 | this._callback = callback; 40 | this._value = value; 41 | return this; 42 | } 43 | 44 | // Read a value from the channel, passing the value to the given callback. 45 | Channel.prototype.take = function (callback) { 46 | if (this._queue.length > 0) { 47 | var q = this._queue.shift(); 48 | sendValue(q._value, q._callback); 49 | sendValue(q._value, callback); 50 | } else { 51 | callback && this._pending.push(callback); 52 | } 53 | }; 54 | 55 | // Places a value into the channel. The callback will be called when the value is 56 | // consumed from the read-end. 57 | Channel.prototype.put = function (value, callback) { 58 | if (this._pending.length > 0) { 59 | var p = this._pending.shift(); 60 | sendValue(value, callback); 61 | sendValue(value, p); 62 | } else { 63 | this._queue.push(new CBV(callback, value)); 64 | } 65 | }; 66 | 67 | // Does any ending actions on the channel. 68 | // The protocol is to have a channel "end" 69 | // by a null value being placed on it. The 70 | // end() method is simply to perform any pending 71 | // ending actions. The default action is to replace 72 | // the end() function with the original end. 73 | Channel.prototype.end = function end() { 74 | this.end = end; 75 | }; 76 | 77 | // Returns a channel that will give you values that come 78 | // on this channel, without actually reading from the channel. 79 | // That is, multiple taps on a channel will get its values 80 | // fanned out. If a channel argument is given, the tapped 81 | // values will go into that channel. 82 | Channel.prototype.tap = function (chan) { 83 | var tapChan = chan || new Channel(); 84 | var self = this; 85 | if (!this._taps) { 86 | this._taps = [tapChan]; 87 | var put = this.put; 88 | this.put = function (value, callback) { 89 | for (var c = 0, cN = this._taps.length; c < cN; ++c) { 90 | this._taps[c].put(value); 91 | } 92 | if (value === null) { 93 | while (this._taps.length > 0) { 94 | this._taps[0].end(); 95 | } 96 | this._taps = null; 97 | this.put = put; 98 | } 99 | if (this._pending.length > 0) { 100 | // Put only if there are takers. Otherwise 101 | // just drop the value. If we don't do this, 102 | // the value will simply pile up if only taps 103 | // are being used on the channel. 104 | put.call(this, value, callback); 105 | } 106 | }; 107 | } else { 108 | this._taps.push(tapChan); 109 | } 110 | 111 | var end = tapChan.end; 112 | tapChan.end = function () { 113 | self._taps.splice(self._taps.indexOf(tapChan), 1); 114 | end.call(this); 115 | }; 116 | return tapChan; 117 | }; 118 | 119 | 120 | // For an end-point channel, applies the given 121 | // function to values received on the channel. 122 | // The second argument to the function is a callback 123 | // that should be called once the processing has completed. 124 | // It is alright to call the callback synchronously. 125 | // It only makes sense to have one processing function 126 | // for a channel. The fn is called with the value as the 127 | // first argument and a loop continuation callback as 128 | // the second argument. 129 | Channel.prototype.process = function (fn) { 130 | var self = this; 131 | function receive(err, value) { 132 | fn(value, loop); 133 | } 134 | function loop(err) { 135 | if (!err) { 136 | self.take(receive); 137 | } 138 | } 139 | loop(null); 140 | return this; 141 | }; 142 | 143 | // Binds a channel to the given named method of the given 144 | // class. If the class has an init() method, it will be called 145 | // with `options.initArgs` to instantiate an object. 146 | // The given `options.methodName` of the resultant object will be 147 | // invoked with the message as the first argument, and a continuation 148 | // callback (a la `process()`) as the second argument. The methodName 149 | // defaults to `receive`. 150 | // 151 | // If `options.spawn` is `true`, then the handler is called with the 152 | // message only and the channel returns to processing other 153 | // messages immediately without waiting for the handling to 154 | // finish. 155 | // 156 | // Calling bind on an already bound channel replaces the previous binding. 157 | Channel.prototype.bind = function (klass, options) { 158 | var self = this, receive, loop; 159 | self._boundClass = klass; 160 | self._boundMethodName = (options && options.methodName) || 'receive'; 161 | self._boundInitArgs = (options && options.initArgs) || []; 162 | self._boundSpawn = (options && options.spawn) || false; 163 | if (!self._bound) { 164 | receive = function (err, msg) { 165 | var handler = new self._boundClass(); // new is not expected to throw. 166 | if (handler.init) { 167 | try { 168 | handler = handler.init.apply(handler, self._boundInitArgs); 169 | } catch (e) { 170 | return loop(err); 171 | } 172 | } 173 | if (self._boundSpawn) { 174 | nextTick(loop); 175 | handler[self._boundMethodName](msg); 176 | } else { 177 | handler[self._boundMethodName](msg, loop); 178 | } 179 | }; 180 | loop = function (err) { 181 | if (!err) { 182 | self.take(receive); 183 | } else { 184 | self._bound = false; 185 | } 186 | }; 187 | self._bound = true; 188 | loop(null); 189 | } 190 | return this; 191 | }; 192 | 193 | function ChannelValue(chan, err, value) { 194 | this.chan = chan; 195 | this.err = err; 196 | this.val = value; 197 | return this; 198 | } 199 | 200 | ChannelValue.prototype.resolve = function () { 201 | if (this.err) { 202 | throw this.err; 203 | } else { 204 | return this.val; 205 | } 206 | }; 207 | 208 | // Makes a callback that will receive the value produced by 209 | // some process and place the result into the channel. The 210 | // "id" exists to identify the one producing the value. 211 | // The "id", "err" and "val" are all available on the 212 | // channel. 213 | Channel.prototype.receive = function () { 214 | var self = this; 215 | return function (err, value) { 216 | self.put(new ChannelValue(self, err, value)); 217 | }; 218 | }; 219 | 220 | // Like receive, but results in the channel being 'filled' 221 | // with the value received on the callback. Once a value 222 | // is received, all subsequent take operations will give 223 | // the same value, and puts will result in an error. 224 | Channel.prototype.resolver = function () { 225 | var self = this; 226 | return function (err, value) { 227 | self.fill(new ChannelValue(self, err, value)); 228 | }; 229 | }; 230 | 231 | // Answers "will read succeed immediately?" 232 | Channel.prototype.canRead = function () { 233 | return this._queue.length > 0 && this._pending.length === 0; 234 | }; 235 | 236 | // Answers "will write succeed immediately?" 237 | Channel.prototype.canWrite = function () { 238 | return this._pending.length > 0 || this._queue.length === 0; 239 | }; 240 | 241 | // Answers "how many values have been placed into the channel?" 242 | // Positive values give the number of values available right away. 243 | // Negative values give the number of pending take operations. 244 | Channel.prototype.backlog = function () { 245 | return this._queue.length - this._pending.length; 246 | }; 247 | 248 | // Makes a new channel whose values are transformed by the given 249 | // function "f". `cond(value)` is a function that specifies a 250 | // condition until which the mapping will continue. The mapper 251 | // is not expected to throw. 252 | Channel.prototype.map = function (f) { 253 | var ch2 = Object.create(this); 254 | var take = this.take; 255 | ch2.take = function (callback) { 256 | take.call(this, function (err, value) { 257 | callback(err, err ? null : f(value)); 258 | }); 259 | }; 260 | return ch2; 261 | }; 262 | 263 | // Makes a new channel and pipes the values in this 264 | // channel to it. Only the values that satisfy the 265 | // predicate function 'f' are piped and others 266 | // are dropped. The filter function is not expected 267 | // to throw. 268 | Channel.prototype.filter = function (f) { 269 | var ch2 = Object.create(this); 270 | var take = this.take; 271 | ch2.take = function (callback) { 272 | take.call(this, function (err, value) { 273 | if (err) { 274 | callback(err, null); 275 | } else if (f(value)) { 276 | callback(err, value); 277 | } else { 278 | ch2.take(callback); // Value dropped 279 | } 280 | }); 281 | }; 282 | return ch2; 283 | }; 284 | 285 | // Makes a new channel, reduces the values produced 286 | // continuously and sends the output to the taker. 287 | // The reducer is not expected to throw. 288 | Channel.prototype.reduce = function (initial, f) { 289 | var ch2 = Object.create(this); 290 | var take = this.take; 291 | var result = initial; 292 | ch2.take = function (callback) { 293 | take.call(this, function (err, value) { 294 | if (err) { 295 | callback(err, null); 296 | } else { 297 | result = f(result, value); 298 | callback(null, result); 299 | } 300 | }); 301 | }; 302 | return ch2; 303 | }; 304 | 305 | // Makes a new channel and pipes the values put into this 306 | // channel in groups of N. 307 | Channel.prototype.group = function (N) { 308 | if (N <= 0) { 309 | throw new Error('Groups need to be at least 1 in size. Given "' + N + '"'); 310 | } 311 | return this.reduce([], function (group, value) { 312 | return (group.length === N) ? [value] : (group.push(value), group); 313 | }).filter(function (g) { return g.length === N; }); 314 | }; 315 | 316 | function resolve(thing, recursive, callback) { 317 | var unresolved = 0; 318 | 319 | if (thing instanceof Channel) { 320 | unresolved += resolveChannel(thing, recursive, callback); 321 | } else if (thing instanceof Array) { 322 | unresolved += resolveArray(thing, recursive, callback); 323 | } else if (thing instanceof Object) { 324 | unresolved += resolveObject(thing, recursive, callback); 325 | } else { 326 | sendValue(thing, callback); 327 | } 328 | 329 | return unresolved; 330 | } 331 | 332 | function resolveChannel(channel, recursive, callback) { 333 | if (recursive) { 334 | channel.take(function receiver(err, value) { 335 | Channel.resolve(value, recursive, callback); 336 | }); 337 | } else { 338 | channel.take(callback); 339 | } 340 | 341 | return 1; 342 | } 343 | 344 | function resolveArray(arr, recursive, callback) { 345 | var unresolved = 0; 346 | 347 | for (var i = 0; i < arr.length; ++i) { 348 | unresolved += resolve(arr[i], recursive, (function (i) { 349 | return function receiver(err, value) { 350 | if (recursive) { 351 | resolve(value, recursive, receiver); 352 | } else { 353 | arr[i] = value; 354 | --unresolved; 355 | if (unresolved === 0) { 356 | callback(null, arr); 357 | } 358 | } 359 | }; 360 | }(i))); 361 | } 362 | 363 | return unresolved; 364 | } 365 | 366 | function resolveObject(obj, recursive, callback) { 367 | unresolved = 0; 368 | Object.keys(obj).forEach(function (k) { 369 | unresolved += resolve(obj[k], recursive, function receiver(err, value) { 370 | if (recursive) { 371 | resolve(value, recursive, receiver); 372 | } else { 373 | obj[k] = value; 374 | --unresolved; 375 | if (unresolved === 0) { 376 | callback(null, obj); 377 | } 378 | } 379 | }); 380 | }); 381 | return unresolved; 382 | } 383 | 384 | // Waits for all channels in the given array to get a value, 385 | // replaces the array element with the received value and calls 386 | // back when all entries have been resolved. If 'recursive' is 387 | // true, then if the value received on a channel is itself a channel, 388 | // it is recursively waited on until final resolution. 389 | Channel.resolve = resolve; 390 | 391 | // Temporarily switches the channel to a mode where it will 392 | // collect the next N items into a group and pass it on to 393 | // the callback. 394 | // 395 | // Use within task like this - 396 | // var ch = new Channel(); 397 | // ... 398 | // x <- ch.takeN(10); 399 | Channel.prototype.takeN = function (N, callback) { 400 | var group = []; 401 | var self = this; 402 | function receive(err, value) { 403 | if (err) { 404 | return sendError(err, callback); 405 | } 406 | if (value !== null) { 407 | group.push(value); 408 | if (group.length < N) { 409 | self.take(receive); 410 | } else { 411 | sendValue(group, callback); 412 | } 413 | } else { 414 | sendValue(group, callback); 415 | } 416 | } 417 | self.take(receive); 418 | }; 419 | 420 | // Takes as many values as it can without blocking. 421 | Channel.prototype.takeSome = function (callback) { 422 | var bl = this.backlog(); 423 | if (bl > 0) { 424 | return this.takeN(bl, callback); 425 | } 426 | sendValue([], callback); 427 | }; 428 | 429 | // Keeps this channel alive until a value is 430 | // received from the given chan. 431 | Channel.prototype.until = function (chan) { 432 | var done = false; 433 | var self = this; 434 | var tapChan = chan.tap(); 435 | tapChan.take(function (err, value) { 436 | done = true; 437 | tapChan.end(); 438 | self.end(); 439 | }); 440 | var ch = this.tap(); 441 | var take = ch.take; 442 | ch.take = function (callback) { 443 | if (done) { 444 | sendValue(null, callback); 445 | } else { 446 | take.call(this, callback); 447 | } 448 | }; 449 | return ch; 450 | }; 451 | 452 | function noop() {} 453 | 454 | // Switches the channel to a state where every time some 455 | // reader takes a value from the channel, they'll get 456 | // `value` delivered immediately. This makes a channel 457 | // behave somewhat like a promise, where until `fill` 458 | // is called, asking for a value will cause a wait, but 459 | // once `fill` is called somewhere, `take` will always 460 | // succeed with a single value. 461 | Channel.prototype.fill = function (value) { 462 | if (this.backlog() > 0) { 463 | throw new Error('Channel::fill cannot be used after Channel::put has been called'); 464 | } 465 | 466 | var origPut = this.put; 467 | 468 | this.take = function (callback) { 469 | sendValue(value, callback); 470 | }; 471 | this.put = function (ignoredValue, callback) { 472 | sendError('filled', callback); 473 | }; 474 | this.fill = noop; 475 | 476 | // If takers are already waiting, satisfy them 477 | // immediately. 478 | while (this.backlog() < 0) { 479 | origPut.call(this, value); 480 | } 481 | 482 | return this; 483 | }; 484 | 485 | // Sends the elements of the given array one by one 486 | // to the channel as readers request values. The 487 | // callback will be called when the last value is 488 | // accepted. 489 | Channel.prototype.stream = function (array, callback) { 490 | var i = 0, self = this; 491 | function next() { 492 | if (i < array.length) { 493 | self.put(array[i++], next); 494 | } else { 495 | sendValue(array, callback); 496 | } 497 | } 498 | next(); 499 | }; 500 | 501 | // Sets up the channel to receive events of the given type 502 | // from the given domElement. (Works only in the browser.) 503 | // `domElement` can either be a string which is taken to be 504 | // a querySelector specifier, an array of DOM nodes, or 505 | // a single DOM node. `eventName` is a string like 'click' 506 | // which gives an event category to bind to. 507 | // 508 | // Note: If you want a channel to not receive events 509 | // too frequently, you can first debounce the channel 510 | // before listening for events, like this - 511 | // 512 | // ch = new Channel(); 513 | // ch.debounce(100).listen('.textarea', 'change'); 514 | // 515 | // The above code will make sure that consecutive change 516 | // events are separated by at least 100ms. The debounce() 517 | // method call produces a wrapper channel object that 518 | // acts as a gatekeeper to the original channel object 519 | // 'ch'. So, while the above way will result in debounced 520 | // actions, you can subsequently call `ch.listen()` to 521 | // bypass debouncing on the same channel. Readers reading 522 | // `ch` will receive events from the debounced elements 523 | // as well from the elements bound directly. 524 | Channel.prototype.listen = function (domElement, eventName) { 525 | var self = this; 526 | var elements = null; 527 | if (typeof domElement === 'string') { 528 | elements = document.querySelectorAll(domElement); 529 | } else if (domElement.length) { 530 | elements = domElement; 531 | } else { 532 | elements = [domElement]; 533 | } 534 | 535 | function listener(event) { 536 | self.put(event); 537 | event.stopPropagation(); 538 | } 539 | 540 | for (var i = 0, N = elements.length; i < N; ++i) { 541 | elements[i].addEventListener(eventName, listener); 542 | } 543 | 544 | var end = this.end; 545 | this.end = function () { 546 | for (var i = 0, N = elements.length; i < N; ++i) { 547 | elements[i].removeEventListener(eventName, listener); 548 | } 549 | end.call(this); 550 | }; 551 | return this; 552 | }; 553 | 554 | // Makes a new channel that receives the values put into 555 | // all the given channels (which is an array of channels). 556 | // The value produced by a merged channel is a wrapper object 557 | // that has three fields - "chan" giving the channel that 558 | // produced the value, "val" giving the value and "ix" 559 | // giving the index of the channel in the array that produced 560 | // the value. The merged channel will received a wrapped object 561 | // that will pass on both values as well as errors from the 562 | // channels being merged. This permits custom error handling instead 563 | // of triggering error propagation in the receiver for every 564 | // channel's error. Not all errors and channels need be equal. 565 | // 566 | // Breaking change: MergedChannelValue no longer has an 'ix' 567 | // field giving the index within the array. You need to branch on 568 | // the channel itself. Alternatively, you can store some reference 569 | // value as a property of the channel object any way. The reason 570 | // for this change is that now the "piper" function is exposed 571 | // as the .add() method of the merged channel, to enable addition 572 | // of new channels to the merged stream on the fly. To remove 573 | // a channel from a merged stream, simply send a null value 574 | // to it. 575 | // 576 | // Breaking change: MergedChannelValue is now ChannelValue. 577 | Channel.merge = function (channels) { 578 | var channel = new Channel(); 579 | 580 | function piper(ch) { 581 | function writer(err, value) { 582 | if (value !== null) { 583 | channel.put(new ChannelValue(ch, err, value), reader); 584 | } else { 585 | // Indicate that the channel is finished. The reader can discard this. 586 | channel.put(new ChannelValue(ch, null, null)); 587 | } 588 | } 589 | function reader(err, value) { 590 | ch.take(writer); 591 | } 592 | reader(null, true); 593 | } 594 | 595 | channel.add = piper; 596 | channels && channels.forEach(piper); 597 | 598 | return channel; 599 | }; 600 | 601 | // It is sometimes useful to also have a value sent to 602 | // an existing channel after a timeout expires. If some 603 | // other process is supposed to write a value to the 604 | // channel and it is taking too long, the value passed 605 | // to the .timeout() call can be tested against to decide 606 | // whether a timeout occurred before the process could 607 | // do its thing. 608 | Channel.prototype.timeout = function (ms, value) { 609 | setTimeout(timeoutTick, ms, this, value); 610 | return this; 611 | }; 612 | 613 | // Makes a "timeout" channel, which'll deliver a value 614 | // a given interval after the channel is created. 615 | Channel.timeout = function (ms, value) { 616 | return (new Channel()).timeout(ms, value); 617 | }; 618 | 619 | function timeoutTick(channel, value) { 620 | channel.put(value); 621 | } 622 | 623 | // Makes a "clock" channel which, once started, will produce 624 | // values counting upwards from `startCounter`, until the 625 | // `stop()` method is called on the channel. Calling `start()` 626 | // will have an effect only when the clock is stopped. 627 | Channel.clock = function (ms) { 628 | var channel = new Channel(); 629 | channel._timer = null; 630 | channel._timeInterval_ms = ms; 631 | channel._counter = 0; 632 | channel.start = clockStart; 633 | channel.stop = clockStop; 634 | return channel; 635 | }; 636 | 637 | function clockTick(clock) { 638 | clock.put(clock._counter++); 639 | } 640 | 641 | function clockStart(startCounter) { 642 | if (!this._timer) { 643 | startCounter = arguments.length < 1 ? 1 : startCounter; 644 | this._counter = startCounter; 645 | this._timer = setInterval(clockTick, this._timeInterval_ms, this); 646 | } 647 | } 648 | 649 | function clockStop() { 650 | if (this._timer) { 651 | clearInterval(this._timer); 652 | this._timer = null; 653 | } 654 | } 655 | 656 | 657 | // Returns a wrapped interface to channel which will 658 | // debounce the values placed on it - i.e. it will 659 | // reject put() operations that occur within a time 660 | // of "ms" milliseconds between each other. 661 | Channel.prototype.debounce = function (ms) { 662 | var ch = Object.create(this); 663 | ch._channel = this; 664 | ch._debounceInterval_ms = ms; 665 | ch._timer = null; 666 | ch.put = debouncingPut; 667 | return ch; 668 | }; 669 | 670 | function realPut(ch, value, callback) { 671 | ch._timer = null; 672 | ch._channel.put(value, callback); 673 | } 674 | 675 | function debouncingPut(value, callback) { 676 | if (this._timer) { 677 | clearTimeout(this._timer); 678 | this._timer = null; 679 | } 680 | this._timer = setTimeout(realPut, this._debounceInterval_ms, this, value, callback); 681 | return this; 682 | } 683 | 684 | 685 | // Wraps the given channel with an interface such 686 | // that put() operations will immediately succeed 687 | // as long as fewer than N values have been placed 688 | // on the channel. 689 | Channel.prototype.buffer = function (N) { 690 | var ch = Object.create(this); 691 | ch._channel = this; 692 | ch._bufferLength = N; 693 | ch.put = bufferedPut; 694 | ch.take = bufferedTake; 695 | return ch; 696 | }; 697 | 698 | function bufferedPut(value, callback) { 699 | if (this.backlog() < this._bufferLength) { 700 | this._channel.put(value); 701 | sendValue(value, callback); 702 | } else { 703 | this._channel.put(value, callback); 704 | } 705 | } 706 | 707 | function bufferedTake(callback) { 708 | this._channel.take(callback); 709 | if (this.backlog() >= this._bufferLength) { 710 | var q = this._queue[this._bufferLength - 1]; 711 | sendValue(q._value, q._callback); 712 | q._callback = null; 713 | } 714 | } 715 | 716 | // Every time a bucket's level falls below the low water mark, 717 | // it waits for the bucket to get full again before delivering 718 | // values to the takers. This is useful when values are expected 719 | // to arrive at a channel roughly periodically, but the rate at 720 | // which they get processed can fluctuate a bit. The buffering 721 | // helps with the fluctuation and the "low water mark" helps ensure 722 | // maintenance of the buffer. 723 | Channel.prototype.bucket = function (fullSize, lowWaterMark) { 724 | var ch = Object.create(this); 725 | ch._channel = this; 726 | ch._bufferLength = fullSize; 727 | ch._bucketLowWaterMark = lowWaterMark || 0; 728 | ch._suspendedTakes = []; 729 | ch.waitingTillFull = true; 730 | ch.take = bucketTake; 731 | ch.put = bucketPut; 732 | return ch; 733 | }; 734 | 735 | function bucketProcSuspendedTakes(bucket) { 736 | while (bucket._suspendedTakes.length > 0) { 737 | bufferedTake.call(bucket, bucket._suspendedTakes.shift()); 738 | } 739 | bucket.waitingTillFull = bucket.backlog() <= bucket._bucketLowWaterMark; 740 | } 741 | 742 | function bucketTake(callback) { 743 | if (this.waitingTillFull) { 744 | if (this.backlog() > this._bufferLength) { 745 | // Full reached. 746 | this.waitingTillFull = false; 747 | this.take(callback); 748 | } else { 749 | this._suspendedTakes.push(callback); 750 | } 751 | } else { 752 | this._suspendedTakes.push(callback); 753 | bucketProcSuspendedTakes(this); 754 | } 755 | } 756 | 757 | function bucketPut(value, callback) { 758 | bufferedPut.call(this, value, callback); 759 | if (this.waitingTillFull) { 760 | if (this.backlog() > this._bufferLength) { 761 | // Full reached. 762 | bucketProcSuspendedTakes(this); 763 | } 764 | } else { 765 | bucketProcSuspendedTakes(this); 766 | } 767 | } 768 | 769 | 770 | // If more than N values have been placed into a channel 771 | // and a writer tries to place one more value, sometimes 772 | // we want the new value to be dropped in order that 773 | // processing requirements don't accumulate. This is 774 | // the purpose of `droppingBuffer` which wraps the 775 | // parent channel's `put` to do this dropping. 776 | // 777 | // A channel with a droppingBuffer will never block a put 778 | // operation. 779 | 780 | Channel.prototype.droppingBuffer = function (N) { 781 | var ch = Object.create(this); 782 | ch._channel = this; 783 | ch._bufferLength = N; 784 | ch.put = droppingPut; 785 | return ch; 786 | }; 787 | 788 | function droppingPut(value, callback) { 789 | if (this.backlog() < this._bufferLength) { 790 | this._channel.put(value); 791 | sendValue(value, callback); 792 | } else { 793 | // Drop the value. 794 | sendValue(null, callback); 795 | } 796 | } 797 | 798 | // In the same situation as with `droppingBuffer`, 799 | // at other times, we want the more recent values 800 | // to take precedence over the values already in 801 | // the queue. In this case, we want to expire the 802 | // old values and replace them with new values. 803 | // That is what `expiringBuffer` does. 804 | // 805 | // A channel with an expiringBuffer will never block a 806 | // put operation. 807 | 808 | Channel.prototype.expiringBuffer = function (N) { 809 | var ch = Object.create(this); 810 | ch._channel = this; 811 | ch._bufferLength = N; 812 | ch.put = expiringPut; 813 | return ch; 814 | }; 815 | 816 | function expiringPut(value, callback) { 817 | while (this.backlog() >= this._bufferLength) { 818 | this.take(); 819 | } 820 | this._channel.put(value); 821 | sendValue(value, callback); 822 | return this; 823 | } 824 | 825 | // Makes a "fanout" channel that can be "connect()"ed to 826 | // other channels to whom the values that come on this channel 827 | // will be copied. Do not call a fanout channel's "take" method 828 | // explicitly. Instead connect other channels to it to receive 829 | // values. Since it may take time to setup connections, you have 830 | // to call ch.start() explicitly to begin piping values to the 831 | // connections, lest some values get missed out. 832 | 833 | Channel.prototype.fanout = function () { 834 | var ch = Object.create(this); 835 | ch.connect = fanoutConnect; 836 | ch.disconnect = fanoutDisconnect; 837 | ch.start = fanoutStart; 838 | ch._channel = this; 839 | ch._connections = []; 840 | ch._started = false; 841 | return ch; 842 | }; 843 | 844 | function fanoutConnect() { 845 | for (var i = 0, N = arguments.length; i < N; ++i) { 846 | this.disconnect(arguments[i]); 847 | this._connections.push(arguments[i]); 848 | } 849 | return this; 850 | } 851 | 852 | function fanoutDisconnect() { 853 | if (arguments.length === 0) { 854 | this._connections = []; 855 | return this; 856 | } 857 | 858 | var N, i, chan, pos; 859 | for (i = 0, N = arguments.length; i < N; ++i) { 860 | chan = arguments[i]; 861 | pos = this._connections.indexOf(chan); 862 | if (pos >= 0) { 863 | this._connections.splice(pos, 1); 864 | } 865 | } 866 | return this; 867 | } 868 | 869 | function fanoutStart() { 870 | var self = this; 871 | if (!self._started) { 872 | self._started = true; 873 | self.take(function receive(err, value) { 874 | if (value !== null) { 875 | for (var i = 0, N = self._connections.length; i < N; ++i) { 876 | self._connections[i].put(value); 877 | } 878 | self.take(receive); 879 | } 880 | }); 881 | } 882 | return self; 883 | } 884 | 885 | module.exports = Channel; 886 | -------------------------------------------------------------------------------- /src/state_machine.js: -------------------------------------------------------------------------------- 1 | // # State machine support for task.js 2 | // 3 | // This file contains miscellaneous state machine management code 4 | // that is used by the code generated by the `task` macro in task.js. 5 | 6 | var Channel = require('./channel'); 7 | 8 | var nextTick = (function () { 9 | return this.setImmediate || process.nextTick; 10 | }()); 11 | 12 | function State() { 13 | this.id = 0; 14 | this.args = [null, null]; 15 | this.err = null; 16 | this.unwinding = []; 17 | this.waiting = 0; 18 | this.isFinished = false; 19 | this.isUnwinding = false; 20 | this.currentErrorStep = null; 21 | this.abort_with_error = null; 22 | return this; 23 | } 24 | 25 | function controlAPIMaker() { 26 | var state_machine = this; 27 | return Object.create({}, { 28 | abort: { 29 | value: function (err) { 30 | if (state_machine.state.waiting > 0) { 31 | state_machine.state.abort_with_error = err; 32 | } else { 33 | state_machine.callback(err); 34 | } 35 | } 36 | }, 37 | isWaiting: { 38 | get: function () { 39 | return state_machine.state.waiting > 0; 40 | } 41 | }, 42 | isFinished: { 43 | get: function () { 44 | return state_machine.state.isFinished; 45 | } 46 | } 47 | }); 48 | } 49 | 50 | function StateMachine(context, callback, fn, task_fn) { 51 | 52 | this.state = new State(); 53 | this.fn = fn; 54 | this.task_fn = task_fn; 55 | this.context = context; 56 | this.finalCallback = callback; 57 | 58 | // The following two will be initialized if the body 59 | // of the state machine contains a finally {} block. 60 | // If not, they can remain null. 61 | this.captureStateVars = null; // Might be initialized to function () { return array; } 62 | this.restoreStateVars = null; // Might be initialized to function (array) { assign state variables; } 63 | 64 | this.boundStep = this.step.bind(this); 65 | this.boundUnwind = this.unwind.bind(this); 66 | this.controlAPIMaker = controlAPIMaker.bind(this); 67 | 68 | // Initialize the jump table structure if not done already. 69 | this.task_fn.cachedJumpTable = this.task_fn.cachedJumpTable || {}; 70 | 71 | return this; 72 | } 73 | 74 | StateMachine.prototype.start = function () { 75 | this.goTo(1); 76 | }; 77 | 78 | StateMachine.prototype.step = function () { 79 | this.state.waiting--; 80 | if (this.state.abort_with_error) { 81 | this.performAbort(); 82 | } else { 83 | this.fn.apply(this.context, this.state.args); 84 | } 85 | }; 86 | 87 | // If an abortion has been requested by the state machine 88 | // user, then bail out on the next step. 89 | StateMachine.prototype.performAbort = function () { 90 | var err = this.state.abort_with_error; 91 | this.state.abort_with_error = null; 92 | this.fn.call(this.context, err); 93 | }; 94 | 95 | StateMachine.prototype.goTo = function (id) { 96 | this.state.id = id; 97 | this.state.strict_unwind = false; 98 | this.state.waiting++; 99 | nextTick(this.boundStep); 100 | }; 101 | 102 | StateMachine.prototype.thenTo = function (id) { 103 | var done = false; 104 | var self = this; 105 | this.state.waiting++; 106 | return function () { 107 | var _self = self; 108 | var _state = _self.state; 109 | _state.waiting--; 110 | if (!done) { 111 | done = true; 112 | _state.id = id; 113 | if (_state.abort_with_error) { 114 | _self.performAbort(); 115 | } else { 116 | _self.fn.apply(_self.context, arguments); 117 | } 118 | } else { 119 | console.error('Callback called repeatedly!'); 120 | } 121 | }; 122 | }; 123 | 124 | StateMachine.prototype.thenToWithErr = function (id) { 125 | var done = false; 126 | var self = this; 127 | this.state.waiting++; 128 | return function (err, result) { 129 | var _self = self; 130 | var _state = _self.state; 131 | _state.waiting--; 132 | if (!done) { 133 | done = true; 134 | _state.id = id; 135 | if (_state.abort_with_error) { 136 | _self.performAbort(); 137 | } else if (arguments.length <= 2) { 138 | // Slightly more efficient in the common case. 139 | _self.fn.call(_self.context, null, err, result); 140 | } else { 141 | var argv = Array.prototype.slice.call(arguments, 0); 142 | argv.unshift(null); // Push the err argument to the explicit range. 143 | _self.fn.apply(_self.context, argv); 144 | } 145 | } else { 146 | console.error('Callback called repeatedly!'); 147 | } 148 | }; 149 | }; 150 | 151 | // StateMachine supports a single global error notification point. 152 | // You can set StateMachine.onerror to an error callback function that 153 | // will be called asynchronously with two arguments - the error and 154 | // the state machine instance within which the error was raised. 155 | // You can use this, for example, to log all such errors. 156 | // 157 | // If this callback is to process an error and err is an instance of 158 | // Error, then an additional '.cspjsStack' property is added. This 159 | // property is an array to which more context will get added as the 160 | // error bubbles up. Each context is expressed in the form - 161 | // task_fn_name: 162 | // where "task_fn_name" is the given name of the async task (so yeah, 163 | // better name your tasks if you want this to be useful) and "id" 164 | // gives the state id responsible for the error. In the case of 165 | // errors raised by "throw", this will refer to the state id immediately 166 | // before the throw. 167 | // 168 | // To locate the specified state, look into the compiled source for 169 | // a "case :" statement under the task named task_fn_name. 170 | // Gathering context this way permits errors to be traced even in 171 | // reorganized code, where source context may or may not be available, 172 | // or JS code may not be stored in files at all. 173 | // 174 | // The overhead of this error context accumulation occurs only at 175 | // error propagation time and almost no cost is added to normal 176 | // control flow. 177 | StateMachine.prototype.callback = function (err) { 178 | this.state.args = Array.prototype.slice.call(arguments); 179 | this.state.err = err; 180 | this.state.strict_unwind = true; 181 | if (err && err instanceof Error) { 182 | err.cspjsStack = err.cspjsStack || []; 183 | err.cspjsStack.push((this.task_fn.name || 'unnamed') + ':' + (this.state.id-1)); 184 | } 185 | err && StateMachine.onerror && nextTick(StateMachine.onerror, err, this); 186 | nextTick(this.boundUnwind); 187 | }; 188 | 189 | StateMachine.prototype.windTo = function (step) { 190 | this.state.isUnwinding = false; 191 | this.goTo(step); 192 | }; 193 | 194 | StateMachine.prototype.unwind = function () { 195 | if (this.state.unwinding.length > 0) { 196 | var where = this.state.unwinding.pop(); 197 | this.state.isUnwinding = true; 198 | if (where.restoreState) { 199 | this.restoreStateVars(where.restoreState); 200 | this.unwind(); 201 | } else if (where.retry) { 202 | this.windTo(where.retry); 203 | } else if (where.phi) { 204 | if (this.state.err || this.state.strict_unwind) { 205 | // If we're strictly unwinding, then regular phi control flow doesn't apply. 206 | nextTick(this.boundUnwind); 207 | } else { 208 | // Normal phi jump. 209 | this.windTo(where.phi); 210 | } 211 | } else if (where.isError) { 212 | if (this.state.err) { 213 | this.state.currentErrorStep = where; 214 | this.goTo(where.step); 215 | } else { 216 | nextTick(this.boundUnwind); 217 | } 218 | } else { 219 | if (where.fn) { 220 | where.fn(); 221 | nextTick(this.boundUnwind); 222 | } else { 223 | this.beginCleanup(where.state); 224 | this.goTo(where.step); 225 | } 226 | } 227 | } else if (!this.state.isFinished) { 228 | this.state.waiting = 0; 229 | this.state.isFinished = true; 230 | this.finalCallback && this.finalCallback.apply(this.context, this.state.args); 231 | } 232 | }; 233 | 234 | StateMachine.prototype.pushCleanupAction = function (context, fn, args) { 235 | var self = this; 236 | self.state.unwinding.push({ 237 | cleanup: true, 238 | fn: function () { 239 | fn.apply(context, args); 240 | } 241 | }); 242 | }; 243 | 244 | StateMachine.prototype.pushCleanupStep = function (id, afterID) { 245 | this.state.unwinding.push({cleanup: true, step: id, state: this.captureStateVars()}); 246 | this.goTo(afterID); 247 | }; 248 | 249 | StateMachine.prototype.pushErrorStep = function (id, retryID) { 250 | this.state.unwinding.push({isError: true, step: id, retryStep: retryID, unwindPoint: this.state.unwinding.length}); 251 | this.goTo(retryID); 252 | }; 253 | 254 | StateMachine.prototype.beginCleanup = function (state) { 255 | this.state.unwinding.push({restoreState: this.captureStateVars()}); 256 | this.restoreStateVars(state); 257 | }; 258 | 259 | // Retry will place the error handler again on the error stack 260 | // and jump to the beginning of the code block that previously 261 | // generated the error. Presumably, some corrective actions have 262 | // been taken already. 263 | StateMachine.prototype.retry = function () { 264 | if (!this.state.currentErrorStep) { 265 | throw new Error('SyntaxError: retry statement can only be used within catch blocks'); 266 | } 267 | 268 | var errorStep = this.state.currentErrorStep; 269 | 270 | // Finally clauses might need to run between the start of the error handler 271 | // and the current retry statement. So we need to unwind through the 272 | // finally clauses before stepping out of the catch block. To do this, 273 | // insert a plain jump into the unwind sequence at the appropriate 274 | // point. And of course, we also restore the error step description 275 | // object on the unwind stack so that the surrounding catch block will 276 | // attempt to handle any new errors that may occur. 277 | this.state.unwinding.splice(errorStep.unwindPoint, 0, errorStep, {retry: errorStep.retryStep}); 278 | 279 | // Enter a "no error" state. 280 | this.state.currentErrorStep = null; 281 | this.state.args = Array.prototype.slice.call(arguments); 282 | this.state.args.unshift(null); 283 | this.state.err = null; 284 | this.state.strict_unwind = true; 285 | 286 | // Begin unwinding through the finallies. 287 | this.phi(); 288 | }; 289 | 290 | // A note on semantics. phi used to be a separate stack, which meant 291 | // that finally blocks that occur within while loops would all execute 292 | // at the end of the while loop only. This is, in general, not desirable 293 | // and it is useful to have the finally code executed once for each 294 | // scope. For this reason, it is better to have the same unwinding 295 | // stack also handle the phi jumps so that error handling code gets 296 | // to run as soon as possible. 297 | // 298 | // Currently, if-then-else, while and switch blocks all delimit scopes 299 | // for execution of finally handlers. 300 | 301 | StateMachine.prototype.pushPhi = function (id, captureState) { 302 | this.state.unwinding.push({phi: id, state: (captureState ? this.captureStateVars() : null)}); 303 | }; 304 | 305 | StateMachine.prototype.phi = function () { 306 | nextTick(this.boundUnwind); 307 | }; 308 | 309 | function JumpTable(id, cases, blockSizes) { 310 | this.id = id; 311 | this.cases = cases; 312 | this.blockSizes = blockSizes; 313 | this.stepIDs = []; 314 | this.beyondID = id; 315 | 316 | var i = 0, j = 0, sum = id + 1, ci; 317 | for (i = 0; i < blockSizes.length; ++i) { 318 | ci = cases[i]; 319 | for (j = 0; j < ci.length; ++j) { 320 | this.stepIDs[ci[j]] = sum; 321 | } 322 | sum += 1 + blockSizes[i]; // +1 for the additional "phi" 323 | } 324 | 325 | this.beyondID = sum; 326 | return this; 327 | } 328 | 329 | JumpTable.prototype.jumpToCase = function (sm, caseVal) { 330 | sm.pushPhi(this.beyondID); 331 | var stepID = this.stepIDs[caseVal]; 332 | if (!stepID) { 333 | throw new Error("Unhandled case '" + caseVal + "' at step " + this.id); 334 | } 335 | sm.goTo(stepID); 336 | }; 337 | 338 | StateMachine.prototype.jumpTable = function (id, cases, blockSizes) { 339 | // cases[i] is an array of case values that all map 340 | // to the same block whose size is given by blockSizes[i]. 341 | if (!cases) { 342 | return this.task_fn.cachedJumpTable[id]; 343 | } 344 | 345 | console.assert(cases.length === blockSizes.length); 346 | 347 | return (this.task_fn.cachedJumpTable[id] = new JumpTable(id, cases, blockSizes)); 348 | }; 349 | 350 | StateMachine.prototype.channel = function () { 351 | return new Channel(); 352 | }; 353 | 354 | StateMachine.prototype.resolve = Channel.resolve; 355 | 356 | module.exports = StateMachine; 357 | -------------------------------------------------------------------------------- /src/stream.js: -------------------------------------------------------------------------------- 1 | 2 | // Extends the Channel class with support 3 | // for Node.js streams. 4 | 5 | var Channel = require('./channel'); 6 | var stream = require('stream'); 7 | 8 | // Non-deterministic behaviour if you create multiple 9 | // writable streams on a single channel. 10 | Channel.prototype.asWritableStream = function () { 11 | var writable = new stream.Writable(); 12 | var chan = this; 13 | writable._write = function (chunk, encoding, done) { 14 | chan.put(chunk, done); 15 | }; 16 | return writable; 17 | }; 18 | 19 | // Non-deterministic behaviour if you make multiple readable streams 20 | // on the same channel. If you want to fan out a channel to multiple 21 | // readable streams, then use Channel.prototype.tap() to tap a channel 22 | // without disrupting its dataflow. 23 | Channel.prototype.asReadableStream = function () { 24 | var readable = new stream.Readable(); 25 | 26 | var chan = this; 27 | 28 | readable._read = function () { 29 | chan.take(receiver); 30 | }; 31 | 32 | function receiver(err, value) { 33 | readable.push(value); 34 | } 35 | 36 | return readable; 37 | }; 38 | 39 | // Simple piping function for continuously reading from 40 | // a readable stream. 41 | Channel.prototype.read = function (readable) { 42 | readable.pipe(this.asWritableStream()); 43 | return this; 44 | }; 45 | 46 | // Simple piping function for continuously writing to 47 | // a writable stream. 48 | Channel.prototype.write = function (writable) { 49 | this.asReadableStream().pipe(writable); 50 | return this; 51 | }; 52 | 53 | module.exports = Channel; 54 | -------------------------------------------------------------------------------- /src/task.js: -------------------------------------------------------------------------------- 1 | // # Macro `task` 2 | // 3 | // `task` is a macro that takes a body that describes a sequence of asynchronous 4 | // operations and expands it to a state machine with very little runtime overhead. 5 | // It is designed so that it can be used with functions that obey the NodeJS style 6 | // callback convention where a callback function of the form `function (err, result) { ... }` 7 | // is passed as the last argument of async calls. A "task" is itself such a function. 8 | // 9 | // In general, a compiled task looks like a function of the form - 10 | // 11 | // function (arg1, arg2, ... , callback) { 12 | // ... state machine code ... 13 | // } 14 | // 15 | // The macro supports the following four forms to provide easy expression of 16 | // pure no-argument scripts and named tasks. 17 | 18 | macro task { 19 | 20 | 21 | // 1. `task { body ... }` produces a `function (callback) { ... }` expression 22 | // 2. `task name { body ... }` produces a `function name(callback) { ... }` declaration. 23 | // 3. `task (arg1, arg2) { body ... }` produces a `function (arg1, arg2, callback) { ... }` expression. 24 | // 4. `task name(arg1, arg2) { body ... }` produces a `function name(arg1, arg2, callback) { ... }` declaration. 25 | // 26 | // The `task` macro goes hand-in-hand with the `Channel` and `StateMachine` modules. 27 | // While the `StateMachine` module is internal and the macro user doesn't need to 28 | // bother about it, the `Channel` module offers a simple way to coordinate multi-tasking 29 | // in JS - in the CSP style of the `Haskell`, `Erlang` and `Go` languages. 30 | 31 | case { $_ { $body ... } } => { 32 | letstx $callback = [makeIdent("callback", #{$_})]; 33 | return #{ 34 | (function ($callback) { 35 | setup_state_machine $_ $callback ($callback) { $body ... } 36 | }) 37 | }; 38 | } 39 | 40 | case { $_ $taskname:ident { $body ... } } => { 41 | letstx $callback = [makeIdent("callback", #{$_})]; 42 | return #{ 43 | function $taskname($callback) { 44 | setup_state_machine $_ $callback ($callback) { $body ... } 45 | } 46 | }; 47 | } 48 | 49 | case { $_ () { $body ... } } => { 50 | letstx $callback = [makeIdent("callback", #{$_})]; 51 | return #{ 52 | (function ($callback) { 53 | setup_state_machine $_ $callback ($callback) { $body ... } 54 | }) 55 | }; 56 | } 57 | 58 | case { $_ ($x:ident (,) ...) { $body ... } } => { 59 | letstx $callback = [makeIdent("callback", #{$_})]; 60 | return #{ 61 | (function ($x (,) ... , $callback) { 62 | setup_state_machine $_ $callback ($x (,) ... , $callback) { $body ... } 63 | }) 64 | }; 65 | } 66 | 67 | case { $_ $taskname:ident() { $body ... } } => { 68 | letstx $callback = [makeIdent("callback", #{$_})]; 69 | return #{ 70 | function $taskname($callback) { 71 | setup_state_machine $_ $callback ($callback) { $body ... } 72 | } 73 | }; 74 | } 75 | 76 | case { $_ $taskname:ident($x:ident (,) ...) { $body ... } } => { 77 | letstx $callback = [makeIdent("callback", #{$_})]; 78 | return #{ 79 | function $taskname($x (,) ... , $callback) { 80 | setup_state_machine $_ $callback ($x (,) ... , $callback) { $body ... } 81 | } 82 | }; 83 | } 84 | } 85 | 86 | // A "task" consists of a sequence of "statements" separated by ";". Each 87 | // statement may be a synchronous action or an asynchronous one, but all 88 | // statements are treated the same by `task`, by inserting an async step 89 | // between them. The following control structures are also supported - 90 | // 91 | // 1. `if { ... }` and `if { ... } else { ... }` 92 | // 2. `while (...) { ... }` 93 | // 3. `for (...;...;...) { ... }` 94 | // 4. `catch (ErrorClass e) { ... }` 95 | // 5. `catch (e) { ... }` 96 | // 6. `finally { ... }` 97 | // 7. `finally func(args ...);` 98 | // 8. `switch (val) { case v1: { } case v2,v3,v4: { } case v5: { } ... }` 99 | // 9. `throw expr;` 100 | // 10. `return expr1 , expr2 , ... ;` 101 | // 102 | // There is no separate `try` statement supported since in my experience 103 | // code that requires a local try-catch within a function almost always 104 | // has a bad design decision in it regarding error management, and/or 105 | // could easily be refactored to make the error concerns clearer. Also, 106 | // syntactically, placing the error handling code encourages postponing 107 | // thinking about error conditions whereas putting catch clauses up front 108 | // forces thinking about them early on .. and close to the code that is 109 | // actually relevant. For example, it is much clearer to state 110 | // "begin a transaction now, if there is any error later on, rollback 111 | // the transaction." which is expressed with this approach as - 112 | // 113 | // var tx = db.begin(); 114 | // catch (e) { 115 | // tx.rollback(); 116 | // } 117 | // ...256 lines of code that can fail... 118 | // 119 | // as opposed to the traditional - 120 | // 121 | // var tx = db.begin(); 122 | // try { 123 | // ...256 lines of code that can fail... 124 | // } catch (e) { 125 | // tx.rollback(); 126 | // throw e; 127 | // } 128 | // 129 | // Note: While there is a `throw e` in the traditional code above, 130 | // there is none in the `catch` clause within a `task`. This is because 131 | // if a catch clause doesn't "handle" the error, it automatically gets 132 | // rethrown. "Handling" an error amounts to `return`ing without an error 133 | // from within a `catch` clause. 134 | // 135 | // The following statement forms are supported within the task body as 136 | // well as within the bodies of the above control structures - 137 | // 138 | // 1. `var x = expr1, y = expr2, ... ;` This is interpreted as declaration 139 | // and initialization of state variables. The initialization part is not 140 | // optional. 141 | // 142 | // 2. `x, y, z <- blah[42].bling().asyncMethod(arg1, arg2);` will insert an 143 | // additional `callback` argument to the method (or function) invocation, 144 | // collect the results passed to the callback of the form 145 | // `function (err, x, y, z) { ... }` and assign them to the state variables 146 | // `x`, `y` and `z`. 147 | // 148 | // 3. `<- blah[42].bling().asyncMethod(arg1, arg2);` will insert a callback 149 | // function of the form `function (err) { ... }` - i.e. no result value 150 | // is expected of the callback. To make this form clearer, you can also 151 | // use `await` instead of the leading `<-`. 152 | // 153 | // 4. `x <- chan EXPR;` expects the expression `EXPR` to evaluate to a 154 | // `Channel` object (see `channel.js`). `x` will be assigned to the 155 | // value produced by the channel when `.take()` is called on it. 156 | // This is a simpler syntax for `var ch = EXPR; x <- ch.take();` 157 | // 158 | // All other statements separated by ";" are treated as synchronous and 159 | // passed through the macro as is. 160 | // 161 | // If you want to work with concurrently executing tasks, use channels 162 | // to coordinate them. Notable, `Channel.merge([ch1, ch2, ...])` 163 | // will make a channel into which all the given channels will be setup 164 | // to pipe their results. The merged channel will yield `{chan: ch, val: value}` 165 | // objects so that you can do different things based on the channel that 166 | // produced the value. 167 | // 168 | // Sometimes, you want to be able to handle an error in a recoverable way 169 | // after the async operation completes. You can use the `<<-` operator for 170 | // that. It works in the same way as the `<-` operator, except that the 171 | // first variable is bound to the error. No async exception is raised with this 172 | // operator. For example - 173 | // 174 | // err, result <<- fs.readFile("somewhere/file.txt", 'utf8'); 175 | // if (err) { 176 | // result = "Default value"; 177 | // } 178 | // 179 | 180 | // ## Setting up the state machine 181 | // 182 | // To setup a state machine, we scan the body to find the machine's state 183 | // variables and declare them up front. This simplifies the need for 184 | // local var declarations in the generated JS ... which are not really 185 | // local anyway. 186 | // 187 | // `step_state` is the real work horse, which 188 | // walks through each statement in the task 189 | // body and compiles it to a single step in 190 | // the state machine. 191 | 192 | macro setup_state_machine { 193 | rule { $task $callback $formals { $body ... } } => { 194 | var StateMachine = arguments.callee.StateMachine || (arguments.callee.StateMachine = require('cspjs/src/state_machine')); 195 | declare_state_arguments $formals ; 196 | var state_machine = new StateMachine(this, $callback, state_machine_fn, arguments.callee); 197 | declare_state_variables $task state_machine 0 ($callback) { $body ... } 198 | function state_machine_fn(err) { 199 | if (err && !state_machine.state.isUnwinding) { return state_machine.callback(err); } 200 | try { 201 | switch (state_machine.state.id) { 202 | case 1: 203 | step_state $task state_machine 1 { $body ... } 204 | } 205 | } catch (e) { 206 | state_machine.callback(e); 207 | } 208 | } 209 | state_machine.start(); 210 | return state_machine.controlAPIMaker; 211 | } 212 | } 213 | 214 | // ## Declaring state variables 215 | // 216 | // To do this, we scan the code and collect all the state variable identifiers 217 | // into a pseudo list syntax that looks like `(x y z ...)`. The `$vars` argument 218 | // to the `declare_state_variables` macro is expected to match this. 219 | 220 | macro declare_state_variables { 221 | rule { $task $state_machine $fin $vars { if ($x ...) { $then ... } else { $else ... } $rest ... } } => { 222 | declare_state_variables $task $state_machine $fin $vars { $then ... $else ... $rest ... } 223 | } 224 | rule { $task $state_machine $fin $vars { if ($x ...) { $then ... } $rest ... } } => { 225 | declare_state_variables $task $state_machine $fin $vars { $then ... $rest ... } 226 | } 227 | // Rewrite for loops using while. 228 | rule { $task $state_machine $fin $vars { for ($init ... ; $cond ... ; $next ...) { $body ... } $rest ... } } => { 229 | declare_state_variables $task $state_machine $fin $vars { $init ... ; while ($cond ...) { $body ... $next ... ; } $rest ... } 230 | } 231 | rule { $task $state_machine $fin $vars { while ($x ...) { $body ... } $rest ... } } => { 232 | declare_state_variables $task $state_machine $fin $vars { $body ... $rest ... } 233 | } 234 | // If a finally block is encountered somewhere in the body, then we 235 | // need to be able to save and restore state variables. So keep track of that. 236 | rule { $task $state_machine $fin $vars { finally { $cleanup ... } $rest ... } } => { 237 | declare_state_variables $task $state_machine 1 $vars { $cleanup ... $rest ... } 238 | } 239 | rule { $task $state_machine $fin $vars { finally $cleanup ... ($args:expr (,) ...) ; $rest ... } } => { 240 | declare_state_variables $task $state_machine $fin $vars { $rest ... } 241 | } 242 | rule { $task $state_machine $fin $vars { catch ($eclass:ident $e:ident) { $handler ... } $rest ... } } => { 243 | declare_state_variables $task $state_machine $fin $vars { var $e = null ; $handler ... $rest ... } 244 | } 245 | rule { $task $state_machine $fin $vars { catch ($e:ident) { $handler ... } $rest ... } } => { 246 | declare_state_variables $task $state_machine $fin $vars { var $e = null ; $handler ... $rest ... } 247 | } 248 | rule { $task $state_machine $fin $vars { switch ($x ...) { $(case $ix:lit (,) ... : { $body ... }) ... } $rest ... } } => { 249 | declare_state_variables $task $state_machine $fin $vars { $($body ...) ... $rest ... } 250 | } 251 | rule { $task $state_machine $fin $vars { $step ... ; $rest ... } } => { 252 | declare_state_variables_step $task $state_machine $fin $vars { $step ... ; } { $rest ... } 253 | } 254 | rule { $task $state_machine $fin $vars { } } => { 255 | declare_unique_varset $task $state_machine $fin $vars ; 256 | } 257 | rule { $task $state_machine $fin () { } } => { 258 | } 259 | } 260 | 261 | // After scanning the entire body, we uniquify the variable set because 262 | // the body may contain multiple declarations of the same variable and 263 | // we don't want to pollute the generated code with repeated var declarations 264 | // as much as we can. 265 | 266 | macro declare_unique_varset { 267 | case { _ $task $state_machine $fin ($v ...) } => { 268 | var vars = #{$v ...}; 269 | var varnames = vars.map(unwrapSyntax); 270 | var uniqvarnames = {}; 271 | varnames.forEach(function (v) { uniqvarnames['%' + v] = true; }); 272 | letstx $uvars ... = Object.keys(uniqvarnames).map(function (v) { return makeIdent(v.substring(1), #{$task}); }); 273 | return #{ declare_varset $task $state_machine $fin ($uvars ...) ; }; 274 | } 275 | } 276 | 277 | macro declare_varset { 278 | rule { $task $state_machine 0 ($v ...) ; } => { 279 | var $v (,) ... ; 280 | } 281 | rule { $task $state_machine 1 ($v ...) ; } => { 282 | var $v (,) ... ; 283 | $state_machine.captureStateVars = function () { 284 | return [$v (,) ...]; 285 | }; 286 | $state_machine.restoreStateVars = function (state) { 287 | var i = 0; 288 | $($v = state[i++];) ... 289 | }; 290 | } 291 | } 292 | 293 | macro declare_state_variables_step { 294 | rule { $task $state_machine $fin ($v ...) { $x:ident := $y ... ; } { $rest ... } } => { 295 | declare_state_variables $task $state_machine $fin ($x $v ...) { $rest ... } 296 | } 297 | rule { $task $state_machine $fin ($v ...) { $x:ident <- $y ... ; } { $rest ... } } => { 298 | declare_state_variables $task $state_machine $fin ($x $v ...) { $rest ... } 299 | } 300 | rule { $task $state_machine $fin ($v ...) { $x:ident <<- $y ... ; } { $rest ... } } => { 301 | declare_state_variables $task $state_machine $fin ($x $v ...) { $rest ... } 302 | } 303 | rule { $task $state_machine $fin ($v ...) { $x:ident (,) ... <- $y ... ; } { $rest ... } } => { 304 | declare_state_variables $task $state_machine $fin ($x ... $v ...) { $rest ... } 305 | } 306 | rule { $task $state_machine $fin ($v ...) { $x:ident (,) ... <<- $y ... ; } { $rest ... } } => { 307 | declare_state_variables $task $state_machine $fin ($x ... $v ...) { $rest ... } 308 | } 309 | rule { $task $state_machine $fin ($v ...) { var $($x:ident = $y:expr) (,) ... ; } { $rest ... } } => { 310 | declare_state_variables $task $state_machine $fin ($x ... $v ...) { $rest ... } 311 | } 312 | rule { $task $state_machine $fin ($v ...) { chan $x:ident (,) ... ; } { $rest ... } } => { 313 | declare_state_variables $task $state_machine $fin ($x ... $v ...) { $rest ... } 314 | } 315 | rule { $task $state_machine $fin $vs { $x ... ; } { $rest ... } } => { 316 | declare_state_variables $task $state_machine $fin $vs { $rest ... } 317 | } 318 | } 319 | 320 | macro declare_state_arguments { 321 | rule { ($x:ident (,) ...) } => { 322 | var argi = 0, $($x = arguments[argi++]) (,) ...; 323 | } 324 | } 325 | 326 | 327 | // ## Compiling the steps of the state machine 328 | // 329 | // The `step_state` macro extracts the relevant bit of code to be compiled into 330 | // a "step" and passes it over to the `step_state_line` macro. This extra layer 331 | // is useful since not all of the syntax in the body of a task are separated by 332 | // ";" markers. The control structures `if`, `while`, `finally` and `catch` do 333 | // not use ";" as separators to keep the code body of a task looking as close 334 | // to traditional javascript as possible. 335 | 336 | macro step_state { 337 | rule { $task $state_machine $id { if ($x ...) { $then ... } else { $else ... } $rest ... } } => { 338 | step_state_line_if_else $task $state_machine $id { if ($x ...) { $then ... } else { $else ... } } { $rest ... } 339 | } 340 | rule { $task $state_machine $id { if ($x ...) { $then ... } $rest ... } } => { 341 | step_state_line_if $task $state_machine $id { if ($x ...) { $then ... } } { $rest ... } 342 | } 343 | // Rewrite for loops using while. 344 | rule { $task $state_machine $id { for ($init ... ; $cond ... ; $next ...) { $body ... } $rest ... } } => { 345 | step_state $task $state_machine $id { $init ... ; while ($cond ...) { $body ... $next ... ; } $rest ... } 346 | } 347 | rule { $task $state_machine $id { while ($x ...) { $body ... } $rest ... } } => { 348 | step_state_line_while $task $state_machine $id { while ($x ...) { $body ... } } { $rest ... } 349 | } 350 | rule { $task $state_machine $id { finally { $cleanup ... } $rest ... } } => { 351 | step_state_line_finally_block $task $state_machine $id { finally { $cleanup ... } } { $rest ... } 352 | } 353 | rule { $task $state_machine $id { finally $cleanup ... ($args:expr (,) ...) ; $rest ... } } => { 354 | step_state_line_finally_expr $task $state_machine $id { finally $cleanup ... ($args (,) ...) ; } { $rest ... } 355 | } 356 | rule { $task $state_machine $id { catch ($x ...) { $handler ... } $rest ... } } => { 357 | step_state_line_catch $task $state_machine $id { catch ($x ...) { $handler ... } } { $rest ... } 358 | } 359 | rule { $task $state_machine $id { switch ($x:expr) { $b ... } $rest ... } } => { 360 | step_state_line_switch $task $state_machine $id { switch ($x) { $b ... } } { $rest ... } 361 | } 362 | rule { $task $state_machine $id { $step ... ; $rest ... } } => { 363 | step_state_line $task $state_machine $id { $step ... ; } { $rest ... } 364 | } 365 | rule { $task $state_machine $id { } } => { 366 | $state_machine.callback(null, true); 367 | break; 368 | } 369 | } 370 | 371 | // ## Counting states 372 | // 373 | // For the control structures that perform branching to different parts of the code, 374 | // we need to be able to determine the state ids of the branch and merge statements. 375 | // `count_states` will count the number of states added by a given block of statements, 376 | // including control structures, so that the jump ahead positions can be determined 377 | // during compilation. 378 | // 379 | // The second argument to `count_states` is a pseudo list of the form `(m n ...)` 380 | // where `m`, `n` are plain integers. The list is summed up at the end by `sumpup_counts` 381 | // to produce the final count. 382 | 383 | macro count_states { 384 | rule { $task ($n ...) { if ($x ...) { $then ... } else { $else ... } $rest ... } } => { 385 | count_states $task (3 $n ...) { $then ... $else ... $rest ... } 386 | } 387 | rule { $task ($n ...) { if ($x ...) { $then ... } $rest ... } } => { 388 | count_states $task (2 $n ...) { $then ... $rest ... } 389 | } 390 | // Rewrite for loops using while. 391 | rule { $task $n { for ($init ... ; $cond ... ; $next ...) { $body ... } $rest ... } } => { 392 | count_states $task $n { $init ... ; while ($cond ...) { $body ... $next ... ; } $rest ... } 393 | } 394 | rule { $task ($n ...) { while ($x ...) { $body ... } $rest ... } } => { 395 | count_states $task (2 $n ...) { $body ... $rest ... } 396 | } 397 | rule { $task ($n ...) { finally { $cleanup ... } $rest ... } } => { 398 | count_states $task (2 $n ...) { $cleanup ... $rest ... } 399 | } 400 | rule { $task ($n ...) { finally $cleanup ... ($args:expr (,) ...) ; $rest ... } } => { 401 | count_states $task (1 $n ...) { $rest ... } 402 | } 403 | rule { $task ($n ...) { catch ($e ...) { $handler ... } $rest ... } } => { 404 | count_states $task (2 $n ...) { $handler ... $rest ... } 405 | } 406 | rule { $task ($n ...) { switch ($x ...) { $(case $ix:lit (,) ... : { $body ... }) ... } $rest ... } } => { 407 | count_states $task (1 $n ...) { $($body ... phi $state_machine ;) ... $rest ... } 408 | } 409 | rule { $task $n { $step ... ; $rest ... } } => { 410 | count_states_line $task $n { $step ... ; } { $rest ... } 411 | } 412 | rule { $task ($n ...) { } } => { 413 | sumup_counts ($n ...) 414 | } 415 | } 416 | 417 | // BUG in sweetjs? Theoretically, it should be possible to merge these into the above 418 | // count_states macro itself, but only this separation works correctly! 419 | macro count_states_line { 420 | rule { $task ($n ...) { $x:ident (,) ... <- chan $y ... ; } { $rest ... } } => { 421 | count_states $task (2 $n ...) { $rest ... } 422 | } 423 | rule { $task ($n ...) { $x:ident (,) ... <- $y ... (); } { $rest ... } } => { 424 | count_states $task (2 $n ...) { $rest ... } 425 | } 426 | rule { $task ($n ...) { $x:ident (,) ... <<- $y ... (); } { $rest ... } } => { 427 | count_states $task (2 $n ...) { $rest ... } 428 | } 429 | rule { $task ($n ...) { $x:ident (,) ... <- $y ... ($args:expr (,) ...); } { $rest ... } } => { 430 | count_states $task (2 $n ...) { $rest ... } 431 | } 432 | rule { $task ($n ...) { $x:ident (,) ... <<- $y ... ($args:expr (,) ...); } { $rest ... } } => { 433 | count_states $task (2 $n ...) { $rest ... } 434 | } 435 | rule { $task ($n ...) { $x:ident := $y ... (); } { $rest ... } } => { 436 | count_states $task (1 $n ...) { $rest ... } 437 | } 438 | rule { $task ($n ...) { $x:ident := $y ... ($args:expr (,) ...); } { $rest ... } } => { 439 | count_states $task (1 $n ...) { $rest ... } 440 | } 441 | rule { $task ($n ...) { $step ... ; } { $rest ... } } => { 442 | count_states $task (1 $n ...) { $rest ... } 443 | } 444 | } 445 | 446 | macro sumup_counts { 447 | case { $_ ($n ...) } => { 448 | var sum = #{$n ...}.map(unwrapSyntax).reduce(function (a,b) { return a + b; }); 449 | letstx $sum = [makeValue(sum, #{$_})]; 450 | return #{$sum}; 451 | } 452 | } 453 | 454 | // ### Branching on conditions 455 | // 456 | // `if { ... } else { ... }` blocks work as expected in normal javascript, except that 457 | // async statements can also be used within them. 458 | 459 | macro step_state_line_if_else { 460 | case { $me $task $state_machine $id { if ($x:expr) { $then ... } else { $else ... } } { $rest ... } } => { 461 | var id = unwrapSyntax(#{$id}); 462 | letstx $id2 = [makeValue(id + 1, #{$id})]; 463 | return #{ 464 | var jumpThen = 1 + (count_states $task (0) { $then ... }); 465 | var jumpElse = 1 + (count_states $task (0) { $else ... }); 466 | $state_machine.pushPhi($id2 + jumpThen + jumpElse); 467 | if (!($x)) { 468 | $state_machine.goTo($id2 + jumpThen); 469 | break; 470 | } 471 | case $id2: 472 | step_state $task $state_machine $id2 { $then ... phi $state_machine ; $else ... phi $state_machine ; $rest ... } 473 | }; 474 | } 475 | } 476 | 477 | macro step_state_line_if { 478 | case { $me $task $state_machine $id { if ($x:expr) { $then ... } } { $rest ... } } => { 479 | var id = unwrapSyntax(#{$id}); 480 | letstx $id2 = [makeValue(id + 1, #{$id})]; 481 | return #{ 482 | var jump = 1 + (count_states $task (0) { $then ... }); 483 | if ($x) { 484 | $state_machine.pushPhi($id2 + jump); 485 | } else { 486 | $state_machine.goTo($id2 + jump); 487 | break; 488 | } 489 | case $id2: 490 | step_state $task $state_machine $id2 { $then ... phi $state_machine; $rest ... } 491 | }; 492 | } 493 | } 494 | 495 | macro step_state_line_switch { 496 | // ### Multi-tasking 497 | // 498 | // `switch (expr) { case 0: { ... } case 1: { ... }}` can be used to manage 499 | // coordination of multiple tasks. The `expr` is an expression whose value 500 | // is matched with the case literals to decide where to branch. The value 501 | // coming in on such a "merged channel" has a `chan` property that gives 502 | // the channel itself that produced the value and a `val` property containing 503 | // the value. You can attach identifiers to your channels and switch based 504 | // on them, or you can using `===` tests on the channels themselves. 505 | // 506 | // There MUST be one `case` clause for each channel in the merge list, or 507 | // an error will be raised at runtime. 508 | // 509 | // You'd use `switch` like this - 510 | // 511 | // function addIndex(chan, ix) { 512 | // chan.ix = ix; 513 | // return chan; 514 | // } 515 | // mch = Channel.merge([ch1, ch2, ... chN].map(addIndex)); 516 | // while (true) { 517 | // x <- chan mch; 518 | // switch (x.chan.ix) { 519 | // case 0: { ... x.val ... } 520 | // case 1: { ... x.val ... } 521 | // } 522 | // } 523 | // 524 | // i.e., for the most part `switch` works like normal in Javascript, except 525 | // that `break;` statements are not needed, and an exception is raised if 526 | // an unhandled case occurs at runtime. 527 | 528 | case { $me $task $state_machine $id { switch ($c:expr) { $(case $ix:lit (,) ... : { $body ... }) ... } } { $rest ... } } => { 529 | var id = unwrapSyntax(#{$id}); 530 | letstx $id2 = [makeValue(id + 1, #{$id})]; 531 | return #{ 532 | var tmp1; 533 | if (!(tmp1 = $state_machine.jumpTable($id))) { 534 | tmp1 = $state_machine.jumpTable($id, [$([$ix (,) ...]) (,) ...], [$((count_states $task (0) { $body ... })) (,) ...]); 535 | } 536 | tmp1.jumpToCase($state_machine, $c); 537 | break; 538 | case $id2: 539 | step_state $task $state_machine $id2 { 540 | $($body ... phi $state_machine ;) ... 541 | $rest ... 542 | } 543 | }; 544 | } 545 | } 546 | 547 | // ### Looping using `while` 548 | // 549 | // The usual `while (cond) { body... }` is supported as well, except that there is no 550 | // `break;' statement support. 551 | 552 | macro step_state_line_while { 553 | case { $me $task $state_machine $id { while ($x:expr) { $body ... } } { $rest ... } } => { 554 | var id = unwrapSyntax(#{$id}); 555 | letstx $id2 = [makeValue(id + 1, #{$id})]; 556 | return #{ 557 | var jumpBody = count_states $task (0) { $body ... }; 558 | if ($x) { 559 | $state_machine.pushPhi($id); 560 | } else { 561 | $state_machine.goTo($id2 + 1 + jumpBody); 562 | break; 563 | } 564 | case $id2: 565 | step_state $task $state_machine $id2 { $body ... phi $state_machine ; $rest ... } 566 | }; 567 | } 568 | } 569 | 570 | // ### Exception mechanism 571 | // 572 | // Error handling inside tasks uses a different and more expressive form of 573 | // exceptions. There is no `try` clause since any statement may throw an 574 | // exception that will be forwarded to the callback provided to the task. 575 | // 576 | // `finally` statements/blocks can be placed anywhere and will register actions 577 | // to be executed before a) reaching the catch clause immediately above or b) 578 | // exiting the block in which they occur. These statements/blocks execute in 579 | // the order opposite to the order in which they were encountered during 580 | // running. If these occur within a loop, then the statements/blocks will 581 | // execute as many times as the loop did, once for every loop iteration. (So be 582 | // aware of what you want to be cleaned up.) 583 | 584 | // `finally funcExpr(args...);` statement causes the `funcExpr` and `args...` to be evaluated 585 | // at the time the statement is encountered, but defers the call itself to be made at unwinding 586 | // time. 587 | // 588 | // `finally obj.method(args...);` is also a supported form. The `obj` and `args` are evaluated 589 | // when the `finally` statement is encountered, but the call itself is performed at cleanup time 590 | // (obviously). 591 | 592 | macro step_state_line_finally_expr { 593 | case { $me $task $state_machine $id { finally $cleanup ... . $methId:ident ($arg:expr (,) ...) ; } { $rest ... } } => { 594 | var id = unwrapSyntax(#{$id}); 595 | letstx $id2 = [makeValue(id + 1, #{$id})]; 596 | /* Evaluate the arguments right now, but call the cleanup function later. */ 597 | return #{ 598 | var tmp1 = $cleanup ... ; 599 | $state_machine.pushCleanupAction(tmp1, tmp1.$methId, [$arg (,) ...]); 600 | case $id2: 601 | step_state $task $state_machine $id2 { $rest ... } 602 | }; 603 | } 604 | case { $me $task $state_machine $id { finally $cleanup ... [ $methExpr:expr ] ($arg:expr (,) ...) ; } { $rest ... } } => { 605 | var id = unwrapSyntax(#{$id}); 606 | letstx $id2 = [makeValue(id + 1, #{$id})]; 607 | /* Evaluate the arguments right now, but call the cleanup function later. */ 608 | return #{ 609 | var tmp1 = $cleanup ... ; 610 | $state_machine.pushCleanupAction(tmp1, tmp1[$methExpr], [$arg (,) ...]); 611 | case $id2: 612 | step_state $task $state_machine $id2 { $rest ... } 613 | }; 614 | } 615 | case { $me $task $state_machine $id { finally $cleanup ... ($arg:expr (,) ...) ; } { $rest ... } } => { 616 | var id = unwrapSyntax(#{$id}); 617 | letstx $id2 = [makeValue(id + 1, #{$id})]; 618 | /* Evaluate the arguments right now, but call the cleanup function later. */ 619 | return #{ 620 | $state_machine.pushCleanupAction(this, $cleanup ... , [$arg (,) ...]); 621 | case $id2: 622 | step_state $task $state_machine $id2 { $rest ... } 623 | }; 624 | } 625 | } 626 | 627 | // `finally { ... }` mark blocks of steps to be run at unwinding time. 628 | 629 | macro step_state_line_finally_block { 630 | case { $me $task $state_machine $id { finally { $cleanup ... } } { $rest ... } } => { 631 | var id = unwrapSyntax(#{$id}); 632 | letstx $id2 = [makeValue(id + 1, #{$id})]; 633 | return #{ 634 | var jumpHandler = count_states $task (0) { $cleanup ... }; 635 | $state_machine.pushCleanupStep($id2, $id2 + 1 + jumpHandler); 636 | break; 637 | case $id2: 638 | step_state $task $state_machine $id2 { $cleanup ... phi $state_machine ; $rest ... } 639 | }; 640 | } 641 | } 642 | 643 | // `catch (e) { ... }` blocks will catch all exceptions thrown by statements 644 | // that follow the block up to the end of the block's scope, bind the error 645 | // to `e` and run the sequence of statements within the `{...}`. 646 | // 647 | // `catch (ErrorClass e) {...}` will catch and handle only those errors `e` 648 | // that satisfy `e instanceof ErrorClass`. Other errors propagate up to catch 649 | // clauses above. 650 | 651 | macro step_state_line_catch { 652 | case { $me $task $state_machine $id { catch ($eclass:ident $e:ident) { $handler ... } } { $rest ... } } => { 653 | var id = unwrapSyntax(#{$id}); 654 | letstx $id2 = [makeValue(id + 1, #{$id})]; 655 | return #{ 656 | var jumpHandler = count_states $task (0) { $handler ... }; 657 | $state_machine.pushErrorStep($id2, $id2 + 1 + jumpHandler); 658 | break; 659 | case $id2: 660 | $e = $state_machine.state.err; 661 | if (!($e && $e instanceof $eclass)) { 662 | $state_machine.phi(); 663 | break; 664 | } 665 | step_state $task $state_machine $id2 { $handler ... phi $state_machine ; $rest ... } 666 | }; 667 | } 668 | 669 | case { $me $task $state_machine $id { catch ($e:ident) { $handler ... } } { $rest ... } } => { 670 | var id = unwrapSyntax(#{$id}); 671 | letstx $id2 = [makeValue(id + 1, #{$id})]; 672 | return #{ 673 | var jumpHandler = count_states $task (0) { $handler ... }; 674 | $state_machine.pushErrorStep($id2, $id2 + 1 + jumpHandler); 675 | break; 676 | case $id2: 677 | $e = $state_machine.state.err; 678 | step_state $task $state_machine $id2 { $handler ... phi $state_machine ; $rest ... } 679 | }; 680 | } 681 | } 682 | 683 | // ## step_state_line 684 | // 685 | // This is the real work horse which walks through each statement and compiles 686 | // it into an asynchronous step in the state machine. 687 | 688 | macro step_state_line { 689 | // ### await 690 | // 691 | // The `await func(args...);` clause is a synonym for `<- func(args...);`. 692 | case { $me $task $state_machine $id { await $y ... (); } { $rest ... } } => { 693 | var id = unwrapSyntax(#{$id}); 694 | letstx $id2 = [makeValue(id + 1, #{$id})]; 695 | return #{ 696 | $y ... ($state_machine.thenTo($id2)); 697 | break; 698 | case $id2: 699 | step_state $task $state_machine $id2 { $rest ... } 700 | }; 701 | } 702 | 703 | case { $me $task $state_machine $id { await $y ... ($args:expr (,) ...); } { $rest ... } } => { 704 | var id = unwrapSyntax(#{$id}); 705 | letstx $id2 = [makeValue(id + 1, #{$id})]; 706 | return #{ 707 | $y ... ($args (,) ... , $state_machine.thenTo($id2)); 708 | break; 709 | case $id2: 710 | step_state $task $state_machine $id2 { $rest ... } 711 | }; 712 | } 713 | 714 | case { $me $task $state_machine $id { await $x:ident ... ; } { $rest ... } } => { 715 | var id = unwrapSyntax(#{$id}); 716 | letstx $id2 = [makeValue(id + 1, #{$id})]; 717 | return #{ 718 | $state_machine.resolve([$x (,) ...], false, $state_machine.thenTo($id2)); 719 | break; 720 | case $id2: 721 | var chans = arguments[1], i = 0; 722 | $($x = chans[i++].resolve();)... 723 | step_state $task $state_machine $id2 { $rest ... } 724 | }; 725 | } 726 | 727 | // ### Taking values from channels 728 | // 729 | // If you have functions that return channels on which they will produce their results, 730 | // then you can use this expression as syntax sugar to get the value out of the returned 731 | // channel. 732 | // 733 | // val <- chan someProcess(arg1, arg1); 734 | 735 | case { $me $task $state_machine $id { $x:ident (,) ... <- chan $y ... ; } { $rest ... } } => { 736 | var id = unwrapSyntax(#{$id}); 737 | letstx $id2 = [makeValue(id + 1, #{$id})], $id3 = [makeValue(id + 2, #{$id})]; 738 | // In this form (ex: z <- chan blah[32].bling(); ), the expression is expected to 739 | // produce a channel, from which a value will be taken. 740 | // 741 | // Type detection is done by looking for a `take` method, so any object that 742 | // has the same `take` protocol as a channel can be used. 743 | return #{ 744 | var tmp1 = $y ...; 745 | if (tmp1 && tmp1.take) { 746 | tmp1.take($state_machine.thenTo($id2)); 747 | } else { 748 | throw new Error('Expected a channel in step ' + $id); 749 | } 750 | break; 751 | case $id2: 752 | var i = 1; 753 | $($x = arguments[i++];) ... 754 | case $id3: 755 | step_state $task $state_machine $id3 { $rest ... } 756 | }; 757 | } 758 | 759 | // ### Retrieving values 760 | // 761 | // Values are retrieved from async steps using the `<-` clause of the form - 762 | // 763 | // x, y, z <- coll[42].thing.asyncMethod(arg1, arg2); 764 | // 765 | // This block and the following are basically the same. The problem is that I don't know 766 | // how to insert the additional callback argument with a preceding comma in one 767 | // case and without one in the other. 768 | // 769 | // If you use ':=' instead of '<-', the operation is started off in parallel and 770 | // the variable on the LHS (only one allowed in this case) will be bound to a new channel 771 | // on which the result can be received. You can subsequently do "await x;" to cause 772 | // x to be bound to the value received on the new channel, and further statements 773 | // can use the value directly. If you have multiple such channels bound to variables 774 | // x, y, z, you can await for a single value from each of them using "await x y z;". 775 | // If any errors occur, an exception will be raised. 776 | case { $me $task $state_machine $id { $x:ident (,) ... <- $y ... (); } { $rest ... } } => { 777 | var id = unwrapSyntax(#{$id}); 778 | letstx $id2 = [makeValue(id + 1, #{$id})], $id3 = [makeValue(id + 2, #{$id})]; 779 | return #{ 780 | $y ... ($state_machine.thenTo($id2)); 781 | break; 782 | case $id2: 783 | var i = 1; 784 | $($x = arguments[i++];) ... 785 | case $id3: 786 | step_state $task $state_machine $id3 { $rest ... } 787 | }; 788 | } 789 | 790 | case { $me $task $state_machine $id { $x:ident (,) ... <<- $y ... (); } { $rest ... } } => { 791 | var id = unwrapSyntax(#{$id}); 792 | letstx $id2 = [makeValue(id + 1, #{$id})], $id3 = [makeValue(id + 2, #{$id})]; 793 | return #{ 794 | $y ... ($state_machine.thenToWithErr($id2)); 795 | break; 796 | case $id2: 797 | var i = 1; 798 | $($x = arguments[i++];) ... 799 | case $id3: 800 | step_state $task $state_machine $id3 { $rest ... } 801 | }; 802 | } 803 | 804 | 805 | case { $me $task $state_machine $id { $x:ident (,) ... <- $y ... ($args:expr (,) ...); } { $rest ... } } => { 806 | var id = unwrapSyntax(#{$id}); 807 | letstx $id2 = [makeValue(id + 1, #{$id})], $id3 = [makeValue(id + 2, #{$id})]; 808 | return #{ 809 | $y ... ($args (,) ... , $state_machine.thenTo($id2)); 810 | break; 811 | case $id2: 812 | var i = 1; 813 | $($x = arguments[i++];) ... 814 | case $id3: 815 | step_state $task $state_machine $id3 { $rest ... } 816 | }; 817 | } 818 | 819 | case { $me $task $state_machine $id { $x:ident (,) ... <<- $y ... ($args:expr (,) ...); } { $rest ... } } => { 820 | var id = unwrapSyntax(#{$id}); 821 | letstx $id2 = [makeValue(id + 1, #{$id})], $id3 = [makeValue(id + 2, #{$id})]; 822 | return #{ 823 | $y ... ($args (,) ... , $state_machine.thenToWithErr($id2)); 824 | break; 825 | case $id2: 826 | var i = 1; 827 | $($x = arguments[i++];) ... 828 | case $id3: 829 | step_state $task $state_machine $id3 { $rest ... } 830 | }; 831 | } 832 | 833 | case { $me $task $state_machine $id { $x:ident := $y ... (); } { $rest ... } } => { 834 | var id = unwrapSyntax(#{$id}); 835 | letstx $id2 = [makeValue(id + 1, #{$id})]; 836 | return #{ 837 | $x = $x || $state_machine.channel(); 838 | $y ... ($x.resolver()); 839 | case $id2: 840 | step_state $task $state_machine $id2 { $rest ... } 841 | }; 842 | } 843 | 844 | case { $me $task $state_machine $id { $x:ident := $y ... ($args:expr (,) ...); } { $rest ... } } => { 845 | var id = unwrapSyntax(#{$id}); 846 | letstx $id2 = [makeValue(id + 1, #{$id})]; 847 | return #{ 848 | $x = $x || $state_machine.channel(); 849 | $y ... ($args (,) ... , $x.resolver()); 850 | case $id2: 851 | step_state $task $state_machine $id2 { $rest ... } 852 | }; 853 | } 854 | 855 | // ### State variable declaration 856 | // 857 | // State variables are shared with expressions in the entire task and can be 858 | // declared anywhere using var statements. Initializers are compulsory. 859 | 860 | case { $me $task $state_machine $id { var $($x:ident = $y:expr) (,) ... ; } { $rest ... } } => { 861 | var id = unwrapSyntax(#{$id}); 862 | letstx $id2 = [makeValue(id + 1, #{$id})]; 863 | return #{ 864 | $($x = $y;) ... 865 | case $id2: 866 | step_state $task $state_machine $id2 { $rest ... } 867 | }; 868 | } 869 | 870 | // Bad idea to use uninitialized vars for channels. 871 | // Now you use "chan x, y, z;" to declare and initialize channels. 872 | case { $me $task $state_machine $id { chan $x:ident (,) ... ; } { $rest ... } } => { 873 | var id = unwrapSyntax(#{$id}); 874 | letstx $id2 = [makeValue(id + 1, #{$id})]; 875 | return #{ 876 | $($x = $x || $state_machine.channel();) ... 877 | case $id2: 878 | step_state $task $state_machine $id2 { $rest ... } 879 | }; 880 | } 881 | 882 | // ### Returning values from a task 883 | // 884 | // `return x, y, ...;` will result in the task winding back up any 885 | // `finally` actions and then providing the given values to the next task 886 | // by calling the last callback argument to the task. Such a statement 887 | // will, obviously, return from within any block within control structures. 888 | // 889 | // Though you can return from anywhere in this implementation, don't return 890 | // from within finally clauses. 891 | 892 | case { $me $task $state_machine $id { return $x:expr (,) ... ; } { $rest ... } } => { 893 | var id = unwrapSyntax(#{$id}); 894 | letstx $id2 = [makeValue(id + 1, #{$id})]; 895 | return #{ 896 | $state_machine.callback(null, $x (,) ...); 897 | break; 898 | case $id2: 899 | step_state $task $state_machine $id2 { $rest ... } 900 | }; 901 | } 902 | 903 | // ### Raising errors 904 | // 905 | // The usual `throw err;` form will cause the error to first bubble up 906 | // the `finally` actions and the installed `catch` sequence and if the 907 | // error survives them all, will be passed on to the task's callback. 908 | // 909 | // Hack: "throw object.err;" can be used as a short hand for 910 | // "if (object.err) { throw object.err; }". i.e. the error is thrown 911 | // only if it is not null or undefined or false. This fits with Node.js's 912 | // callback convention where `err === null` tests whether there is an 913 | // error or not. So throwing a `null` doesn't make sense. 914 | 915 | case { $me $task $state_machine $id { throw $e:expr ; } { $rest ... } } => { 916 | var id = unwrapSyntax(#{$id}); 917 | letstx $id2 = [makeValue(id + 1, #{$id})]; 918 | return #{ 919 | var tmp1 = $e; 920 | if (tmp1) { $state_machine.callback(tmp1); break; } 921 | case $id2: 922 | step_state $task $state_machine $id2 { $rest ... } 923 | }; 924 | } 925 | 926 | // ### Retrying a failed operation. 927 | // 928 | // Within a catch block, you can use the retry statement 929 | // 930 | // retry; 931 | // 932 | // to jump control again to the beginning of the code that 933 | // the catch block traps errors for ... which is immediately 934 | // after the ending brace of the catch block. 935 | 936 | case { $me $task $state_machine $id { retry ; } { $rest ... } } => { 937 | var id = unwrapSyntax(#{$id}); 938 | letstx $id2 = [makeValue(id + 1, #{$id})]; 939 | return #{ 940 | $state_machine.retry(); 941 | break; 942 | case $id2: 943 | step_state $task $state_machine $id2 { $rest ... } 944 | }; 945 | } 946 | 947 | 948 | // ## Internals 949 | // 950 | // ### `phi` 951 | // 952 | // Used to merge states when branching using `if`, `while` and `switch`. 953 | 954 | case { $me $task $state_machine $id { phi $state_machine ; } { $rest ... } } => { 955 | var id = unwrapSyntax(#{$id}); 956 | letstx $id2 = [makeValue(id + 1, #{$id})]; 957 | return #{ 958 | $state_machine.phi(); 959 | break; 960 | case $id2: 961 | step_state $task $state_machine $id2 { $rest ... } 962 | }; 963 | } 964 | 965 | // ### Synchronous statements 966 | // 967 | // Any statement that doesn't match the above structures are considered 968 | // to be executed synchronously. While each sync step is given its own id, 969 | // there isn't an async separation between these steps. The plus side of that 970 | // is that one more event-loop cycle is avoided, but the minus is that we 971 | // lose the otherwise more fine grained multi-tasking we get. 972 | // 973 | // I may change my mind about whether or not to introduce an additional 974 | // async step, but that decision won't impact the meaning of the code. 975 | 976 | case { $me $task $state_machine $id { $x ... ; } { $rest ... } } => { 977 | var id = unwrapSyntax(#{$id}); 978 | letstx $id2 = [makeValue(id + 1, #{$id})]; 979 | return #{ 980 | $x ... ; 981 | case $id2: 982 | step_state $task $state_machine $id2 { $rest ... } 983 | }; 984 | } 985 | } 986 | 987 | export task 988 | 989 | 990 | 991 | -------------------------------------------------------------------------------- /stream.js: -------------------------------------------------------------------------------- 1 | module.exports = require('cspjs/src/stream.js'); // Redirect 2 | -------------------------------------------------------------------------------- /test/channel.sjs: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Channel = require('cspjs/channel'); 3 | 4 | describe('Channel', function () { 5 | describe('#put()', function () { 6 | 7 | it('should not call the callback immediately', function (done) { 8 | var ch = new Channel(); 9 | var cond = false; 10 | ch.put(5, function () { 11 | cond = true; 12 | }); 13 | ch.take(function (err, value) { 14 | cond = true; 15 | done(); 16 | }); 17 | assert.equal(cond, false); 18 | }); 19 | 20 | it('should pass values in the sequence they are received', task { 21 | var ch = new Channel(), 22 | seq = [4, 5, 6, 3, 5, 23, 24, 1000, 73, 42], 23 | taken = [], 24 | i = 0; 25 | for (i = 0; i < seq.length; ++i) { 26 | ch.put(seq[i]); 27 | } 28 | for (i = 0; i < seq.length; ++i) { 29 | v <- chan ch; 30 | taken.push(v); 31 | } 32 | assert.deepEqual(taken, seq); 33 | }); 34 | 35 | }); 36 | 37 | describe('#group()', function () { 38 | it('should collect N objects at a time', task { 39 | var ch = new Channel(), chg = ch.group(3); 40 | for (var i = 0; i < 10; ++i) { 41 | ch.put(i); 42 | } 43 | x <- chan chg; 44 | assert.deepEqual(x, [0,1,2]); 45 | x <- chan chg; 46 | assert.deepEqual(x, [3,4,5]); 47 | x <- chan chg; 48 | assert.deepEqual(x, [6,7,8]); 49 | }); 50 | }); 51 | 52 | describe('#takeN()', function () { 53 | it('should take N values', task { 54 | var ch = new Channel(), seq = [2,3,4,5,6,7,8,9]; 55 | for (var i = 0; i < seq.length; ++i) { 56 | ch.put(seq[i]); 57 | } 58 | x <- ch.takeN(5); 59 | assert.deepEqual(x, [2,3,4,5,6]); 60 | }); 61 | }); 62 | 63 | describe('#fill()', function () { 64 | it('should repeatedly give the same value', task { 65 | var ch = new Channel(); 66 | ch.fill(42); 67 | x <- ch.takeN(5); 68 | assert.deepEqual(x, [42,42,42,42,42]); 69 | }); 70 | }); 71 | 72 | describe('#timeout()', function () { 73 | it('should wait a while to yield a value', task { 74 | var ch = Channel.timeout(100); 75 | var start = Date.now(); 76 | <- chan ch; 77 | var end = Date.now(); 78 | assert.ok(end - start > 90); 79 | }); 80 | }); 81 | 82 | describe('#stream()', function () { 83 | it('should have a fixed backlog', task { 84 | var ch = new Channel(); 85 | ch.stream([1,2,3,4,5,6,7,8,9]); 86 | assert.equal(ch.backlog(), 1); 87 | x <- ch.takeN(3); 88 | assert.deepEqual(x, [1,2,3]); 89 | assert.equal(ch.backlog(), 1); 90 | }); 91 | }); 92 | 93 | describe('#merge()', function () { 94 | it('should pass values from any channel', task { 95 | var chs = [100, 50, 200].map(Channel.timeout); 96 | var chm = Channel.merge(chs); 97 | x <- chan chm; 98 | assert.equal(x.chan, chs[1]); 99 | x <- chan chm; 100 | assert.equal(x.chan, chs[0]); 101 | x <- chan chm; 102 | assert.equal(x.chan, chs[2]); 103 | }); 104 | }); 105 | 106 | describe('#debounce()', function () { 107 | it('should not pass events too close in time', task { 108 | var ch = (new Channel()).debounce(100); 109 | ch.put(100); 110 | <- chan Channel.timeout(50); 111 | ch.put(200); 112 | <- chan Channel.timeout(200); 113 | ch.put(300); 114 | x <- chan ch; 115 | assert.equal(x, 200); 116 | x <- chan ch; 117 | assert.equal(x, 300); 118 | }); 119 | }); 120 | 121 | describe('#buffer()', function () { 122 | it('should not wait if the channel backlog is smaller than the buffer length', task { 123 | var ch = (new Channel()).buffer(5); 124 | await ch.put(1); 125 | await ch.put(2); 126 | await ch.put(3); 127 | await ch.put(4); 128 | await ch.put(5); // None of these puts should result in a wait. 129 | assert.equal(ch.backlog(), 5); 130 | task { <- chan Channel.timeout(100); <- chan ch; }(); 131 | var start = Date.now(); 132 | await ch.put(6); // .. but this put should wait for a read. 133 | var end = Date.now(); 134 | assert.ok(end - start > 90); 135 | }); 136 | }); 137 | 138 | describe('#droppingBuffer()', function () { 139 | it('should drop puts after N if no taking is happening', task { 140 | var ch = (new Channel()).droppingBuffer(4); 141 | await ch.put(1); 142 | await ch.put(2); 143 | await ch.put(3); 144 | await ch.put(4); 145 | await ch.put(5); 146 | await ch.put(6); // None of these puts should result in a wait. 147 | x <- ch.takeN(4); 148 | await ch.put(7); 149 | await ch.put(8); 150 | assert.deepEqual(x, [1,2,3,4]); 151 | x <- ch.takeN(2); 152 | assert.deepEqual(x, [7,8]); 153 | }); 154 | }); 155 | 156 | describe('#expiringBuffer()', function () { 157 | it('should drop puts after N if no taking is happening', task { 158 | var ch = (new Channel()).expiringBuffer(4); 159 | await ch.put(1); 160 | await ch.put(2); 161 | await ch.put(3); 162 | await ch.put(4); 163 | await ch.put(5); 164 | await ch.put(6); // None of these puts should result in a wait. 165 | x <- ch.takeN(4); 166 | await ch.put(7); 167 | await ch.put(8); 168 | assert.deepEqual(x, [3,4,5,6]); 169 | }); 170 | }); 171 | 172 | describe('#map()', function () { 173 | it('should apply given transformation to values on the channel', task { 174 | var ch = new Channel(), ch2 = ch.map(function (x) { return x * x; }); 175 | ch.put(1); 176 | ch.put(2); 177 | ch.put(3); 178 | ch.put(4); 179 | squares <- ch2.takeN(4); 180 | assert.deepEqual(squares, [1,4,9,16]); 181 | }); 182 | }); 183 | 184 | describe('#reduce()', function () { 185 | it('should apply given reduction to values on the channel', task { 186 | var ch = new Channel(), ch2 = ch.reduce(0, function (sum, x) { return sum + x; }); 187 | ch.put(1); 188 | ch.put(2); 189 | ch.put(3); 190 | ch.put(4); 191 | sum <- ch2.takeN(4); 192 | assert.deepEqual(sum, [1,3,6,10]); 193 | }); 194 | }); 195 | 196 | describe('#filter()', function () { 197 | it('should only pass on values that satisfy the filter', task { 198 | var ch = new Channel(), ch2 = ch.filter(function (x) { return x % 2 === 1; }); 199 | ch.put(1); 200 | ch.put(2); 201 | ch.put(3); 202 | ch.put(4); 203 | ch.put(5); 204 | ch.put(6); 205 | values <- ch2.takeN(3); 206 | assert.deepEqual(values, [1,3,5]); 207 | }); 208 | }); 209 | 210 | describe('#group()', function () { 211 | it('should group values', task { 212 | var ch = new Channel(), ch2 = ch.group(3); 213 | ch.put(1); 214 | ch.put(2); 215 | ch.put(3); 216 | ch.put(4); 217 | ch.put(5); 218 | ch.put(6); 219 | values <- ch2.takeN(2); 220 | assert.deepEqual(values, [[1,2,3],[4,5,6]]); 221 | }); 222 | }); 223 | }); 224 | -------------------------------------------------------------------------------- /test/control.sjs: -------------------------------------------------------------------------------- 1 | var Channel = require('cspjs/channel'); 2 | var StateMachine = require('cspjs/src/state_machine'); 3 | var assert = require('assert'); 4 | 5 | describe('errors', function () { 6 | describe('catch', function () { 7 | it('must reject errors that are not of the declared class', task { 8 | catch (e) { 9 | return true; 10 | } 11 | 12 | catch (Error e) { 13 | assert.fail(); 14 | } 15 | 16 | throw "boom!"; 17 | }); 18 | 19 | it('must accept errors of the declared class', task { 20 | catch (e) { 21 | assert.fail(); 22 | } 23 | 24 | catch (Error e) { 25 | return true; 26 | } 27 | 28 | throw new Error("boom!"); 29 | }); 30 | 31 | it('must bubble up an error if a catch has no return in it', task { 32 | catch (e) { 33 | assert.equal(e, "boom!"); 34 | return true; 35 | } 36 | 37 | catch (e) { 38 | assert.equal(e, "boom!"); 39 | e = "poof!"; 40 | } 41 | 42 | throw "boom!"; 43 | }); 44 | 45 | it('must bubble up unhandled errors in reverse order', task { 46 | var arr = []; 47 | catch (e) { 48 | assert.deepEqual(arr, [3,2,1]); 49 | return true; 50 | } 51 | 52 | catch (e) { arr.push(1); } 53 | catch (e) { arr.push(2); } 54 | catch (e) { arr.push(3); } 55 | 56 | throw "boom!"; 57 | }); 58 | 59 | it('must be limited to if block', task { 60 | var reached = false; 61 | catch (e) { 62 | assert.equal(reached, false); 63 | return true; 64 | } 65 | 66 | if (true) { 67 | catch (e) { 68 | reached = true; 69 | } 70 | } 71 | 72 | throw "boom!"; 73 | }); 74 | 75 | it('must be limited to else block', task { 76 | var reached = false; 77 | catch (e) { 78 | assert.equal(reached, false); 79 | return true; 80 | } 81 | 82 | if (false) { 83 | } else { 84 | catch (e) { 85 | reached = true; 86 | } 87 | } 88 | 89 | throw "boom!"; 90 | 91 | }); 92 | 93 | it('must be limited to while loop scope', task { 94 | var reached = false; 95 | catch (e) { 96 | assert.equal(reached, false); 97 | return true; 98 | } 99 | 100 | var n = 4; 101 | while (n-- > 0) { 102 | catch (e) { 103 | reached = true; 104 | } 105 | } 106 | 107 | throw "boom!"; 108 | }); 109 | 110 | it('must be limited to for loop scope', task { 111 | var reached = false; 112 | catch (e) { 113 | assert.equal(reached, false); 114 | return true; 115 | } 116 | 117 | for (var n = 4; n > 0; n--) { 118 | catch (e) { 119 | reached = true; 120 | } 121 | } 122 | 123 | throw "boom!"; 124 | }); 125 | 126 | it('must be limited to switch block scope', task { 127 | var reached = false; 128 | catch (e) { 129 | assert.equal(reached, false); 130 | return true; 131 | } 132 | 133 | switch (1) { 134 | case 1: { 135 | catch (e) { 136 | reached = true; 137 | } 138 | } 139 | case 2: { 140 | catch (e) { 141 | reached = true; 142 | } 143 | } 144 | } 145 | 146 | throw "boom!"; 147 | }); 148 | 149 | it('must permit retries', task retries { 150 | var tries = 1; 151 | var arr = []; 152 | catch (e) { 153 | ++tries; 154 | if (tries < 5) { 155 | retry; 156 | } 157 | assert.deepEqual(arr, [1,2,3,4]); 158 | return true; 159 | } 160 | 161 | arr.push(tries); 162 | throw "bomb!"; 163 | }); 164 | }); 165 | 166 | describe('finally_block', function () { 167 | it('must unwind in reverse order on normal return', task { 168 | var subtask = task { 169 | var arr = []; 170 | finally { arr.push(1); } 171 | finally { arr.push(2); } 172 | finally { arr.push(3); } 173 | return arr; 174 | }; 175 | 176 | arr <- subtask(); 177 | assert.deepEqual(arr, [3,2,1]); 178 | }); 179 | 180 | it('must unwind in reverse order', task { 181 | var arr = []; 182 | catch (e) { 183 | assert.deepEqual(arr, [3, 2, 1]); 184 | return true; 185 | } 186 | finally { arr.push(1); } 187 | finally { arr.push(2); } 188 | finally { arr.push(3); } 189 | throw "error"; 190 | }); 191 | 192 | it('must keep state variables intact during unwinding', task { 193 | var arr = []; 194 | catch (e) { 195 | assert.deepEqual(arr, [1,2,3]); 196 | return true; 197 | } 198 | for (var i = 1; i <= 3; ++i) { 199 | finally { arr.push(i); } 200 | } 201 | throw "error"; 202 | }); 203 | }); 204 | 205 | describe('finally_statement', function () { 206 | it('must unwind in reverse order on normal return', task { 207 | var subtask = task { 208 | var arr = []; 209 | finally arr.push(1); 210 | finally arr.push(2); 211 | finally arr.push(3); 212 | return arr; 213 | }; 214 | 215 | arr <- subtask(); 216 | assert.deepEqual(arr, [3,2,1]); 217 | }); 218 | 219 | it('must unwind in reverse order', task { 220 | var arr = []; 221 | catch (e) { 222 | assert.deepEqual(arr, [3, 2, 1]); 223 | return true; 224 | } 225 | finally arr.push(1); 226 | finally arr.push(2); 227 | finally arr.push(3); 228 | throw "error"; 229 | }); 230 | 231 | it('must keep state variables intact during unwinding', task { 232 | var arr = []; 233 | catch (e) { 234 | assert.deepEqual(arr, [1, 2, 3]); 235 | return true; 236 | } 237 | for (var i = 1; i <= 3; ++i) { 238 | finally arr.push(i); 239 | } 240 | throw "error"; 241 | }); 242 | }); 243 | 244 | describe('catch_finally', function () { 245 | it('finally and catch must execute in reverse order', task { 246 | var arr = []; 247 | catch (e) { 248 | assert.deepEqual(arr, [3,2,1]); 249 | return true; 250 | } 251 | 252 | finally { arr.push(1); } 253 | catch (e) { arr.push(2); } 254 | finally { arr.push(3); } 255 | 256 | throw "boom!"; 257 | }); 258 | it('finally must execute even when catch returns', task { 259 | var subtask = task { 260 | var arr = []; 261 | finally { arr.push(1); } 262 | catch (e) { arr.push(2); return arr; } 263 | finally { arr.push(3); } 264 | throw "boom!"; 265 | }; 266 | 267 | arr <- subtask(); 268 | assert.deepEqual(arr, [3,2,1]); 269 | }); 270 | }); 271 | }); 272 | 273 | describe('if_then_else', function () { 274 | describe('if_then', function () { 275 | it('must branch on the condition being truthy', task { 276 | var branched = false; 277 | if (2 + 3 === 5) { 278 | branched = true; 279 | } 280 | assert.ok(branched); 281 | }); 282 | it('must branch on the condition being truthy', task { 283 | var branched = false; 284 | if (2 + 3 < 5) { 285 | branched = true; 286 | throw "boom!"; 287 | } 288 | assert.ok(branched === false); 289 | }); 290 | 291 | function mockAsync(callback) { 292 | process.nextTick(function () { 293 | callback(null, 42); 294 | }); 295 | } 296 | 297 | it('must not touch variables if a branch is not taken', task { 298 | var value = 24; 299 | if (value > 30) { 300 | value <- mockAsync(); 301 | } 302 | assert.equal(value, 24); 303 | }); 304 | 305 | it('must modify a variable if a branch with an async assignment is taken', task { 306 | var value = 24; 307 | if (value < 30) { 308 | value <- mockAsync(); 309 | } 310 | assert.equal(value, 42); 311 | }); 312 | 313 | it('must not modify the way return works', task { 314 | var subtask = task { 315 | var branched = false; 316 | if (2 + 3 < 6) { 317 | branched = true; 318 | return branched; 319 | } 320 | console.log('You should not see this message!'); 321 | return false; 322 | }; 323 | result <- subtask(); 324 | assert.equal(result, true); 325 | }); 326 | }); 327 | describe('else', function () { 328 | it('must branch on the condition being truthy', task { 329 | var branched = null; 330 | if (2 + 3 === 5) { 331 | branched = "then"; 332 | } else { 333 | branched = "else"; 334 | } 335 | assert.equal(branched, "then"); 336 | }); 337 | it('must branch on the condition being truthy', task { 338 | var branched = null; 339 | if (2 + 3 < 5) { 340 | branched = "then"; 341 | throw "boom!"; 342 | } else { 343 | branched = "else"; 344 | } 345 | assert.equal(branched, "else"); 346 | }); 347 | it('must not modify the way return works', task { 348 | var subtask = task { 349 | var branched = false; 350 | if (2 + 3 < 5) { 351 | console.log('dummy'); 352 | } else { 353 | branched = true; 354 | return branched; 355 | } 356 | console.log('You should not see this message!'); 357 | return false; 358 | }; 359 | result <- subtask(); 360 | assert.equal(result, true); 361 | }); 362 | }); 363 | }); 364 | 365 | describe('switch', function () { 366 | it('must work within for loop', task { 367 | var i = 0, arr = []; 368 | finally { 369 | assert.deepEqual(arr, ["one", "two", "three", "end"]); 370 | } 371 | for (i = 0; i < 3; ++i) { 372 | switch (i) { 373 | case 0: { arr.push("one"); } 374 | case 1: { arr.push("two"); } 375 | case 2: { arr.push("three"); } 376 | } 377 | } 378 | arr.push("end"); 379 | }); 380 | it('must switch correctly on numeric index', task { 381 | var choice = {1: "one", 2: "two", 3: "three"}; 382 | for (var key = 0, value; key < 3; ++key) { 383 | switch (key) { 384 | case 0: { value = "one"; } 385 | case 1: { value = "two"; } 386 | case 2: { value = "three"; } 387 | } 388 | assert.equal(value, choice[key+1]); 389 | } 390 | }); 391 | it('must switch correctly on string index', task { 392 | var choice = {"ek": "one", "do": "two", "teen": "three"}; 393 | var keys = Object.keys(choice); 394 | for (var key = 0, value; key < 3; ++key) { 395 | switch (keys[key]) { 396 | case "do": { value = "two"; } 397 | case "ek": { value = "one"; } 398 | case "teen": { value = "three"; } 399 | } 400 | assert.equal(value, choice[keys[key]]); 401 | } 402 | }); 403 | it('must throw an error on unhandled integer case', task { 404 | catch (Error e) { 405 | return true; 406 | } 407 | 408 | switch (3) { 409 | case 1, 2: { return true; } 410 | } 411 | 412 | assert.fail(); 413 | }); 414 | it('must throw an error on unhandled string case', task { 415 | catch (Error e) { 416 | return true; 417 | } 418 | 419 | switch ("three") { 420 | case "one", "two": { return true; } 421 | } 422 | 423 | assert.fail(); 424 | }); 425 | }); 426 | 427 | describe('dfvars', function () { 428 | function greet(msg, callback) { 429 | setTimeout(callback, 10, null, msg); 430 | } 431 | 432 | describe(':=', function () { 433 | it('must declare and initialize a channel variable', task { 434 | x := greet('hello'); 435 | assert.ok(x instanceof Channel); 436 | }); 437 | 438 | it('must run in parallel', task { 439 | x := greet('one'); 440 | y := greet('two'); 441 | assert.ok(x instanceof Channel); 442 | assert.ok(y instanceof Channel); 443 | await x y; 444 | assert.equal(x, 'one'); 445 | assert.equal(y, 'two'); 446 | }); 447 | 448 | it('must also work across tasks', task { 449 | var t1 = task (ch1, ch2) { 450 | ch1 := greet('hello'); 451 | await ch1; 452 | assert.equal(ch1, 'hello'); 453 | ch2 := greet('world'); 454 | await ch2; 455 | assert.equal(ch2, 'world'); 456 | }; 457 | 458 | var x = new Channel(), y = new Channel(); 459 | t1(x, y); 460 | await x y; 461 | assert.equal(x, 'hello'); 462 | assert.equal(y, 'world'); 463 | }); 464 | 465 | }); 466 | 467 | describe('chan', function () { 468 | it('must declare channels', task { 469 | var t1 = task (ch1, ch2) { 470 | ch1 := greet('hello'); 471 | await ch1; 472 | assert.equal(ch1, 'hello'); 473 | ch2 := greet('world'); 474 | await ch2; 475 | assert.equal(ch2, 'world'); 476 | }; 477 | 478 | chan x, y; 479 | t1(x, y); 480 | await x y; 481 | assert.equal(x, 'hello'); 482 | assert.equal(y, 'world'); 483 | }); 484 | }); 485 | 486 | describe('<<-', function () { 487 | it('must return err instead of throwing up', task { 488 | var t1 = task (arg) { 489 | throw "error: " + arg; 490 | }; 491 | 492 | err, result <<- t1("meow"); 493 | assert.equal(err, "error: meow"); 494 | assert.equal(result, undefined); 495 | }); 496 | }); 497 | 498 | describe('StateMachine.onerror', function () { 499 | var errorCount = 0; 500 | StateMachine.onerror = function (err, sm) { 501 | ++errorCount; 502 | }; 503 | it('must be called on any error, anywhere', task { 504 | var currentError = errorCount; 505 | catch (e) { 506 | assert.equal(errorCount, currentError + 1); 507 | return true; 508 | } 509 | 510 | throw new Error('test'); 511 | }); 512 | }); 513 | 514 | describe('cspjsStack', function () { 515 | task t1 { 516 | await t2(); 517 | } 518 | task t2 { 519 | await t3(); 520 | } 521 | task t3 { 522 | throw new Error('just for kicks'); 523 | } 524 | 525 | it('must show nested async calls on error return', task asyncStackTest { 526 | catch (e) { 527 | assert.deepEqual(e.cspjsStack, [ 528 | 't3:0', 529 | 't2:1', 530 | 't1:1', 531 | 'asyncStackTest:6', 532 | ]); 533 | return true; 534 | } 535 | 536 | var someVar = "someValue"; 537 | await t1(); 538 | }); 539 | }); 540 | }); 541 | --------------------------------------------------------------------------------