{"id":16660557,"url":"https://github.com/mpenet/hirundo","last_synced_at":"2026-02-17T10:44:28.652Z","repository":{"id":63940326,"uuid":"569841327","full_name":"mpenet/hirundo","owner":"mpenet","description":"Helidon 4.x - RING clojure adapter","archived":false,"fork":false,"pushed_at":"2026-02-16T18:11:03.000Z","size":172,"stargazers_count":108,"open_issues_count":1,"forks_count":8,"subscribers_count":6,"default_branch":"main","last_synced_at":"2026-02-16T19:54:37.125Z","etag":null,"topics":["clojure","helidon","loom","ring"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"epl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mpenet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":"FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":"mpenet"}},"created_at":"2022-11-23T18:30:06.000Z","updated_at":"2026-02-16T18:11:07.000Z","dependencies_parsed_at":"2024-01-12T12:00:18.090Z","dependency_job_id":"a7c1f90f-0383-484d-b716-6fe03c9e7d6d","html_url":"https://github.com/mpenet/hirundo","commit_stats":null,"previous_names":["mpenet/mina"],"tags_count":47,"template":false,"template_full_name":null,"purl":"pkg:github/mpenet/hirundo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpenet%2Fhirundo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpenet%2Fhirundo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpenet%2Fhirundo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpenet%2Fhirundo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mpenet","download_url":"https://codeload.github.com/mpenet/hirundo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mpenet%2Fhirundo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29540353,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T08:11:05.436Z","status":"ssl_error","status_checked_at":"2026-02-17T08:09:38.860Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojure","helidon","loom","ring"],"created_at":"2024-10-12T10:29:46.217Z","updated_at":"2026-02-17T10:44:28.641Z","avatar_url":"https://github.com/mpenet.png","language":"Clojure","funding_links":["https://github.com/sponsors/mpenet"],"categories":[],"sub_categories":[],"readme":"# Hirundo [![Clojars Project](https://img.shields.io/clojars/v/com.s-exp/hirundo.svg)](https://clojars.org/com.s-exp/hirundo)\n\n\u003cimg src=\"https://github.com/mpenet/hirundo/assets/106390/7fb900d8-c8cb-4211-9c40-1caa97c1c269\" data-canonical-src=\"https://github.com/mpenet/hirundo/assets/106390/7fb900d8-c8cb-4211-9c40-1caa97c1c269\" width=\"150\" height=\"150\" /\u003e\n\n\n\n[Helidon/Nima](https://helidon.io/nima)\n[RING](https://github.com/ring-clojure/ring/blob/master/SPEC) compliant adapter\nfor clojure, loom based\n\n## Usage\n\n```clojure\n(require '[s-exp.hirundo :as hirundo])\n(require '[s-exp.hirundo.websocket :as ws])\n\n(def server\n  (hirundo/start! {;; regular ring handler\n                :http-handler (fn [{:as request :keys [body headers ...]}]\n                                {:status 200\n                                 :body \"Hello world\"\n                                 :headers {\"Something\" \"Interesting\"}})\n\n                ;; websocket endpoints\n                :websocket-endpoints {\"/ws\" {:message (fn [session data _last-msg]\n                                                        ;; echo back data\n                                                        (ws/send! session data true))\n                                             :open (fn [session] (prn :opening-session))\n                                             :close (fn [_session status reason]\n                                                      (prn :closed-session status reason))\n                                             :error (fn [session error]\n                                                      (prn :error error))\n                                             ;; :subprotocols [\"chat\"]\n                                             ;; :extensions [\"foobar\"]\n                                             ;; :http-upgrade (fn [headers] ...)\n                                             }}\n                :port 8080}))\n;; ...\n\n(hirundo/stop! server)\n\n```\n\nThere is nothing special to its API, you use hirundo as you would use any blocking\nhttp adapter like jetty; it is RING compliant so compatible with most/all\nmiddlewares out there.\n\n## Supported options\n\n* `:host` - host of the default socket, defaults to 127.0.0.1\n\n* `:port` - port the server listens to, defaults to random free port\n\n* `:http-handler` - ring handler function\n\n* `:websocket-endpoints` - /!\\ subject to changes - (map-of string-endpoint\n  handler-fns-map), where handler if can be of `:message`, `:ping`, `:pong`,\n  `:close`, `:error`, `:open`, `:http-upgrade`. `handler-fns-map` can also\n  contain 2 extra keys, `:extensions`, `:subprotocols`, which are sets of\n  subprotocols and protocol extensions acceptable by the server. Alternatively\n  you can pass a fn that emits a map, or directly an Helidon WsListener. Both\n  these options can be useful if you need to keep state for a connection\n  lifecycle.\n\n* `:write-queue-length` \n\n* `:backlog` \n\n* `:max-payload-size` \n\n* `:write-queue-length`\n\n* `:receive-buffer-size` \n\n* `:connection-options`(map-of `:socket-receive-buffer-size` `:socket-send-buffer-size` `:socket-reuse-address` `:socket-keep-alive` `:tcp-no-delay` `:read-timeout` `:connect-timeout`)\n\n* `:tls` - A `io.helidon.nima.common.tls.Tls` instance\n\n\nYou can hook into the server builder via `s-exp.hirundo.options/set-server-option!`\nmultimethod at runtime and add/modify whatever you want if you need anything\nextra we don't provide (yet).\n\nhttp2 (h2 \u0026 h2c) is supported out of the box, iif a client connects with http2\nit will do the protocol switch automatically.\n\n## SSE (Server-Sent Events)\n\nHirundo provides built-in SSE support via `s-exp.hirundo.sse/stream!`.\nCall it from within a ring handler — it takes over the response, streaming\nevents to the client until the channel is closed or the client disconnects.\n\n`stream!` returns a map of `{:input-ch :close-ch}`. Put event maps onto\n`input-ch` to send them; close `input-ch` or `close-ch` to end the stream.\n\n```clojure\n(require '[s-exp.hirundo.sse :as sse])\n(require '[clojure.core.async :as async])\n\n(defn my-handler [request]\n  (let [{:keys [input-ch close-ch]} (sse/stream! request)]\n      (async/\u003e!! input-ch {:event \"update\"\n                          :data [\"{\\\"count\\\": 1}\"]\n                          :id \"1\"})\n      ;; close to end the SSE stream\n      (async/close! input-ch)))\n\n```\n\nMessages are maps with keys `:event`, `:data`, `:id`, `:retry`.\n`:data` is a vector of strings — each entry becomes a separate `data:` line\nper the SSE spec.\n\n### Options\n\n* `:input-ch` - user-provided `core.async` channel. If not provided, one is\n  created with a buffer of 10.\n* `:close-ch` - user-provided `core.async` promise channel for close signaling.\n  If not provided, one is created internally.\n* `:headers` - extra headers to merge into the SSE response.\n* `:compression` - compression settings map. Defaults to\n  `{:type :brotli :quality 4 :window-size 18}`. Only applied when the client\n  sends `accept-encoding: br`. Keys:\n  * `:type` - compression type (currently only `:brotli`)\n  * `:quality` - compression quality (0-11)\n  * `:window-size` - LZ77 window size (10-24)\n* `:heartbeat-ms` - interval in ms for heartbeat SSE comments to detect client\n  disconnect (default 1500).\n\n### Brotli compression\n\nWhen `:compression` is set, the response is compressed with brotli (`content-encoding: br`).\nThis can significantly reduce bandwidth for text-heavy SSE streams.\n\n```clojure\n(let [{:keys [input-ch]} (sse/stream! request\n                                      :compression {:type :brotli\n                                                    :quality 4\n                                                    :window-size 18})]\n  ;; put events onto input-ch\n  )\n```\n\n### Client disconnect detection\n\nA heartbeat SSE comment (`:` line, ignored by clients) is sent periodically. When\nthe any write fails (client gone), both channels are closed and the stream ends.\nConfigure the interval with `:heartbeat-ms`.\n\n### Datastar demo\n\nThe `demo/` directory contains a full [Datastar](https://data-star.dev/) example\nthat demonstrates SSE streaming with hirundo. It includes:\n\n- **Live Clock** — streams the current time every second via `patch-elements`\n- **Counter** — click to start a counting stream\n- **Shop Stats** — uses `patch-signals` to stream reactive state (order count, revenue, last item sold) without replacing DOM elements\n- **Live Feed** — simulated event log with brotli compression\n\nRun it with:\n\n```\ncd demo \u0026\u0026 clj -M -m s-exp.hirundo.demo\n```\n\nThen open http://localhost:8080.\n\n## Installation\n\nNote: You need to use java **21**\n\nhttps://clojars.org/com.s-exp/hirundo\n\n## Running the tests \n\n```\nclj -X:test\n```\n\n## Building Uberjars with hirundo\n\nBecause of the way helidon handles service configuration we need to carefuly\ncraft the uberjar with merged resources for some entries. \n\nYou will need to provide `:conflict-handlers` for the uberjar task that\nconcatenates some of the files from resources found in helidon module\ndependencies.\n\n\nPay attention to the `b/uber` call here:\n\n```clj\n(ns build\n  (:refer-clojure :exclude [test])\n  (:require [clojure.data.json :as json]\n            [clojure.java.io :as io]\n            [clojure.tools.build.api :as b]\n            [clojure.tools.build.tasks.uber :as uber]))\n\n(def lib 'foo/bar)\n(def version \"0.1.0-SNAPSHOT\")\n(def main 'foo.bar.baz)\n(def class-dir \"target/classes\")\n(defn- uber-opts [opts]\n  (assoc opts\n         :lib lib :main main\n         :uber-file (format \"target/%s-%s.jar\" lib version)\n         :basis (b/create-basis {})\n         :class-dir class-dir\n         :src-dirs [\"src\"]\n         :ns-compile [main]))\n\n(defn append-json\n  [{:keys [path in existing state]}]\n  {:write\n   {path\n    {:append false\n     :string\n     (json/write-str\n      (concat (json/read-str (slurp existing))\n              (json/read-str (#'uber/stream-\u003estring in))))}}})\n\n(defn ci \"Run the CI pipeline of tests (and build the uberjar).\" [opts]\n  (b/delete {:path \"target\"})\n  (let [opts (uber-opts opts)]\n    (println \"\\nCopying source...\")\n    (b/copy-dir {:src-dirs [\"src\"] :target-dir class-dir})\n    (println (str \"\\nCompiling \" main \"...\"))\n    (b/compile-clj opts)\n    (println \"\\nBuilding JAR...\")\n    \n    ;; HERE is the important part\n    (b/uber (assoc opts :conflict-handlers\n                   {\"META-INF/helidon/service.loader\" :append-dedupe\n                    \"META-INF/helidon/feature-metadata.properties\" :append-dedupe\n                    \"META-INF/helidon/config-metadata.json\" append-json\n                    \"META-INF/helidon/service-registry.json\" append-json})))\n\n  opts)\n```\n\n## License\n\nCopyright © 2023 Max Penet\n\nDistributed under the Eclipse Public License version 1.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmpenet%2Fhirundo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmpenet%2Fhirundo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmpenet%2Fhirundo/lists"}