Idiomatic conversion of node.js APIs to ClojureScript
Asked Answered
C

2

6

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?
Cosh answered 2/9, 2016 at 11:52 Comment(0)
P
8

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))
Petaloid answered 7/9, 2016 at 16:0 Comment(2)
Does try/catch example works with first snippet as is, without changes?Cosh
Yes. Basically, the <? macro (as outlined in the article) just checks if the item on the channel is an instance of js/Error and if so, re-throws it.Petaloid
B
1

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.

Bag answered 29/9, 2017 at 8:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.