{"id":49297848,"url":"https://github.com/bigskysoftware/ssexi","last_synced_at":"2026-04-26T05:00:38.076Z","repository":{"id":353833297,"uuid":"1220945779","full_name":"bigskysoftware/ssexi","owner":"bigskysoftware","description":"ssexi.js - a companion to fixi.js","archived":false,"fork":false,"pushed_at":"2026-04-25T18:54:25.000Z","size":17,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-25T20:31:59.480Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://fixiproject.org","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bigskysoftware.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2026-04-25T14:50:06.000Z","updated_at":"2026-04-25T18:54:30.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/bigskysoftware/ssexi","commit_stats":null,"previous_names":["bigskysoftware/ssexi"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/bigskysoftware/ssexi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fssexi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fssexi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fssexi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fssexi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bigskysoftware","download_url":"https://codeload.github.com/bigskysoftware/ssexi/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bigskysoftware%2Fssexi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32286271,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"online","status_checked_at":"2026-04-26T02:00:05.962Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":[],"created_at":"2026-04-26T05:00:21.832Z","updated_at":"2026-04-26T05:00:38.066Z","avatar_url":"https://github.com/bigskysoftware.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1\u003e\u0026#x1F4E1; ssexi.js - \u003ci\u003estreaming HTML \u0026 events for fixi.js\u003c/i\u003e\u003c/h1\u003e\n\nssexi is a companion library for [fixi.js](https://github.com/bigskysoftware/fixi) that adds automatic\n[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) (SSE) support.\n\nPart of the [fixi project](https://fixiproject.org).\n\nWhen a fixi `fetch()` returns a response with `Content-Type: text/event-stream`, ssexi takes over and\nstreams HTML into the target element as messages arrive.\n\nHere is an example:\n\n```html\n\n\u003cscript src=\"fixi.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"ssexi.js\"\u003e\u003c/script\u003e\n\n\u003cbutton fx-action=\"/stream\"\n        fx-swap=\"beforeend\"\n        fx-target=\"#output\"\u003e\n    Start Stream\n\u003c/button\u003e\n\u003cdiv id=\"output\"\u003e\u003c/div\u003e\n```\n\nWhen the button is clicked, fixi issues a `GET` to `/stream`. If the server responds with\n`Content-Type: text/event-stream`, ssexi parses the SSE stream and swaps each message's `data` into the\n`#output` div, appending via `beforeend`.\n\nNo special attributes are needed; ssexi detects SSE responses automatically.\n\n## Minimalism\n\nssexi shares [fixi's](https://github.com/bigskysoftware/fixi) philosophy of radical minimalism. It adds SSE streaming\nsupport in a single file with no additional attributes, no configuration, and no dependencies beyond fixi itself.\n\nLike fixi, ssexi takes advantage of modern JavaScript features:\n\n* [`async` generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*)\n  for parsing SSE streams\n* The [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) via `ReadableStream.getReader()`\n* [`TextDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder) for streaming byte-to-text decoding\n\nA hard constraint is that the *unminified, uncompressed* size of ssexi.js stays below the\nminified + gzipped size of [preact](https://bundlephobia.com/package/preact). Current sizes\nare listed on the [fixi project site](https://fixiproject.org).\n\nThe ssexi project consists of four files:\n\n* [`ssexi.js`](ssexi.js), the code for the library\n* [`test.html`](test.html), the test suite for the library\n* This [`README.md`](README.md), which is the documentation\n* [`npm.sh`](npm.sh), which generates npm releases of the library\n\n## Installing\n\nssexi is designed to be easily [vendored](https://htmx.org/essays/vendoring/), that is, copied, into your project\nalongside your copy of fixi:\n\n```bash\ncurl https://raw.githubusercontent.com/bigskysoftware/ssexi/refs/heads/main/ssexi.js \u003e\u003e ssexi.js\n```\n\nYou can also use the JSDelivr CDN for local development or testing:\n\n```html\n\n\u003cscript src=\"https://cdn.jsdelivr.net/gh/bigskysoftware/ssexi@main/ssexi.js\"\u003e\u003c/script\u003e\n```\n\nFinally, ssexi is available on NPM as the [`ssexi`](https://www.npmjs.com/package/ssexi) package.\n\n## Support\n\nYou can get support for ssexi via:\n\n* [Github Issues](https://github.com/bigskysoftware/ssexi/issues)\n* [The htmx Discord `#fixi` channel](https://htmx.org/discord)\n\n## Modus Operandi\n\nssexi is implemented as a single `fx:config` event listener. I encourage you to look at\n[the source](ssexi.js); it is short enough to read in a few minutes.\n\n### Integration With fixi\n\nWhen fixi fires the [`fx:config`](https://github.com/bigskysoftware/fixi#fxconfig) event, ssexi wraps the `cfg.fetch`\nfunction. The wrapper calls the real `fetch()`, checks the `Content-Type` header of the response, and if it contains\n`text/event-stream`, ssexi takes over:\n\n1. An [`fx:sse:open`](#fxsseopen) event is fired on the target element\n2. The response body is read as a stream and parsed according to the\n   [SSE specification](https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation)\n3. For each message, an [`fx:sse:message`](#fxssemessage) event is fired\n4. **Unnamed messages** (no `event:` field) have their `data` swapped into the target element\n5. **Named messages** (with `event:` field) are dispatched as [`fx:sse:{eventName}`](#fxsseeventname) events and are\n   **not** swapped\n6. When the stream ends, an [`fx:sse:close`](#fxsseclose) event is fired\n\nIf the response is not `text/event-stream`, it passes through to fixi untouched.\n\n### Accept Header\n\nLoading ssexi sets a default `Accept: text/html, text/event-stream` header on every fixi\nrequest, so that backends doing content negotiation can decide whether to return a one-shot\nHTML fragment or an SSE stream from the same URL.  The header is added with `??=`, so any\n`Accept` you've already set (in an `fx:config` listener, or via `window.fixiCfg.headers`)\nwins:\n\n```js\nelt.addEventListener('fx:config', (e) =\u003e {\n    // overrides ssexi's default for this element\n    e.detail.cfg.headers.Accept = 'text/event-stream'\n})\n```\n\n`text/html` is always listed so auth redirects, error pages, and HTML-only endpoints keep\nworking unchanged.  Servers that don't look at `Accept` are unaffected.\n\n### SSE Parsing\n\nssexi implements a compliant SSE parser as an async generator. It handles:\n\n* Line endings: `\\r\\n`, `\\r`, or `\\n`\n* Comments (lines starting with `:`)\n* Multi-line `data` fields (joined with `\\n`)\n* The `event`, `id`, and `retry` fields\n* Chunked delivery (partial lines buffered across reads)\n\n### The `cfg.sse` Object\n\nWhen ssexi detects an SSE response, it creates a `cfg.sse` object on the fixi config with the following properties:\n\n* `lastEventId` - the `id` of the most recently received message (updated as messages arrive)\n* `retry` - the most recent `retry:` value from the server (in milliseconds), or `null`\n* `reader` - the\n  [`ReadableStreamDefaultReader`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader)\n  for the response body\n\nThese properties are available in all ssexi events and provide the plugin points needed to implement\n[reconnection](#reconnection), [background disconnecting](#background-tab-handling), and\n[stream cancellation](#cancelling-via-the-reader):\n\n```js\ntarget.addEventListener(\"fx:sse:close\", (evt) =\u003e {\n    let {lastEventId, retry} = evt.detail.cfg.sse\n    // use lastEventId and retry to implement reconnection logic\n})\n```\n\n```js\ntarget.addEventListener(\"fx:sse:open\", (evt) =\u003e {\n    let reader = evt.detail.cfg.sse.reader\n    // store reader reference for later cancellation\n})\n```\n\n### Swapping\n\nFor SSE responses, ssexi uses the `fx-swap` value from fixi's config.\n\nCommon swap styles for SSE:\n\n| `fx-swap`    | behavior                                                                               |\n|--------------|----------------------------------------------------------------------------------------|\n| `innerHTML`  | Each message **replaces** the target's content (good for progressive rendering)        |\n| `beforeend`  | Each message is **appended** to the target (good for chat, feeds, logs)                |\n| `afterbegin` | Each message is **prepended** to the target                                            |\n| `outerHTML`  | First message **replaces** the target element, subsequent messages **append after** it |\n\n#### `outerHTML` Behavior\n\nWhen `fx-swap` is `outerHTML` (fixi's default), ssexi handles it specially for streaming:\n\n1. The **first** message replaces the target element via `outerHTML`, just as fixi normally would\n2. **Subsequent** messages are appended after the replaced content via `afterend`\n3. An internal anchor element is used to track the insertion point and is removed when the stream ends\n\nThis means the original target element is replaced by the first message's HTML, and subsequent messages accumulate\nafter it. Because the original target is replaced, ssexi events after the first message will bubble through the\nanchor's parent rather than the original target; listen on a parent element or `document` when using `outerHTML`:\n\n```js\ndocument.addEventListener(\"fx:sse:message\", (evt) =\u003e {\n    console.log(\"message:\", evt.detail.message.data)\n})\n```\n\nYou can also set `cfg.sseSwap` in the `fx:config` event to use a different swap style for SSE than for normal\nresponses:\n\n```js\ndocument.addEventListener(\"fx:config\", (evt) =\u003e {\n    evt.detail.cfg.sseSwap = \"beforeend\"\n})\n```\n\n#### Routing One Stream To Multiple Targets\n\nAn SSE message's `event:` field is normally a name (and dispatches `fx:sse:{name}` without\nswapping; see [`fx:sse:{eventName}`](#fxsseeventname)). As a special case, if the `event:`\nvalue parses as JSON, ssexi treats it as a per-message override of the swap parameters.\nAll fields are optional:\n\n| field        | default                          | effect                                                    |\n|--------------|----------------------------------|-----------------------------------------------------------|\n| `target`     | `cfg.target`                     | CSS selector for where this message's data is swapped     |\n| `swap`       | `cfg.sseSwap` / `cfg.swap`       | Swap style for this message (`innerHTML`, `beforeend`, ...) |\n| `transition` | none                             | If truthy, wrap this swap in `document.startViewTransition` |\n\n```\nevent: {\"target\":\"#clock\"}\ndata: 12:34:56\n\nevent: {\"target\":\"#log\",\"swap\":\"beforeend\"}\ndata: \u003cdiv class=\"line\"\u003euser signed in\u003c/div\u003e\n\nevent: {\"transition\":true}\ndata: \u003cp\u003esame target, but morphed via a view transition\u003c/p\u003e\n\n```\n\nThis lets one SSE connection fan out to several panels at once, each with its own swap\nmode. `target` is resolved with `document.querySelector`; if it doesn't match anything\nthe message is dropped silently.\n\nThe JSON must start with `{` to be recognised; anything else is treated as a regular\nnamed event and dispatched without swapping.\n\n### Transitions\n\nssexi does **not** wrap every swap in a [View\nTransition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API). View\ntransitions don't queue (a new one cancels the previous one's `.finished` promise), so\nwrapping each frame of a streamed response would either serialise the stream into\nmulti-second sequences or strand a transition mid-flight. The default is plain swaps;\nreach for ordinary CSS transitions on the swapped content for continuous animations.\n\nFor occasional, deliberate moments where a view transition *is* what you want, set\n`{\"transition\": true}` in a JSON event (see the routing table above). ssexi will\n`await cfg.transition(swap).finished` for that single message before reading the next\none, so the rest of the stream stays paused while the transition plays. Use it sparingly\non slow-moving streams; firing transition messages back-to-back will still cause earlier\nones to abort.\n\n## Events\n\nssexi fires the following events on the **target element**. All events bubble, are composed, and are cancelable.\n\n\u003ctable\u003e\n\u003cthead\u003e\n\u003ctr\u003e\n  \u003cth\u003eevent\u003c/th\u003e\n  \u003cth\u003edetail\u003c/th\u003e\n  \u003cth\u003edescription\u003c/th\u003e\n\u003c/tr\u003e\n\u003c/thead\u003e\n\u003ctbody\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ca href=\"#fxsseopen\"\u003e\u003ccode\u003efx:sse:open\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecfg\u003c/code\u003e, \u003ccode\u003eresponse\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFired when an SSE stream is detected.  Cancel to prevent processing.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ca href=\"#fxssemessage\"\u003e\u003ccode\u003efx:sse:message\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecfg\u003c/code\u003e, \u003ccode\u003emessage\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFired for every SSE message \u003ci\u003ebefore\u003c/i\u003e swapping.  Cancel to stop the stream.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ca href=\"#fxsseswapped\"\u003e\u003ccode\u003efx:sse:swapped\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecfg\u003c/code\u003e, \u003ccode\u003emessage\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFired \u003ci\u003eafter\u003c/i\u003e a message's content has been swapped into the target.  Use this for post-swap reactions like auto-scroll.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ca href=\"#fxsseeventname\"\u003e\u003ccode\u003efx:sse:{eventName}\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecfg\u003c/code\u003e, \u003ccode\u003emessage\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFired for messages with an \u003ccode\u003eevent:\u003c/code\u003e field.  These are \u003cb\u003enot\u003c/b\u003e swapped.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ca href=\"#fxsseclose\"\u003e\u003ccode\u003efx:sse:close\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecfg\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFired when the stream ends normally.\u003c/td\u003e\n\u003c/tr\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003ca href=\"#fxsseerror\"\u003e\u003ccode\u003efx:sse:error\u003c/code\u003e\u003c/a\u003e\u003c/td\u003e\n\u003ctd\u003e\u003ccode\u003ecfg\u003c/code\u003e, \u003ccode\u003eerror\u003c/code\u003e\u003c/td\u003e\n\u003ctd\u003eFired if an error occurs during streaming.\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/tbody\u003e\n\u003c/table\u003e\n\n### `fx:sse:open`\n\nFired on the target element when a response with `Content-Type: text/event-stream` is detected. The `evt.detail`\ncontains `cfg` (the fixi config object) and `response` (the fetch Response).\n\nIf you call `preventDefault()` on this event, the stream will not be processed and the target will not be modified.\n\n### `fx:sse:message`\n\nFired for **every** SSE message (both named and unnamed). The `evt.detail.message` object has the following\nproperties:\n\n* `data` - the message data (multi-line `data:` fields joined with `\\n`)\n* `event` - the event name (empty string if unnamed)\n* `id` - the message id (empty string if not set)\n* `retry` - the reconnection delay in milliseconds (if a `retry:` field was present), or `null`\n\nIf you call `preventDefault()` on this event, the stream will stop processing (the current message will not be\nswapped or dispatched, and no further messages will be read).\n\nYou can also use this event to modify the message data before it is swapped:\n\n```js\ntarget.addEventListener(\"fx:sse:message\", (evt) =\u003e {\n    evt.detail.message.data = markdown(evt.detail.message.data)\n})\n```\n\n### `fx:sse:swapped`\n\nFired on the target element **after** an unnamed message's `data` has been swapped in.\nThe `evt.detail` is the same shape as `fx:sse:message` (`cfg`, `message`), but at this\npoint the new content is already in the DOM, so reading layout properties returns post-swap\nvalues. Useful for auto-scroll, syntax-highlighting newly streamed code, etc.:\n\n```html\n\u003cdiv id=\"log\" on-fx:sse:swapped=\"this.scrollTop = this.scrollHeight\"\u003e\u003c/div\u003e\n```\n\nNot fired for named events (which aren't swapped) or for cancelled `fx:sse:message` events.\n\n### `fx:sse:{eventName}`\n\nWhen an SSE message has an `event:` field, ssexi dispatches a custom event with that name prefixed by `fx:sse:`.\nFor example, a message with `event: status` will fire `fx:sse:status` on the target element.\n\nNamed events are **not** swapped into the DOM; they are for JavaScript handling:\n\n```js\ntarget.addEventListener(\"fx:sse:status\", (evt) =\u003e {\n    console.log(\"status update:\", evt.detail.message.data)\n})\n```\n\n### `fx:sse:close`\n\nFired when the SSE stream ends normally (the server closes the connection).\n\n### `fx:sse:error`\n\nFired if an error occurs during stream processing. The `evt.detail.error` property contains the thrown value.\n\n## Server Side\n\nYour server endpoint should respond with `Content-Type: text/event-stream` and send\n[SSE-formatted](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format)\nmessages:\n\n```\ndata: \u003cp\u003eFirst update\u003c/p\u003e\n\ndata: \u003cp\u003eSecond update\u003c/p\u003e\n\nevent: done\ndata: finished\n\n```\n\nEach message is one or more `data:` lines followed by a blank line. Messages without an `event:` field will have their\n`data` swapped into the target. Messages with an `event:` field will be dispatched as DOM events.\n\n### Example: Python/Flask\n\n```python\nfrom flask import Flask, Response\nimport time\n\napp = Flask(__name__)\n\n\n@app.route('/stream')\ndef stream():\n    def generate():\n        for i in range(5):\n            yield f\"data: \u003cp\u003eMessage {i + 1}\u003c/p\u003e\\n\\n\"\n            time.sleep(1)\n        yield \"event: done\\ndata: finished\\n\\n\"\n\n    return Response(generate(), content_type='text/event-stream')\n```\n\n### Example: Node/Express\n\n```javascript\napp.get('/stream', (req, res) =\u003e {\n    res.setHeader('Content-Type', 'text/event-stream')\n    res.setHeader('Cache-Control', 'no-cache')\n    let i = 0\n    let interval = setInterval(() =\u003e {\n        if (++i \u003e 5) {\n            res.write('event: done\\ndata: finished\\n\\n')\n            res.end()\n            clearInterval(interval)\n        } else {\n            res.write(`data: \u003cp\u003eMessage ${i}\u003c/p\u003e\\n\\n`)\n        }\n    }, 1000)\n})\n```\n\n## Examples\n\n### Streaming Chat\n\n```html\n\n\u003cform fx-action=\"/chat\" fx-method=\"POST\"\n      fx-swap=\"beforeend\" fx-target=\"#messages\"\u003e\n    \u003cinput name=\"message\" placeholder=\"Type a message...\"\u003e\n    \u003cbutton\u003eSend\u003c/button\u003e\n\u003c/form\u003e\n\u003cdiv id=\"messages\"\u003e\u003c/div\u003e\n```\n\nEach SSE message from the server appends a new HTML fragment to the `#messages` div.\n\n### Progressive Rendering\n\n```html\n\n\u003cbutton fx-action=\"/render\" fx-swap=\"innerHTML\" fx-target=\"#content\"\u003e\n    Load Content\n\u003c/button\u003e\n\u003cdiv id=\"content\"\u003eClick to load...\u003c/div\u003e\n```\n\nEach SSE message replaces the content of `#content`, allowing the server to progressively refine the output.\n\n### Closing a Stream on a Named Event\n\n```html\n\n\u003cdiv id=\"feed\"\u003e\u003c/div\u003e\n\u003cscript\u003e\n    document.getElementById(\"feed\").addEventListener(\"fx:sse:done\", (evt) =\u003e {\n        console.log(\"stream complete\")\n    })\n\u003c/script\u003e\n\u003cbutton fx-action=\"/feed\" fx-swap=\"beforeend\" fx-target=\"#feed\"\u003e\n    Start Feed\n\u003c/button\u003e\n```\n\nWhen the server sends `event: done`, the `fx:sse:done` event fires on the target. The stream continues to\ncompletion naturally; the named event is simply dispatched for your code to react to.\n\n### Stopping a Stream Early\n\nYou can stop processing a stream by canceling the `fx:sse:message` event:\n\n```html\n\n\u003cbutton fx-action=\"/long-stream\" fx-swap=\"beforeend\" fx-target=\"#out\"\u003e\n    Start\n\u003c/button\u003e\n\u003cbutton onclick=\"document.getElementById('out').dataset.stop = 'true'\"\u003e\n    Stop\n\u003c/button\u003e\n\u003cdiv id=\"out\"\u003e\u003c/div\u003e\n\u003cscript\u003e\n    document.getElementById(\"out\").addEventListener(\"fx:sse:message\", (evt) =\u003e {\n        if (evt.target.dataset.stop) evt.preventDefault()\n    })\n\u003c/script\u003e\n```\n\n## Reconnection and Lifecycle\n\nssexi supports three opt-in config flags for managing stream lifecycle. Set them in an\n`fx:config` listener (or on the returned cfg before the stream starts):\n\n| flag                          | behavior                                                                 |\n|-------------------------------|--------------------------------------------------------------------------|\n| `cfg.sseReconnect`            | On close or error, wait `sse.retry` ms (or 3000) and re-fetch with a `Last-Event-ID` header. |\n| `cfg.ssePauseOnHidden`        | Cancel the reader when `document.hidden`; resume (with `Last-Event-ID`) when visible. |\n| `cfg.sseDisconnectOnHidden`   | Close the stream when `document.hidden`. No resume; the caller must re-trigger. |\n\nExample:\n\n```js\nbtn.addEventListener(\"fx:config\", (e) =\u003e {\n    e.detail.cfg.sseReconnect = true\n    e.detail.cfg.ssePauseOnHidden = true\n})\n```\n\n### `cfg.sse.close()`\n\nAt any time you can stop the stream (and the reconnect loop) by calling `cfg.sse.close()`.\nIt sets `cfg.sse.closed = true` and cancels the underlying reader:\n\n```js\ntarget.addEventListener(\"fx:sse:message\", (e) =\u003e {\n    if (shouldStop(e.detail.message)) e.detail.cfg.sse.close()\n})\n```\n\n### Custom Reconnect Policy\n\nIf the built-in reconnect doesn't match your needs (e.g. you want exponential backoff),\nleave `cfg.sseReconnect` off and implement your own in an `fx:sse:close` / `fx:sse:error`\nlistener using `cfg.trigger` to re-fire the triggering event:\n\n```js\ndocument.addEventListener(\"fx:sse:close\", (evt) =\u003e {\n    let cfg = evt.detail.cfg, elt = cfg.trigger.target\n    if (!elt.isConnected) return\n    let attempt = elt.__ssexiAttempt = (elt.__ssexiAttempt || 0) + 1\n    let delay = Math.min((cfg.sse?.retry || 500) * 2 ** (attempt - 1), 60000)\n    delay += delay * 0.3 * (Math.random() * 2 - 1) // jitter\n    setTimeout(() =\u003e elt.dispatchEvent(new Event(cfg.trigger.type)), delay)\n})\n```\n\nNote that cancelling the reader will cause an `fx:sse:error` event to fire (not `fx:sse:close`), since the stream\ndid not end naturally. You can alternatively use `cfg.abort()` to abort the underlying fetch, which has the same\neffect.\n\n## Mocking\n\nYou can mock SSE responses the same way you mock regular fixi responses, by replacing `cfg.fetch` in the\n`fx:config` event. The mock should return a `Response` with a `ReadableStream` body and\n`Content-Type: text/event-stream`:\n\n```js\ndocument.addEventListener(\"fx:config\", (evt) =\u003e {\n    evt.detail.cfg.fetch = () =\u003e {\n        let encoder = new TextEncoder()\n        let messages = [\"data: hello\\n\\n\", \"data: world\\n\\n\"]\n        let i = 0\n        let stream = new ReadableStream({\n            pull(controller) {\n                if (i \u003c messages.length)\n                    controller.enqueue(encoder.encode(messages[i++]))\n                else\n                    controller.close()\n            }\n        })\n        return Promise.resolve(\n            new Response(stream, {headers: {'Content-Type': 'text/event-stream'}})\n        )\n    }\n})\n```\n\n## LICENCE\n\n```\nZero-Clause BSD\n=============\n\nPermission to use, copy, modify, and/or distribute this software for\nany purpose with or without fee is hereby granted.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL\nWARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES\nOF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE\nFOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY\nDAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN\nAN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT\nOF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbigskysoftware%2Fssexi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbigskysoftware%2Fssexi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbigskysoftware%2Fssexi/lists"}