Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Idiomatic conversion of node.js APIs to ClojureScript

I'm writing an Electron application, and in this application I need to interact with some of the Node.js APIs - read files, get directory entries, listen to events.

Of course, I can write ClojureScript same way I would write JavaScript, but I want to know what is ClojureScripts take on callback-style APIs, streams, EventEmitters and how do I write wrappers around node.js APIs in a way that does not to look alien in ClojureScript.

To be specific:

  1. How do write an API that wraps callback-style node.js API. (say, fs.readdir)
  2. How do I interact with EventEmitter-like APIs?
  3. (Probably close to p.2) How do I work with node.js streams API?
like image 359
Yury Solovyov Avatar asked Sep 02 '16 11:09

Yury Solovyov


2 Answers

In my experience, the simplest way of handling these issues is to use core.async.

Callback Style

For example, reading a directory:

(def fs (js/require "fs"))

(defn read-dir [path]
  (let [out (async/chan)]
    (.readdir fs path
      (fn [err files]
        (async/put! (if err err files))
        (async/close! out)))
    out))

I pass the result in to a channel, even if that result is an error. This way, the caller can handle the error by doing. E.g.:

(let [res (<! (read-dir "."))]
  (if (instance? js/Error res)
    (throw res)
    (do-something res))

In my own projects I use cljs-asynchronize, which allows you to convert NodeJS callback style functions in to core.async compatible functions. For example, this is the same as the first example:

(defn read-dir [path]
  (asynchronize 
    (.readdir fs path ...)))

Lastly, for a nicer way of handling errors through channels, I personally found "Error Handling with Clojure Async" quite useful. So, you can write the error handling code above like:

(try 
  (let [res (<? (read-dir "."))]
    (do-something res))
  (catch js/Error e
    (handle-error e))

Streams

Stream API's are even simpler:

(defn create-read-stream [path]
   (let [out (async/chan)
         stream (.createReadStream fs path)]
     (.on stream "close" #(async/close! out))
     (.on stream "data" #(async/put! out %))
     out))
like image 57
Antonis Kalou Avatar answered Oct 19 '22 10:10

Antonis Kalou


I think the answer to your question is "It depends.".

Always consider all options. Just because core.async makes sense in one setting does not make it the best option everywhere.

It is fine to use a normal callback if that is called once only and requires no coordination with anything else (eg. fs.readdir).

core.async is great for stream like things where you get a certain chain of events that you want to accumulate into a final result (eg. "data","data","data","close","end").

Promises are another option as well.

The more coordination you need the more sense core.async makes. If you don't need any consider the alternatives.

like image 30
Thomas Heller Avatar answered Oct 19 '22 08:10

Thomas Heller