├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bb.edn ├── build.clj ├── build.edn ├── deps.edn ├── src └── com │ └── xadecimal │ ├── async_style.clj │ └── async_style │ └── impl.clj └── test └── com └── xadecimal └── async_style_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *.jar 4 | *.class 5 | /lib/ 6 | /classes/ 7 | /target/ 8 | /checkouts/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .lein-failures 13 | .nrepl-port 14 | .cpcache/ 15 | .lsp/ 16 | .clj-kondo/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | Types of changes: 9 | - Added for new features. 10 | - Changed for changes in existing functionality. 11 | - Deprecated for soon-to-be removed features. 12 | - Removed for now removed features. 13 | - Fixed for any bug fixes. 14 | - Security in case of vulnerabilities. 15 | 16 | ## [Unreleased] 17 | 18 | ## [0.1.0] - 2025-04-24 19 | 20 | ### Added 21 | 22 | - Initial release 23 | - Core task macros: async, blocking, and compute, each targeting a purpose‑built thread pool (async‑pool, blocking‑pool, compute‑pool). 24 | - Awaiting & blocking helpers: await, wait, await*, and wait* to retrieve asynchronous results with or without throwing. 25 | - Cancellation primitives: cancel!, cancelled?, and check-cancelled! for cooperative cancellation and interrupt propagation. 26 | - Error predicates: error? and ok? to distinguish normal values from Throwable results. 27 | - Promise‑chan combinators: catch, finally, then, chain, and handle for ergonomic error handling and result piping. 28 | - Concurrency utilities: sleep, defer, timeout, race, any, all, and all-settled to orchestrate asynchronous workflows. 29 | - Convenience macros: ado (asynchronous do), alet (asynchronous let), clet (concurrent let with dependency rewriting), and time (wall‑clock timing that understands channels). 30 | - Implicit try/catch/finally support in async, blocking, compute, and await, enabling inline exception handling syntax. 31 | - Java interrupt integration: automatic Thread.interrupt signalling for cancelled blocking and compute tasks where safe. 32 | - Railway‑style result handling via non‑throwing await* / wait* helpers that treat exceptions as values. 33 | - Comprehensive test suite covering all public API functions and macros. 34 | - Extensive README with feature overview, usage examples, and guidance on best‑practice thread‑pool selection. 35 | 36 | [unreleased]: https://github.com/xadecimal/async-style/compare/0.1.0...HEAD 37 | [0.1.0]: https://github.com/xadecimal/async-style/tree/0.1.0 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 xadecimal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async-style 2 | 3 | > **async / await for Clojure.** 4 | 5 | A Clojure library that brings JavaScript's intuitive async/await and Promise APIs to Clojure, along with utilities for cancellation, timeouts, racing, and more. It brings familiar async/await style of programming to Clojure. 6 | 7 | --- 8 | 9 | ## 📖 Table of Contents 10 | - [Features](#-features) 11 | - [Quick Start](#-quick-start) 12 | - [Installation](#-installation) 13 | - [Why async-style?](#why-async-style) 14 | - [Core Concepts](#core-concepts) 15 | - [Pools](#pools-async-blocking-compute) 16 | - [Awaiting](#awaiting-await-wait-await-wait) 17 | - [Errors](#errors) 18 | - [Cancellation](#cancellation) 19 | - [API Overview](#api-overview) 20 | - [Helpers: ado, alet, clet, time](#helpers-ado-alet-clet-time) 21 | - [Contributing](#-contributing) 22 | - [License](#-license) 23 | - [Support](#️-support) 24 | 25 | --- 26 | 27 | ## 🚀 Features 28 | 29 | - **Async/Await Syntax:** Familiar JavaScript-style asynchronous programming in Clojure. 30 | - **Rich Error Handling:** Built-in mechanisms for handling asynchronous exceptions gracefully. 31 | - **Flexible Execution Pools:** Optimized for I/O-bound, compute-heavy, and lightweight tasks. 32 | - **Cancellation Support:** Easily cancel asynchronous operations, ensuring efficient resource management. 33 | - **Comprehensive Utilities:** Timeout, sleep, chaining tasks, racing tasks, and more. 34 | 35 | --- 36 | 37 | ## 🔥 Quick Start 38 | 39 | ```clojure 40 | (ns example.core 41 | (:require [com.xadecimal.async-style :as a 42 | :refer [async blocking compute await wait cancel! cancelled?]])) 43 | 44 | ;; Simple async operation 45 | (async 46 | (println "Sum:" (await (async (+ 1 2 3))))) 47 | 48 | ;; Blocking I/O, asynchronous waiting 49 | (async 50 | (println (await (blocking 51 | (Thread/sleep 500) 52 | "Blocking completed!")))) 53 | 54 | ;; Blocking I/O, synchronous waiting 55 | (println "Sync blocking result:" 56 | (wait (blocking 57 | (Thread/sleep 500) 58 | "Blocking sync done"))) 59 | 60 | ;; Heavy compute, asynchronous waiting 61 | (async 62 | (println "Factorial:" 63 | (await (compute (reduce * (range 1 20)))))) 64 | 65 | ;; Heavy compute, synchronous waiting 66 | (println "Sync compute factorial:" 67 | (wait (compute (reduce * (range 1 20))))) 68 | 69 | ;; Handle an error 70 | (async 71 | (println "Divide:" (await (async (/ 1 0)))) 72 | (catch ArithmeticException _ 73 | (println "Can't divide by zero!"))) 74 | 75 | ;; Cancel an operation 76 | (let [task (blocking (Thread/sleep 5000) 77 | (when-not (cancelled?) 78 | (println "This won't print")))] 79 | (Thread/sleep 1000) 80 | (cancel! task "Cancelled!") 81 | (println "Task result:" (wait task))) 82 | ``` 83 | 84 | --- 85 | 86 | ## 📦 Installation 87 | 88 | ### Leiningen 89 | 90 | ```clojure 91 | [com.xadecimal/async-style "0.1.0"] 92 | ``` 93 | 94 | ### Clojure CLI (deps.edn) 95 | 96 | ```clojure 97 | {:deps {com.xadecimal/async-style {:mvn/version "0.1.0"}}} 98 | ``` 99 | 100 | --- 101 | 102 | ## Why async-style? 103 | 104 | Core.async and the CSP style is powerful, but its `go` blocks and channels can feel low‑level compared to JS Promises and async/await. **async-style** provides: 105 | 106 | - **Familiar ergonomics**: `async`/`await` like in JavaScript, Python, C#, etc. 107 | - **Expanded ergonomics**: `blocking`/`wait` for I/O and `compute`/`wait` for heavy compute. 108 | - **Separate pools**: dedicated threads for async control flow (`async`), blocking I/O (`blocking`) and CPU‑bound work (`compute`). 109 | - **Built on core.async**: core.async under the hood, can be used alongside it, promises are core.async's `promise-chan`. 110 | - **First-class error handling**: unlike core.async, errors are bubbled up and properly handled. 111 | - **First‑class cancellation**: propagate cancellation through promise‑chans. 112 | - **Rich composition**: `then`, `chain`, `race`, `all`, `any`, `all-settled`. 113 | - **Convenient macros**: `ado`, `alet`, `clet` for sequential, ordered, or concurrent binding. 114 | - **Railway programming**: supports railway programming with `async`/`await*`, if preferred. 115 | 116 | --- 117 | 118 | ## Core Concepts 119 | 120 | ### Pools: `async`, `blocking`, `compute` 121 | 122 | async-style follows the best practice outlined here: [Best practice for async/blocking/compute pools](https://gist.github.com/djspiewak/46b543800958cf61af6efa8e072bfd5c) 123 | 124 | Meaning it offers `async`/`blocking`/`compute`, each are meant to specialize the work you will be doing so they get executed on a pool that is most optimal. 125 | 126 | #### When used with core.async <= 1.7 127 | 128 | | Macro | Executor pool | Use for… | 129 | |------------|-----------------------------------------|-------------------------------------| 130 | | `async` | core.async’s go‑dispatch (8 threads) | async control flow | light CPU work | 131 | | `blocking` | unbounded cached threads | blocking I/O, sleeps | 132 | | `compute` | fixed agent pool (cores + 2 threads) | CPU‑intensive work, do not block | 133 | 134 | #### When used with core.async >= 1.8 135 | 136 | | Macro | Executor pool | Use for… | 137 | |------------|-----------------------------------------|-------------------------------------| 138 | | `async` | unbounded cached threads | async control flow | light CPU work | 139 | | `blocking` | unbounded cached threads | blocking I/O, sleeps | 140 | | `compute` | fixed agent pool (cores + 2 threads) | CPU‑intensive work, do not block | 141 | 142 | #### When used with core.async >= 1.8 with virtual threads 143 | 144 | | Macro | Executor pool | Use for… | 145 | |------------|-----------------------------------------|-------------------------------------| 146 | | `async` | virtual thread executor | async control flow | light CPU work | 147 | | `blocking` | virtual thread executor | blocking I/O, sleeps | 148 | | `compute` | fixed agent pool (cores + 2 threads) | CPU‑intensive work, do not block | 149 | 150 | ### Awaiting: `await`, `wait`, `await*`, `wait*` 151 | 152 | - **`await`** (inside `async`): parks current go‑thread until a promise‑chan completes; re‑throws errors. 153 | - **`wait`** (outside `async`): blocks calling thread for a result or throws error. 154 | - **`await*` / `wait*`**: return exceptions as values (no throw). 155 | 156 | `await` is like your JavaScript await, but just like core.async's `go`, it cannot park across function boundaries, 157 | so you have to be careful when you use macros like `map` or `run!`, since those use higher-order functions, 158 | you cannot `await` with them. This is true in JavaScript's await as well, but it's less common to use 159 | higher-order functions in JS, so it doesn't feel as restrictive. Once core.async support for virtual thread is added, and if you run under a JDK that supports them, this limitation will go away. 160 | 161 | `wait` is the counter-part to `await`, it is like `await` but synchronous. It doesn't color your functions, but will block your thread. 162 | 163 | `await*` / `wait*` are variants that return the exception as a value, instead of throwing. 164 | 165 | ### Errors 166 | 167 | #### Implicit `try` / `catch` / `finally` 168 | 169 | All of `async`, `blocking`, `compute` and `await`: 170 | 171 | 1. **Automatically wrap** your body in a single `try` if you include any trailing 172 | `(catch …)` or `(finally …)` forms. 173 | 2. **Pull in** any `(catch Type e …)` or `(finally …)` at the end of your block 174 | into that `try`, so you don’t have to write it yourself. 175 | 176 | ```clojure 177 | ;; without implicit try, you’d need: 178 | (async 179 | (try 180 | (/ 1 0) 181 | (catch ArithmeticException e 182 | (println "oops:" e)) 183 | (finally 184 | (println "done")))) 185 | 186 | ;; with implicit try, just append catch/finally: 187 | (async 188 | (/ 1 0) 189 | (catch ArithmeticException e 190 | (println "oops:" e)) 191 | (finally 192 | (println "done"))) 193 | ``` 194 | 195 | - Missing catch/finally? No wrapping is done (no overhead). 196 | - Only catch or only finally? Works the same. 197 | - Multiple catch? All are honored in order. 198 | 199 | This gives you JS‑style inline error handling right in your async blocks. 200 | 201 | #### Error Handling Combinators 202 | 203 | async-style gives you first‑class combinators to catch, recover, inspect or always run cleanup on errors: 204 | 205 | - **`catch`** 206 | Intercept errors of a given type or predicate, return a fallback value: 207 | ```clojure 208 | ;; recover ArithmeticException to 0 209 | (-> (async (/ 1 0)) 210 | (catch ArithmeticException (fn [_] 0))) 211 | ``` 212 | 213 | - **`finally`** 214 | Always run a side‑effect (cleanup, logging) on both success or error, then re‑deliver the original result or exception: 215 | ```clojure 216 | (-> (async (/ 1 0)) 217 | (finally (fn [v] (println "Completed with" v)))) 218 | ``` 219 | 220 | - **`handle`** 221 | Always invoke a single handler on the outcome (error or value) and deliver its return: 222 | ```clojure 223 | ;; log & wrap both success and error 224 | (-> (async (/ 1 0)) 225 | (handle (fn [v] (str "Result:" v)))) 226 | ``` 227 | 228 | - **`then`** 229 | Attach a success callback that is skipped if an error occurred (short‑circuits on error): 230 | ```clojure 231 | (-> (async 5) 232 | (then #(+ % 3)) 233 | (then println)) 234 | ``` 235 | 236 | - **`chain`** 237 | Shorthand for threading multiple `then` calls, with the same short‑circuit behavior: 238 | ```clojure 239 | (-> (async 5) 240 | (chain inc #(* 2 %) dec) 241 | (handle println)) 242 | ``` 243 | 244 | #### Railway style with `await*` / `wait*` 245 | 246 | By default `await`/`wait` will **throw** any exception taken from a chan. If you prefer **railway programming** (errors as values), use: 247 | 248 | - **`await*`** (parking, non‑throwing) 249 | - **`wait*`** (blocking, non‑throwing) 250 | 251 | They return either the successful value *or* the `Throwable`. Functions `error?` and `ok?` can than be used to branch on their result: 252 | 253 | ```clojure 254 | (async 255 | (let [result (await* (async (/ 1 0)))] 256 | (if (error? result) 257 | (println "Handled error:" (ex-message result)) 258 | (println "Success:" result)))) 259 | ``` 260 | 261 | Errors are always modeled this way in async-style, unlike in JS which wraps errors in a map, in async-style they are either an error? or an ok? result: 262 | 263 | ```clojure 264 | (async 265 | (let [vals (await* (all-settled [(async 1) (async (/ 1 0)) (async 3)]))] 266 | (println 267 | (map (fn [v] (if (error? v) :err v)) vals)))) 268 | ;; ⇒ (1 :err 3) 269 | ``` 270 | 271 | ### Cancellation 272 | 273 | Cancellation in **async-style** is **cooperative**—you signal it with `cancel!`, but your code must check for it to actually stop. 274 | 275 | - **`cancel!`** 276 | ```clojure 277 | (cancel! ch) ; cancel with CancellationException 278 | (cancel! ch custom-val) ; cancel with custom-val (must not be nil) 279 | ``` 280 | Marks the promise‑chan `ch` as cancelled. If the block hasn’t started, it immediately fulfills; otherwise it waits for you to check. 281 | 282 | - **`cancelled?`** 283 | ```clojure 284 | (when-not (cancelled?) …) 285 | ``` 286 | Returns `true` inside `async`/`blocking`/`compute` if `cancel!` was called. Does **not** throw. 287 | 288 | - **`check-cancelled!`** 289 | ```clojure 290 | (check-cancelled!) ; throws InterruptedException if cancelled 291 | ``` 292 | Throws immediately when cancelled, so you can use it at safe points to short‑circuit heavy loops or I/O. 293 | 294 | - **Handling cancellation downstream** 295 | - `await` / `wait` will re‑throw a `CancellationException` (or return your custom val). 296 | 297 | ```clojure 298 | (def work 299 | (async 300 | (loop [i 0] 301 | (check-cancelled!) 302 | (println "step" i) 303 | (recur (inc i))))) 304 | 305 | ;; let it run a bit… 306 | (Thread/sleep 50) 307 | (cancel! work) 308 | 309 | ;; downstream: 310 | (async 311 | (await work) 312 | (catch CancellationException _ 313 | (println "Work was cancelled!"))) 314 | ``` 315 | 316 | --- 317 | 318 | ## API Overview 319 | 320 | ### Task Creation 321 | 322 | | Function / Macro | Description | 323 | |------------------|--------------------------------------------------| 324 | | `async` | Run code on the async‑pool | 325 | | `blocking` | Run code on blocking‑pool | 326 | | `compute` | Run code on compute‑pool | 327 | | `sleep` | `async` sleep for _ms_ | 328 | | `defer` | delay execution by _ms_ then fulfill value or fn | 329 | 330 | ### Chaining & Composition 331 | 332 | | Function | Description | 333 | |----------------------|------------------------------------------------------------------| 334 | | `await` / `wait` | Retrieve result (throws on error) | 335 | | `await*` / `wait*` | Retrieve result or exception as a value (railway style) | 336 | | `then` | Run callback on success (skips on error) | 337 | | `chain` | Thread multiple `then` calls (short‑circuits on first error) | 338 | | `catch` | Recover from errors of a given type or predicate | 339 | | `finally` | Always run side‑effect on success or error, then re‑deliver | 340 | | `handle` | Run handler on outcome (error or success) and deliver its return | 341 | | `error?` / `ok?` | Predicates to distinguish exception values from normal results | 342 | 343 | ### Timeouts & Delays 344 | 345 | | Function | Description | 346 | |-------------|------------------------------------------------------------------------------------------------------------------------------------------------------| 347 | | `sleep` | Asynchronously pause for _ms_ milliseconds; returns a promise‑chan that fulfills with `nil` after the delay. | 348 | | `defer` | Wait _ms_ milliseconds, then asynchronously deliver a given value or call a provided fn; returns a promise‑chan. | 349 | | `timeout` | Wrap a channel with a _ms_ deadline—if it doesn’t fulfill in time, cancels the original and delivers a `TimeoutException` (or your custom fallback). | 350 | 351 | ### Racing & Gathering 352 | 353 | | Function | Description | 354 | |---------------|---------------------------------------------------------------------------| 355 | | `race` | First chan (or error) to complete “wins”; cancels all others | 356 | | `any` | First **successful** chan; ignores errors; errors aggregated if all fail | 357 | | `all` | Wait for all; short‑circuits on first error; returns vector of results | 358 | | `all-settled` | Wait for all, return vector of all results or errors | 359 | 360 | ### Helpers: `ado`, `alet`, `clet`, `time` 361 | 362 | | Macro | Description | 363 | |-------|-------------------------------------------------------------------------------------------------------------------------| 364 | | `ado` | Asynchronous `do`: execute expressions one after another, awaiting each; returns a promise‑chan of the last expression. | 365 | | `alet`| Asynchronous `let`: like `let` but each binding is awaited in order before evaluating the body. | 366 | | `clet`| Concurrent `let`: evaluates all bindings in parallel, auto‑awaiting any dependencies between them. | 367 | | `time`| Measure wall‑clock time of a sync or async expression; prints the elapsed ms and returns the expression’s value. | 368 | 369 | ### Asynchronous `let` (`alet`) and Concurrent `let` (`clet`) 370 | 371 | Bind async expressions just like a normal `let`—but choose between sequential or parallel evaluation: 372 | 373 | | Macro | Semantics | 374 | |--------|---------------------------------------------------------------------------------------------------------------------------------------------| 375 | | `alet` | **Sequential**: awaits each binding in order before evaluating the next. | 376 | | `clet` | **Concurrent**: starts all bindings in parallel, auto‑awaiting any that depend on previous ones while letting independent bindings overlap. | 377 | 378 | #### `alet` example 379 | 380 | ```clojure 381 | (alet 382 | [a (defer 100 1) 383 | b (defer 100 2) 384 | c (defer 100 3)] 385 | (println "Sum =" (+ a b c))) 386 | ;; Prints "Sum = 6" after ~300 ms 387 | ``` 388 | 389 | #### `clet` example 390 | 391 | ```clojure 392 | (clet 393 | [a (defer 100 1) 394 | b (defer a 2) ;; waits for `a` 395 | c (defer 100 3)] ;; independent of `a` and `b` 396 | (println "Sum =" (+ a b c))) 397 | ;; Prints "Sum = 6" after ~200 ms (a and c run in parallel; b waits on a) 398 | ``` 399 | 400 | - Use **`alet`** when bindings must run in sequence. 401 | - Use **`clet`** to maximize concurrency, with dependencies automatically respected. 402 | 403 | --- 404 | 405 | ## 📖 Contributing 406 | 407 | Contributions, issues, and feature requests are welcome! Feel free to submit a pull request or open an issue on [GitHub](https://github.com/xadecimal/async-style). 408 | 409 | --- 410 | 411 | ## 📃 License 412 | 413 | Distributed under the MIT License. See [LICENSE](./LICENSE) for more details. 414 | 415 | --- 416 | 417 | ## ❤️ Support 418 | 419 | If you find `async-style` useful, star ⭐️ the project and share it with the community! 420 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:tasks 2 | {test (do (println "Testing with core.async 1.8+") 3 | (clojure "-M:test") 4 | (println "Now testing with core.async 1.7") 5 | (clojure "-M:test:test-1.7")) 6 | gen (clojure "-X:build gen") 7 | lint (clojure "-X:build lint") 8 | deploy (clojure "-X:build deploy") 9 | install (clojure "-X:build install") 10 | update-documents (clojure "-X:build update-documents") 11 | bump-major-version (clojure "-X:build bump-major-version") 12 | bump-minor-version (clojure "-X:build bump-minor-version") 13 | bump-patch-version (clojure "-X:build bump-patch-version") 14 | release (do (run 'lint) 15 | (run 'test) 16 | (run 'install)) 17 | publish {:requires ([clojure.edn :as edn]) 18 | :task (let [version (-> (edn/read-string (slurp "build.edn")) :version)] 19 | (run 'release) 20 | (run 'update-documents) 21 | (shell "git add .") 22 | (try (shell "git diff --quiet --cached") 23 | (catch Exception _ 24 | (shell 25 | (str "git commit -m \"Prepared for new release \"" version)))) 26 | (shell "git push") 27 | (shell (str "git tag -a " version " -m \"Release v\"" version)) 28 | (shell (str "git push origin " version)) 29 | (run 'deploy))} 30 | publish-patch (do (run 'bump-patch-version) 31 | (run 'publish)) 32 | publish-minor (do (run 'bump-minor-version) 33 | (run 'publish)) 34 | publish-major (do (run 'bump-major-version) 35 | (run 'publish))}} 36 | -------------------------------------------------------------------------------- /build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [com.xadecimal.async-style.impl :as impl] 3 | [com.xadecimal.expose-api :as ea] 4 | [build-edn.main :as build-edn])) 5 | 6 | (defn lint 7 | [m] 8 | (build-edn/lint m)) 9 | 10 | (defn deploy 11 | [m] 12 | (build-edn/deploy m)) 13 | 14 | (defn install 15 | [m] 16 | (build-edn/install m)) 17 | 18 | (defn update-documents 19 | [m] 20 | (build-edn/update-documents m)) 21 | 22 | (defn bump-major-version 23 | [m] 24 | (build-edn/bump-major-version m)) 25 | 26 | (defn bump-minor-version 27 | [m] 28 | (build-edn/bump-minor-version m)) 29 | 30 | (defn bump-patch-version 31 | [m] 32 | (build-edn/bump-patch-version m)) 33 | 34 | (defn gen 35 | [m] 36 | (ea/expose-api 37 | {:file-path "./src/com/xadecimal/async_style.clj" 38 | :ns-code `(~'ns ~'com.xadecimal.async-style 39 | "Definitions: 40 | async: asynchronously running on the async-pool, await and others will park, use it for polling and small compute tasks 41 | blocking: asynchronously running on the blocking-pool, use it for running blocking operations and blocking io 42 | compute: asynchronously running on the compute-pool, use it for running heavy computation, don't block it 43 | settle(d): when a channel is delivered a value and closed, or in the case of a promise-chan, it means the promise-chan was fulfilled and will forever return the same value every time it is taken for and additional puts are ignored. 44 | fulfill(ed): when a channel is delivered a value, but not necessarily closed 45 | join(ed): when a channel returns a channel, joining is the process of further taking from the returned channel until a value is returned, thus unrolling a channel of channel of channel of ... 46 | async-pool: the core.async go block executor, it is fixed size, defaulting to 8 threads, don't soft or hard block it 47 | blocking-pool: the core.async thread block executor, it is caching, unbounded and not pre-allocated, use it for blocking operations and blocking io 48 | compute-pool: the clojure.core Agent pooledExecutor, it is fixed size bounded to cpu cores + 2 and pre-allocated, use it for heavy computation, don't block it" 49 | (:refer-clojure :exclude ~'[await time]) 50 | (:require ~'[com.xadecimal.async-style.impl :as impl])) 51 | :vars [#'impl/error? 52 | #'impl/ok? 53 | #'impl/cancelled? 54 | #'impl/check-cancelled! 55 | #'impl/cancel! 56 | #'impl/await* 57 | #'impl/wait* 58 | #'impl/async 59 | #'impl/blocking 60 | #'impl/compute 61 | #'impl/await 62 | #'impl/wait 63 | #'impl/catch 64 | #'impl/finally 65 | #'impl/then 66 | #'impl/chain 67 | #'impl/handle 68 | #'impl/sleep 69 | #'impl/defer 70 | #'impl/timeout 71 | #'impl/race 72 | #'impl/any 73 | #'impl/all-settled 74 | #'impl/all 75 | #'impl/ado 76 | #'impl/alet 77 | #'impl/clet 78 | #'impl/time]})) 79 | -------------------------------------------------------------------------------- /build.edn: -------------------------------------------------------------------------------- 1 | {:lib com.xadecimal/async-style 2 | :version "0.1.0" 3 | :description "JavaScript's async/await and promise APIs ported to Clojure, plus some extra goodies to let you program in an async style." 4 | :licenses [{:name "MIT License" 5 | :url "https://github.com/xadecimal/async-style/blob/main/LICENSE"}] 6 | :documents [{:file "README.md" 7 | :match ":deps \\{com\\.xadecimal\\/async-style" 8 | :action :replace 9 | :keep-indent? true 10 | :text "{:deps {com.xadecimal/async-style {:mvn/version \"{{version}}\"}}}"} 11 | {:file "README.md" 12 | :match "\\[com\\.xadecimal\\/async-style" 13 | :action :replace 14 | :keep-indent? true 15 | :text "[com.xadecimal/async-style \"{{version}}\"]"}]} 16 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.12.0"} 3 | org.clojure/core.async {:mvn/version "1.8.741"}} 4 | :aliases 5 | {:test {:extra-paths ["test"] 6 | :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"} 7 | com.xadecimal/testa {:mvn/version "0.1.0"}} 8 | :exec-fn cognitect.test-runner.api/test 9 | :exec-args {:dirs ["test"] 10 | :patterns ["com.xadecimal.async-style.*"]} 11 | :main-opts ["-m" "cognitect.test-runner" 12 | "-d" "test" 13 | "-r" "com.xadecimal.async-style.*"]} 14 | :test-1.7 {:override-deps {org.clojure/core.async {:mvn/version "1.7.701"}}} 15 | ;; Run with clj -T:build function-in-build 16 | :build {:extra-deps {com.github.liquidz/build.edn {:mvn/version "0.11.266"} 17 | com.xadecimal/expose-api {:mvn/version "0.3.0"}} 18 | :extra-paths ["."] 19 | :ns-default build}}} 20 | -------------------------------------------------------------------------------- /src/com/xadecimal/async_style.clj: -------------------------------------------------------------------------------- 1 | ;;;; DO NOT MODIFY ;;;; 2 | ;;; WARNING ;;; 3 | 4 | ;; This class was auto-generated by expose-api, do not manually edit 5 | ;; or your edits might get overwritten when it gets re-generated. 6 | ;; Instead modify the expose-api generation. 7 | ;; Refer to https://github.com/xadecimal/expose-api 8 | 9 | ;;; WARNING ;;; 10 | ;;;; DO NOT MODIFY ;;; 11 | 12 | (ns com.xadecimal.async-style 13 | "Definitions: 14 | async: asynchronously running on the async-pool, await and others will park, use it for polling and small compute tasks 15 | blocking: asynchronously running on the blocking-pool, use it for running blocking operations and blocking io 16 | compute: asynchronously running on the compute-pool, use it for running heavy computation, don't block it 17 | settle(d): when a channel is delivered a value and closed, or in the case of a promise-chan, it means the promise-chan was fulfilled and will forever return the same value every time it is taken for and additional puts are ignored. 18 | fulfill(ed): when a channel is delivered a value, but not necessarily closed 19 | join(ed): when a channel returns a channel, joining is the process of further taking from the returned channel until a value is returned, thus unrolling a channel of channel of channel of ... 20 | async-pool: the core.async go block executor, it is fixed size, defaulting to 8 threads, don't soft or hard block it 21 | blocking-pool: the core.async thread block executor, it is caching, unbounded and not pre-allocated, use it for blocking operations and blocking io 22 | compute-pool: the clojure.core Agent pooledExecutor, it is fixed size bounded to cpu cores + 2 and pre-allocated, use it for heavy computation, don't block it" 23 | (:refer-clojure :exclude [await time]) 24 | (:require [com.xadecimal.async-style.impl :as impl])) 25 | 26 | (defn error? 27 | "Returns true if v is considered an error as per async-style's error 28 | representations, false otherwise. Valid error representations in async-style 29 | for now are: 30 | * instances of Throwable" 31 | {:inline (fn ([v] ` (com.xadecimal.async-style.impl/error? ~v)))} 32 | ([v] (com.xadecimal.async-style.impl/error? v))) 33 | 34 | (defn ok? 35 | "Returns true if v is not considered an error as per async-style's error 36 | representations, false otherwise. Valid error representations in async-style 37 | for now are: 38 | * instances of Throwable" 39 | {:inline (fn ([v] ` (com.xadecimal.async-style.impl/ok? ~v)))} 40 | ([v] (com.xadecimal.async-style.impl/ok? v))) 41 | 42 | (defn cancelled? 43 | "Returns true if execution context was cancelled and thus should be 44 | interrupted/short-circuited, false otherwise. 45 | 46 | Users are expected, when inside an execution block like async, blocking or 47 | compute, to check using (cancelled? or check-cancelled!) as often as they can 48 | in case someone tried to cancel their execution, in which case they should 49 | interrupt/short-circuit the work as soon as they can." 50 | {:inline (fn ([] ` (com.xadecimal.async-style.impl/cancelled?)))} 51 | ([] (com.xadecimal.async-style.impl/cancelled?))) 52 | 53 | (defn check-cancelled! 54 | "Throws if execution context was cancelled and thus should be 55 | interrupted/short-circuited, returns nil. 56 | 57 | Users are expected, when inside an execution block like async, blocking or 58 | compute, to check using (cancelled? or check-cancelled!) as often as they can 59 | in case someone tried to cancel their execution, in which case they should 60 | interrupt/short-circuit the work as soon as they can." 61 | {:inline (fn ([] ` (com.xadecimal.async-style.impl/check-cancelled!)))} 62 | ([] (com.xadecimal.async-style.impl/check-cancelled!))) 63 | 64 | (defn cancel! 65 | "When called on chan, tries to tell processes currently executing over the 66 | chan that they should interrupt and short-circuit (aka cancel) their execution 67 | as soon as they can, as it is no longer needed. 68 | 69 | The way cancellation is conveyed is by settling the return channel of async, 70 | blocking and compute blocks to a CancellationException, unless passed a v 71 | explicitly, in which case it will settle it with v. 72 | 73 | That means by default a block that has its execution cancelled will return a 74 | CancellationException and thus awaiters and other takers of its result will 75 | see the exception and can handle it accordingly. If instead you want to cancel 76 | the block so it returns a value, pass in a v and the awaiters and takers will 77 | receive that value instead. You can't set nil as the cancelled value, 78 | attempting to do so will throw an IllegalArgumentException. 79 | 80 | It is up to processes inside async, blocking and compute blocks to properly 81 | check for cancellation on a channel." 82 | {:inline (fn 83 | ([chan] ` (com.xadecimal.async-style.impl/cancel! ~chan)) 84 | ([chan v] ` (com.xadecimal.async-style.impl/cancel! ~chan ~v)))} 85 | ([chan] (com.xadecimal.async-style.impl/cancel! chan)) 86 | ([chan v] (com.xadecimal.async-style.impl/cancel! chan v))) 87 | 88 | (defmacro await* 89 | "Parking takes from chan-or-value so that any exception is returned, and with 90 | taken result fully joined." 91 | {} 92 | ([chan-or-value] ` (com.xadecimal.async-style.impl/await* ~chan-or-value))) 93 | 94 | (defn wait* 95 | "Blocking takes from chan-or-value so that any exception is returned, and with 96 | taken result fully joined." 97 | {:inline (fn 98 | ([chan-or-value] 99 | ` 100 | (com.xadecimal.async-style.impl/wait* ~chan-or-value)))} 101 | ([chan-or-value] (com.xadecimal.async-style.impl/wait* chan-or-value))) 102 | 103 | (defmacro async 104 | "Asynchronously execute body on the async-pool with support for cancellation, 105 | implicit-try, and returning a promise-chan settled with the result or any 106 | exception thrown. 107 | 108 | body will run on the async-pool, so if you plan on doing something blocking 109 | or compute heavy, use blocking or compute instead." 110 | {} 111 | ([& body] ` (com.xadecimal.async-style.impl/async ~@body))) 112 | 113 | (defmacro blocking 114 | "Asynchronously execute body on the blocking-pool with support for 115 | cancellation, implicit-try, and returning a promise-chan settled with the 116 | result or any exception thrown. 117 | 118 | body will run on the blocking-pool, so use this when you will be blocking or 119 | doing blocking io only." 120 | {} 121 | ([& body] ` (com.xadecimal.async-style.impl/blocking ~@body))) 122 | 123 | (defmacro compute 124 | "Asynchronously execute body on the compute-pool with support for 125 | cancellation, implicit-try, and returning a promise-chan settled with the 126 | result or any exception thrown. 127 | 128 | body will run on the compute-pool, so use this when you will be doing heavy 129 | computation, and don't block, if you're going to block use blocking 130 | instead. If you're doing a very small computation, like polling another chan, 131 | use async instead." 132 | {} 133 | ([& body] ` (com.xadecimal.async-style.impl/compute ~@body))) 134 | 135 | (defmacro await 136 | "Parking takes from chan-or-value so that any exception taken is re-thrown, 137 | and with taken result fully joined. 138 | 139 | Supports implicit-try to handle thrown exceptions such as: 140 | 141 | (async 142 | (await (async (/ 1 0)) 143 | (catch ArithmeticException e 144 | (println e)) 145 | (catch Exception e 146 | (println \"Other unexpected excpetion\")) 147 | (finally (println \"done\"))))" 148 | {} 149 | ([chan-or-value & body] 150 | ` 151 | (com.xadecimal.async-style.impl/await ~chan-or-value ~@body))) 152 | 153 | (defmacro wait 154 | "Blocking takes from chan-or-value so that any exception taken is re-thrown, 155 | and with taken result fully joined. 156 | 157 | Supports implicit-try to handle thrown exceptions such as: 158 | 159 | (wait (async (/ 1 0)) 160 | (catch ArithmeticException e 161 | (println e)) 162 | (catch Exception e 163 | (println \"Other unexpected excpetion\")) 164 | (finally (println \"done\")))" 165 | {} 166 | ([chan-or-value & body] 167 | ` 168 | (com.xadecimal.async-style.impl/wait ~chan-or-value ~@body))) 169 | 170 | (defn catch 171 | "Parking takes fully joined value from chan. If value is an error of 172 | pred-or-type, will call error-handler with it. 173 | 174 | Returns a promise-chan settled with the value or the return of the 175 | error-handler. 176 | 177 | error-handler will run on the async-pool, so if you plan on doing something 178 | blocking or compute heavy, remember to wrap it in a blocking or compute 179 | respectively." 180 | {:inline (fn 181 | ([chan error-handler] 182 | ` 183 | (com.xadecimal.async-style.impl/catch ~chan ~error-handler)) 184 | ([chan pred-or-type error-handler] 185 | ` 186 | (com.xadecimal.async-style.impl/catch ~chan ~pred-or-type 187 | ~error-handler)))} 188 | ([chan error-handler] 189 | (com.xadecimal.async-style.impl/catch chan error-handler)) 190 | ([chan pred-or-type error-handler] 191 | (com.xadecimal.async-style.impl/catch chan pred-or-type error-handler))) 192 | 193 | (defn finally 194 | "Parking takes fully joined value from chan, and calls f with it no matter if 195 | the value is ok? or error?. 196 | 197 | Returns a promise-chan settled with the taken value, and not the return of f, 198 | which means f is implied to be doing side-effect(s). 199 | 200 | f will run on the async-pool, so if you plan on doing something blocking or 201 | compute heavy, remember to wrap it in a blocking or compute respectively." 202 | {:inline (fn ([chan f] ` (com.xadecimal.async-style.impl/finally ~chan ~f)))} 203 | ([chan f] (com.xadecimal.async-style.impl/finally chan f))) 204 | 205 | (defn then 206 | "Asynchronously executes f with the result of chan once available, unless chan 207 | results in an error, in which case f is not executed. 208 | 209 | Returns a promise-chan settled with the result of f or the error. 210 | 211 | f will run on the async-pool, so if you plan on doing something blocking or 212 | compute heavy, remember to wrap it in a blocking or compute respectively." 213 | {:inline (fn ([chan f] ` (com.xadecimal.async-style.impl/then ~chan ~f)))} 214 | ([chan f] (com.xadecimal.async-style.impl/then chan f))) 215 | 216 | (defn chain 217 | "Chains multiple then together starting with chan like: 218 | (-> chan (then f1) (then f2) (then fs) ...) 219 | 220 | fs will all run on the async-pool, so if you plan on doing something blocking 221 | or compute heavy, remember to wrap it in a blocking or compute respectively." 222 | {:inline (fn 223 | ([chan & fs] ` (com.xadecimal.async-style.impl/chain ~chan ~@fs)))} 224 | ([chan & fs] 225 | (clojure.core/apply com.xadecimal.async-style.impl/chain chan fs))) 226 | 227 | (defn handle 228 | "Asynchronously executes f with the result of chan once available (f result), 229 | unlike then, handle will always execute f, when chan's result is an error f is 230 | called with the error (f error). 231 | 232 | Returns a promise-chan settled with the result of f. 233 | 234 | Alternatively, one can pass an ok-handler and an error-handler and the 235 | respective one will be called based on if chan's result is ok (ok-handler 236 | result) or an error (error-handler error). 237 | 238 | f, ok-handler and error-handler will all run on the async-pool, so if you 239 | plan on doing something blocking or compute heavy, remember to wrap it in a 240 | blocking or compute respectively." 241 | {:inline (fn 242 | ([chan f] ` (com.xadecimal.async-style.impl/handle ~chan ~f)) 243 | ([chan ok-handler error-handler] 244 | ` 245 | (com.xadecimal.async-style.impl/handle ~chan 246 | ~ok-handler 247 | ~error-handler)))} 248 | ([chan f] (com.xadecimal.async-style.impl/handle chan f)) 249 | ([chan ok-handler error-handler] 250 | (com.xadecimal.async-style.impl/handle chan ok-handler error-handler))) 251 | 252 | (defn sleep 253 | "Asynchronously sleep ms time, returns a promise-chan which settles after ms 254 | time." 255 | {:inline (fn ([ms] ` (com.xadecimal.async-style.impl/sleep ~ms)))} 256 | ([ms] (com.xadecimal.async-style.impl/sleep ms))) 257 | 258 | (defn defer 259 | "Waits ms time and then asynchronously executes value-or-fn, returning a 260 | promsie-chan settled with the result. 261 | 262 | value-or-fn will run on the async-pool, so if you plan on doing something 263 | blocking or compute heavy, remember to wrap it in a blocking or compute 264 | respectively." 265 | {:inline (fn 266 | ([ms value-or-fn] 267 | ` 268 | (com.xadecimal.async-style.impl/defer ~ms ~value-or-fn)))} 269 | ([ms value-or-fn] (com.xadecimal.async-style.impl/defer ms value-or-fn))) 270 | 271 | (defn timeout 272 | "If chan fulfills before ms time has passed, return a promise-chan settled 273 | with the result, else returns a promise-chan settled with a TimeoutException 274 | or the result of timed-out-value-or-fn. 275 | 276 | In the case of a timeout, chan will be cancelled. 277 | 278 | timed-out-value-or-fn will run on the async-pool, so if you plan on doing 279 | something blocking or compute heavy, remember to wrap it in a blocking or 280 | compute respectively." 281 | {:inline (fn 282 | ([chan ms] ` (com.xadecimal.async-style.impl/timeout ~chan ~ms)) 283 | ([chan ms timed-out-value-or-fn] 284 | ` 285 | (com.xadecimal.async-style.impl/timeout ~chan 286 | ~ms 287 | ~timed-out-value-or-fn)))} 288 | ([chan ms] (com.xadecimal.async-style.impl/timeout chan ms)) 289 | ([chan ms timed-out-value-or-fn] 290 | (com.xadecimal.async-style.impl/timeout chan ms timed-out-value-or-fn))) 291 | 292 | (defn race 293 | "Returns a promise-chan that settles as soon as one of the chan in chans 294 | fulfill, with the value taken (and joined) from that chan. 295 | 296 | Unlike any, this will also return the first error? to be returned by one of 297 | the chans. So if the first chan to fulfill does so with an error?, race will 298 | return a promise-chan settled with that error. 299 | 300 | Once a chan fulfills, race cancels all the others." 301 | {:inline (fn ([chans] ` (com.xadecimal.async-style.impl/race ~chans)))} 302 | ([chans] (com.xadecimal.async-style.impl/race chans))) 303 | 304 | (defn any 305 | "Returns a promise-chan that settles as soon as one of the chan in chans 306 | fulfills in ok?, with the value taken (and joined) from that chan. 307 | 308 | Unlike race, this will ignore chans that fulfilled with an error?. So if the 309 | first chan to fulfill does so with an error?, any will keep waiting for 310 | another chan to eventually fulfill in ok?. 311 | 312 | If all chans fulfill in error?, returns an error containing the list of all 313 | the errors. 314 | 315 | Once a chan fulfills with an ok?, any cancels all the others." 316 | {:inline (fn ([chans] ` (com.xadecimal.async-style.impl/any ~chans)))} 317 | ([chans] (com.xadecimal.async-style.impl/any chans))) 318 | 319 | (defn all-settled 320 | "Takes a seqable of chans as an input, and returns a promise-chan that settles 321 | after all of the given chans have fulfilled in ok? or error?, with a vector of 322 | the taken ok? results and error? results of the input chans. 323 | 324 | It is typically used when you have multiple asynchronous tasks that are not 325 | dependent on one another to complete successfully, or you'd always like to 326 | know the result of each chan even when one errors. 327 | 328 | In comparison, the promise-chan returned by all may be more appropriate if 329 | the tasks are dependent on each other / if you'd like to immediately stop upon 330 | any of them returning an error?." 331 | {:inline (fn ([chans] ` (com.xadecimal.async-style.impl/all-settled ~chans)))} 332 | ([chans] (com.xadecimal.async-style.impl/all-settled chans))) 333 | 334 | (defn all 335 | "Takes a seqable of chans as an input, and returns a promise-chan that settles 336 | after all of the given chans have fulfilled in ok?, with a vector of the taken 337 | ok? results of the input chans. This returned promise-chan will settle when 338 | all of the input's chans have fulfilled, or if the input seqable contains no 339 | chans (only values or empty). It settles in error? immediately upon any of the 340 | input chans returning an error? or non-chans throwing an error?, and will 341 | contain the error? of the first taken chan to return one." 342 | {:inline (fn ([chans] ` (com.xadecimal.async-style.impl/all ~chans)))} 343 | ([chans] (com.xadecimal.async-style.impl/all chans))) 344 | 345 | (defmacro ado 346 | "Asynchronous do. Execute expressions one after the other, awaiting the result 347 | of each one before moving on to the next. Results are lost to the void, same 348 | as clojure.core/do, so side effects are expected. Returns a promise-chan which 349 | settles with the result of the last expression when the entire do! is done." 350 | {} 351 | ([& exprs] ` (com.xadecimal.async-style.impl/ado ~@exprs))) 352 | 353 | (defmacro alet 354 | "Asynchronous let. Binds result of async expressions to local binding, executing 355 | bindings in order one after the other." 356 | {} 357 | ([bindings & exprs] 358 | ` 359 | (com.xadecimal.async-style.impl/alet ~bindings ~@exprs))) 360 | 361 | (defmacro clet 362 | "Concurrent let. Executes all bound expressions in an async block so that 363 | the bindings run concurrently. If a later binding or the body depends on an 364 | earlier binding, that reference is automatically replaced with an await. 365 | In a blocking/compute context, await is transformed to wait for proper 366 | blocking behavior. 367 | 368 | Notes: 369 | * Bindings are evaluated in the async-pool; therefore, they should not 370 | perform blocking I/O or heavy compute directly. If you need to do blocking 371 | operations or heavy compute, wrap the binding in a blocking or compute call. 372 | * This macro only supports simple symbol bindings; destructuring (vector or 373 | map destructuring) is not supported. 374 | * It will transform symbols even inside quoted forms, so literal code in quotes 375 | may be rewritten unexpectedly. 376 | * Inner local bindings (e.g. via a nested let) that shadow an outer binding are 377 | not handled separately; the macro will attempt to rewrite every occurrence, 378 | which may lead to incorrect replacements. 379 | * Anonymous functions that use parameter names identical to outer bindings 380 | will also be rewritten, which can cause unintended behavior if they are meant 381 | to shadow those bindings." 382 | {} 383 | ([bindings & body] ` (com.xadecimal.async-style.impl/clet ~bindings ~@body))) 384 | 385 | (defmacro time 386 | "Evaluates expr and prints the time it took. Returns the value of expr. If 387 | expr evaluates to a channel, it waits for channel to fulfill before printing 388 | the time it took." 389 | {} 390 | ([expr] ` (com.xadecimal.async-style.impl/time ~expr)) 391 | ([expr print-fn] ` (com.xadecimal.async-style.impl/time ~expr ~print-fn))) 392 | 393 | ;;;; DO NOT MODIFY ;;;; 394 | ;;; WARNING ;;; 395 | 396 | ;; This class was auto-generated by expose-api, do not manually edit 397 | ;; or your edits might get overwritten when it gets re-generated. 398 | ;; Instead modify the expose-api generation. 399 | ;; Refer to https://github.com/xadecimal/expose-api 400 | 401 | ;;; WARNING ;;; 402 | ;;;; DO NOT MODIFY ;;; 403 | -------------------------------------------------------------------------------- /src/com/xadecimal/async_style/impl.clj: -------------------------------------------------------------------------------- 1 | (ns com.xadecimal.async-style.impl 2 | (:refer-clojure :exclude [await time]) 3 | (:require [clojure.core.async :as a] 4 | [clojure.core.async.impl.dispatch :as d]) 5 | (:import [clojure.core.async.impl.channels ManyToManyChannel] 6 | [clojure.lang Agent] 7 | [java.util.concurrent CancellationException TimeoutException ThreadPoolExecutor] 8 | [java.util.concurrent.locks ReentrantLock])) 9 | 10 | 11 | ;; TODO: add support for CSP style, maybe a process-factory that creates processes with ins/outs channels of buffer 1 and connectors between them 12 | ;; TODO: Add ClojureScript support 13 | ;; TODO: Consider adding resolved, rejected and try, similar to the JS Promise APIs 14 | ;; TODO: Consider supporting await for... like in Python or JS 15 | ;; TODO: Consider if I should wrap the returned promise-chan in my own type, then I could have a proper cancel-chan and other stuff on it as well, instead of cramming all semantics on the promise-chan 16 | 17 | (def ^:private executor-for 18 | "the core.async 1.8+ executor-for var, nil if we're on an older version of 19 | core.async that doesn't have it" 20 | (requiring-resolve `d/executor-for)) 21 | 22 | (def ^:private ^ThreadPoolExecutor compute-pool 23 | "the clojure.core Agent pooledExecutor, it is fixed size bounded to cpu cores 24 | + 2 and pre-allocated, use it for heavy computation, don't block it" 25 | Agent/pooledExecutor) ; Fixed bounded to cpu core + 2 and pre-allocated 26 | 27 | (def ^:private blocking-pool 28 | "the core.async thread block executor, it is caching, unbounded and not 29 | pre-allocated, use it for blocking operations and blocking io" 30 | (if executor-for 31 | ;; Used by a/io-thread 32 | (@executor-for :io) 33 | ;; Used by a/thread 34 | @#'a/thread-macro-executor)) 35 | 36 | (def ^:private async-pool 37 | "the core.async go block executor, it is fixed size, defaulting to 8 threads, 38 | don't soft or hard block it" 39 | (if executor-for 40 | ;; Used by a/go in 1.8+ 41 | (@executor-for :core-async-dispatch) 42 | ;; Used by a/go 43 | @d/executor)) 44 | 45 | 46 | (defn- make-cancellation-exception 47 | [] 48 | (CancellationException. "Operation was cancelled.")) 49 | 50 | (defn- make-interrupted-exception 51 | [] 52 | (InterruptedException. "Operation was interrupted.")) 53 | 54 | (defn- implicit-try 55 | "Wraps body in an implicit (try body) and uses the last forms of body if they 56 | are one or more catch, a finally or a combination of those as the catch(s) and 57 | finally block of the try. 58 | 59 | Example: 60 | (implicit-try '((println 100) (/ 1 0) (catch ArithmeticException e (println e)))) 61 | => ((try (println 100) (/ 1 0) (catch ArithmeticException e (println e))))" 62 | [body] 63 | (let [[try' rem] (split-with #(or (not (seqable? %)) (not (#{'catch 'finally} (first %)))) body) 64 | [catches rem] (split-with #(and (seqable? %) (= 'catch (first %))) rem) 65 | finally (or (when (and (seqable? (first rem)) (= 'finally (ffirst rem))) (first rem)) 66 | (when (and (seqable? (second rem)) (= 'finally (-> rem second first))) (second rem)))] 67 | (when (not= `(~@try' ~@(when catches catches) ~@(when finally [finally])) body) 68 | (throw (ex-info "Bad syntax, form must either not have a catch and finally block, or it must end with one or more catch blocks followed by a finally block in that order, or it must end with one or more catch blocks, or it must end with a single finally block." {}))) 69 | `(~@(if (not (or (seq catches) finally)) 70 | body 71 | [`(try 72 | ~@try' 73 | ~@(when catches 74 | catches) 75 | ~@(when finally 76 | [finally]))])))) 77 | 78 | (defn- settle! 79 | "Puts v into chan if it is possible to do so immediately (uses offer!) and 80 | closes chan. If v is nil it will just close chan. Returns true if offer! of v 81 | in chan succeeded or v was nil, false otherwise." 82 | [chan v] 83 | (if (nil? v) 84 | (do 85 | (a/close! chan) 86 | true) 87 | (let [ret (a/offer! chan v)] 88 | (a/close! chan) 89 | ret))) 90 | 91 | (defn error? 92 | "Returns true if v is considered an error as per async-style's error 93 | representations, false otherwise. Valid error representations in async-style 94 | for now are: 95 | * instances of Throwable" 96 | [v] 97 | (instance? Throwable v)) 98 | 99 | (defn ok? 100 | "Returns true if v is not considered an error as per async-style's error 101 | representations, false otherwise. Valid error representations in async-style 102 | for now are: 103 | * instances of Throwable" 104 | [v] 105 | (not (error? v))) 106 | 107 | (defn- chan? 108 | "Returns true if v is a core.async channel, false otherwise." 109 | [v] 110 | (instance? ManyToManyChannel v)) 111 | 112 | (def ^:private ^:dynamic *cancellation-chan*) 113 | (alter-meta! #'*cancellation-chan* assoc :doc 114 | "Used by the cancellation machinery, will be bound to a channel 115 | that will indicate if the current execution has been cancelled. 116 | Users are expected when inside an execution block like async, 117 | blocking or compute to check this channel using cancelled? to 118 | see if someone tried to cancel their execution, in which case 119 | they should short-circuit as soon as they can.") 120 | 121 | (defn cancelled? 122 | "Returns true if execution context was cancelled and thus should be 123 | interrupted/short-circuited, false otherwise. 124 | 125 | Users are expected, when inside an execution block like async, blocking or 126 | compute, to check using (cancelled? or check-cancelled!) as often as they can 127 | in case someone tried to cancel their execution, in which case they should 128 | interrupt/short-circuit the work as soon as they can." 129 | [] 130 | (if (or (some? (a/poll! *cancellation-chan*)) 131 | (.isInterrupted (Thread/currentThread))) 132 | true 133 | false)) 134 | 135 | (defn check-cancelled! 136 | "Throws if execution context was cancelled and thus should be 137 | interrupted/short-circuited, returns nil. 138 | 139 | Users are expected, when inside an execution block like async, blocking or 140 | compute, to check using (cancelled? or check-cancelled!) as often as they can 141 | in case someone tried to cancel their execution, in which case they should 142 | interrupt/short-circuit the work as soon as they can." 143 | [] 144 | (when (cancelled?) 145 | (throw (make-interrupted-exception)))) 146 | 147 | (defn cancel! 148 | "When called on chan, tries to tell processes currently executing over the 149 | chan that they should interrupt and short-circuit (aka cancel) their execution 150 | as soon as they can, as it is no longer needed. 151 | 152 | The way cancellation is conveyed is by settling the return channel of async, 153 | blocking and compute blocks to a CancellationException, unless passed a v 154 | explicitly, in which case it will settle it with v. 155 | 156 | That means by default a block that has its execution cancelled will return a 157 | CancellationException and thus awaiters and other takers of its result will 158 | see the exception and can handle it accordingly. If instead you want to cancel 159 | the block so it returns a value, pass in a v and the awaiters and takers will 160 | receive that value instead. You can't set nil as the cancelled value, 161 | attempting to do so will throw an IllegalArgumentException. 162 | 163 | It is up to processes inside async, blocking and compute blocks to properly 164 | check for cancellation on a channel." 165 | ([chan] 166 | (when (chan? chan) 167 | (settle! chan (make-cancellation-exception)))) 168 | ([chan v] 169 | (when (nil? v) 170 | (throw (IllegalArgumentException. "Can't put nil as the cancelled value."))) 171 | (when (chan? chan) 172 | (settle! chan v)))) 173 | 174 | (defn- compute-call 175 | "Executes f in the compute-pool, returning immediately to the calling thread. 176 | Returns a channel which will receive the result of calling f when completed, 177 | then close." 178 | [f] 179 | (let [c (a/chan 1) 180 | returning-to-chan (fn [bf] 181 | #(try 182 | (when-some [ret (bf)] 183 | (a/>!! c ret)) 184 | (finally (a/close! c))))] 185 | (->> f bound-fn* returning-to-chan (.execute compute-pool)) 186 | c)) 187 | 188 | (defmacro compute' 189 | "Executes the body in the compute-pool, returning immediately to the calling 190 | thread. Returns a channel which will receive the result of the body when 191 | completed, then close." 192 | [& body] 193 | (let [compute-call- compute-call] 194 | `(~compute-call- (^:once fn* [] ~@body)))) 195 | 196 | (defmacro with-lock 197 | "Run body while holding the given ReentrantLock." 198 | [^ReentrantLock lock & body] 199 | `(do 200 | (.lock ~lock) 201 | (try 202 | ~@body 203 | (finally 204 | (.unlock ~lock))))) 205 | 206 | (defn- async' 207 | "Wraps body in a way that it executes in an async or blocking block with 208 | support for cancellation, implicit-try, and returning a promise-chan settled 209 | with the result or any exception thrown." 210 | [body execution-type] 211 | (let [settle- settle!] 212 | (case execution-type 213 | :async `(let [ret# (a/promise-chan)] 214 | (a/go 215 | (binding [*cancellation-chan* ret#] 216 | (when-not (cancelled?) 217 | (~settle- ret# 218 | (try ~@(implicit-try body) 219 | (catch Throwable t# 220 | t#)))))) 221 | ret#) 222 | `(let [ret# (a/promise-chan) 223 | interrupter-thread# (volatile! nil) 224 | interrupt-lock# (ReentrantLock.) 225 | interrupter# (a/go 226 | (when (some? (a/ chan (then f1) (then f2) (then fs) ...) 431 | 432 | fs will all run on the async-pool, so if you plan on doing something blocking 433 | or compute heavy, remember to wrap it in a blocking or compute respectively." 434 | [chan & fs] 435 | (reduce 436 | (fn [chan f] (then chan f)) 437 | chan fs)) 438 | 439 | (defn handle 440 | "Asynchronously executes f with the result of chan once available (f result), 441 | unlike then, handle will always execute f, when chan's result is an error f is 442 | called with the error (f error). 443 | 444 | Returns a promise-chan settled with the result of f. 445 | 446 | Alternatively, one can pass an ok-handler and an error-handler and the 447 | respective one will be called based on if chan's result is ok (ok-handler 448 | result) or an error (error-handler error). 449 | 450 | f, ok-handler and error-handler will all run on the async-pool, so if you 451 | plan on doing something blocking or compute heavy, remember to wrap it in a 452 | blocking or compute respectively." 453 | ([chan f] 454 | (async 455 | (let [v (await* chan)] 456 | (f v)))) 457 | ([chan ok-handler error-handler] 458 | (async 459 | (let [v (await* chan)] 460 | (if (error? v) 461 | (error-handler v) 462 | (ok-handler v)))))) 463 | 464 | (defn sleep 465 | "Asynchronously sleep ms time, returns a promise-chan which settles after ms 466 | time." 467 | [ms] 468 | (async (a/ (count body) 1) 720 | (cons `do body) 721 | (first body)) 722 | false)] 723 | `(let [~@(apply concat binds)] 724 | (async ~body-form))))) 725 | 726 | (defmacro time 727 | "Evaluates expr and prints the time it took. Returns the value of expr. If 728 | expr evaluates to a channel, it waits for channel to fulfill before printing 729 | the time it took." 730 | ([expr] 731 | `(time ~expr (fn [time-ms#] (prn (str "Elapsed time: " time-ms# " msecs"))))) 732 | ([expr print-fn] 733 | (let [chan?- chan?] 734 | `(let [start# (System/nanoTime) 735 | prn-time-fn# (fn prn-time-fn# 736 | ([~'_] (prn-time-fn#)) 737 | ([] (~print-fn (/ (double (- (System/nanoTime) start#)) 1000000.0)))) 738 | ret# ~expr] 739 | (if (~chan?- ret#) 740 | (-> ret# (handle prn-time-fn#)) 741 | (prn-time-fn#)) 742 | ret#)))) 743 | -------------------------------------------------------------------------------- /test/com/xadecimal/async_style_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.xadecimal.async-style-test 2 | (:refer-clojure :exclude [await time]) 3 | (:require [clojure.test :refer [deftest is]] 4 | [com.xadecimal.async-style :as a :refer :all] 5 | [com.xadecimal.async-style.impl :as aimpl] 6 | [com.xadecimal.testa :refer [testa q! dq!]]) 7 | (:import [java.lang AssertionError] 8 | [java.util.concurrent CancellationException TimeoutException] 9 | [clojure.core.async.impl.channels ManyToManyChannel] 10 | [clojure.lang ExceptionInfo])) 11 | 12 | 13 | (deftest error?-tests 14 | (testa "Tests if something is considered an async-style error." 15 | (is (error? (ex-info "" {}))) 16 | (is (not (error? 10)))) 17 | 18 | (testa "All Throwables are considered async-style errors, and will count as an 19 | error when returned by an async block, or any promise-chan." 20 | (is (error? (try (throw (Throwable.)) 21 | (catch Throwable t 22 | t)))))) 23 | 24 | 25 | (deftest ok?-tests 26 | (testa "Tests if something is not an async-style error?" 27 | (is (ok? 10)) 28 | (is (ok? "hello")) 29 | (is (ok? :error)) 30 | (is (not (ok? (try (throw (Throwable.)) 31 | (catch Throwable t 32 | t))))))) 33 | 34 | 35 | (deftest cancelled?-tests 36 | (testa "Async-style supports cancellation, you have to explicitly check for 37 | cancelled? inside your async blocks." 38 | (-> (async (loop [i 100 acc 0] 39 | (if (and (pos? i) (not (cancelled?))) 40 | (recur (dec i) (+ acc i)) 41 | acc))) 42 | (handle q!)) 43 | (is (= 5050 (dq!)))) 44 | 45 | (testa "Here it is cancelling your loop." 46 | (let [result (atom 0) 47 | promise-chan (async (loop [i 100 acc 0] 48 | (reset! result acc) 49 | (Thread/sleep 1) 50 | (if (and (pos? i) (not (cancelled?))) 51 | (recur (dec i) (+ acc i)) 52 | acc)))] 53 | (handle promise-chan q!) 54 | (Thread/sleep 30) 55 | (cancel! promise-chan) 56 | (is (instance? CancellationException (dq!))) 57 | (is (> 5050 @result)))) 58 | 59 | (testa "When used outside an async block, it throws." 60 | (is (thrown? IllegalArgumentException (cancelled?)))) 61 | 62 | (testa "Returns true if cancelled." 63 | (let [promise-chan (async (Thread/sleep 60) 64 | (q! (cancelled?)))] 65 | (Thread/sleep 30) 66 | (cancel! promise-chan)) 67 | (is (dq!))) 68 | 69 | (testa "False otherwise." 70 | (async (q! (cancelled?))) 71 | (is (not (dq!))))) 72 | 73 | 74 | (deftest check-cancelled!-tests 75 | (testa "Async-style supports cancellation, you have to explicitly check for 76 | it using check-cancelled! inside your async blocks." 77 | (-> (async (loop [i 100 acc 0] 78 | (check-cancelled!) 79 | (if (pos? i) 80 | (recur (dec i) (+ acc i)) 81 | acc))) 82 | (handle q!)) 83 | (is (= 5050 (dq!)))) 84 | 85 | (testa "Here it is cancelling your loop." 86 | (let [result (atom 0) 87 | promise-chan (async (loop [i 100 acc 0] 88 | (check-cancelled!) 89 | (reset! result acc) 90 | (Thread/sleep 1) 91 | (if (pos? i) 92 | (recur (dec i) (+ acc i)) 93 | acc)))] 94 | (handle promise-chan q!) 95 | (Thread/sleep 30) 96 | (cancel! promise-chan) 97 | (is (instance? CancellationException (dq!))) 98 | (is (> 5050 @result)))) 99 | 100 | (testa "When used outside an async block, it throws." 101 | (is (thrown? IllegalArgumentException (check-cancelled!)))) 102 | 103 | (testa "Throws InterruptedException if cancelled." 104 | (let [promise-chan (async (Thread/sleep 60) 105 | (check-cancelled!) 106 | (catch InterruptedException e 107 | (q! e)))] 108 | (Thread/sleep 30) 109 | (cancel! promise-chan)) 110 | (is (instance? InterruptedException (dq!)))) 111 | 112 | (testa "Returns nil and doesn't throw otherwise." 113 | (async (q! (check-cancelled!)) 114 | (catch CancellationException e 115 | (q! e))) 116 | (is (nil? (dq!))))) 117 | 118 | 119 | (deftest cancel!-tests 120 | (testa "You can use cancel! to cancel an async block, this is best effort. 121 | If the block has not started executing yet, it will cancel, otherwise it 122 | needs to be the async block explicitly checks for cancelled? at certain 123 | points in time and short-circuit/interrupt, or cancel! will not be able to 124 | actually cancel." 125 | (let [promise-chan (async "I am cancelled")] 126 | (cancel! promise-chan) 127 | (is (= CancellationException (type (wait* promise-chan)))))) 128 | 129 | (testa "If the block has started executing, it won't cancel without explicit 130 | cancellation? check." 131 | (let [promise-chan (async "I am not cancelled")] 132 | (Thread/sleep 30) 133 | (cancel! promise-chan) 134 | (is (= "I am not cancelled" (wait* promise-chan))))) 135 | 136 | (testa "If the block checks for cancellation explicitly, it can still be 137 | cancelled." 138 | (let [promise-chan (async (Thread/sleep 100) 139 | "I am cancelled")] 140 | (Thread/sleep 30) 141 | (cancel! promise-chan) 142 | (is (= CancellationException (type (wait* promise-chan)))))) 143 | 144 | (testa "A value can be specified when cancelling, which is returned by the 145 | promise-chan of the async block instead of returning a CancellationException." 146 | (let [promise-chan (async (Thread/sleep 100) 147 | "I am cancelled")] 148 | (Thread/sleep 30) 149 | (cancel! promise-chan "We had to cancel this.") 150 | (is (= "We had to cancel this." (wait* promise-chan))))) 151 | 152 | (testa "Any value can be set as the cancelled val." 153 | (let [promise-chan (blocking (Thread/sleep 100) "Not cancelled")] 154 | (cancel! promise-chan 123) 155 | (is (= 123 (wait promise-chan)))) 156 | (let [promise-chan (blocking (Thread/sleep 100) "Not cancelled")] 157 | (cancel! promise-chan :foo) 158 | (is (= :foo (wait promise-chan)))) 159 | (let [promise-chan (blocking (Thread/sleep 100) "Not cancelled")] 160 | (cancel! promise-chan "cool") 161 | (is (= "cool" (wait promise-chan)))) 162 | (let [promise-chan (blocking (Thread/sleep 100) "Not cancelled")] 163 | (cancel! promise-chan {:complex true}) 164 | (is (= {:complex true} (wait promise-chan)))) 165 | (let [promise-chan (blocking (Thread/sleep 100) "Not cancelled")] 166 | (cancel! promise-chan (ex-info "Even exceptions" {:error true})) 167 | (is (thrown? ExceptionInfo (wait promise-chan))))) 168 | 169 | (testa "Except for nil, since nil is ambiguous between has the chan been 170 | cancelled or is there just nothing to poll!, so if you try to cancel! with nil 171 | it throws." 172 | (let [promise-chan (blocking (Thread/sleep 100) "Not cancelled")] 173 | (is (thrown-with-msg? IllegalArgumentException #"Can't put nil .*" 174 | (cancel! promise-chan nil))) 175 | (is (= "Not cancelled" (wait promise-chan))))) 176 | 177 | (testa "So just like in core.async, you need to use something else to represent 178 | nil in that case, like :nil or (reduced nil)" 179 | (let [promise-chan (blocking (Thread/sleep 100) "Not cancelled")] 180 | (cancel! promise-chan :nil) 181 | (is (= :nil (wait promise-chan)))) 182 | (let [promise-chan (blocking (Thread/sleep 100) "Not cancelled")] 183 | (cancel! promise-chan (reduced nil)) 184 | (is (nil? @(wait promise-chan)))))) 185 | 186 | 187 | (deftest await*-tests 188 | (testa "Takes a value from a chan." 189 | (async 190 | (q! (await* (async "Hello!")))) 191 | (is (= "Hello!" (dq!)))) 192 | 193 | (testa "Parks if none are available yet, resumes only once a value is available." 194 | (async 195 | (let [pc (async (Thread/sleep 100) 196 | "This will only have a value after 100ms")] 197 | (q! (System/currentTimeMillis)) 198 | (q! (await* pc)) 199 | (q! (System/currentTimeMillis)))) 200 | (let [before-time (dq!) 201 | ret (dq!) 202 | after-time (dq!)] 203 | (is (>= (- after-time before-time) 100)) 204 | (is (= "This will only have a value after 100ms" ret)))) 205 | 206 | (testa "Throws if used outside an async block" 207 | (is (thrown? AssertionError (await* (async))))) 208 | 209 | (testa "Works on values as well, will just return it immediately." 210 | (async (q! (await* :a-value)) 211 | (q! (await* 1)) 212 | (q! (await* ["a" "b"])) 213 | (q! (System/currentTimeMillis)) 214 | (q! (await* :a-value)) 215 | (q! (System/currentTimeMillis))) 216 | (is (= :a-value (dq!))) 217 | (is (= 1 (dq!))) 218 | (is (= ["a" "b"] (dq!))) 219 | (let [before-time (dq!) 220 | _ (dq!) 221 | after-time (dq!)] 222 | (is (<= (- after-time before-time) 1)))) 223 | 224 | (testa "If an exception is returned by chan, it will return the exception, 225 | it won't throw." 226 | (async 227 | (q! (await* (async (/ 1 0))))) 228 | (is (= ArithmeticException (type (dq!))))) 229 | 230 | (testa "Taken value will be fully joined. That means if the value taken is 231 | itself a chan, <= (- after-time before-time) 100)) 249 | (is (= "This will only have a value after 100ms" ret)))) 250 | 251 | (testa "Should not be used inside an async block, since it will block instead 252 | of parking." 253 | (async (q! (wait* (async "Don't do this even though it works.")))) 254 | (is (= "Don't do this even though it works." (dq!)))) 255 | 256 | (testa "Works on values as well, will just return it immediately." 257 | (is (= :a-value (wait* :a-value))) 258 | (is (= 1 (wait* 1))) 259 | (is (= ["a" "b"] (wait* ["a" "b"]))) 260 | (let [before-time (System/currentTimeMillis) 261 | _ (wait* :a-value) 262 | after-time (System/currentTimeMillis)] 263 | (is (<= (- after-time before-time) 1)))) 264 | 265 | (testa "If an exception is returned by chan, it will return the exception, 266 | it won't throw." 267 | (is (= ArithmeticException (type (wait* (async (/ 1 0))))))) 268 | 269 | (testa "Taken value will be fully joined. That means if the value taken is 270 | itself a chan, < (async (+ 1 2)) 280 | (wait))))) 281 | (testa "Takes a value from a chan." 282 | (is (= "Hello!" (wait (async "Hello!"))))) 283 | 284 | (testa "Blocks if none are available yet, resumes only once a value is available." 285 | (let [pc (async (Thread/sleep 100) 286 | "This will only have a value after 100ms") 287 | before-time (System/currentTimeMillis) 288 | ret (wait pc) 289 | after-time (System/currentTimeMillis)] 290 | (is (>= (- after-time before-time) 100)) 291 | (is (= "This will only have a value after 100ms" ret)))) 292 | 293 | (testa "Should not be used inside an async block, since it will block instead 294 | of parking." 295 | (async (q! (wait (async "Don't do this even though it works.")))) 296 | (is (= "Don't do this even though it works." (dq!)))) 297 | 298 | (testa "Works on values as well, will just return it immediately." 299 | (is (= :a-value (wait :a-value))) 300 | (is (= 1 (wait 1))) 301 | (is (= ["a" "b"] (wait ["a" "b"]))) 302 | (let [before-time (System/currentTimeMillis) 303 | _ (wait :a-value) 304 | after-time (System/currentTimeMillis)] 305 | (is (<= (- after-time before-time) 1)))) 306 | 307 | (testa "If an exception is returned by chan, it will re-throw the exception." 308 | (is (thrown? ArithmeticException (wait (async (/ 1 0)))))) 309 | 310 | (testa "Taken value will be fully joined. That means if the value taken is 311 | itself a chan, wait will also take from it, until it eventually takes a non chan 312 | value." 313 | (is (= 1 (wait (async (async (async 1))))))) 314 | 315 | (testa "Takes a value from a chan." 316 | (is (= "Hello!" (wait (async "Hello!"))))) 317 | 318 | (testa "Blocks if none are available yet, resumes only once a value is available." 319 | (let [pc (async (Thread/sleep 100) 320 | "This will only have a value after 100ms") 321 | before-time (System/currentTimeMillis) 322 | ret (wait pc) 323 | after-time (System/currentTimeMillis)] 324 | (is (>= (- after-time before-time) 100)) 325 | (is (= "This will only have a value after 100ms" ret)))) 326 | 327 | (testa "Should not be used inside an async block, since it will block instead 328 | of parking." 329 | (async (q! (wait (async "Don't do this even though it works.")))) 330 | (is (= "Don't do this even though it works." (dq!)))) 331 | 332 | (testa "Works on values as well, will just return it immediately." 333 | (is (= :a-value (wait :a-value))) 334 | (is (= 1 (wait 1))) 335 | (is (= ["a" "b"] (wait ["a" "b"]))) 336 | (let [before-time (System/currentTimeMillis) 337 | _ (wait :a-value) 338 | after-time (System/currentTimeMillis)] 339 | (is (<= (- after-time before-time) 1)))) 340 | 341 | (testa "If an exception is returned by chan, it will re-throw the exception." 342 | (is (thrown? ArithmeticException (wait (async (/ 1 0)))))) 343 | 344 | (testa "Taken value will be fully joined. That means if the value taken is 345 | itself a chan, wait will also take from it, until it eventually takes a non chan 346 | value." 347 | (is (= 1 (wait (async (async (async 1)))))))) 348 | 349 | 350 | (deftest async-tests 351 | (testa "Async is used to run some code asynchronously on the async-pool." 352 | (-> (async (+ 1 2)) 353 | (handle q!)) 354 | (is (= 3 (dq!)))) 355 | 356 | (testa "It wraps any form, so even a single value works." 357 | (-> (async 1) 358 | (handle q!)) 359 | (is (= 1 (dq!)))) 360 | 361 | (testa "You can use it to create async functions, by wrapping the fn body with it." 362 | (-> ((fn fetch-data [] (async :data))) 363 | (handle q!)) 364 | (is (= :data (dq!)))) 365 | 366 | (testa "The function can return anything." 367 | (-> ((fn fetch-data [] (async (map inc [1 2 3])))) 368 | (handle q!)) 369 | (is (= [2 3 4] (dq!)))) 370 | 371 | (testa "You can freely throw inside async, the exception will be returned as a value." 372 | (-> (async (throw (ex-info "Error fetching data" {}))) 373 | (handle q!)) 374 | (is (= "Error fetching data" (ex-message (dq!))))) 375 | 376 | (testa "This works for async functions as well." 377 | (-> ((fn fetch-data [] (async (throw (ex-info "Error fetching data" {}))))) 378 | (handle q!)) 379 | (is (= "Error fetching data" (ex-message (dq!))))) 380 | 381 | (testa "Async returns a promise-chan immediately, and does not block." 382 | (let [t1 (System/currentTimeMillis) 383 | chan (async (Thread/sleep 1000) :done) 384 | t2 (System/currentTimeMillis)] 385 | (is (= ManyToManyChannel (type chan))) 386 | (is (< (- t2 t1) 100)) 387 | (handle chan q!) 388 | (is (= :done (dq! 2000))))) 389 | 390 | (testa "Async should be used to shuffle data around, or for very small computations. 391 | Because all blocks will run in a fixed size thread pool, which by default has only 8 392 | threads. This means you can't have more than 8 concurrent active blocks, others will 393 | get queued up and have to wait for one of those 8 to finish. That means avoid doing 394 | blocking operations or long running computations, for which you should use blocking 395 | or compute instead. In core.async 1.8+ this is no longer true, as the async pool 396 | has been replaced by an unbounded pool, but it is still a good practice to use 397 | it only for async control flow." 398 | (-> (for [i (range 10)] 399 | (async (Thread/sleep 1000) i)) 400 | (all) 401 | (handle q!)) 402 | (if @#'aimpl/executor-for 403 | (is (= [0 1 2 3 4 5 6 7 8 9] (dq! 1100))) 404 | (is (= :timeout (dq! 1100))))) 405 | 406 | (testa "Async supports implicit-try, meaning you can catch/finally on it as you would 407 | with try/catch/finally. It's similar to if you always wrapped the inside in a try." 408 | (-> (async (/ 1 0) 409 | (catch ArithmeticException e 410 | 0)) 411 | (handle q!)) 412 | (is (= 0 (dq!)))) 413 | 414 | (testa "Async example of implicit-try with a finally." 415 | (-> (async (+ 1 1) 416 | (finally (q! :finally-called))) 417 | (handle q!)) 418 | (is (= :finally-called (dq!))) 419 | (is (= 2 (dq!)))) 420 | 421 | (testa "Or with catch and finally together." 422 | (-> (async (/ 1 0) 423 | (catch ArithmeticException e 424 | (q! :catch-called) 425 | 0) 426 | (finally (q! :finally-called))) 427 | (handle q!)) 428 | (is (= :catch-called (dq!))) 429 | (is (= :finally-called (dq!))) 430 | (is (= 0 (dq!)))) 431 | 432 | (testa "Async block can be cancelled, they will skip their execution if cancelled 433 | before they begin executing." 434 | (cancel! (async (q! :will-timeout-due-to-cancel))) 435 | (is (= :timeout (dq!)))) 436 | 437 | (testa "If you plan on doing lots of work, and want to support it being cancellable, 438 | you can explictly check for cancellation to interupt your work. By default a 439 | cancelled async block returns a CancellationException." 440 | (let [work (async (loop [i 0] 441 | (when-not (cancelled?) 442 | (recur (inc i)))))] 443 | (Thread/sleep 5) 444 | (cancel! work) 445 | (handle work q!)) 446 | (is (= CancellationException (type (dq! 50))))) 447 | 448 | (testa "You can explicitly set a value when cancelling, and the cancelled async 449 | chan will return that value instead of throwing a CancellationException." 450 | (let [work (async (loop [i 0] 451 | (when-not (cancelled?) 452 | (recur (inc i)))))] 453 | (Thread/sleep 5) 454 | (cancel! work :had-to-cancel) 455 | (handle work q!)) 456 | (is (= :had-to-cancel (dq!)))) 457 | 458 | (testa "Java platform level cancellation is not supported in async, and 459 | therefore, if you block inside it, which you are not supposed to do, you won't 460 | be able to cancel those blocking ops." 461 | (let [task (async (Thread/sleep 500) 462 | (println "This will print") 463 | (q! :was-cancelled))] 464 | (Thread/sleep 100) 465 | (cancel! task :cancelled) 466 | (q! (wait task)) 467 | (is (= :cancelled (dq!))) 468 | (is (= :was-cancelled (dq!)))))) 469 | 470 | 471 | (deftest blocking-tests 472 | (testa "Blocking is used to run some blocking code asynchronously on the blocking-pool." 473 | (-> (blocking (+ 1 2)) 474 | (handle q!)) 475 | (is (= 3 (dq!)))) 476 | 477 | (testa "It wraps any form, so even a single value works." 478 | (-> (blocking 1) 479 | (handle q!)) 480 | (is (= 1 (dq!)))) 481 | 482 | (testa "You can use it to create blocking functions, by wrapping the fn body with it." 483 | (-> ((fn fetch-data [] (blocking :data))) 484 | (handle q!)) 485 | (is (= :data (dq!)))) 486 | 487 | (testa "The function can return anything." 488 | (-> ((fn fetch-data [] (blocking (map inc [1 2 3])))) 489 | (handle q!)) 490 | (is (= [2 3 4] (dq!)))) 491 | 492 | (testa "You can freely throw inside blocking, the exception will be returned as a value." 493 | (-> (blocking (throw (ex-info "Error fetching data" {}))) 494 | (handle q!)) 495 | (is (= "Error fetching data" (ex-message (dq!))))) 496 | 497 | (testa "This works for blocking functions as well." 498 | (-> ((fn fetch-data [] (blocking (throw (ex-info "Error fetching data" {}))))) 499 | (handle q!)) 500 | (is (= "Error fetching data" (ex-message (dq!))))) 501 | 502 | (testa "Blocking returns a promise-chan immediately, and does not block." 503 | (let [t1 (System/currentTimeMillis) 504 | chan (blocking (Thread/sleep 1000) :done) 505 | t2 (System/currentTimeMillis)] 506 | (is (= ManyToManyChannel (type chan))) 507 | (is (< (- t2 t1) 100)) 508 | (handle chan q!) 509 | (is (= :done (dq! 2000))))) 510 | 511 | (testa "Blocking should be used when what you do inside the block is going to 512 | block the running thread. Because unlike async and compute, which both run on 513 | shared thread pool with a limited number of threads, blocking creates as many 514 | threads as are needed so all your blocking blocks run concurrently." 515 | (-> (for [i (range 200)] 516 | (blocking (Thread/sleep 1000) i)) 517 | (all) 518 | (handle q!)) 519 | (is (= (range 200) (dq! 1100)))) 520 | 521 | (testa "Blocking supports implicit-try, meaning you can catch/finally on it as you would 522 | with try/catch/finally. It's similar to if you always wrapped the inside in a try." 523 | (-> (blocking (/ 1 0) 524 | (catch ArithmeticException e 525 | 0)) 526 | (handle q!)) 527 | (is (= 0 (dq!)))) 528 | 529 | (testa "Blocking example of implicit-try with a finally." 530 | (-> (blocking (+ 1 1) 531 | (finally (q! :finally-called))) 532 | (handle q!)) 533 | (is (= :finally-called (dq!))) 534 | (is (= 2 (dq!)))) 535 | 536 | (testa "Or with catch and finally together." 537 | (-> (blocking (/ 1 0) 538 | (catch ArithmeticException e 539 | (q! :catch-called) 540 | 0) 541 | (finally (q! :finally-called))) 542 | (handle q!)) 543 | (is (= :catch-called (dq!))) 544 | (is (= :finally-called (dq!))) 545 | (is (= 0 (dq!)))) 546 | 547 | (testa "Blocking block can be cancelled, they will skip their execution if cancelled 548 | before they begin executing." 549 | (cancel! (blocking (q! :will-timeout-due-to-cancel))) 550 | (is (= :timeout (dq!)))) 551 | 552 | (testa "If you plan on doing lots of work, and want to support it being cancellable, 553 | you can explictly check for cancellation to interupt your work. By default a 554 | cancelled blocking block returns a CancellationException." 555 | (let [work (blocking (loop [i 0] 556 | (when-not (cancelled?) 557 | (recur (inc i)))))] 558 | (Thread/sleep 5) 559 | (cancel! work) 560 | (handle work q!)) 561 | (is (= CancellationException (type (dq! 50))))) 562 | 563 | (testa "You can explicitly set a value when cancelling, and the cancelled blocking 564 | chan will return that value instead of throwing a CancellationException." 565 | (let [work (blocking (loop [i 0] 566 | (when-not (cancelled?) 567 | (recur (inc i)))))] 568 | (Thread/sleep 5) 569 | (cancel! work :had-to-cancel) 570 | (handle work q!)) 571 | (is (= :had-to-cancel (dq!)))) 572 | 573 | (testa "Java platform level cancellation is also supported, it will cause 574 | an interrupt of the thread, therefore even cancelling the Java operation, if 575 | it supports handling thread interrupted." 576 | (let [task (blocking (Thread/sleep 500) 577 | (println "This won't print") 578 | (q! :was-not-cancelled))] 579 | (Thread/sleep 100) 580 | (cancel! task :cancelled) 581 | (q! (wait task)) 582 | (is (= :cancelled (dq!))) 583 | (is (not= :was-not-cancelled (dq!)))))) 584 | 585 | 586 | (deftest compute-tests 587 | (testa "Compute is used to run some code asynchronously on the compute-pool." 588 | (-> (compute (+ 1 2)) 589 | (handle q!)) 590 | (is (= 3 (dq!)))) 591 | 592 | (testa "It wraps any form, so even a single value works." 593 | (-> (compute 1) 594 | (handle q!)) 595 | (is (= 1 (dq!)))) 596 | 597 | (testa "You can use it to create compute functions, by wrapping the fn body with it." 598 | (-> ((fn fetch-data [] (compute :data))) 599 | (handle q!)) 600 | (is (= :data (dq!)))) 601 | 602 | (testa "The function can return anything." 603 | (-> ((fn fetch-data [] (compute (map inc [1 2 3])))) 604 | (handle q!)) 605 | (is (= [2 3 4] (dq!)))) 606 | 607 | (testa "You can freely throw inside compute, the exception will be returned as a value." 608 | (-> (compute (throw (ex-info "Error fetching data" {}))) 609 | (handle q!)) 610 | (is (= "Error fetching data" (ex-message (dq!))))) 611 | 612 | (testa "This works for compute functions as well." 613 | (-> ((fn fetch-data [] (compute (throw (ex-info "Error fetching data" {}))))) 614 | (handle q!)) 615 | (is (= "Error fetching data" (ex-message (dq!))))) 616 | 617 | (testa "Compute returns a promise-chan immediately, and does not block." 618 | (let [t1 (System/currentTimeMillis) 619 | chan (compute (Thread/sleep 1000) :done) 620 | t2 (System/currentTimeMillis)] 621 | (is (= ManyToManyChannel (type chan))) 622 | (is (< (- t2 t1) 100)) 623 | (handle chan q!) 624 | (is (= :done (dq! 2000))))) 625 | 626 | (testa "Compute should be used for long computations. Because all blocks will 627 | run in a fixed size thread pool, which by default is the number of cores on your 628 | computer + 2. This means you can't have more concurrent active blocks, others will 629 | get queued up and have to wait for one of those to finish. When doing long computations, 630 | " 631 | (-> (for [i (->> (-> (Runtime/getRuntime) .availableProcessors) 632 | (+ 2) 633 | inc 634 | range)] 635 | (compute (Thread/sleep 1000) i)) 636 | (all) 637 | (handle q!)) 638 | (is (= :timeout (dq! 1100)))) 639 | 640 | (testa "Compute supports implicit-try, meaning you can catch/finally on it as you would 641 | with try/catch/finally. It's similar to if you always wrapped the inside in a try." 642 | (-> (compute (/ 1 0) 643 | (catch ArithmeticException e 644 | 0)) 645 | (handle q!)) 646 | (is (= 0 (dq!)))) 647 | 648 | (testa "Compute example of implicit-try with a finally." 649 | (-> (compute (+ 1 1) 650 | (finally (q! :finally-called))) 651 | (handle q!)) 652 | (is (= :finally-called (dq!))) 653 | (is (= 2 (dq!)))) 654 | 655 | (testa "Or with catch and finally together." 656 | (-> (compute (/ 1 0) 657 | (catch ArithmeticException e 658 | (q! :catch-called) 659 | 0) 660 | (finally (q! :finally-called))) 661 | (handle q!)) 662 | (is (= :catch-called (dq!))) 663 | (is (= :finally-called (dq!))) 664 | (is (= 0 (dq!)))) 665 | 666 | (testa "Compute block can be cancelled, they will skip their execution if cancelled 667 | before they begin executing." 668 | (cancel! (compute (q! :will-timeout-due-to-cancel))) 669 | (is (= :timeout (dq!)))) 670 | 671 | (testa "If you plan on doing lots of work, and want to support it being cancellable, 672 | you can explictly check for cancellation to interupt your work. By default a 673 | cancelled compute block returns a CancellationException." 674 | (let [work (compute (loop [i 0] 675 | (when-not (cancelled?) 676 | (recur (inc i)))))] 677 | (Thread/sleep 5) 678 | (cancel! work) 679 | (handle work q!)) 680 | (is (= CancellationException (type (dq! 50))))) 681 | 682 | (testa "You can explicitly set a value when cancelling, and the cancelled compute 683 | chan will return that value instead of throwing a CancellationException." 684 | (let [work (compute (loop [i 0] 685 | (when-not (cancelled?) 686 | (recur (inc i)))))] 687 | (Thread/sleep 5) 688 | (cancel! work :had-to-cancel) 689 | (handle work q!)) 690 | (is (= :had-to-cancel (dq!)))) 691 | 692 | (testa "Java platform level cancellation is also supported, it will cause 693 | an interrupt of the thread, therefore even cancelling the Java operation, if 694 | it supports handling thread interrupted." 695 | (let [task (compute (Thread/sleep 500) 696 | (println "This won't print") 697 | (q! :was-not-cancelled))] 698 | (Thread/sleep 100) 699 | (cancel! task :cancelled) 700 | (q! (wait task)) 701 | (is (= :cancelled (dq!))) 702 | (is (not= :was-not-cancelled (dq!)))))) 703 | 704 | 705 | (deftest await-tests 706 | (testa "Await is used to wait on the result of an async operation." 707 | (async 708 | (-> (async (+ 1 2)) 709 | (await) 710 | (q!))) 711 | (is (= 3 (dq!)))) 712 | 713 | (testa "You can only use await inside an async block, or it'll error." 714 | (is (thrown? AssertionError (await (async (+ 1 2)))))) 715 | 716 | (testa "You can await into a let to wait for the result of some async operation." 717 | (async 718 | (let [res (await (async (+ 1 2)))] 719 | (q! res))) 720 | (is (= 3 (dq!)))) 721 | 722 | (testa "You cannot await into a def, this is a current limitation, core.async suffers 723 | from the same, you can't (async ((fn [] (await (async (+ 1 1)))))) 798 | (handle q!)) 799 | (is (= AssertionError (type (dq!))))) 800 | 801 | (testa "In that case, you need the inner functions to also be async." 802 | (-> (async ((fn [] (async (await (async (+ 1 1))))))) 803 | (handle q!)) 804 | (is (= 2 (dq!)))) 805 | 806 | (testa "This means the following also does not work, same as core.async, because 807 | for internally creates inner functions." 808 | (-> (async (doall 809 | (for [x [(async 1) (async 2) (async 3)]] 810 | (inc (await x))))) 811 | (handle q!)) 812 | (is (= AssertionError (type (dq!))))) 813 | 814 | (testa "But since we're working in an async-style, we can wrap the inner logic in 815 | async blocks again." 816 | (-> (doall 817 | (for [x [(async 1) (async 2) (async 3)]] 818 | (async (inc (await x))))) 819 | (all-settled) 820 | (handle q!)) 821 | (is (= [2 3 4] (dq!)))) 822 | 823 | (testa "Or, like in core.async, try to use an alternate construct that doesn't wrap 824 | things in inner functions." 825 | (-> (async (loop [[x & xs] [(async 1) (async 2) (async 3)] res []] 826 | (if x 827 | (recur xs (conj res (inc (await x)))) 828 | res))) 829 | (handle q!)) 830 | (is (= [2 3 4] (dq!)))) 831 | 832 | (testa "Or, await all async operations in the sequence first, and then increment." 833 | (-> (async (doall 834 | (for [x (await (all-settled [(async 1) (async 2) (async 3)]))] 835 | (inc x)))) 836 | (handle q!)) 837 | (is (= [2 3 4] (dq!)))) 838 | 839 | (testa "Or, leverage more of the other async-style functions." 840 | (-> [(async 1) (async 2) (async 3)] 841 | (all-settled) 842 | (handle #(for [x %] (inc x))) 843 | (handle q!)) 844 | (is (= [2 3 4] (dq!)))) 845 | 846 | (testa "Takes a value from a chan." 847 | (async 848 | (q! (await (async "Hello!")))) 849 | (is (= "Hello!" (dq!)))) 850 | 851 | (testa "Parks if none are available yet, resumes only once a value is available." 852 | (async 853 | (let [pc (async (Thread/sleep 100) 854 | "This will only have a value after 100ms")] 855 | (q! (System/currentTimeMillis)) 856 | (q! (await pc)) 857 | (q! (System/currentTimeMillis)))) 858 | (let [before-time (dq!) 859 | ret (dq!) 860 | after-time (dq!)] 861 | (is (>= (- after-time before-time) 100)) 862 | (is (= "This will only have a value after 100ms" ret)))) 863 | 864 | (testa "Throws if used outside an async block" 865 | (is (thrown? AssertionError (await (async))))) 866 | 867 | (testa "Works on values as well, will just return it immediately." 868 | (async (q! (await :a-value)) 869 | (q! (await 1)) 870 | (q! (await ["a" "b"])) 871 | (q! (System/currentTimeMillis)) 872 | (q! (await :a-value)) 873 | (q! (System/currentTimeMillis))) 874 | (is (= :a-value (dq!))) 875 | (is (= 1 (dq!))) 876 | (is (= ["a" "b"] (dq!))) 877 | (let [before-time (dq!) 878 | _ (dq!) 879 | after-time (dq!)] 880 | (is (<= (- after-time before-time) 1)))) 881 | 882 | (testa "If an exception is returned by chan, it will re-throw the exception." 883 | (async (try 884 | (await (async (/ 1 0))) 885 | (catch ArithmeticException _ 886 | (q! :thrown)))) 887 | (is (= :thrown (dq!)))) 888 | 889 | (testa "Taken value will be fully joined. That means if the value taken is 890 | itself a chan, await will also take from it, until it eventually takes a non chan 891 | value." 892 | (async 893 | (q! (await (async (async (async 1)))))) 894 | (is (= 1 (dq!))))) 895 | 896 | 897 | (deftest catch-tests 898 | (testa "Catch awaits the async value and returns it, but if it was an error 899 | it calls the provided error handler with the error, and instead return what that 900 | returns." 901 | (-> (async (/ 1 0)) 902 | (catch (fn [_e] 10)) 903 | (handle q!)) 904 | (is (= 10 (dq!)))) 905 | (testa "You can provide a type to filter the error on, it will only call the 906 | error handler if the error is of that type." 907 | (-> (async (/ 1 0)) 908 | (catch ArithmeticException (fn [_e] 10)) 909 | (handle q!) 910 | (wait*)) 911 | (-> (async (/ 1 0)) 912 | (catch IllegalStateException (fn [_e] 10)) 913 | (handle q!) 914 | (wait*)) 915 | (is (= 10 (dq!))) 916 | (is (not= 10 (dq!)))) 917 | (testa "So you can chain it to handle different type of errors in different 918 | ways." 919 | (-> (async (/ 1 0)) 920 | (catch IllegalStateException (fn [_e] 1)) 921 | (catch ArithmeticException (fn [_e] 2)) 922 | (handle q!)) 923 | (is (= 2 (dq!)))) 924 | (testa "You can provide a predicate instead as well, to filter on the error." 925 | (-> (async (throw (ex-info "An error" {:type :foo}))) 926 | (catch (fn [e] (= :foo (-> e ex-data :type))) (fn [_e] 10)) 927 | (handle q!) 928 | (wait*)) 929 | (-> (async (throw (ex-info "An error" {:type :foo}))) 930 | (catch (fn [e] (= :bar (-> e ex-data :type))) (fn [_e] 10)) 931 | (handle q!) 932 | (wait*)) 933 | (is (= 10 (dq!))) 934 | (is (not= 10 (dq!))))) 935 | 936 | 937 | (deftest finally-tests 938 | (testa "Finally runs a function f after a value is fulfilled on the chan for 939 | side-effect." 940 | (-> (async (+ 1 2)) 941 | (finally (fn [v] (q! (str "We got " v)))) 942 | (handle q!)) 943 | (is (= "We got 3" (dq!))) 944 | (is (= 3 (dq!)))) 945 | (testa "The function f is called even if it errored." 946 | (-> (async (/ 1 0)) 947 | (finally (fn [v] (q! v))) 948 | (handle q!)) 949 | (is (error? (dq!))) 950 | (is (error? (dq!)))) 951 | (testa "It's nice to combine with catch" 952 | (-> (async (/ 1 0)) 953 | (catch (fn [e] :error)) 954 | (finally (fn [v] (q! (str "We got " v)))) 955 | (handle q!)) 956 | (is (= "We got :error" (dq!))) 957 | (is (= :error (dq!))))) 958 | 959 | 960 | (deftest then-tests 961 | (testa "To do something after a value is fulfilled use then" 962 | (-> (async (+ 1 2)) 963 | (then inc) 964 | (then -) 965 | (then q!)) 966 | (is (= -4 (dq!)))) 967 | (testa "It won't be called if an error occurred, instead the error will be 968 | returned and 'then' short-circuits." 969 | (-> (async (/ 1 0)) 970 | (then inc) 971 | (handle q!)) 972 | (is (error? (dq!)))) 973 | (testa "Which is nice to combine with catch and finally." 974 | (-> (async (/ 1 0)) 975 | (then inc) 976 | (catch (fn [_e] 0)) 977 | (finally (fn [_v] (q! "Compute done"))) 978 | (handle q!)) 979 | (is (= "Compute done" (dq!))) 980 | (is (= 0 (dq!))))) 981 | 982 | 983 | (deftest chain-tests 984 | (testa "If you want to chain operations, as if by 'then', you an use chain 985 | as well, which can be cleaner." 986 | (-> (async (+ 1 2)) 987 | (chain inc inc #(* 2 %) -) 988 | (handle q!)) 989 | (is (= -10 (dq!)))) 990 | (testa "Like 'then', it short-circuit on first error and returns the error 991 | instead." 992 | (-> (async (+ 1 2)) 993 | (chain inc #(/ % 0) #(* 2 %) -) 994 | (handle q!)) 995 | (is (error? (dq!)))) 996 | (testa "Which makes it nice to combine with catch and finally." 997 | (-> (async (+ 1 2)) 998 | (chain inc #(/ % 0) #(* 2 %) -) 999 | (catch (fn [_e] 0)) 1000 | (finally (fn [_v] (q! "Compute done"))) 1001 | (handle q!)) 1002 | (is (= "Compute done" (dq!))) 1003 | (is (= 0 (dq!))))) 1004 | 1005 | 1006 | (deftest handle-tests 1007 | (testa "Handle is used to handle the fulfilled chan result no matter if 1008 | it succeeded or errored." 1009 | (-> (async (+ 1 2)) 1010 | (handle q!)) 1011 | (is (= 3 (dq!))) 1012 | (-> (async (/ 1 0)) 1013 | (handle q!)) 1014 | (is (error? (dq!)))) 1015 | (testa "You can pass it two handlers if you want a different handling if 1016 | the result is a success versus an error." 1017 | (-> (async (+ 1 2)) 1018 | (handle (fn on-ok [v] (inc v)) 1019 | (fn on-error [_e] 0)) 1020 | (handle q!)) 1021 | (is (= 4 (dq!))) 1022 | (-> (async (/ 1 0)) 1023 | (handle (fn on-ok [v] (inc v)) 1024 | (fn on-error [_e] 0)) 1025 | (handle q!)) 1026 | (is (= 0 (dq!))))) 1027 | 1028 | 1029 | (deftest sleep-tests 1030 | (testa "Sleep can be used to sleep X millisecond time inside an async process." 1031 | (-> (async (let [curr-time (System/currentTimeMillis) 1032 | _ (await (sleep 400)) 1033 | end-time (System/currentTimeMillis)] 1034 | (- end-time curr-time))) 1035 | (handle q!)) 1036 | (is (<= 400 (dq!) 500)))) 1037 | 1038 | 1039 | (deftest defer-tests 1040 | (testa "Defer can be used to schedule something in the future." 1041 | (defer 400 #(q! :done)) 1042 | (is (= :timeout (dq! 300))) 1043 | (is (= :done (dq!)))) 1044 | (testa "Or to delay returning a value" 1045 | (-> (defer 400 :done) 1046 | (handle q!)) 1047 | (is (= :timeout (dq! 300))) 1048 | (is (= :done (dq!))))) 1049 | 1050 | 1051 | (deftest timeout-tests 1052 | (testa "When you want to wait for a chan to fulfill up too some time in 1053 | millisecond you use timeout. It returns a TimeoutException on timeout." 1054 | (-> (timeout (sleep 1000) 500) 1055 | (handle q!)) 1056 | (is (instance? TimeoutException (dq!)))) 1057 | (testa "Or if you prefer you can have it return a value of your choosing on 1058 | timeout instead." 1059 | (-> (timeout (sleep 1000) 500 :it-timed-out) 1060 | (handle q!)) 1061 | (is (= :it-timed-out (dq!)))) 1062 | (testa "Or if you prefer you can have it run and return a function f of your 1063 | choosing on timeout instead." 1064 | (-> (timeout (sleep 1000) 500 #(do (q! "It timed out!") :it-timed-out)) 1065 | (handle q!)) 1066 | (is (= "It timed out!" (dq!))) 1067 | (is (= :it-timed-out (dq!)))) 1068 | (testa "When it doesn't time out, it returns the value from the chan." 1069 | (-> (timeout (async (+ 1 2)) 500) 1070 | (handle q!)) 1071 | (is (= 3 (dq!)))) 1072 | (testa "When it does time out, chan will also be cancelled if possible." 1073 | (-> (timeout 1074 | (blocking (loop [i 0] 1075 | (when-not (cancelled?) 1076 | (q! i) 1077 | (Thread/sleep 100) 1078 | (recur (inc i))))) 1079 | 500 1080 | :it-timed-out) 1081 | (handle q!) 1082 | (wait)) 1083 | (let [[x & xs] (loop [xs [] x (dq!)] 1084 | (if (= x :it-timed-out) 1085 | (into [x] xs) 1086 | (recur (conj xs x) (dq!))))] 1087 | (is (> (count xs) 3)) 1088 | (is (= :it-timed-out x))))) 1089 | 1090 | 1091 | (deftest race-tests 1092 | (testa "Race returns the first chan to fulfill." 1093 | (-> (race [(defer 100 1) (defer 100 2) (async 3)]) 1094 | (handle q!)) 1095 | (is (= 3 (dq!)))) 1096 | 1097 | (testa "If the first chan to fulfill does so with an error?, it still 1098 | wins the race and the error gets returned in the promise-chan." 1099 | (-> (race [(async (throw (ex-info "error" {}))) 1100 | (defer 100 :ok)]) 1101 | (handle q!)) 1102 | (is (error? (dq!)))) 1103 | 1104 | (testa "Passing an empty seq will return a closed promise-chan, which in 1105 | turn returns nil when taken from." 1106 | (-> (race []) 1107 | (handle q!)) 1108 | (is (nil? (dq!)))) 1109 | 1110 | (testa "Passing nil will return a closed promise-chan, which in 1111 | turn returns nil when taken from." 1112 | (-> (race nil) 1113 | (handle q!)) 1114 | (is (nil? (dq!)))) 1115 | 1116 | (testa "chans can also contain values which will be treated as an already 1117 | settled chan which returns itself." 1118 | (-> (race [1 (defer 100 2)]) 1119 | (handle q!)) 1120 | (is (= 1 (dq!)))) 1121 | 1122 | (testa "When passing values, they are still racing asynchronously against 1123 | each other, and the result order is non-deterministic and should 1124 | not be depended on." 1125 | (is (not 1126 | (every? 1127 | #{1} 1128 | (doall 1129 | (for [i (range 3000)] 1130 | (-> (race [1 2 3 4 5 6 7 8 9 (async 10)]) 1131 | (wait)))))))) 1132 | 1133 | (testa "Race cancels all other chans once one of them fulfills." 1134 | (-> (race [(blocking 1135 | (Thread/sleep 100) 1136 | (catch InterruptedException _ 1137 | (q! :cancelled))) 1138 | (blocking 1139 | (Thread/sleep 100) 1140 | (catch InterruptedException _ 1141 | (q! :cancelled))) 1142 | (blocking 1143 | (Thread/sleep 100) 1144 | (catch InterruptedException _ 1145 | (q! :cancelled))) 1146 | (async :done)]) 1147 | (handle q!)) 1148 | (is (= :done (dq!))) 1149 | (is (= :cancelled (dq!))) 1150 | (is (= :cancelled (dq!))) 1151 | (is (= :cancelled (dq!)))) 1152 | 1153 | (testa "Even if the first one to fulfill did so with an error, others are 1154 | still cancelled." 1155 | (-> (race [(blocking 1156 | (Thread/sleep 100) 1157 | (catch InterruptedException _ 1158 | (q! :cancelled))) 1159 | (blocking 1160 | (Thread/sleep 100) 1161 | (catch InterruptedException _ 1162 | (q! :cancelled))) 1163 | (blocking 1164 | (Thread/sleep 100) 1165 | (catch InterruptedException _ 1166 | (q! :cancelled))) 1167 | (async (throw (ex-info "error" {})))]) 1168 | (handle q!)) 1169 | (is (error? (dq!))) 1170 | (is (= :cancelled (dq!))) 1171 | (is (= :cancelled (dq!))) 1172 | (is (= :cancelled (dq!))))) 1173 | 1174 | 1175 | (deftest any-tests 1176 | (testa "Any returns the first chan to fulfill with an ok?." 1177 | (-> (any [(async (throw (ex-info "error" {}))) (defer 100 2) (defer 50 3)]) 1178 | (handle q!)) 1179 | (is (= 3 (dq!)))) 1180 | 1181 | (testa "As we saw, chans that fulfill in error? are ignored, so when all chans 1182 | fulfill in error?, any returns a promise-chan settled with an error of 1183 | its own. This error will contain the list of all errors taken from all 1184 | chans and its ex-data will have a key :type with value :all-errored." 1185 | (-> (any [(async (throw (ex-info "error" {}))) 1186 | (async (throw (ex-info "error" {}))) 1187 | (async (throw (ex-info "error" {})))]) 1188 | (handle q!)) 1189 | (let [ret (dq!)] 1190 | (is (error? ret)) 1191 | (is (= :all-errored (:type (ex-data ret)))))) 1192 | 1193 | (testa "Passing an empty seq will return a closed promise-chan, which in 1194 | turn returns nil when taken from." 1195 | (-> (any []) 1196 | (handle q!)) 1197 | (is (nil? (dq!)))) 1198 | 1199 | (testa "Passing nil will return a closed promise-chan, which in 1200 | turn returns nil when taken from." 1201 | (-> (any nil) 1202 | (handle q!)) 1203 | (is (nil? (dq!)))) 1204 | 1205 | (testa "chans can also contain values which will be treated as an already 1206 | settled chan which returns itself." 1207 | (-> (any [1 (defer 100 2)]) 1208 | (handle q!)) 1209 | (is (= 1 (dq!)))) 1210 | 1211 | (testa "When passing values, they are still racing asynchronously against 1212 | each other, and the result order is non-deterministic and should 1213 | not be depended on." 1214 | (is (not 1215 | (every? 1216 | #{1} 1217 | (doall 1218 | (for [i (range 3000)] 1219 | (-> (any [1 2 3 4 5 6 7 8 9 (async 10)]) 1220 | (wait)))))))) 1221 | 1222 | (testa "Any cancels all other chans once one of them fulfills in ok?." 1223 | (-> (any [(blocking 1224 | (Thread/sleep 100) 1225 | (catch InterruptedException _ 1226 | (q! :cancelled))) 1227 | (blocking 1228 | (Thread/sleep 100) 1229 | (catch InterruptedException _ 1230 | (q! :cancelled))) 1231 | (blocking 1232 | (Thread/sleep 100) 1233 | (catch InterruptedException _ 1234 | (q! :cancelled))) 1235 | (defer 30 :done) 1236 | (async (q! :throwed) (throw (ex-info "" {})))]) 1237 | (handle q!)) 1238 | (is (= :throwed (dq!))) 1239 | (is (= :done (dq!))) 1240 | (is (= :cancelled (dq!))) 1241 | (is (= :cancelled (dq!))) 1242 | (is (= :cancelled (dq!)))) 1243 | 1244 | (testa "If the first one to fulfill did so with an error, others are 1245 | not cancelled. But as soon as any of the other fulfill, the remaining 1246 | ones will cancel." 1247 | (-> (any [(blocking 1248 | (Thread/sleep 30) 1249 | (q! :not-cancelled) 1250 | (catch InterruptedException _ 1251 | (q! :cancelled))) 1252 | (blocking 1253 | (Thread/sleep 60) 1254 | (q! :not-cancelled) 1255 | (catch InterruptedException _ 1256 | (q! :cancelled))) 1257 | (async (throw (ex-info "error" {})))]) 1258 | (handle q!)) 1259 | (is (= :not-cancelled (dq!))) 1260 | (is (= :not-cancelled (dq!))) 1261 | (is (= :cancelled (dq!))))) 1262 | 1263 | 1264 | (deftest all-settled-tests 1265 | (testa "all-settled returns a promise-chan settled with a vector of the taken values 1266 | from all chans once they all fulfill" 1267 | (-> (all-settled [(async 1) (async 1) (async 1)]) 1268 | (handle q!)) 1269 | (is (= [1 1 1] (dq!)))) 1270 | 1271 | (testa "This is true even if any of the chan fulfills with an error." 1272 | (let [error (ex-info "error" {})] 1273 | (-> (all-settled [(async 1) (async (throw error)) (async 1)]) 1274 | (handle q!)) 1275 | (is (= [1 error 1] (dq!))))) 1276 | 1277 | (testa "Or even if all of them fulfill with an error." 1278 | (let [error (ex-info "error" {})] 1279 | (-> (all-settled [(async (throw error)) (async (throw error)) (async (throw error))]) 1280 | (handle q!)) 1281 | (is (= [error error error] (dq!))))) 1282 | 1283 | (testa "Passing an empty seq will return a promise-chan settled with an 1284 | empty vector." 1285 | (-> (all-settled []) 1286 | (handle q!)) 1287 | (is (= [] (dq!)))) 1288 | 1289 | (testa "Same with passing nil, it will return a promise-chan settled with an 1290 | empty vector." 1291 | (-> (all-settled nil) 1292 | (handle q!)) 1293 | (is (= [] (dq!)))) 1294 | 1295 | (testa "The returned promise-chan will always contain a vector? even if chans isn't 1296 | one." 1297 | (-> (all-settled (list (async 1) (async 1) (async 1))) 1298 | (handle q!)) 1299 | (is (vector? (dq!)))) 1300 | 1301 | (testa "chans can contain values as well as channels, in which case they 1302 | will be treated as an already settled chan which returns itself." 1303 | (-> (all-settled [1 2 (async 1)]) 1304 | (handle q!)) 1305 | (is (= [1 2 1] (dq!)))) 1306 | 1307 | (testa "The order of the taken values in the vector settled into the returned 1308 | promise-chan are guaranteed to correspond to the same order as the chans. 1309 | Thus order can be relied upon." 1310 | (is 1311 | (every? 1312 | #(= [1 2 3 4 5 6 7 8 9 10] %) 1313 | (doall 1314 | (for [i (range 3000)] 1315 | (-> (all-settled [1 (async 2) 3 (async 4) (async 5) 6 (async 7) 8 9 (async 10)]) 1316 | (wait)))))))) 1317 | 1318 | 1319 | (deftest all-tests 1320 | (testa "Awaits all promise-chan in given seq and returns a vector of 1321 | their results in order. Promise-chan obviously run concurrently, 1322 | so the entire operation will take as long as the longest one." 1323 | (async 1324 | (q! (await (all [1 (defer 100 2) (defer 200 3)])))) 1325 | (is (= [1 2 3] (dq! 250)))) 1326 | 1327 | (testa "Short-circuits as soon as one promise-chan errors, returning 1328 | the error." 1329 | (async 1330 | (try 1331 | (await (all [(defer 10 #(/ 1 0)) (defer 100 2) (defer 200 3)])) 1332 | (catch Exception e 1333 | (q! e)))) 1334 | (is (= ArithmeticException (type (dq! 50))))) 1335 | 1336 | (testa 1337 | "Can contain non-promise-chan as well." 1338 | (async 1339 | (q! (await (all [1 2 3])))) 1340 | (is (= [1 2 3] (dq!)))) 1341 | 1342 | (testa "But since all is not a macro, if one of them throw it throws 1343 | immediately as the argument to all get evaluated." 1344 | (try (all [1 (defer 10 2) (/ 1 0)]) 1345 | (catch Exception e 1346 | (q! e))) 1347 | (is (= ArithmeticException (type (dq!))))) 1348 | 1349 | (testa "This is especially important when using some of the helper 1350 | functions instead of plain async/await, because it will 1351 | throw instead of all returning the error." 1352 | (try 1353 | (-> (all [1 (defer 10 2) (/ 1 0)]) 1354 | (catch (q! :this-wont-work))) 1355 | (catch Exception _ 1356 | (q! :this-will))) 1357 | (is (= :this-will (dq!)))) 1358 | 1359 | (testa "If given an empty seq, returns a promise-chan settled 1360 | to nil." 1361 | (async 1362 | (q! (await (all [])))) 1363 | (is (nil? (dq!)))) 1364 | 1365 | (testa "If given nil, returns a promise-chan settled to nil." 1366 | (async 1367 | (q! (await (all [])))) 1368 | (is (nil? (dq!))))) 1369 | 1370 | 1371 | (deftest ado-tests 1372 | (testa "Use ado to perform asynchronous ops one after the other and 1373 | return the result of the last one only." 1374 | (-> (ado (blocking (q! :log)) 1375 | (blocking (q! :prep-database)) 1376 | (blocking :run-query)) 1377 | (handle q!)) 1378 | (is (= :log (dq!))) 1379 | (is (= :prep-database (dq!))) 1380 | (is (= :run-query (dq!))))) 1381 | 1382 | 1383 | (deftest alet-tests 1384 | (testa "Like Clojure's let, except each binding is awaited." 1385 | (alet 1386 | [a (defer 100 1) 1387 | b (defer 100 2) 1388 | c (defer 100 3)] 1389 | (q! (+ a b c))) 1390 | (is (= 6 (dq!)))) 1391 | 1392 | (testa "Like Clojure's let, this is not parallel, the first binding is 1393 | awaited, then the second, then the third, etc." 1394 | (alet 1395 | [a (defer 100 1) 1396 | b (defer 100 2) 1397 | c (defer 100 3)] 1398 | (q! (+ a b c))) 1399 | (is (= :timeout (dq! 200)))) 1400 | 1401 | (testa 1402 | "Let bindings can depend on previous ones." 1403 | (alet 1404 | [a 1 1405 | b (defer 100 (+ a a)) 1406 | c (inc b)] 1407 | (q! c)) 1408 | (is (= 3 (dq!))))) 1409 | 1410 | 1411 | (deftest clet-tests 1412 | (testa "Like Clojure's let, but it runs bindings concurrently." 1413 | (time 1414 | (clet 1415 | [a (defer 100 1) 1416 | b (defer 100 2) 1417 | c (defer 100 3)] 1418 | (q! (+ a b c))) 1419 | q!) 1420 | (is (= 6 (dq!))) 1421 | (is (< 100 (dq!) 110))) 1422 | 1423 | (testa "Unless one binding depends on a previous one, in which case that binding 1424 | will await the other, but only the dependent bindings will be sequenced after what 1425 | they depend on, others will still run concurrently." 1426 | (time 1427 | (clet 1428 | [a (defer 100 100) 1429 | b (defer a 100) 1430 | c (defer b 100) 1431 | d (defer 100 0)] 1432 | (q! (+ a b c d))) 1433 | q!) 1434 | (is (= 300 (dq!))) 1435 | (is (< 300 (dq!) 320))) 1436 | 1437 | (testa "Bindings evaluate on the async-pool, which means they should not do 1438 | any blocking or heavy compute operation. If you need to do a blocking or compute 1439 | heavy operation wrap it in blocking or compute accordingly." 1440 | (time 1441 | (clet 1442 | [a (compute (reduce + (mapv #(Math/sqrt %) (range 1e6)))) 1443 | b (blocking (Thread/sleep 100) 100) 1444 | c (defer 100 100) 1445 | d (defer 100 b)] 1446 | (q! (+ a b c d))) 1447 | q!) 1448 | (is (= 6.666664664588418E8 (dq!))) 1449 | (is (< 200 (dq!) 220))) 1450 | 1451 | (testa "You can nest async inside blocking and compute and so on and it'll still 1452 | properly rewrite to await or wait depending on which the binding is inside. This is 1453 | not useful, this is just to test that it works." 1454 | (time 1455 | (clet 1456 | [a (compute (reduce + (mapv #(Math/sqrt %) (range 1e6)))) 1457 | b (defer 100 100) 1458 | c (blocking (Thread/sleep b) (async b (blocking b))) 1459 | d (defer 100 b)] 1460 | (q! (+ a b c d))) 1461 | q!) 1462 | (is (= 6.666664664588418E8 (dq!))) 1463 | (is (< 200 (dq!) 220))) 1464 | 1465 | (testa "It works with sets too." 1466 | (clet [a 10 1467 | b #{:a a}] 1468 | (q! b)) 1469 | (is (= #{:a 10} (dq!)))) 1470 | 1471 | (testa "It works with maps too." 1472 | (clet [a 10 1473 | b {a a}] 1474 | (q! b)) 1475 | (is (= {10 10} (dq!)))) 1476 | 1477 | (testa "It preserves meta." 1478 | (clet 1479 | [a ^{:foo "bar"} [1 2 3] 1480 | b a] 1481 | (q! b)) 1482 | (is (= {:foo "bar"} (meta (dq!))))) 1483 | 1484 | (testa "Testing some edge case, proper rewriting inside the body should also 1485 | happen where bindings are replaced by await or wait depending on their context." 1486 | (time 1487 | (clet 1488 | [x 1 1489 | a (compute x) 1490 | b (blocking (inc a)) 1491 | c (async (inc b)) 1492 | d (compute (async (inc c)))] 1493 | (q! (+ a 1494 | (await (compute b)) 1495 | (await (async c)) 1496 | (await (blocking d)) 1497 | (await (compute (async a))) 1498 | (await (blocking (async a))) 1499 | (await (async (blocking a))) 1500 | (await (async (compute a))) 1501 | (await (async a (blocking a (async a))))))) 1502 | q!) 1503 | (is (= 15 (dq!))) 1504 | (is (< 0 (dq!) 50)) 1505 | (is (= '(let* 1506 | [x (com.xadecimal.async-style.impl/async 1) 1507 | a (com.xadecimal.async-style.impl/async 1508 | (compute (com.xadecimal.async-style.impl/wait x))) 1509 | b (com.xadecimal.async-style.impl/async 1510 | (blocking (inc (com.xadecimal.async-style.impl/wait a)))) 1511 | c (com.xadecimal.async-style.impl/async 1512 | (async (inc (com.xadecimal.async-style.impl/await b)))) 1513 | d (com.xadecimal.async-style.impl/async 1514 | (compute (async (inc (com.xadecimal.async-style.impl/await c))))) 1515 | e (com.xadecimal.async-style.impl/async 1516 | (inc (com.xadecimal.async-style.impl/await d)))] 1517 | (com.xadecimal.async-style.impl/async 1518 | (+ (com.xadecimal.async-style.impl/await a) 1519 | (com.xadecimal.async-style.impl/await e) 1520 | (await (compute (com.xadecimal.async-style.impl/wait b))) 1521 | (await (async (com.xadecimal.async-style.impl/await c))) 1522 | (await (blocking (com.xadecimal.async-style.impl/wait d))) 1523 | (await (compute (async (com.xadecimal.async-style.impl/await a)))) 1524 | (await (blocking (async (com.xadecimal.async-style.impl/await a)))) 1525 | (await (async (blocking (com.xadecimal.async-style.impl/wait a)))) 1526 | (await (async (compute (com.xadecimal.async-style.impl/wait a)))) 1527 | (await (async (compute (com.xadecimal.async-style.impl/wait a)))) 1528 | (await 1529 | (async (com.xadecimal.async-style.impl/await a) 1530 | (blocking (com.xadecimal.async-style.impl/wait a) 1531 | (async (com.xadecimal.async-style.impl/await a)))))))) 1532 | (macroexpand '(com.xadecimal.async-style.impl/clet 1533 | [x 1 1534 | a (compute x) 1535 | b (blocking (inc a)) 1536 | c (async (inc b)) 1537 | d (compute (async (inc c))) 1538 | e (inc d)] 1539 | (+ a 1540 | e 1541 | (await (compute b)) 1542 | (await (async c)) 1543 | (await (blocking d)) 1544 | (await (compute (async a))) 1545 | (await (blocking (async a))) 1546 | (await (async (blocking a))) 1547 | (await (async (compute a))) 1548 | (await (async (compute a))) 1549 | (await (async a (blocking a (async a))))))))))) 1550 | 1551 | 1552 | (deftest time-tests 1553 | (testa "If you want to measure the time an async ops takes to fulfill, you can 1554 | wrap it with time. By default it prints to stdout." 1555 | (-> (sleep 1000) 1556 | (time) 1557 | wait)) 1558 | (testa "You can pass a custom print-fn as the second argument instead." 1559 | (-> (sleep 1000) 1560 | (time q!) 1561 | wait) 1562 | (is (< 1000 (dq!) 1010))) 1563 | (testa "It can also be used to time non-async ops." 1564 | (-> (+ 1 2 3) 1565 | (time q!)) 1566 | (is (< 0 (dq!) 1))) 1567 | (testa "Unlike clojure.core/time, this one returns the value of what it 1568 | times and not nil." 1569 | (is (= 3 (time (+ 1 2)))))) 1570 | --------------------------------------------------------------------------------