I don’t know ClojureScript very well, but I think this would be the simplest setup:
(defn article-update-last-opened-async
[db id]
(let [out (promise-chan)
params (array (js/Date.now) id)
sql "UPDATE articles SET last_opened = ? WHERE article_id = ?"]
(.run db sql params
(fn [err]
(this-as this
(if err
(put! out (ex-info "Failed to update article" {:error :sql-error :id id}))
(put! out {:last-id (.-lastID this)
:changes (.-changes this)})))))
out))
(defn article-get-by-id-async
[db id]
(let [out (promise-chan)
params (array id)
sql "SELECT * FROM articles WHERE article_id = ?"]
(.get db sql params
(fn [err row]
(if err
(put! out (ex-info "Failed to get article" {:error :sql-error :id id}))
(put! out (js->clj row :keywordize-keys true)))))))
(defn article-get-async
"Fetches an article, and computes the `:word-data` for it. Sets `last_opened` value before fetching."
[db id cb]
(let [out (promise-chan)]
(go
(let [article-update-last-opened (<! (article-update-last-opened-async db id))
article (<! (article-get-by-id-async db id))]
(put! out (words-get-for-article article cb))))
out))
(go (<! (article-get-async db 1234 cb)))
You could probably create a sql-run-async
and sql-get-async
function and use those instead for all your queries, like:
(defn sql-run-async
[db sql params]
(let [out (promise-chan)
(.run db sql params
(fn [err]
(this-as this
(if err
(put! out (ex-info "Failed to run sql query" {:error :sql-error :sql sql :params params}))
(put! out this)))))
out))
And refactor the above with those so you don’t need to repeat the pattern for each of your queries.
Edit: Let me explain a bit…
The Brave and True book is showing you how to model an application using CSP, concurrent sequential processes. That’s a different style of programming. In your case you’ve got functions with callbacks returning a single result by calling the callback. And it seems you want to continue to use functions, but want them to be async.
A channel normally is more like a stream in that it is assumed it will have many values passing through it over time. But your functions will return a single value each time they are called. A promise-chan is a special channel just for that, it mimics a promise like behavior, which is that you expect a single value to be delivered at some point in the future, where as a regular channel is meant to deliver many values at various points in the future.
Now a function is something that takes parameters and return a single value. An async function is a function that takes parameters and returns a container of value immediately, but which will deliver the logical result to that returned container later in time.
This is what I did here. Our functions take parameters and immediately return a promise-chan which is a container that will have a single value delivered to it sometime later.
Because the returned promise-chan is a possibly empty container, if you want to do something that depends on the value to be delivered, or just after you know that whatever was happening async completed successfully, you need a way to coordinate the order. You can do that with <!
inside a go
block. Everything inside the go
block after the call to <!
will only happen after the channel has had a value delivered to it and extracted by <!
. But a function that depends on an async value must itself be async, which is why everything becomes async in our example (in Clojure JVM you could block to return to a synchronous model, but not in ClojureScript).
Now there’s a very different way to model things, which is that instead of using async functions, you could use Processes.
A process is different from a function. A process doesn’t take parameters, instead it takes messages from channels, process them, and possibly puts new messages on the same or other channels.
Programming with processes is very different to programming with functions or even programming with async functions. You’d need to rethink how you modeled your application.
For example, in your case, you could have a querier process, it would be a process that listens to incoming queries on some query channel. For each incoming query, it could perform the query to the DB and then put a message on a query result channel when the result of it comes back from the DB.
This is no longer a function though, now you have a process instead. You don’t “call” a process the way you “call” a function, you’d need to send it a message by putting a query over the channel it listens for them.
So instead of calling run
on the db with SQL and params, you would call put! or >!
on the querier process’ input channel. And instead of getting the result from a return value, you’d take! or <!
from its output channel.
It’s a very different way to model a concurrent application. Anyways, that’s what the Brave book I believe is explaining.
With core.async you can do both this CSP style of programming, or you can use it more traditionally to create async functions like I did.