Skip to content

Repository files navigation

Wire

Wire

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 Build Clojars Project License: MIT

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

Artifacts

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 everything wire-core ships plus the Reagent wrappers and reagent / cljsjs/react* deps. The drop-in for projects that were on 3.0.0.
  • com.cleancoders.c3kit/wire-core — React-free. Same c3kit.wire.* namespaces minus the Reagent wrappers, plus c3kit.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.

Repository Layout

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.

Development

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

Local Development with :local/root or :git/url

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.

Contributing

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.

Deployment

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.

  1. Open a PR bumping VERSION and CHANGES.md.
  2. Merge to master and 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.
  3. Actions → ReleaseRun workflow.
  4. Approve the clojars deployment 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.

Break glass

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.

Service Worker (offline caching)

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-Cookie is a forbidden response-header name, so headers.get("Set-Cookie") returns nil inside the service worker. A Set-Cookie response is not detected and will be cached if nothing else blocks it.
  • Vary is not a CORS-safelisted response header, so on a cross-origin cors response headers.get("Vary") is nil and the Vary guard is silently skipped. It works only same-origin.
  • Same-origin cookie-session auth is invisible: cookie headers are stripped from Request objects in ServiceWorkerGlobalScope.

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.

License

MIT © Clean Coders.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages