├── .gitignore ├── scripts ├── build-none ├── test-none ├── watch-none ├── build-advanced ├── test-advanced ├── watch-advanced ├── repl └── repl.clj ├── test └── cljs_callback_heaven │ ├── runner.cljs │ └── core_test.cljs ├── index.js ├── externs.js ├── src └── cljs_callback_heaven │ ├── macros.clj │ └── core.cljs ├── README.md └── project.clj /.gitignore: -------------------------------------------------------------------------------- 1 | TODO 2 | -------------------------------------------------------------------------------- /scripts/build-none: -------------------------------------------------------------------------------- 1 | lein cljsbuild once none 2 | -------------------------------------------------------------------------------- /scripts/test-none: -------------------------------------------------------------------------------- 1 | lein doo node test-none 2 | -------------------------------------------------------------------------------- /scripts/watch-none: -------------------------------------------------------------------------------- 1 | lein cljsbuild auto none 2 | -------------------------------------------------------------------------------- /scripts/build-advanced: -------------------------------------------------------------------------------- 1 | lein cljsbuild once main 2 | -------------------------------------------------------------------------------- /scripts/test-advanced: -------------------------------------------------------------------------------- 1 | lein doo node test-advanced 2 | -------------------------------------------------------------------------------- /scripts/watch-advanced: -------------------------------------------------------------------------------- 1 | lein cljsbuild auto main 2 | -------------------------------------------------------------------------------- /scripts/repl: -------------------------------------------------------------------------------- 1 | echo -e "After this launches, run (load-file repl.clj)" 2 | rlwrap -r -m -q '\\"' -b "(){}[],^%3@\\\";:'" lein repl 3 | -------------------------------------------------------------------------------- /test/cljs_callback_heaven/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns cljs-callback-heaven.runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [cljs-callback-heaven.core-test])) 4 | 5 | (doo-tests 'cljs-callback-heaven.core-test) 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./out-none/goog/bootstrap/nodejs'); 2 | require('./target/cljs-callback-heaven-none'); 3 | require('./out-none/cljs_callback_heaven/core'); 4 | cljs_callback_heaven.core._main(process.argv[2], process.argv[3], process.argv[4], process.argv[5], process.argv[6]); 5 | -------------------------------------------------------------------------------- /externs.js: -------------------------------------------------------------------------------- 1 | var minimist = function (arg1) {}; 2 | 3 | var node = {}; 4 | node.process = {}; 5 | node.process.argv = {}; 6 | node.process.env = {}; 7 | node.process.env.HOME = {}; 8 | node.process.exit = function() {}; 9 | 10 | var process = {}; 11 | process.argv = {}; 12 | process.argv.HOME = {}; 13 | -------------------------------------------------------------------------------- /scripts/repl.clj: -------------------------------------------------------------------------------- 1 | (require '[cljs.repl]) 2 | (require '[cljs.repl.node]) 3 | 4 | (cljs.repl/repl 5 | (cljs.repl.node/repl-env) 6 | :watch "src" 7 | :output-dir "out-repl" 8 | :repl-requires '[[cljs.nodejs :as node] 9 | [cljs.core.async :refer [put! chan !]]]) ; throw in some stock core.async for good measure 10 | -------------------------------------------------------------------------------- /src/cljs_callback_heaven/macros.clj: -------------------------------------------------------------------------------- 1 | (ns cljs-callback-heaven.macros) 2 | 3 | (defmacro ? ~c)} func) 9 | (cljs.core.async/? ~c ~E-msg)} func) 15 | (cljs.core.async/1 ~c)} func) 23 | (cljs.core.async/1 ~c ~E-msg)} func) 29 | (cljs.core.async/2 ~c)} func) 37 | (cljs.core.async/2 ~c ~E-msg)} func) 43 | (cljs.core.async/3 ~c)} func) 51 | (cljs.core.async/3 ~c ~E-msg)} func) 57 | (cljs.core.async/! alts!]]) 10 | (:require-macros [cljs.core.async.macros :refer [go go-loop]] 11 | [cljs-callback-heaven.macros :refer [n functions inverse the priority of which argument to jam into 23 | ;; its channel. Instead of jamming a success (i.e., nth) value ONLY IF there are 24 | ;; no error values, these >n functions start by jamming the nth value 25 | ;; if at all possible. This can sometimes be useful with async functions 26 | ;; that generate both a success value and an error value at the same time. 27 | 28 | ;; this fn equivalent to (>? ..) when parent fn executes cb with only one argument 29 | (defn >1 30 | "If at all possible, jams the 1st callback argument into the input channel." 31 | ([c] (fn [arg1] (go (>! c (chan-sanitized arg1))))) 32 | ([c E-msg] (fn [arg1] (go (if arg1 (>! c (chan-sanitized arg1)) (>! c E-msg)))))) 33 | 34 | (defn >2 35 | "If at all possible, jams the 2nd callback argument into the input channel." 36 | ([c] 37 | (fn [err, res] (go (cond 38 | res (>! c (chan-sanitized res)) 39 | :else (>! c (chan-sanitized err)))))) 40 | ([c E-msg] 41 | (fn [err, res] (go (cond 42 | res (>! c (chan-sanitized res)) 43 | :else (>! c (chan-sanitized E-msg))))))) 44 | 45 | (defn >3 46 | "If at all possible, jams the 3rd callback argument into the input channel." 47 | ([c] 48 | (fn [err1, err2, res] (go (cond 49 | res (>! c (chan-sanitized res)) 50 | err1 (>! c (chan-sanitized err1)) 51 | :else (>! c (chan-sanitized err2)))))) 52 | ([c E-msg] 53 | (fn [err1, err2, res] (go (cond 54 | res (>! c (chan-sanitized res)) 55 | err1 (>! c (chan-sanitized E-msg)) 56 | :else (>! c (chan-sanitized E-msg))))))) 57 | 58 | ;; This function generates an [error-first callback](http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/) 59 | (defn >? 60 | "Jams the first truthy argument of a callback function into a channel." 61 | ([c] 62 | (fn [& args] 63 | (go-loop [a args] 64 | (if (= 0 (count a)) (>! c false) 65 | (if (first a) 66 | (>! c (first (chan-sanitized a))) 67 | (recur (rest a))))))) 68 | ([c E-msg] 69 | (fn [& args] 70 | (go-loop [a args] 71 | (if (= 0 (count a)) 72 | (>! c E-msg) 73 | (if (first a) 74 | (if (> (count a) 1) 75 | (>! c E-msg) 76 | (>! c (first (chan-sanitized a)))) 77 | (recur (rest a)))))))) 78 | 79 | (set! *main-cli-fn* -main) 80 | -------------------------------------------------------------------------------- /test/cljs_callback_heaven/core_test.cljs: -------------------------------------------------------------------------------- 1 | ; _____ _ 2 | ; |_ _|__ ___| |_ ___ 3 | ; | |/ _ \/ __| __/ __| 4 | ; | | __/\__ \ |_\__ \ 5 | ; |_|\___||___/\__|___/ 6 | ; 7 | 8 | (ns cljs-callback-heaven.core-test 9 | (:require [cljs-callback-heaven.core :as core] 10 | [cljs.test :refer-macros [deftest is testing run-tests async]] 11 | [cljs.nodejs :as node] 12 | [cljs.core.async :refer [buffer offer! poll! close! take! put! chan ! alts!]] 13 | [clojure.string :as s]) ; often useful when testing 14 | (:require-macros [cljs.core.async.macros :refer [go go-loop]] 15 | [cljs-callback-heaven.macros :refer [1-test 46 | (async done 47 | (go 48 | (let [c1 (chan 1) c2 (chan 1) c3 (chan 1)] 49 | (async-1 "suc" (core/>1 c1)) 50 | (async-1 "err" (core/>1 c2)) 51 | (async-1 "err" (core/>1 c3 "ERROR:")) 52 | (is (= (2-test 58 | (async done 59 | (go 60 | (let [c1 (chan 1) c2 (chan 1) c3 (chan 1) c4 (chan 1) c5 (chan 1)] 61 | (async-2 "suc" (core/>2 c1)) 62 | (async-2 "err" (core/>2 c2)) 63 | (async-2 "err" (core/>2 c3 "ERROR:")) 64 | (async-2-saturated "sat" (core/>2 c4)) 65 | (async-2-saturated "sat" (core/>2 c5 "ERROR:")) 66 | (is (= (3-test 74 | (async done 75 | (go 76 | (let [c1 (chan 1) c2 (chan 1) c3 (chan 1) c4 (chan 1) c5 (chan 1) c6 (chan 1) c7 (chan 1)] 77 | (async-3 "suc" (core/>3 c1)) 78 | (async-3 "err1" (core/>3 c2)) 79 | (async-3 "err2" (core/>3 c3)) 80 | (async-3 "err1" (core/>3 c4 "ERROR:")) 81 | (async-3 "err2" (core/>3 c5 "ERROR:")) 82 | (async-3-saturated "sat" (core/>3 c6)) 83 | (async-3-saturated "sat" (core/>3 c7 "ERROR:")) 84 | (is (= (?-one 94 | (testing "Mirror of the >1 tests." 95 | (async done 96 | (go 97 | (let [c1 (chan 1) c2 (chan 1) c3 (chan 1)] 98 | (async-1 "suc" (core/>? c1)) 99 | (async-1 "err" (core/>? c2)) 100 | (async-1 "err" (core/>? c3 "ERROR:")) 101 | (is (= (?-two 107 | (testing "Mirror of the >2 tests." 108 | (async done 109 | (go 110 | (let [c1 (chan 1) c2 (chan 1) c3 (chan 1) c4 (chan 1) c5 (chan 1)] 111 | (async-2 "suc" (core/>? c1)) 112 | (async-2 "err" (core/>? c2)) 113 | (async-2 "err" (core/>? c3 "ERROR:")) 114 | (is (= (?-three 120 | (testing "Mirror of the >3 tests." 121 | (async done 122 | (go 123 | (let [c1 (chan 1) c2 (chan 1) c3 (chan 1) c4 (chan 1) c5 (chan 1)] 124 | (async-3 "suc" (core/>? c1)) 125 | (async-3 "err1" (core/>? c2)) 126 | (async-3 "err2" (core/>? c3)) 127 | (async-3 "err1" (core/>? c4 "ERROR:")) 128 | (async-3 "err2" (core/>? c5 "ERROR:")) 129 | (is (= (! c res))) (! c err) 31 | (go (>! c res)))))) (? ..)` function to create a callback of arbitrary length, which then jams its first non-nil value into a channel. For example, if `c` is a channel, then `(>! c)` outputs the following callback function: 38 | 39 | (fn [& args] 40 | (go-loop [a args] 41 | (if (= 0 (count a)) (>! c false) 42 | (if (first a) 43 | (>! c (first (chan-sanitized a))) 44 | (recur (rest a)))))) 45 | 46 | Notice that this callback function doesn't specify how many arguments it can take. This means, in particular, that we can use it as an all-purpose callback for many javascript async functions (given that javascript tends to abide by the [error-first callback pattern](http://fredkschott.com/post/2014/03/understanding-error-first-callbacks-in-node-js/)). For example: 47 | 48 | (.readFile (nodejs/require "fs") "path/to/file" "utf8" (>? c)) 49 | 50 | And, in case we want a custom error message to placed into our channel, `>?` is capable of taking an optional third argument: 51 | 52 | (.readFile (nodejs/require "fs") "path/to/file" "utf8" (>? c "ERROR: This is a custom error which will be jammed into c in case readFile fails.")) 53 | 54 | ## 3.2 Forcing the nth Argument of a Callback into a Channel 55 | 56 | Sometimes it can be useful to force the *nth* member of a callback argument into a channel, regardless of whether the other callback arguments contain error values or not. In these cases, you can use `>1`, `>2`, and `>3` to target the first, second, and third callback arguments. In the case of node's `.readFile`, we can use `>2`: 57 | 58 | (.readFile (nodejs/require "fs") "path/to/file" "utf8" (>2 c)) ;;this will jam the second argument of the generated callback into c, regardless of whether the first argument is truthy 59 | 60 | ## 3.3 Jam an Asynchronous JS Function's Callback Value into a Channell in a One Line 61 | 62 | Recall above how we went about jamming the callback value of an async function into a channel: 63 | 64 | (go 65 | (let [c (chan 1)] 66 | (.readFile (nodejs/require "fs") "path/to/file" "utf8" (fn [err, res] (go (>! c res))) (?` callback discussed above, and wrapping the code in a go block. It can also handle custom error messages: 73 | 74 | (1`, `>2`, or `>3` in place of `>?`, respectively. 77 | 78 | ## 3.4 Printing from Channels 79 | 80 | Printing from channels is frequently useful. The cumbersome way to do this is as follows: 81 | 82 | (go (println (", lein-npm uses the fields below to assemble a temporary package.json 32 | :package { :name "cljs-callback-heaven" 33 | :version "0.0.1" ; you must update this manually; `lein npm version` doesn't seem to do it 34 | :bin "target/cljs-callback-heaven.js" ; we target the cljsbuild :main profile from below 35 | :description "N/A" 36 | :main "target/cljs-callback-heaven.js" ; we target the cljsbuild :main profile from below 37 | :repository {:type "git" :url "git+https://github.com/georgewsinger/cljs-callback-heaven.git"} 38 | :keywords ["clojure" "clojurescript"] 39 | :author "George Singer" 40 | :license "MIT" 41 | :private false 42 | :homepage "https://github.com/georgewsinger/cljs-callback-heaven#readme"}} 43 | 44 | :cljsbuild { :builds { ;; main is the default cljsbuild profile, marked by its use of the :advanced compilation mode 45 | :main { 46 | :source-paths ["src"] 47 | :compiler { :optimizations :advanced 48 | :target :nodejs 49 | :output-dir "out-advanced" 50 | :output-to "target/cljs-callback-heaven.js" 51 | :externs ["externs.js"] 52 | :verbose true 53 | :pretty-print true }} 54 | 55 | ;; on the other end of the spectrum is "none", named for its compilation mode 56 | :none { 57 | :source-paths ["src"] 58 | :compiler { :optimizations :none 59 | :target :nodejs 60 | :output-dir "out-none" 61 | :output-to "target/cljs-callback-heaven-none.js" 62 | :externs ["externs.js"] 63 | :verbose true 64 | :pretty-print true }} 65 | 66 | ;; lein doo uses this profile 67 | :test-none { 68 | :source-paths ["src" "test"] ; note the added "test" directory 69 | :compiler { :optimizations :none 70 | :target :nodejs 71 | :output-dir "out-test-none" 72 | :output-to "target/cljs-callback-heaven-test-none.js" 73 | :externs ["externs.js"] 74 | :verbose true 75 | :main cljs-callback-heaven.runner 76 | :pretty-print true }} 77 | 78 | ;; lein doo uses this profile 79 | :test-advanced { 80 | :source-paths ["src" "test"] ; note the added "test" directory 81 | :compiler { :optimizations :advanced 82 | :target :nodejs 83 | :output-dir "out-test-advanced" 84 | :output-to "target/cljs-callback-heaven-test-advanced.js" 85 | :externs ["externs.js"] 86 | :verbose true 87 | :main cljs-callback-heaven.runner 88 | :pretty-print true }}}}) 89 | --------------------------------------------------------------------------------