Unified event system for Clojure/ClojureScript.
Combines:
Design principles:
Usage: (require '[hive.events :as ev])
;; Register effect handlers (ev/reg-fx :http (fn [request] ...)) (ev/reg-fx :db (fn [tx] ...))
;; Register event handlers (ev/reg-event-fx :user/login [ev/debug ev/validate] ; interceptors (fn [{:keys [db]} [_ credentials]] {:db (assoc db :loading? true) :http {:method :post :url "/login" :body credentials}}))
;; Dispatch events (ev/dispatch [:user/login {:email "..." :password "..."}])
Unified event system for Clojure/ClojureScript.
Combines:
- re-frame interceptor chain (portable to JVM)
- re-frame effects/coeffects system
- domino-style declarative models with async support
Design principles:
- Events as data
- Pure handlers returning effect maps
- Interceptors for cross-cutting concerns
- Async-first with core.async
Usage:
(require '[hive.events :as ev])
;; Register effect handlers
(ev/reg-fx :http (fn [request] ...))
(ev/reg-fx :db (fn [tx] ...))
;; Register event handlers
(ev/reg-event-fx :user/login
[ev/debug ev/validate] ; interceptors
(fn [{:keys [db]} [_ credentials]]
{:db (assoc db :loading? true)
:http {:method :post :url "/login" :body credentials}}))
;; Dispatch events
(ev/dispatch [:user/login {:email "..." :password "..."}])Async extensions for hive-events.
Provides advanced async patterns beyond basic dispatch:
JVM-first with core.async, graceful degradation on CLJS.
Async extensions for hive-events. Provides advanced async patterns beyond basic dispatch: - Timeout-wrapped dispatch - Retry with backoff - Debounced dispatch - Event streams (pub/sub) - Saga/process manager patterns JVM-first with core.async, graceful degradation on CLJS.
Coeffects (cofx) system - dependency injection for event handlers.
Ported from re-frame/cofx.cljc with JVM compatibility.
Coeffects are input data that handlers need but shouldn't fetch themselves. They enable pure handlers by injecting all dependencies.
Built-in coeffects:
Usage: ;; Register a coeffect handler (reg-cofx :now (fn [coeffects] (assoc coeffects :now (System/currentTimeMillis))))
;; Inject into event handler (reg-event-fx :log/timestamp [(inject-cofx :now)] (fn [{:keys [db now]} _] {:db (assoc db :last-timestamp now)}))
Coeffects (cofx) system - dependency injection for event handlers.
Ported from re-frame/cofx.cljc with JVM compatibility.
Coeffects are input data that handlers need but shouldn't fetch themselves.
They enable pure handlers by injecting all dependencies.
Built-in coeffects:
- :db - Current application state
- :event - The event being processed
Usage:
;; Register a coeffect handler
(reg-cofx :now
(fn [coeffects]
(assoc coeffects :now (System/currentTimeMillis))))
;; Inject into event handler
(reg-event-fx :log/timestamp
[(inject-cofx :now)]
(fn [{:keys [db now]} _]
{:db (assoc db :last-timestamp now)}))Deterministic FSM workflow engine for hive-events.
Adapted from yogthos/maestro — state machine runner for expressing workflows as data. Integrated with hive-events fx/cofx system.
(require '[hive.events.fsm :as fsm])
;; Define FSM spec
(def my-workflow
{:fsm {::fsm/start {:handler (fn [resources data]
(assoc data :step :ready))
:dispatches [[::process (constantly true)]]}
::process {:handler (fn [resources data]
(assoc data :result ((:compute resources) data)))
:dispatches [[::fsm/end (fn [{:keys [result]}] (some? result))]
[::fsm/error (constantly true)]]}}
:opts {:max-trace 100}})
;; Compile and run
(-> (fsm/compile my-workflow)
(fsm/run {:compute my-fn} {:data {:input 42}}))
::start — initial state::end — terminal success, returns data::halt — pausable, returns FSM state for resume::error — terminal failure(fn [resources data] new-data)(fn [resources data] {:data new-data, :fx [[:effect-id params]]})
Effects are processed through the hive.events.fx pipeline after transition.run-sub-fsm and make-sub-fsm-handlerBased on yogthos/maestro v0.2.1 (MIT License).
Deterministic FSM workflow engine for hive-events.
Adapted from yogthos/maestro — state machine runner for expressing
workflows as data. Integrated with hive-events fx/cofx system.
## Usage
```clojure
(require '[hive.events.fsm :as fsm])
;; Define FSM spec
(def my-workflow
{:fsm {::fsm/start {:handler (fn [resources data]
(assoc data :step :ready))
:dispatches [[::process (constantly true)]]}
::process {:handler (fn [resources data]
(assoc data :result ((:compute resources) data)))
:dispatches [[::fsm/end (fn [{:keys [result]}] (some? result))]
[::fsm/error (constantly true)]]}}
:opts {:max-trace 100}})
;; Compile and run
(-> (fsm/compile my-workflow)
(fsm/run {:compute my-fn} {:data {:input 42}}))
```
## Privileged States
- `::start` — initial state
- `::end` — terminal success, returns data
- `::halt` — pausable, returns FSM state for resume
- `::error` — terminal failure
## Integration with hive-events
- Handlers are pure: `(fn [resources data] new-data)`
- Handlers may return fx-enhanced results:
`(fn [resources data] {:data new-data, :fx [[:effect-id params]]})`
Effects are processed through the hive.events.fx pipeline after transition.
- Subscriptions can dispatch events on state path changes
- Pre/post hooks for interceptor-style concerns
- EDN specs compiled with SCI
- Sub-FSM composition via `run-sub-fsm` and `make-sub-fsm-handler`
Based on yogthos/maestro v0.2.1 (MIT License).Effects (fx) system - side effect handlers.
Ported from re-frame/fx.cljc with JVM compatibility.
Effects are declarative descriptions of side effects. Each effect type has a registered handler that performs the actual work.
Built-in effects:
Usage: ;; Register a custom effect handler (reg-fx :http (fn [{:keys [method url on-success]}] (http-request method url (fn [response] (dispatch [on-success response])))))
;; Event handler returns effects map (reg-event-fx :user/login (fn [{:keys [db]} [_ credentials]] {:db (assoc db :loading? true) :http {:method :post :url "/api/login" :body credentials}}))
Effects (fx) system - side effect handlers.
Ported from re-frame/fx.cljc with JVM compatibility.
Effects are declarative descriptions of side effects.
Each effect type has a registered handler that performs the actual work.
Built-in effects:
- :db - Update application state
- :dispatch - Dispatch another event
- :dispatch-n - Dispatch multiple events
Usage:
;; Register a custom effect handler
(reg-fx :http
(fn [{:keys [method url on-success]}]
(http-request method url
(fn [response]
(dispatch [on-success response])))))
;; Event handler returns effects map
(reg-event-fx :user/login
(fn [{:keys [db]} [_ credentials]]
{:db (assoc db :loading? true)
:http {:method :post :url "/api/login" :body credentials}}))Interceptor chain implementation.
Ported from re-frame/interceptor.cljc with JVM compatibility.
An interceptor is a map with optional keys:
Context is a map containing:
Interceptor chain implementation. Ported from re-frame/interceptor.cljc with JVM compatibility. An interceptor is a map with optional keys: - :id - keyword identifier - :before - fn [context] -> context (pre-processing) - :after - fn [context] -> context (post-processing) Context is a map containing: - :coeffects - input data (including :event and :db) - :effects - output data (side effects to perform) - :queue - interceptors yet to execute - :stack - interceptors already executed (for :after phase)
Minimal pluggable logging for hive-events.
Three levels: :debug, :warn, :error
Default behavior:
Override: (set-log-fn! (fn [level msg & args] ;; your logging here ))
Disable: (set-log-fn! (fn [& _]))
Restore default: (set-log-fn! nil)
Minimal pluggable logging for hive-events.
Three levels: :debug, :warn, :error
Default behavior:
- JVM: prints to *err* (stderr)
- JS: uses console.log/warn/error
Override:
(set-log-fn! (fn [level msg & args]
;; your logging here
))
Disable:
(set-log-fn! (fn [& _]))
Restore default:
(set-log-fn! nil)Multimethod-based event dispatch + pure orchestration for hive-events.
Two complementary subsystems in one namespace:
Registration:
(register-handler! event-id handler-fn) — register handler (no interceptors)(register-handler! event-id interceptors handler-fn) — with interceptors(remove-handler! event-id) — remove a registered handlerDispatch:
(dispatch-sync event) — synchronous dispatch + fx execution(event-id event) — extract event-id (dispatch fn)Query:
(handler-registered? event-id) — check if handler existsHandler signature: (fn [coeffects event] effects-map)
Compile a batch of cross-tool operations into a wave-based execution plan. Pure functions — no side effects, no IO, no requiring-resolve.
Pipeline: normalize → validate → expand-batch → assign-waves → compile-spec
(normalize-op op) — normalize string keys, auto-generate ID(validate-ops ops) — validate IDs, deps, cycles (Kahn's algorithm)(expand-batch-macro ops) — expand 'batch' command into fan-out sub-ops(assign-waves ops) — topological sort into dependency-ordered wave groups(compile-multi-spec ops) — end-to-end: validate → expand → waves → FSM spechive-events (this ns) — pure compilation (validate, sort, waves)
↓
hive-mcp tools/multi — bridge (resolve handlers, execute ops, format)
↓
hive-mcp consolidated — MCP facade (JSON schema, tool def)
SOLID: OCP — Open for extension via defmethod handlers SOLID: SRP — Pure orchestration logic, no side effects CLARITY: L — Layered: pure compilation separate from execution
Multimethod-based event dispatch + pure orchestration for hive-events. Two complementary subsystems in one namespace: ## 1. Handler Dispatch (multimethod-based) Registration: - `(register-handler! event-id handler-fn)` — register handler (no interceptors) - `(register-handler! event-id interceptors handler-fn)` — with interceptors - `(remove-handler! event-id)` — remove a registered handler Dispatch: - `(dispatch-sync event)` — synchronous dispatch + fx execution - `(event-id event)` — extract event-id (dispatch fn) Query: - `(handler-registered? event-id)` — check if handler exists Handler signature: `(fn [coeffects event] effects-map)` ## 2. Pure Orchestration (multi-op compilation) Compile a batch of cross-tool operations into a wave-based execution plan. Pure functions — no side effects, no IO, no requiring-resolve. Pipeline: normalize → validate → expand-batch → assign-waves → compile-spec - `(normalize-op op)` — normalize string keys, auto-generate ID - `(validate-ops ops)` — validate IDs, deps, cycles (Kahn's algorithm) - `(expand-batch-macro ops)` — expand 'batch' command into fan-out sub-ops - `(assign-waves ops)` — topological sort into dependency-ordered wave groups - `(compile-multi-spec ops)` — end-to-end: validate → expand → waves → FSM spec ## Architecture ``` hive-events (this ns) — pure compilation (validate, sort, waves) ↓ hive-mcp tools/multi — bridge (resolve handlers, execute ops, format) ↓ hive-mcp consolidated — MCP facade (JSON schema, tool def) ``` SOLID: OCP — Open for extension via defmethod handlers SOLID: SRP — Pure orchestration logic, no side effects CLARITY: L — Layered: pure compilation separate from execution
Observer seam for dispatched events.
The event registry holds ONE handler per event id. Observers are the extension point for cross-cutting side effects that must not own, replace or know about that handler.
Extend by implementing IEventObserver — EventIdObserver (a fixed set of
event ids) and GlobalObserver (every event) are supplied; a consumer may
register any other implementation.
Contract: observers run AFTER the handler's effects have been applied, in observer-id order, and each is isolated — an observer that throws is logged and never propagates to the handler or to sibling observers.
Observer seam for dispatched events. The event registry holds ONE handler per event id. Observers are the extension point for cross-cutting side effects that must not own, replace or know about that handler. Extend by implementing IEventObserver — `EventIdObserver` (a fixed set of event ids) and `GlobalObserver` (every event) are supplied; a consumer may register any other implementation. Contract: observers run AFTER the handler's effects have been applied, in observer-id order, and each is isolated — an observer that throws is logged and never propagates to the handler or to sibling observers.
Event router - registration and dispatch.
Ported from re-frame with JVM-first async support via core.async.
Supports two types of event handlers:
Dispatch modes:
Usage: ;; Initialize with app state atom (init! (atom {}))
;; Register handlers (reg-event-db :counter/inc (fn [db [_ amount]] (update db :count + amount)))
(reg-event-fx :user/fetch (fn [{:keys [db]} [_ user-id]] {:db (assoc db :loading? true) :http {:url (str "/users/" user-id)}}))
;; Dispatch events (dispatch [:counter/inc 5]) (dispatch-sync [:counter/inc 1])
Event router - registration and dispatch.
Ported from re-frame with JVM-first async support via core.async.
Supports two types of event handlers:
- reg-event-db: Handler receives db, returns new db
- reg-event-fx: Handler receives coeffects, returns effects map
Dispatch modes:
- dispatch: Async dispatch (queued, processed in order)
- dispatch-sync: Synchronous dispatch (immediate processing)
Usage:
;; Initialize with app state atom
(init! (atom {}))
;; Register handlers
(reg-event-db :counter/inc
(fn [db [_ amount]]
(update db :count + amount)))
(reg-event-fx :user/fetch
(fn [{:keys [db]} [_ user-id]]
{:db (assoc db :loading? true)
:http {:url (str "/users/" user-id)}}))
;; Dispatch events
(dispatch [:counter/inc 5])
(dispatch-sync [:counter/inc 1])Schema-driven event registration — the hive-events leg of the malli macro
layer. reg-event-schema registers an fx handler whose EVENT PAYLOAD (the
second element of the [id payload] event vector) is coerced + validated against
ONE registered malli schema-key by a :before interceptor, before the handler
runs — declare the event's payload schema once, its guard is wired.
hive-events is cljc/cljs-safe and malli's derive lever is clj-only, so compile-op is resolved LAZILY via requiring-resolve (guarded). When it is unresolvable — cljs, or hive-spi absent — the interceptor is a pass-through, so registration never fails and the module stays platform-safe.
Schema-driven event registration — the hive-events leg of the malli macro layer. `reg-event-schema` registers an fx handler whose EVENT PAYLOAD (the second element of the [id payload] event vector) is coerced + validated against ONE registered malli schema-key by a :before interceptor, before the handler runs — declare the event's payload schema once, its guard is wired. hive-events is cljc/cljs-safe and malli's derive lever is clj-only, so compile-op is resolved LAZILY via requiring-resolve (guarded). When it is unresolvable — cljs, or hive-spi absent — the interceptor is a pass-through, so registration never fails and the module stays platform-safe.
Which registered handlers a reload has left behind.
A registration captures a function VALUE. (reg-fx :k handle-k) puts the fn
OBJECT in the registry, not the var. Reload the namespace and #'handle-k
gets a new root binding while the registry keeps the old closure, so the
image runs code that is no longer on disk.
Every other signal reports success: the namespace appears in the reload's
:loaded list, the var carries the new arglists, the addon remounts green.
The only observable difference is IDENTITY, which is what this namespace
measures. stale-entries compares each registered function against the
current root value of the var it was compiled from and names the ones that
no longer match.
Two shapes are invisible here, both on purpose:
(reg-fx :k (fn [v] ...)), has no var to compare
against. It is also the shape a reload fixes by itself, because the
top-level form that registers it re-runs.(reg-fx :k #'handle-k), resolves through
the var on every call and so cannot go stale.What is left is the defect: a registration that names a var and is guarded
so it runs once, under a defonce or behind an initialized? flag. clj-reload
preserves both, so the guarded registration never re-runs.
JVM only. The mapping from a function back to its var is read off the compiled class name, which ClojureScript does not carry; there the scan answers with an empty result rather than a wrong one.
Which registered handlers a reload has left behind. A registration captures a function VALUE. `(reg-fx :k handle-k)` puts the fn OBJECT in the registry, not the var. Reload the namespace and `#'handle-k` gets a new root binding while the registry keeps the old closure, so the image runs code that is no longer on disk. Every other signal reports success: the namespace appears in the reload's `:loaded` list, the var carries the new arglists, the addon remounts green. The only observable difference is IDENTITY, which is what this namespace measures. `stale-entries` compares each registered function against the current root value of the var it was compiled from and names the ones that no longer match. Two shapes are invisible here, both on purpose: - An ANONYMOUS handler, `(reg-fx :k (fn [v] ...))`, has no var to compare against. It is also the shape a reload fixes by itself, because the top-level form that registers it re-runs. - A handler registered AS A VAR, `(reg-fx :k #'handle-k)`, resolves through the var on every call and so cannot go stale. What is left is the defect: a registration that names a var and is guarded so it runs once, under a `defonce` or behind an `initialized?` flag. clj-reload preserves both, so the guarded registration never re-runs. JVM only. The mapping from a function back to its var is read off the compiled class name, which ClojureScript does not carry; there the scan answers with an empty result rather than a wrong one.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |