sieppari
Small, fast, and complete interceptor library for Clojure/Script with built-in support for common async libraries.
Noun Siepata (Intercept)
sieppari, someone or something that intercepts
What it does
Interceptors, like in Pedestal, but with minimal implementation and optimal performance.
The core Sieppari depends on Clojure and nothing else.
If you are new to interceptors, check the
Pedestal Interceptors documentation.
If you are familiar with interceptors you might want to jump to Differences to Pedestal
below.
First example
(ns example.simple
(:require [sieppari.core :as sieppari]))
;; interceptor, in enter update value in `[:request :x]` with `inc`
(def inc-x-interceptor
{:enter (fn [ctx]
(update-in ctx [:request :x] inc))})
;; handler, take `:x` from request, apply `inc`, and return an map with `:y`
(defn handler [request]
{:y (inc (:x request))})
(sieppari/execute
[inc-x-interceptor handler]
{:x 40})
;=> {:y 42}
Async
Any step in the execution pipeline (:enter
, :leave
, :error
) can return either a context map (synchronous execution) or an instance of AsyncContext
- indicating asynchronous execution.
By default, clojure deferrables satisfy the AsyncContext
protocol.
Using sieppari.core/execute
with async steps will block:
;; async interceptor, in enter double value of `[:response :y]`:
(def multiply-y-interceptor
{:leave (fn [ctx]
(future
(Thread/sleep 1000)
(update-in ctx [:response :y] * 2)))})
(sieppari/execute
[inc-x-interceptor multiply-y-interceptor handler]
{:x 40})
; ... 1 second later:
;=> {:y 84}
There is also a non-blocking version of execute
:
(let [respond (promise)
raise (promise)]
(sieppari/execute
[inc-x-interceptor multiply-y-interceptor handler]
{:x 40}
respond
raise) ; returns nil immediately
(deref respond 2000 :timeout))
; ... 1 second later:
;=> {:y 84}
External Async Libraries
To add a support for one of the supported external async libraries, just add a dependency to them and require
the
respective Sieppari namespace. Currently supported async libraries are:
- core.async -
sieppari.async.core-async
, clj & cljs - Manifold -
sieppari.async.manifold
clj - Promesa -
sieppari.async.promesa
clj & cljs
To extend Sieppari async support to other libraries, just extend the AsyncContext
protocol.
core.async
Requires dependency to [org.clojure/core.async "0.4.474"]
or higher.
(require '[clojure.core.async :as a])
(defn multiply-x-interceptor [n]
{:enter (fn [ctx]
(a/go (update-in ctx [:request :x] * n)))})
(sieppari/execute
[inc-x-interceptor (multiply-x-interceptor 10) handler]
{:x 40})
;=> {:y 411}
manifold
Requires dependency to [manifold "0.1.8"]
or higher.
(require '[manifold.deferred :as d])
(defn minus-x-interceptor [n]
{:enter (fn [ctx]
(d/success-deferred (update-in ctx [:request :x] - n)))})
(sieppari/execute
[inc-x-interceptor (minus-x-interceptor 10) handler]
{:x 40})
;=> {:y 31}
promesa
Requires dependency to [funcool/promesa "2.0.0-SNAPSHOT"]
or higher.
(require '[promesa.core :as p])
(defn divide-x-interceptor [n]
{:enter (fn [ctx]
(p/promise (update-in ctx [:request :x] / n)))})
(sieppari/execute
[inc-x-interceptor (divide-x-interceptor 10) handler]
{:x 40})
;=> {:y 41/10}
Performance
Sieppari aims for minimal functionality and can therefore be quite fast. Complete example to test performance is included.
The example creates a chain of 100 interceptors that have
clojure.core/identity
as :enter
and :leave
functions and then
executes the chain. The async tests also have 100 interceptors, but
in async case they all return core.async
channels on enter and leave.
Executor | Execution time lower quantile |
---|---|
Pedestal sync | 64 µs |
Sieppari sync | 9 µs |
Pedestal async | 410 µs |
Sieppari async | 396 µs |
- MacBook Pro (Retina, 15-inch, Mid 2015), 2.5 GHz Intel Core i7, 16 MB RAM
- Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
- Clojure 1.9.0
Differences to Pedestal
Execution
io.pedestal.interceptor.chain/execute
executes Contextssieppari.core/execute
executes Requests (which are internally wrapped inside a Context for interceptors)
Errors
- In Pedestal the
error
handler takes two arguments, thectx
and the exception. - In Sieppari the
error
handlers takes just one argument, thectx
, and the exception is in thectx
under the key:error
. - In Pedestal the
error
handler resolves the exception by returning thectx
, and continues the error stage by re-throwing the exception. - In Sieppari the
error
handler resolves the exception by returning thectx
with the:error
removed. To continue in the error stage, just return thectx
with the exception still at:error
. - In Pedestal the exception are wrapped in other exceptions.
- In Sieppari exceptions are not wrapped.
- Pedestal interception execution catches
java.lang.Throwable
for error processing. Sieppari catchesjava.lang.Exception
. This means that things like out of memory or class loader failures are not captured by Sieppari.
Async
- Pedestal transfers thread local bindings from call-site into async interceptors.
- Sieppari does not support this.
REPL
In order to start a node figwheel REPL for local development use:
clojure -A:fig:test-cljs:nrepl
Then in the REPL:
user=> (require 'figwheel.main.api)
nil
user=> (figwheel.main.api/start "dev")
...
Thanks
- Original idea from Pedestal Interceptors.
License
Copyright © 2018-2019 Metosin Oy
Distributed under the Eclipse Public License 2.0.