A library component of c3kit - Clean Coders Clojure Kit.
"If you look very closely at just one wire in the cage, you cannot see the other wires." - Marilyn Frye
Wire is a toolset for building rich-client web applications, clojure on the server side and clojurescript on the client side.
- ajax.clj(s) : AJAX comm between client and server
- api(c).clj(c)(s) : common api for data used by ajax and websocket
- assets.clj : update asset filenames when the content changes
- flash(c).clj(c)(s) : flash messages
- refresh.clj : dynamic code-reloading in development
- spec-helper(c).clj(c)(s) : help test client code
- verbose.clj : print request/response info in development
- websocket(c).clj(c)(s) : support for websocket comm between client and server
- dragndrop.cljs : simple client side drag and drop
- js.cljs : common js fns and features
- util.cljs : misc utilities
Starting with 4.0.0, wire publishes two independent jars to Clojars. Pick one — pulling both onto the same classpath produces duplicate c3kit.wire.* entries.
com.cleancoders.c3kit/wire— React-flavored. Self-contained: ships everythingwire-coreships plus the Reagent wrappers andreagent/cljsjs/react*deps. The drop-in for projects that were on 3.0.0.com.cleancoders.c3kit/wire-core— React-free. Samec3kit.wire.*namespaces minus the Reagent wrappers, plusc3kit.wire.core.{ajax,rest,websocket}. No React deps. Pull this if your project doesn't use React.
The two artifacts share source for everything below the React layer; the React-flavored namespaces (c3kit.wire.ajax/rest/websocket/flash/spec-helper) are thin Reagent wrappers over the corresponding c3kit.wire.core.* namespaces.
The repo mirrors the artifact split: React-bearing code lives under *-react/ directories, React-free code lives under their unsuffixed siblings.
src/clj JVM code (used by both jars)
src/cljc cross-platform shared code (used by both jars)
src/cljs React-free CLJS — c3kit.wire.core.* and friends (used by both jars)
src/cljs-react React-flavored CLJS wrappers (wire jar only)
spec/clj JVM specs
spec/cljc shared specs
spec/cljs React-free CLJS specs
spec/cljs-react React-flavored CLJS specs
dev/ JVM dev resources (datomic config, build script, demo assets)
dev-core/ scaffold cljs config for the React-free test run
dev-react/ scaffold cljs config for the React-bearing test run
deps.edn is organized so that the default :deps is the React-free core dep set, and a :react alias adds reagent + cljsjs/react* plus the src/cljs-react, spec/cljs-react, and dev-react paths on top. There is a single source of truth for the core dep list; the React layer is purely additive.
JVM tests don't need React (Reagent is CLJS-only), so they're the same regardless of which artifact you're working on:
clj -M:test:spec
clj -M:test:spec -a # auto runner
CLJS tests run the React-free suite by default. Add :react to also run the React-bearing wrappers and their specs:
# wire-core (React-free) — what publishes as the wire-core jar
clj -M:test:cljs once
clj -M:test:cljs # auto runner
# wire (with React) — what publishes as the wire jar
clj -M:test:react:cljs once
clj -M:test:react:cljs # auto runner
The same :react toggle applies to any CLJS workflow — chain it in when you need the Reagent layer, leave it out for the React-free core.
clj -M:test:cljs once doubles as the classpath-isolation guarantee: if a c3kit.wire.core.* namespace ever accidentally (:require [reagent.core ...]), this run fails because reagent isn't on the classpath without :react.
# Install Redis (needed by some integration specs)
brew install redis
Maven consumers (the :mvn/version case) are unaffected by the deps.edn layout — they consume the published poms, which list the full set of runtime deps for whichever artifact they pulled.
If you depend on c3kit-wire via :local/root or :git/url, your project reads this deps.edn directly. The default :deps is React-free, so add the :react alias to your alias chain (or pin reagent / cljsjs/react* yourself) when you need the Reagent layer.
See CONTRIBUTING.md for the development workflow, TDD expectations, the dual-jar story, and pull-request requirements. This project follows the Contributor Covenant Code of Conduct. Security issues should be reported privately — see SECURITY.md.
Releases run in CI. clj -T:build deploy refuses to run outside GitHub Actions,
so the sanctioned path always carries the CI check and leaves an audit trail.
- Open a PR bumping
VERSIONandCHANGES.md. - Merge to
masterand wait for Build to go green. The version bump is part of the merged commit, so the commit CI validated is the commit that gets released. - Actions → Release → Run workflow.
- Approve the
clojarsdeployment when prompted. Required reviewers are the release maintainers.
The workflow verifies Build succeeded for that exact commit, refuses a version that is already tagged, builds both jars, publishes them to Clojars, and only then pushes the version tag. A failed publish therefore leaves no tag.
clj -T:build jar builds both jars without publishing. clj -T:build install
installs them to ~/.m2 for local testing. Both jars share the same VERSION.
Only when the release workflow itself cannot run — Actions unavailable, or a broken
workflow blocking an urgent patch. This skips the CI check, so note its use in
CHANGES.md for that release.
CLOJARS_USERNAME=<username> \
CLOJARS_PASSWORD=<deploy token> \
EMERGENCY_RELEASE=<the exact version in VERSION, e.g. 4.2.2> \
clj -T:build emergency-publish
CLOJARS_PASSWORD is a Clojars deploy token generated at https://clojars.org/tokens, not your account password.
EMERGENCY_RELEASE must equal the version being released exactly; a
mismatched or unset value aborts. It still refuses a dirty working tree and an
already-tagged version.
Two namespaces: c3kit.wire.service-worker.core runs inside the service worker
(ServiceWorkerGlobalScope); c3kit.wire.service-worker.register runs on the page.
Caching is secure by default: same-origin, ok, non-opaque, GET-only, with hard
blocks on Cache-Control: no-store/no-cache/private and Vary: */Cookie/Authorization.
Credentials are detected via Authorization header or credentials: "include" and are
blocked by default.
Security note — threat-model limitations (read before caching anything private):
The hard blocks reliably cover Authorization-header and credentials: "include" auth
and, on same-origin responses, Cache-Control: private/no-cache/no-store and Vary.
Two guards cannot fire in a live browser — do not rely on them:
Set-Cookieis a forbidden response-header name, soheaders.get("Set-Cookie")returnsnilinside the service worker. ASet-Cookieresponse is not detected and will be cached if nothing else blocks it.Varyis not a CORS-safelisted response header, so on a cross-origincorsresponseheaders.get("Vary")isniland theVaryguard is silently skipped. It works only same-origin.- Same-origin cookie-session auth is invisible: cookie headers are stripped from
Requestobjects inServiceWorkerGlobalScope.
For any private, per-user, session-cookie, or Set-Cookie endpoint, route it explicitly
to network-only (or omit caching) — the secure-by-default gate alone is not sufficient.
Strategies available inside the service worker: cache-first, network-first,
stale-while-revalidate, cache-only, network-only. Each is a factory — call
it with an opts map, register the result as a route handler.
Your service worker entry (compiled to /service-worker.js):
(ns my-app.service-worker
(:require [c3kit.wire.service-worker.core :as sw]))
(defn -main []
(sw/precache! ["/" "/css/app.css" "/img/logo.png"] (str "shell-" my-app/version))
(sw/register-route! #"/img/" (sw/cache-first {:cache "images"}))
(sw/register-route! #"/api/" (sw/network-first {:cache "api"}))
(sw/register-route! "/" (sw/stale-while-revalidate {:cache "pages"}))
(sw/register-route! #"/static/" (sw/cache-only {:cache "static"}))
(sw/set-default! (sw/network-only {}))
(sw/start!))On the page:
(ns my-app.main
(:require [c3kit.wire.service-worker.register :as swr]))
(swr/register! {:url "/service-worker.js"
:on-update (fn [_] (js/console.log "update available"))})
;; To remove the service worker (e.g. when rolling back):
(swr/unregister!)Embed your app version in cache names (e.g. (str "shell-" version)) to get
automatic purge of prior-deploy caches on activate.
stale-while-revalidate caveat: background revalidation is fire-and-forget and is
not wrapped in event.waitUntil, so a browser may terminate the service worker before
the refresh completes. The next request still serves the prior cached value and
re-triggers revalidation. For must-refresh resources prefer network-first.
precache! atomicity: URLs passed to precache! are handed to cache.addAll, which
is atomic — if any single URL returns a 4xx/5xx or network-fails, the entire install
fails and the service worker never activates. Every URL must be an absolute or
same-origin path that resolves successfully at install time.
Security footguns — :allow-cross-origin and :cache-credentialed: these opts
are explicit opt-outs of the secure-by-default caching gate. Use them only with a
tight matcher (exact host + path, narrow regexp) that covers exactly the resources
you intend to cache. A broad matcher (e.g. (fn [_] true)) with either opt enabled
can cause the cache to store attacker-influenced cross-origin responses or per-user
credentialed responses, potentially leaking private data across sessions.
MIT © Clean Coders.
