├── .gitignore ├── README.md ├── project.clj ├── src ├── cljs │ ├── node.cljs │ └── node_macros.clj └── redlobster │ ├── events.cljs │ ├── http.cljs │ ├── https.cljs │ ├── io.cljs │ ├── macros.clj │ ├── mongo.cljs │ ├── promise.cljs │ └── stream.cljs └── test ├── phantom └── test.js └── redlobster ├── events_test.cljs └── promise_test.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | /js 6 | pom.xml 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | /.repl 13 | /node_modules 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Red Lobster 2 | 3 | ![Dog Fort](https://raw.github.com/bodil/dogfort/master/dogfort.jpg) 4 | 5 | Red Lobster is a toolkit for working asynchronously on Node in 6 | ClojureScript, and is the mechanism through which 7 | [Dog Fort](https://github.com/bodil/dogfort) gets things done. It 8 | wraps Node's `EventEmitter` and `Stream` types, and provides some 9 | useful abstractions; in particular, promises. 10 | 11 | ## Promises 12 | 13 | A Red Lobster promise is much like a promise in Clojure, except 14 | instead of running in its own thread, it can be realised from async 15 | code, and you can attach event listeners to it to respond to its 16 | realisation. 17 | 18 | ```clojure 19 | (ns user 20 | (:require [redlobster.promise :as p])) 21 | 22 | (def my-promise (p/promise)) 23 | 24 | (p/on-realised my-promise 25 | #(print (str "promise succeeded: " %)) 26 | #(print (str "promise failed: " %))) 27 | 28 | (p/realise my-promise "cheezburger") 29 | ;; prints "promise succeeded: cheezburger" 30 | ``` 31 | 32 | ### The Short Form 33 | 34 | There's also a `promise` macro that helps you write async code to 35 | realise a promise. The macro returns a new promise, and takes a set of 36 | forms that it executes immediately, and makes two functions `realise` 37 | and `realise-error` available inside the macro's scope for realising 38 | the promise. This lets you conveniently realise a promise through 39 | multiple levels of callbacks. 40 | 41 | ```clojure 42 | (ns user 43 | (:require [redlobster.promise :as p]) 44 | (:use-macros [redlobster.macros :only [promise]])) 45 | 46 | (def fs (js/require "fs")) 47 | 48 | (defn read-file [path] 49 | (promise 50 | (.readFile fs path 51 | (fn [err data] 52 | (if err 53 | (realise-error err) 54 | (realise data)))))) 55 | 56 | (def file-promise (read-file "/etc/passwd")) 57 | (p/on-realised file-promise 58 | #(print (str "File contents:\n" %)) 59 | #(print "Error reading file!")) 60 | ``` 61 | 62 | ### Dereferencing Promises 63 | 64 | Promises can also, obvoiusly, be dereferenced, but, unlike Clojure 65 | promises, this doesn't block until the promise has been realised. 66 | Notice that dereferencing doesn't distinguish between success or error 67 | states; you'll have to use the `failed?` function to determine whether 68 | the promise failed. 69 | 70 | ```clojure 71 | (def my-promise (p/promise)) 72 | @my-promise 73 | ; => :redlobster.promise/not-realised 74 | 75 | (p/realise my-promise "like a boss") 76 | @my-promise 77 | ; => "like a boss" 78 | ``` 79 | 80 | ### Chaining Promises 81 | 82 | A promise can also be chained to another promise, either through 83 | simply calling `realise` with a new promise as the realised value, 84 | which will automatically realise the promise with the new promise's 85 | value once that promise realises, or through the `waitp` macro, which 86 | takes a promise, a success handler and an error handler, and returns a 87 | new promise bound to the original promise, realised through the same 88 | `realise` function the `promise` macro makes available: 89 | 90 | ```clojure 91 | (ns user 92 | (:require [redlobster.promise :as p]) 93 | (:use-macros [redlobster.macros :only [promise waitp]])) 94 | 95 | (defn read-file-or-default [path] 96 | (let [file-promise (read-file path)] 97 | (waitp file-promise 98 | #(realise %) 99 | #(realise "default content")))) 100 | ``` 101 | 102 | ### Waiting For Promises 103 | 104 | There's a `when-realised` macro which lets you create a promise that 105 | waits for a list of other promises to finish before evaluating its 106 | body and realising the new promise with the result of the evaluation. 107 | This is useful when waiting for a number of async operations to 108 | finish. 109 | 110 | ```clojure 111 | (ns user 112 | (:require [redlobster.promise :as p]) 113 | (:use-macros [redlobster.macros :only [when-realised]])) 114 | 115 | (let [file-promise (read-file "/etc/passwd")] 116 | (when-realised [file-promise] 117 | (.write (.-stdout js/process) @file-promise))) 118 | ; writes the contents of /etc/passwd to stdout. 119 | ; returns a promise that will realise when the code has executed. 120 | ``` 121 | 122 | The `let` + `when-realised` construction above is a common pattern, so 123 | there's a `let-realised` macro for combining the two. The example 124 | above would have been better written like this: 125 | 126 | ```clojure 127 | (ns user 128 | (:require [redlobster.promise :as p]) 129 | (:use-macros [redlobster.macros :only [let-realised]])) 130 | 131 | (let-realised 132 | [file-promise (read-file "/etc/passwd")] 133 | (.write (.-stdout js/process) @file-promise)) 134 | ``` 135 | 136 | ### Waiting And Chaining 137 | 138 | Because `when-realised` and `let-realised` return promises that are 139 | realised to the result of evaluating their bodies, and because you can 140 | chain promises together by realising a promise with another promise, 141 | you can easily create multi-step promises like this: 142 | 143 | ```clojure 144 | (ns user 145 | (:require [redlobster.promise :as p]) 146 | (:use-macros [redlobster.macros :only [let-realised]])) 147 | 148 | (let-realised 149 | [filename-promise (read-file "/tmp/filename-inside.txt")] 150 | (let-realised 151 | [file-promise (read-file @filename-promise)] 152 | (.write (.-stdout js/process) @file-promise))) 153 | ; reads a filename from /tmp/filename-inside.txt, and then 154 | ; reads the contents of that file, printing the result to stdout. 155 | ``` 156 | 157 | ### Wrapping Node Callbacks 158 | 159 | A very common idiom in Node is the error/result callback. A function 160 | takes a callback as its last argument, which in turn takes two 161 | arguments: an error argument, which will be null upon success, and a 162 | result argument. Callbacks start with handling any non-null error, and 163 | proceed with dealing with the result if there was no error. 164 | 165 | ```javascript 166 | fs.readFile("/etc/passwd", function(error, result) { 167 | if (error) throw error; 168 | console.log(result); 169 | }); 170 | ``` 171 | 172 | When we're using promises instead of callbacks, it's generally useful 173 | to wrap constructs like these in a promise. That's easily accomplished 174 | by using the `defer-node` macro. For instance, it lets us rewrite the 175 | `read-file` function from the previous examples very succinctly: 176 | 177 | ```clojure 178 | (ns user 179 | (:require [redlobster.promise :as p]) 180 | (:use-macros [redlobster.macros :only [let-realised defer-node]])) 181 | 182 | (def fs (js/require "fs")) 183 | 184 | (defn read-file [path] 185 | (defer-node (.readFile fs path))) 186 | ``` 187 | 188 | You can pass a function as a second argument to `defer-node`, which 189 | will be applied to the result of the operation prior to realising the 190 | promise. An excellent candidate for this would be `js->clj`, but for 191 | the sake of example, let's make the `read-file` function more shouty. 192 | 193 | ```clojure 194 | (ns user 195 | (:require [redlobster.promise :as p] 196 | [clojure.string :as str]) 197 | (:use-macros [redlobster.macros :only [let-realised defer-node]])) 198 | 199 | (def fs (js/require "fs")) 200 | 201 | (defn read-file [path] 202 | (defer-node (.readFile fs path) str/upper-case)) 203 | ``` 204 | 205 | At this point you're probably thinking, "wait, what if I use that 206 | function to transform the result into another promise?" Of course, 207 | that's an excellent way of chaining together Node API operations; 208 | let's rewrite the chaining example above using this technique. 209 | 210 | ```clojure 211 | (ns user 212 | (:require [redlobster.promise :as p]) 213 | (:use-macros [redlobster.macros :only [defer-node]])) 214 | 215 | (def fs (js/require "fs")) 216 | 217 | (defer-node (.readFile fs "/tmp/filename-inside.txt") 218 | (fn [result] (defer-node (.readFile fs result) 219 | (fn [result] (.write (.-stdout js/process) result))))) 220 | ``` 221 | 222 | # License 223 | 224 | Copyright 2012 Bodil Stokke and Matthew Molloy 225 | 226 | Licensed under the Apache License, Version 2.0 (the "License"); you 227 | may not use this file except in compliance with the License. You may 228 | obtain a copy of the License at 229 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). 230 | 231 | Unless required by applicable law or agreed to in writing, software 232 | distributed under the License is distributed on an "AS IS" BASIS, 233 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 234 | implied. See the License for the specific language governing 235 | permissions and limitations under the License. 236 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject redlobster "0.2.4" 2 | :description "Promises for ClojureScript" 3 | :url "https://github.com/bodil/redlobster" 4 | :license {:name "Apache License, version 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 6 | :plugins [[lein-cljsbuild "0.3.0"] 7 | [lein-npm "0.6.1"]] 8 | :npm {:dependencies [[mongodb "2.0.42"]]} 9 | :profiles 10 | {:dev 11 | {:dependencies [[org.bodil/cljs-noderepl "0.1.6"] 12 | [com.cemerick/piggieback "0.0.2"] 13 | [org.clojure/clojurescript "1.7.170"] 14 | [org.clojure/clojure "1.7.0"] 15 | ] 16 | :plugins [[org.bodil/lein-noderepl "0.1.6"]] 17 | ; :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} 18 | }} 19 | 20 | :cljsbuild {:test-commands 21 | {"phantom" ["phantomjs" "test/phantom/test.js"] 22 | "node" ["node" "js/test.js"] 23 | "nashorn" ["jjs" "js/test.js"]} 24 | :builds {:dev {:source-paths ["src"] 25 | :compiler 26 | {:output-to "js/main.js" 27 | :optimizations :simple 28 | :pretty-print true 29 | :jar true}} 30 | :test {:source-paths ["test"] 31 | :compiler 32 | {:output-to "js/test.js" 33 | :optimizations :simple 34 | :pretty-print true}}}} 35 | :aliases {"build" ["trampoline" "run" "-m" "redlobster.build" "redlobster.io"]} 36 | ) 37 | -------------------------------------------------------------------------------- /src/cljs/node.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs.node 2 | (:use-macros [cljs.node-macros :only [require]])) 3 | 4 | (defn log [& args] (apply (.-log js/console) (map str args))) 5 | 6 | (defn on-node? [] 7 | (try (string? process.versions.node) 8 | (catch js/Error e false))) 9 | -------------------------------------------------------------------------------- /src/cljs/node_macros.clj: -------------------------------------------------------------------------------- 1 | (ns cljs.node-macros 2 | (:refer-clojure :exclude [require])) 3 | 4 | (defmacro require [path sym] 5 | (if (vector? sym) 6 | `(do 7 | ~@(for [s sym] 8 | `(def ~s (aget (js/require ~path) ~(str s))))) 9 | `(def ~sym (js/require ~path)))) 10 | -------------------------------------------------------------------------------- /src/redlobster/events.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.events) 2 | 3 | ;; Protocol 4 | 5 | (defprotocol IEventEmitter 6 | (on [emitter event listener]) 7 | (once [emitter event listener]) 8 | (remove-listener [emitter event listener]) 9 | (remove-all-listeners [emitter]) 10 | (remove-all-listeners [emitter event]) 11 | (listeners [emitter event]) 12 | (emit [emitter event data])) 13 | 14 | ;; Utility functions 15 | 16 | (defn unpack-event [event] 17 | (if (keyword? event) 18 | (name event) 19 | event)) 20 | 21 | (defn wrap-once [emitter event listener] 22 | (fn once-off [x] 23 | (listener x) 24 | (remove-listener emitter event once-off))) 25 | 26 | ;; Default implementation 27 | 28 | (defn- def-add-listener [type listener] 29 | (fn [this] 30 | (let [listeners (or (get this type) #{})] 31 | (assoc this type (conj listeners listener))))) 32 | 33 | (defn- def-rem-listener [type listener] 34 | (fn [this] 35 | (let [listeners (or (get this type) #{})] 36 | (assoc this type (disj listeners listener))))) 37 | 38 | (deftype DefaultEventEmitter [events] 39 | IEventEmitter 40 | (on [this event listener] 41 | (swap! events (def-add-listener event listener))) 42 | (once [this event listener] 43 | (set! (.-__redlobster_event_once listener) true) 44 | (swap! events (def-add-listener event listener))) 45 | (remove-listener [this event listener] 46 | (swap! events (def-rem-listener event listener))) 47 | (remove-all-listeners [this] 48 | (reset! events {})) 49 | (remove-all-listeners [this event] 50 | (swap! events #(dissoc % event))) 51 | (listeners [this event] 52 | (get @events event)) 53 | (emit [this event data] 54 | (doseq [listener (listeners this event)] 55 | (listener data) 56 | (when (.-__redlobster_event_once listener) 57 | (remove-listener this event listener))))) 58 | 59 | ;; Implementations 60 | 61 | (def ^:private implementations 62 | [(fn impl-node [] 63 | (try 64 | (let [EventEmitter (.-EventEmitter (js/require "events"))] 65 | {:constructor (fn [] (EventEmitter.)) 66 | :type :node 67 | :init 68 | (fn [] 69 | (extend-protocol IEventEmitter 70 | EventEmitter 71 | (on [emitter event listener] 72 | (.on emitter (unpack-event event) listener)) 73 | (once [emitter event listener] 74 | (.once emitter (unpack-event event) listener)) 75 | (remove-listener [emitter event listener] 76 | (.removeListener emitter (unpack-event event) listener)) 77 | (remove-all-listeners [emitter] 78 | (.removeAllListeners emitter)) 79 | (remove-all-listeners [emitter event] 80 | (.removeAllListeners emitter (unpack-event event))) 81 | (listeners [emitter event] 82 | (js->clj (.listeners emitter (unpack-event event)))) 83 | (emit [emitter event data] 84 | (.emit emitter (unpack-event event) data))))}) 85 | (catch js/Error e nil))) 86 | 87 | (fn impl-default [] 88 | {:constructor (fn [] (DefaultEventEmitter. (atom {}))) 89 | :type :default 90 | :init (fn [])})]) 91 | 92 | ;; Initialise the first available implementation 93 | 94 | (let [emitter (some #(%) implementations)] 95 | (if (nil? emitter) (throw (js/Error. "No supported EventEmitter found")) 96 | (do 97 | (def event-emitter (:constructor emitter)) 98 | (def emitter-type (:type emitter)) 99 | ((:init emitter))))) 100 | -------------------------------------------------------------------------------- /src/redlobster/http.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.http 2 | (:require-macros [cljs.node-macros :as n]) 3 | (:require [redlobster.events :as e] 4 | [redlobster.promise :as p] 5 | [redlobster.stream :as s]) 6 | (:use [cljs.node :only [log]]) 7 | (:use-macros [redlobster.macros :only [promise let-realised]])) 8 | 9 | (n/require "http" http) 10 | 11 | (defn request 12 | ([options body] 13 | (promise 14 | (let [req (.request http (clj->js options) #(realise %))] 15 | (e/on req "error" #(realise-error %)) 16 | (if body (s/write-stream req body) 17 | (.end req))))) 18 | ([options] 19 | (request options nil))) 20 | -------------------------------------------------------------------------------- /src/redlobster/https.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.https 2 | (:require-macros [cljs.node-macros :as n]) 3 | (:require [redlobster.events :as e] 4 | [redlobster.promise :as p] 5 | [redlobster.stream :as s]) 6 | (:use [cljs.node :only [log]]) 7 | (:use-macros [redlobster.macros :only [promise let-realised]])) 8 | 9 | (n/require "https" https) 10 | 11 | (defn request 12 | ([options body] 13 | (promise 14 | (let [req (.request https (clj->js options) #(realise %))] 15 | (e/on req "error" #(realise-error %)) 16 | (if body (s/write-stream req body) 17 | (.end req))))) 18 | ([options] 19 | (request options nil))) 20 | -------------------------------------------------------------------------------- /src/redlobster/io.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.io 2 | (:require-macros [cljs.node-macros :as n]) 3 | (:require [redlobster.promise :as p] 4 | [redlobster.stream :as s] 5 | [redlobster.http :as http] 6 | [redlobster.https :as https] 7 | ) 8 | (:use [cljs.node :only [log]]) 9 | (:use-macros [redlobster.macros :only [let-realised waitp]])) 10 | 11 | (n/require "url" url) 12 | 13 | (defn parse-url [path] 14 | (.parse url path)) 15 | 16 | (defn http-url? [path] 17 | (= "http:" (.-protocol (parse-url path)))) 18 | 19 | (defn https-url? [path] 20 | (= "https:" (.-protocol (parse-url path)))) 21 | 22 | (defn file-url? [path] 23 | (let [p (parse-url path)] 24 | (and (or 25 | (= "file:" (.-protocol p)) 26 | (not (.-protocol p))) 27 | (or 28 | (= "" (.-host p)) 29 | (not (.-host p)))))) 30 | 31 | (defn- slurp-http [path] 32 | (let-realised 33 | [res (http/request path)] 34 | (s/read-stream @res))) 35 | 36 | (defn- slurp-https [path] 37 | (let-realised 38 | [res (https/request path)] 39 | (s/read-stream @res))) 40 | 41 | (defn- slurp-file [path] 42 | (s/read-stream (s/read-file path))) 43 | 44 | (defn slurp [path] 45 | (cond 46 | (http-url? path) (slurp-http path) 47 | (https-url? path) (slurp-https path) 48 | (file-url? path) (slurp-file path) 49 | :else (p/promise-fail {:redlobster.io/unknown-path path}))) 50 | 51 | (defn- binary-slurp-http [path] 52 | (let-realised [res (http/request url)] 53 | (s/read-binary-stream @res))) 54 | 55 | (defn- binary-slurp-https [path] 56 | (let-realised [res (https/request url)] 57 | (s/read-binary-stream @res))) 58 | 59 | (defn- binary-slurp-file [path] 60 | (s/read-binary-stream (s/read-file path))) 61 | 62 | (defn binary-slurp [path] 63 | (cond 64 | (http-url? path) (binary-slurp-http path) 65 | (https-url? path) (binary-slurp-https path) 66 | (file-url? path) (binary-slurp-file path) 67 | :else (p/promise-fail {:redlobster.io/unknown-path path}))) 68 | 69 | (defn- http-success? [res] 70 | (let [status (.-statusCode res)] 71 | (and (>= status 200) 72 | (< status 300)))) 73 | 74 | (defn- spit-http [path data] 75 | (let [o (parse-url path)] 76 | (set! (.-method path) "PUT") 77 | (waitp (http/request o data) 78 | #(if (http-success? %) 79 | (realise nil) 80 | (realise-error {:redlobster.http/status-code (.-statusCode %)})) 81 | #(realise-error %)))) 82 | 83 | (defn- spit-https [path data] 84 | (let [o (parse-url path)] 85 | (set! (.-method path) "PUT") 86 | (waitp (https/request o data) 87 | #(if (http-success? %) 88 | (realise nil) 89 | (realise-error {:redlobster.http/status-code (.-statusCode %)})) 90 | #(realise-error %)))) 91 | 92 | (defn- spit-file [path data] 93 | (s/write-stream (s/write-file path) data)) 94 | 95 | (defn spit [path data] 96 | (cond 97 | (http-url? path) (spit-http path data) 98 | (https-url? path) (spit-https path data) 99 | (file-url? path) (spit-file path data) 100 | :else (p/promise-fail {:redlobster.io/unknown-path path}))) 101 | -------------------------------------------------------------------------------- /src/redlobster/macros.clj: -------------------------------------------------------------------------------- 1 | (ns redlobster.macros 2 | (:refer-clojure :exclude [promise await])) 3 | 4 | (defmacro defer 5 | "Run the given forms in the next tick of the event loop, or if the 6 | first argument is a number, run the following forms after the given 7 | number of milliseconds have elapsed." 8 | [& forms] 9 | (if (number? (first forms)) 10 | `(js/setTimeout (fn [] ~@(rest forms)) ~(first forms)) 11 | `(js/setTimeout (fn [] ~@forms) 0))) 12 | 13 | (defmacro promise 14 | "Return a promise that will be realised by the given forms. The functions 15 | `realise` and `realise-error` will be available inside the macro body, and one 16 | of these should be called at some point within the forms to realise the promise." 17 | [& forms] 18 | `(let [promise# (redlobster.promise/promise) 19 | realise# (fn [promise# value#] 20 | (redlobster.promise/realise promise# value#)) 21 | realise-error# (fn [promise# value#] 22 | (redlobster.promise/realise-error promise# value#)) 23 | ~'realise (partial realise# promise#) 24 | ~'realise-error (partial realise-error# promise#)] 25 | ~@forms 26 | promise#)) 27 | 28 | (defmacro waitp 29 | "Creates a promise that waits for another promise to be realised, and 30 | calls the provided success or failure function respectively to realise the 31 | created promise. As with the `promise` macro, the functions `realise` and 32 | `realise-error` will be available inside the macro body and should be called 33 | to realise the promise." 34 | [join-promise success failure] 35 | `(let [promise# (redlobster.promise/promise) 36 | realise# (fn [promise# value#] 37 | (redlobster.promise/realise promise# value#)) 38 | realise-error# (fn [promise# value#] 39 | (redlobster.promise/realise-error promise# value#)) 40 | ~'realise (partial realise# promise#) 41 | ~'realise-error (partial realise-error# promise#)] 42 | (redlobster.promise/on-realised ~join-promise 43 | ~success 44 | ~failure) 45 | promise#)) 46 | 47 | (defmacro when-realised 48 | "Given a sequence of promises, defer execution of the body until they have 49 | all been successfully realised. If one or more of the promises fail, do not 50 | execute anything. Return a promise that will realise with the result of 51 | evaluating the forms, or fail with the value of the first dependent promise 52 | to fail." 53 | [promises & forms] 54 | `(redlobster.promise/defer-until-realised 55 | ~promises 56 | (fn [] ~@forms))) 57 | 58 | (defmacro let-realised 59 | "Like `when-realised`, except it takes a binding form of variable/promise 60 | pairs instead of just a list of variables, and binds these to the macro scope." 61 | [bindings & forms] 62 | `(let ~bindings 63 | (redlobster.promise/defer-until-realised 64 | ~(vec (map first (partition 2 bindings))) 65 | (fn [] ~@forms)))) 66 | 67 | (defmacro let-coll-realised 68 | "Like `let-realised`, except that it takes a single binding that returns a 69 | collection and waits for its elements to realise." 70 | [[binding collection] & forms] 71 | `(let [~binding ~collection] 72 | (redlobster.promise/defer-until-realised 73 | ~binding 74 | (fn [] 75 | (let [~binding (map deref ~binding)] 76 | ~@forms))))) 77 | 78 | (defmacro if-let-realised 79 | [[binding value] success fail] 80 | "Takes a binding form of a single promise and executes success or fail. 81 | Success or fail need not be functions." 82 | `(let [~binding ~value] 83 | (redlobster.promise/on-realised 84 | ~binding 85 | (fn [_#] ~success) 86 | (fn [_#] ~fail)) 87 | ~binding)) 88 | 89 | (defmacro defer-node 90 | "Appends a callback to a given form which takes two arguments `[error value]` 91 | and executes it, returning a promise that will fail with `error` if `error` 92 | is truthy, and realise with `value` if `error` is falsy. This is a common 93 | Node callback idiom, so this macro can be useful for wrapping Node calls in 94 | promises, eg.: 95 | 96 | (defer-node (.readFile fs \"/etc/passwd\")) 97 | 98 | The above code will call fs.readFile() and return a promise that will realise 99 | with the file's contents when the operation is done, or fail with an appropriate 100 | error if the operation returns one. 101 | 102 | Optionally, you can specify a transformer function to apply to the success value 103 | before it's realised. `js->clj` is a likely candidate." 104 | ([form transformer] 105 | `(let [promise# (redlobster.promise/promise) 106 | callback# (fn [error# value#] 107 | (if error# 108 | (redlobster.promise/realise-error promise# error#) 109 | (redlobster.promise/realise promise# 110 | (~transformer value#))))] 111 | (~@form callback#) 112 | promise#)) 113 | ([form] 114 | `(defer-node ~form identity))) 115 | -------------------------------------------------------------------------------- /src/redlobster/mongo.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.mongo 2 | (:use-macros [redlobster.macros :only [defer-node]]) 3 | (:require [redlobster.promise :as p] 4 | [cljs.node :as node])) 5 | 6 | (def mongodb (js/require "mongodb")) 7 | 8 | (def Db (aget mongodb "Db")) 9 | (def Server (aget mongodb "Server")) 10 | (def Collection (aget mongodb "Collection")) 11 | (def ^:export ObjectID (aget mongodb "ObjectID")) 12 | 13 | (defn connect 14 | ([host port db] 15 | (let [server (Server. host port)] 16 | (defer-node (.open (Db. db server (clj->js {:journal true})))))) 17 | ([host db] 18 | (connect host 27017 db)) 19 | ([db] 20 | (connect "localhost" db))) 21 | 22 | (defn collection [db coll] 23 | (Collection. db coll)) 24 | 25 | (defn save! [coll doc] 26 | (let [doc (clj->js doc)] 27 | (defer-node (.save coll doc) js->clj))) 28 | 29 | (defn find-all [coll query] 30 | (defer-node (.find coll (clj->js query)) 31 | #(defer-node (.toArray %) js->clj))) 32 | 33 | (defn update-id! [coll id updater] 34 | (defer-node (.find coll (clj->js {:_id (ObjectID. id)})) 35 | (fn [cursor] 36 | (defer-node (.nextObject cursor) 37 | (fn [doc] 38 | (let [doc (updater (js->clj doc))] 39 | (save! coll doc))))))) 40 | 41 | (defn delete-id! [coll id] 42 | (let [_id (ObjectID. id)] 43 | (defer-node (.remove coll (clj->js {:_id _id}) 44 | (clj->js {:safe true})) 45 | js->clj))) 46 | -------------------------------------------------------------------------------- /src/redlobster/promise.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.promise 2 | (:require-macros [cljs.node-macros :as n]) 3 | (:require [redlobster.events :as e])) 4 | 5 | (defprotocol IPromise 6 | (realised? [this]) 7 | (failed? [this]) 8 | (realise [this value]) 9 | (realise-error [this value]) 10 | (on-realised [this on-success on-error])) 11 | 12 | (defn promise? [v] 13 | (satisfies? IPromise v)) 14 | 15 | (deftype Promise [ee] 16 | IDeref 17 | (-deref [this] 18 | (let [realised (.-__realised ee) 19 | value (.-__value ee)] 20 | (cond 21 | (not realised) :redlobster.promise/not-realised 22 | :else value))) 23 | IPromise 24 | (realised? [this] 25 | (if (nil? (.-__realised ee)) false true)) 26 | (failed? [this] 27 | (and (realised? this) (= :error (.-__realised ee)))) 28 | (realise [this value] 29 | (if (realised? this) 30 | (when-not (= :redlobster.promise/timeout @this) 31 | (throw :redlobster.promise/already-realised)) 32 | (if (promise? value) 33 | (on-realised value 34 | #(realise this %) 35 | #(realise-error this %)) 36 | (do 37 | (set! (.-__realised ee) :success) 38 | (set! (.-__value ee) value) 39 | (e/emit ee :realise-success value))))) 40 | (realise-error [this value] 41 | (if (realised? this) 42 | (when-not (= :redlobster.promise/timeout @this) 43 | (throw :redlobster.promise/already-realised)) 44 | (if (promise? value) 45 | (on-realised value 46 | #(realise this %) 47 | #(realise-error this %)) 48 | (do 49 | (set! (.-__realised ee) :error) 50 | (set! (.-__value ee) value) 51 | (e/emit ee :realise-error value))))) 52 | (on-realised [this on-success on-error] 53 | (if (realised? this) 54 | (if (failed? this) (on-error @this) (on-success @this)) 55 | (doto ee 56 | (e/on :realise-success on-success) 57 | (e/on :realise-error on-error))))) 58 | 59 | (defn promise 60 | ([] 61 | (Promise. 62 | (let [ee (e/event-emitter)] 63 | (set! (.-__realised ee) nil) 64 | (set! (.-__value ee) nil) 65 | ee))) 66 | ([success-value] 67 | (doto (promise) 68 | (realise success-value)))) 69 | 70 | (defn promise-fail [error-value] 71 | (doto (promise) 72 | (realise-error error-value))) 73 | 74 | (defn await 75 | "Takes a list of promises, and creates a promise that will realise as 76 | `:redlobster.promise/realised` when each promise has successfully realised, 77 | or if one or more of the promises fail, fail with the value of the first 78 | failing promise. 79 | 80 | If the first argument is the keyword `:all`, then instead of failing when 81 | one of the promises fails, it will just wait for all promises to realise 82 | and realise itself with `:redlobster.promise/realised` regardless of the 83 | success or failure of any promise." 84 | [& promises] 85 | (let [await-all (= (first promises) :all) 86 | promises (if await-all (rest promises) promises) 87 | p (promise) 88 | total (count promises) 89 | count (atom 0) 90 | done (atom false)] 91 | (doseq [subp promises] 92 | (let [succ (fn [_] 93 | (when (not @done) 94 | (swap! count inc) 95 | (when (= total @count) 96 | (reset! done true) 97 | (realise p :redlobster.promise/realised)))) 98 | fail (if await-all succ 99 | (fn [err] 100 | (when (not @done) 101 | (reset! done true) 102 | (realise-error p err))))] 103 | (on-realised subp succ fail))) 104 | p)) 105 | 106 | (defn defer-until-realised [promises callback] 107 | (let [p (promise)] 108 | (on-realised (apply await promises) 109 | (fn [_] (realise p (callback))) 110 | (fn [error] (realise-error p error))) 111 | p)) 112 | 113 | (defn on-event 114 | "Creates a promise that fulfills with an event object when the matching 115 | event is triggered on the EventEmitter. This promise cannot fail; it will 116 | either succeed or never realise." 117 | [ee type] 118 | (let [p (promise)] 119 | (e/once ee type 120 | (fn [event] (realise p event))) 121 | p)) 122 | 123 | (defn timeout 124 | "Sets a promise to fail with `:redlobster.promise/timeout` after a 125 | specified number of milliseconds. 126 | 127 | A promise that has timed out will not throw an error when you try to 128 | realise it, but the realised value will remain 129 | `:redlobster.promise/timeout`." 130 | [promise timeout] 131 | (let [timeout-func #(when-not (realised? promise) 132 | (realise-error promise :redlobster.promise/timeout))] 133 | (js/setTimeout timeout-func timeout))) 134 | -------------------------------------------------------------------------------- /src/redlobster/stream.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.stream 2 | (:require-macros [cljs.node-macros :as n]) 3 | (:require [redlobster.promise :as p] 4 | [redlobster.events :as e]) 5 | (:use [cljs.node :only [log]]) 6 | (:use-macros [redlobster.macros :only [promise when-realised]])) 7 | 8 | (n/require "stream" Stream) 9 | (try 10 | (n/require "fs" [createReadStream createWriteStream]) 11 | (catch :default e)) 12 | 13 | (defprotocol IStream 14 | (readable? [this]) 15 | (writable? [this]) 16 | (set-encoding [this encoding]) 17 | (pause [this]) 18 | (resume [this]) 19 | (destroy [this]) 20 | (pipe [this destination]) 21 | (pipe [this destination options]) 22 | (write [this data]) 23 | (write [this data encoding]) 24 | (end [this]) 25 | (end [this data]) 26 | (end [this data encoding]) 27 | (destroy-soon [this])) 28 | 29 | (defn stream? [v] 30 | (satisfies? IStream v)) 31 | 32 | (extend-protocol IStream 33 | Stream 34 | (readable? [this] (.-readable this)) 35 | (writable? [this] (.-writable this)) 36 | (set-encoding [this encoding] (.setEncoding this encoding)) 37 | (pause [this] (.pause this)) 38 | (resume [this] (.resume this)) 39 | (destroy [this] (.destroy this)) 40 | (pipe [this destination] (.pipe this destination)) 41 | (pipe [this destination options] (.pipe this destination (clj->js options))) 42 | (write [this data] (.write this data)) 43 | (write [this data encoding] (.write this data encoding)) 44 | (end [this] (.end this)) 45 | (end [this data] (.end this data)) 46 | (end [this data encoding] (.end this data encoding)) 47 | (destroy-soon [this] (.destroySoon this))) 48 | 49 | (defn read-file [path] 50 | (createReadStream path)) 51 | 52 | (defn write-file [path] 53 | (createWriteStream path)) 54 | 55 | (defn- append-data [current data encoding] 56 | (let [data (if (instance? js/Buffer data) 57 | (.toString data encoding) 58 | data)] 59 | (str current data))) 60 | 61 | (defn read-stream [stream & [encoding]] 62 | (promise 63 | (let [content (atom "") 64 | encoding (or encoding "utf8")] 65 | (e/on stream :error #(realise-error %)) 66 | (e/on stream :data 67 | (fn [data] 68 | (swap! content append-data data encoding))) 69 | (e/on stream :end #(realise @content))))) 70 | 71 | (defn read-binary-stream [stream] 72 | (promise 73 | (let [arrays #js[]] 74 | (e/on stream :error realise-error) 75 | (e/on stream :data 76 | (fn [data] 77 | (.push arrays data))) 78 | (e/on stream :end #(realise (js/Buffer.concat arrays)))))) 79 | 80 | (defn write-stream [stream data & [encoding]] 81 | (promise 82 | (e/on stream :close #(realise nil)) 83 | (e/on stream :error #(realise-error %)) 84 | (cond 85 | (stream? data) (.pipe data stream) 86 | (instance? js/Buffer data) 87 | (do 88 | (.write stream data) 89 | (.end stream)) 90 | (string? data) 91 | (do 92 | (if encoding 93 | (.write stream data encoding) 94 | (.write stream data)) 95 | (.end stream)) 96 | :else 97 | (do 98 | (.end stream) 99 | (realise-error :redlobster.stream/unknown-datatype))))) 100 | -------------------------------------------------------------------------------- /test/phantom/test.js: -------------------------------------------------------------------------------- 1 | /*global console:true, phantom:true, WebPage:true */ 2 | 3 | var page = new WebPage(); 4 | page.onConsoleMessage = function(msg) { 5 | console.log(msg); 6 | }; 7 | 8 | page.onLoadFinished = function() { 9 | page.injectJs("js/test.js"); 10 | phantom.exit(); 11 | }; 12 | 13 | page.open("about:blank"); 14 | -------------------------------------------------------------------------------- /test/redlobster/events_test.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.events-test 2 | (:require [redlobster.events :as e])) 3 | 4 | (def test-me (atom 0)) 5 | 6 | (def ee (e/event-emitter)) 7 | (e/on ee :ohai (fn [e] (swap! test-me #(+ % e)))) 8 | (e/emit ee :ohai 2) 9 | (e/emit ee :ohai 3) 10 | 11 | (assert (= @test-me 5)) 12 | 13 | (reset! test-me 0) 14 | 15 | (e/once ee :single (fn [e] (swap! test-me #(+ % e)))) 16 | (e/emit ee :single 2) 17 | (e/emit ee :single 3) 18 | 19 | (assert (= @test-me 2)) 20 | -------------------------------------------------------------------------------- /test/redlobster/promise_test.cljs: -------------------------------------------------------------------------------- 1 | (ns redlobster.promise-test 2 | (:use-macros [redlobster.macros :only [promise]]) 3 | (:require [redlobster.promise :as p])) 4 | 5 | (def test (atom 2)) 6 | 7 | (let [promise (p/promise)] 8 | (p/on-realised promise 9 | (fn [v] (swap! test #(+ % v))) 10 | (fn [_] (assert false))) 11 | (p/realise promise 3) 12 | (assert (= @test 5) 13 | "on-realised success listener should be called with realised value")) 14 | 15 | (reset! test 2) 16 | (let [promise (p/promise)] 17 | (p/on-realised promise 18 | (fn [_] (assert false)) 19 | (fn [v] (swap! test #(+ % v)))) 20 | (p/realise-error promise 2) 21 | (assert (= @test 4) 22 | "on-realised error listener should be called with error value")) 23 | 24 | (let [promise (p/promise)] 25 | (assert (= @promise :redlobster.promise/not-realised) 26 | "unrealised promise should resolve to :not-realised") 27 | (assert (not (p/realised? promise)) 28 | "realised? should return false for unrealised promises") 29 | (assert (not (p/failed? promise)) 30 | "failed? should return false for unrealised promises") 31 | (p/realise promise "ohai") 32 | (assert (p/realised? promise) 33 | "realised? should return true for realised promises") 34 | (assert (not (p/failed? promise)) 35 | "failed? should return false for successful promises") 36 | (assert (= @promise "ohai") 37 | "realised promise should deref to realised value")) 38 | 39 | (let [promise (p/promise)] 40 | (p/realise-error promise "boom") 41 | (assert (p/realised? promise) 42 | "realised? should return true for realised promises") 43 | (assert (p/failed? promise) 44 | "failed? should return true for failed promises") 45 | (assert (= @promise "boom") 46 | "failed promise should deref to error value")) 47 | --------------------------------------------------------------------------------