/kvlt

Multi-target Clojure/script HTTP client

Primary LanguageClojureThe UnlicenseUnlicense

Hail Satan Build Status

Attempts to present a uniform, asychronous client interface for HTTP across JVM / Node / browsers.

Latest documentation / examples

Clojars Project

Features

  • Supports Clojure/JVM, Clojurescript/Node and Clojurescript/Browser
  • Individual deferred values are exposed via promises (kvlt.core), or asynchronous channels (kvlt.chan)
  • core.async-based support for Websockets and Server-sent Events
  • Raw responses available as Javascript typed arrays (on Node, and in browsers with XHR Level 2 support)
  • Ring-like API

Requirements

  • Clojure use requires JDK8

Todo / Notes

  • Automated/CI testing is currently limited to JVM, Node and recent Chrome & Firefox builds
  • No support for streamed requests/responses. Open to suggestions about how this might be handled across platforms
  • Young project, etc. - please file issues

Examples

kvlt.core/request! returns a promesa promise, which can be manipulated using promise-specific (e.g. promesa/then) operations, or treated as a monad using the primitives from cats. Below, we're working with something like:

(ns kvlt.examples
  (:require [kvlt.core :as kvlt]
            [promesa.core :as p]))

The default :method is :get:

(p/alet [{:keys [status]} (p/await (kvlt/request! {:url url}))]
  (is (= status 200)))

Explicit Callback

(p/then
 (kvlt/request! {:url url})
 (fn [{:keys [status]}]
   (is (= status 200))))

core.async

The kvlt.chan namespace parallels the promise-driven kvlt.core, using asynchronous channels to communicate deferred values.

(go
  (let [{:keys [status]} (<! (kvlt.chan/request! {:url url}))]
    (is (= status 200))))

Writing Data

In addition to Ring-style :form-params/:type, metadata may be applied to :body, indicating the desired content-type and body serialization:

(p/alet [{:keys [body]}
         (p/await
          (kvlt/request!
           {:url    url
            :method :post
            :body   ^:kvlt.body/edn {:hello "world"}
            :as     :auto}))]
  (is (= (body :hello) "world")))

:as :auto causes the :body key in the response to be processed in accord with the response's content-type. :as :edn, in this case, would have the same effect.

Errors

(p/catch
 (kvlt/request! {:url (str url "/404")})
 (fn [e]
   (is (= :not-found ((ex-data e) :type)))))

All requests resulting in exceptional response codes, or more fundamental (e.g. transport) errors will cause the returned promise's error branch to be followed with an ExceptionInfo instance - i.e. an Exception/Error with an associated response map retrievable via ex-data.

Example Map

{:headers {:content-type "text/plain" ...}
 :type   :not-found
 :reason :not-found
 :status 404
 ...}

More request/response examples

(def events (kvlt.core/event-source! url))

events is a core.async channel (rather, something a lot like a core.async channel, which'll terminate the SSE connection when async/close!'d).

Assuming no event types or identifiers are supplied by the server, a value on events looks something like:

{:type :message
 :data "String\nfrom\nthe server"}

More SSE examples

Websockets

kvlt.core/websocket! takes a URL, and returns a promise which'll resolve to a core.async channel:

(p/alet [ws (p/await (kvlt/websocket! "http://localhost:5000/ws" {:format :edn}))]
  (go
    (>! ws {:climate :good, :bribery :tolerated})
    (let [instructions (<! ws)]
      (is (instructions :proceed?)))))

Closing the ws channel will terminate the websocket connection.

More Websocket examples

License

kvlt is free and unencumbered public domain software. For more information, see http://unlicense.org/ or the accompanying UNLICENSE file.