-
Notifications
You must be signed in to change notification settings - Fork 150
Add SSE primitive and transform utilities #831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
f4889b4
Initial commit of SSE primitives
davedbase abd9bb3
Improved types and switched my author email out
davedbase 15b7092
Added helpful transforms
davedbase 032fe1a
Adding more transforms and preparing for an initial release
davedbase 66a20c7
Reverting the author emails and will submit this separately
davedbase 4e15653
Re-ran prettier
davedbase 31e887d
Trying to appease the lint gods
davedbase a36382c
Removed old irrelevant badge
davedbase 56199ce
Moved transforms to a single file
davedbase 31c158b
Delete changeset
davedbase 15d015a
Adjusting export detail to fix linting
davedbase 3987d4f
Combined WORKERS.md into README
davedbase 8abab23
Added clarity on bundler vs non-bundler imports + vite link
davedbase d1fbcad
Collapsed class into the primitive
davedbase c29af66
Remove unnecessary line
davedbase 6680ab9
Move transforms to utils package
davedbase 2fb2c85
Promote to stage 2
davedbase cc8d47c
Remove transform functions from sse/package.json
thetarnav 0f24994
Added documentation about transform utilities
davedbase bd54654
Merge branch 'main' of https://github.com/davedbase/solid-primitives
davedbase b8fe38f
Minor fixes for the linter
davedbase File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| "@solid-primitives/sse": minor | ||
| --- | ||
|
|
||
| Initial release of `@solid-primitives/sse`. | ||
|
|
||
| ### Primitives | ||
|
|
||
| - `makeSSE(url, options?)` — base non-reactive primitive. Creates an `EventSource`, attaches handlers, and returns `[source, cleanup]`. No Solid lifecycle dependency. | ||
| - `createSSE(url, options?)` — reactive primitive. Accepts a static or signal URL, closes on owner disposal, and exposes `data`, `error`, `readyState`, `close`, and `reconnect`. | ||
| - `makeSSEWorker(target, options?)` — runs the `EventSource` connection inside a Web Worker or SharedWorker, keeping network I/O off the main thread. The reactive API is identical to `createSSE`. | ||
|
|
||
| ### Built-in transformers | ||
|
|
||
| - `json` — parse message data as a single JSON value | ||
| - `ndjson` — parse newline-delimited JSON into an array | ||
| - `lines` — split message data into a `string[]` by newline | ||
| - `number` — parse message data as a number via `Number()` | ||
| - `safe(transform, fallback?)` — fault-tolerant wrapper; returns `fallback` instead of throwing on bad input | ||
| - `pipe(a, b)` — compose two transforms into one | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # @solid-primitives/sse | ||
|
|
||
| ## 0.0.100 | ||
|
|
||
| ### Initial release | ||
|
|
||
| - `makeSSE` — base non-reactive primitive wrapping the browser `EventSource` API | ||
| - `createSSE` — reactive primitive with signals for `data`, `error`, and `readyState`, reactive URL support, SSR safety, and configurable app-level reconnection |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| MIT License | ||
|
|
||
| Copyright (c) 2021 Solid Primitives Working Group | ||
|
|
||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
|
|
||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
|
|
||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| <p> | ||
| <img width="100%" src="https://assets.solidjs.com/banner?type=Primitives&background=tiles&project=SSE" alt="Solid Primitives SSE"> | ||
| </p> | ||
|
|
||
| # @solid-primitives/sse | ||
|
|
||
| [](https://bundlephobia.com/package/@solid-primitives/sse) | ||
| [](https://www.npmjs.com/package/@solid-primitives/sse) | ||
| [](https://github.com/solidjs-community/solid-primitives#contribution-process) | ||
|
|
||
| Primitives for [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) using the browser's built-in `EventSource` API. | ||
|
|
||
| - [`makeSSE`](#makesse) — Base non-reactive primitive. Creates an `EventSource` and returns a cleanup function. No Solid lifecycle. | ||
| - [`createSSE`](#createsse) — Reactive primitive. Accepts a reactive URL, integrates with Solid's owner lifecycle, and returns signals for `data`, `error`, and `readyState`. | ||
| - [`makeSSEWorker`](./WORKERS.md) — Runs the SSE connection inside a Web Worker or SharedWorker. See [WORKERS.md](./WORKERS.md). | ||
| - [Built-in transformers](./TRANSFORMS.md) — `json`, `ndjson`, `lines`, `number`, `safe`, `pipe`. See [TRANSFORMS.md](./TRANSFORMS.md). | ||
|
davedbase marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| npm install @solid-primitives/sse | ||
| # or | ||
| pnpm add @solid-primitives/sse | ||
| ``` | ||
|
|
||
| ## `makeSSE` | ||
|
|
||
| Creates a raw `EventSource` connection without any Solid lifecycle management. Event handlers are attached immediately. You are responsible for calling the returned cleanup function. | ||
|
|
||
| This is the foundation primitive — `createSSE` uses it internally. | ||
|
|
||
| ```ts | ||
| import { makeSSE } from "@solid-primitives/sse"; | ||
|
|
||
| const [source, cleanup] = makeSSE("https://api.example.com/events", { | ||
| onOpen: () => console.log("Connected"), | ||
| onMessage: e => console.log("Message:", e.data), | ||
| onError: e => console.error("Error:", e), | ||
| events: { | ||
| // Named SSE event types (server sends `event: update`) | ||
| update: e => console.log("Update:", e.data), | ||
| }, | ||
| }); | ||
|
|
||
| // When done: | ||
| cleanup(); | ||
| ``` | ||
|
|
||
| ### Definition | ||
|
|
||
| ```ts | ||
| function makeSSE( | ||
| url: string | URL, | ||
| options?: SSEOptions, | ||
| ): [source: EventSource, cleanup: VoidFunction]; | ||
|
|
||
| type SSEOptions = { | ||
| withCredentials?: boolean; | ||
| onOpen?: (event: Event) => void; | ||
| onMessage?: (event: MessageEvent) => void; | ||
| onError?: (event: Event) => void; | ||
| events?: Record<string, (event: MessageEvent) => void>; | ||
| }; | ||
| ``` | ||
|
|
||
| ## `createSSE` | ||
|
|
||
| Reactive SSE primitive. Connects on creation, closes when the owner is disposed, and reacts to URL changes. | ||
|
|
||
| ```ts | ||
| import { createSSE, SSEReadyState } from "@solid-primitives/sse"; | ||
|
|
||
| const { data, readyState, error, close, reconnect } = createSSE<{ message: string }>( | ||
| "https://api.example.com/events", | ||
| { | ||
| transform: JSON.parse, | ||
| reconnect: { retries: 3, delay: 2000 }, | ||
| }, | ||
| ); | ||
|
|
||
| return ( | ||
| <div> | ||
| <Show when={readyState() === SSEReadyState.OPEN} fallback={<p>Connecting…</p>}> | ||
| <p>Latest: {data()?.message ?? "—"}</p> | ||
| </Show> | ||
| <Show when={error()}> | ||
| <p style="color:red">Connection error</p> | ||
| </Show> | ||
| <button onClick={close}>Disconnect</button> | ||
| <button onClick={reconnect}>Reconnect</button> | ||
| </div> | ||
| ); | ||
| ``` | ||
|
|
||
| ### Reactive URL | ||
|
|
||
| When the URL is a signal accessor, the connection is replaced whenever the URL changes: | ||
|
|
||
| ```ts | ||
| const [userId, setUserId] = createSignal("user-1"); | ||
|
|
||
| const { data } = createSSE<Notification>( | ||
| () => `https://api.example.com/notifications/${userId()}`, | ||
| { transform: JSON.parse }, | ||
| ); | ||
| ``` | ||
|
|
||
| Changing `userId()` will close the existing connection and open a new one to the updated URL. | ||
|
|
||
| ### Options | ||
|
|
||
| | Option | Type | Default | Description | | ||
| | ----------------- | ------------------------------------------- | ----------- | ---------------------------------------- | | ||
| | `withCredentials` | `boolean` | `false` | Send credentials with the request | | ||
| | `onOpen` | `(e: Event) => void` | — | Called when the connection opens | | ||
| | `onMessage` | `(e: MessageEvent) => void` | — | Called on each unnamed `message` event | | ||
| | `onError` | `(e: Event) => void` | — | Called on error | | ||
| | `events` | `Record<string, (e: MessageEvent) => void>` | — | Handlers for named SSE event types | | ||
| | `initialValue` | `T` | `undefined` | Initial value of the `data` signal | | ||
| | `transform` | `(raw: string) => T` | identity | Parse raw string data, e.g. `JSON.parse` | | ||
| | `reconnect` | `boolean \| SSEReconnectOptions` | `false` | App-level reconnect on terminal errors | | ||
|
|
||
| **`SSEReconnectOptions`:** | ||
|
|
||
| | Option | Type | Default | Description | | ||
| | --------- | -------- | ---------- | ----------------------------- | | ||
| | `retries` | `number` | `Infinity` | Max reconnect attempts | | ||
| | `delay` | `number` | `3000` | Milliseconds between attempts | | ||
|
|
||
| ### Return value | ||
|
|
||
| | Property | Type | Description | | ||
| | ------------ | ---------------------------------------- | ------------------------------------------------ | | ||
| | `source` | `Accessor<SSESourceHandle \| undefined>` | Underlying source instance; `undefined` on SSR | | ||
| | `data` | `Accessor<T \| undefined>` | Latest message data | | ||
| | `error` | `Accessor<Event \| undefined>` | Latest error event | | ||
| | `readyState` | `Accessor<SSEReadyState>` | `SSEReadyState.CONNECTING` / `.OPEN` / `.CLOSED` | | ||
| | `close` | `VoidFunction` | Close the connection | | ||
| | `reconnect` | `VoidFunction` | Force-close and reopen | | ||
|
|
||
| ### `SSEReadyState` | ||
|
|
||
| Named constants for the connection state, exported as a plain object so they are tree-shakeable and work with every bundler: | ||
|
|
||
| ```ts | ||
| import { SSEReadyState } from "@solid-primitives/sse"; | ||
|
|
||
| SSEReadyState.CONNECTING; // 0 | ||
| SSEReadyState.OPEN; // 1 | ||
| SSEReadyState.CLOSED; // 2 | ||
| ``` | ||
|
|
||
| ### A note on reconnection | ||
|
|
||
| `EventSource` has native browser-level reconnection built in. For transient network drops the browser automatically retries. The `reconnect` option in `createSSE` is for _application-level_ reconnection — it fires only when `readyState` becomes `SSEReadyState.CLOSED`, meaning the browser has given up entirely. You generally do not need `reconnect: true` for normal usage. | ||
|
|
||
| ## Built-in transformers | ||
|
|
||
| Ready-made `transform` functions for the most common SSE data formats. See [TRANSFORMS.md](./TRANSFORMS.md) for full documentation and examples. | ||
|
|
||
| | Transformer | Description | | ||
| | ---------------------------------------------------------------------- | --------------------------------------------------------------- | | ||
| | [`json`](./TRANSFORMS.md#json) | Parse data as a single JSON value | | ||
| | [`ndjson`](./TRANSFORMS.md#ndjson) | Parse newline-delimited JSON into an array | | ||
| | [`lines`](./TRANSFORMS.md#lines) | Split data into a `string[]` by newline | | ||
| | [`number`](./TRANSFORMS.md#number) | Parse data as a number via `Number()` | | ||
| | [`safe(transform, fallback?)`](./TRANSFORMS.md#safetransform-fallback) | Fault-tolerant wrapper — returns `fallback` instead of throwing | | ||
| | [`pipe(a, b)`](./TRANSFORMS.md#pipea-b) | Compose two transforms into one | | ||
|
|
||
| ## Integration with `@solid-primitives/event-bus` | ||
|
|
||
| Because `bus.emit` matches the `(event: MessageEvent) => void` shape of `onMessage`, you can wire them directly: | ||
|
|
||
| ```ts | ||
| import { createSSE } from "@solid-primitives/sse"; | ||
| import { createEventBus } from "@solid-primitives/event-bus"; | ||
|
|
||
| const bus = createEventBus<string>(); | ||
|
|
||
| createSSE("https://api.example.com/events", { | ||
| onMessage: e => bus.emit(e.data), | ||
| }); | ||
|
|
||
| bus.listen(msg => console.log("received:", msg)); | ||
| ``` | ||
|
|
||
| ### Multi-channel SSE with `createEventHub` | ||
|
|
||
| For streams that use multiple named event types: | ||
|
|
||
| ```ts | ||
| import { createSSE } from "@solid-primitives/sse"; | ||
| import { createEventBus, createEventHub } from "@solid-primitives/event-bus"; | ||
|
|
||
| type OrderEvent = { id: string; total: number }; | ||
| type InventoryEvent = { sku: string; qty: number }; | ||
|
|
||
| const hub = createEventHub({ | ||
| order: createEventBus<OrderEvent>(), | ||
| inventory: createEventBus<InventoryEvent>(), | ||
| }); | ||
|
|
||
| createSSE("https://api.example.com/stream", { | ||
| events: { | ||
| order: e => hub.emit("order", JSON.parse(e.data)), | ||
| inventory: e => hub.emit("inventory", JSON.parse(e.data)), | ||
| }, | ||
| }); | ||
|
|
||
| hub.on("order", event => console.log("New order:", event)); | ||
| ``` | ||
|
|
||
| ### Building a reactive message list | ||
|
|
||
| ```ts | ||
| import { createSSE } from "@solid-primitives/sse"; | ||
| import { createStore } from "solid-js/store"; | ||
|
|
||
| const [messages, setMessages] = createStore<string[]>([]); | ||
|
|
||
| createSSE("https://api.example.com/events", { | ||
| onMessage: e => setMessages(msgs => [...msgs, e.data]), | ||
| }); | ||
|
|
||
| return <For each={messages}>{msg => <p>{msg}</p>}</For>; | ||
| ``` | ||
|
|
||
| ## Running SSE in a Worker | ||
|
|
||
| For high-frequency streams or performance-sensitive apps you can offload the `EventSource` connection to a Web Worker, keeping network I/O off the main thread. The reactive API (`data`, `readyState`, `reconnect`, …) is identical — only the transport moves. | ||
|
|
||
| See [WORKERS.md](./WORKERS.md) for setup instructions, SharedWorker usage, and the full type reference. | ||
|
|
||
| ## Changelog | ||
|
|
||
| See [CHANGELOG.md](./CHANGELOG.md). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| # Built-in transformers | ||
|
|
||
| Ready-made `transform` functions for the most common SSE data formats. Pass one as the `transform` option to `createSSE`: | ||
|
|
||
| ```ts | ||
| import { createSSE, json } from "@solid-primitives/sse"; | ||
|
|
||
| const { data } = createSSE<{ status: string }>(url, { transform: json }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## `json` | ||
|
|
||
| Parse the message data as a single JSON value. Equivalent to `JSON.parse` but named for consistency with the other transformers. | ||
|
|
||
| ```ts | ||
| import { createSSE, json } from "@solid-primitives/sse"; | ||
|
|
||
| const { data } = createSSE<{ status: string; ts: number }>(url, { transform: json }); | ||
| // data() === { status: "ok", ts: 1718000000 } | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## `ndjson` | ||
|
|
||
| Parse the message data as [newline-delimited JSON](https://ndjson.org/) (NDJSON / JSON Lines). Each non-empty line is parsed as a separate JSON value and the transformer returns an array. | ||
|
|
||
| Use this when the server batches multiple objects into one SSE event: | ||
|
|
||
| ``` | ||
| data: {"id":1,"type":"tick"} | ||
| data: {"id":2,"type":"tick"} | ||
|
|
||
| ``` | ||
|
|
||
| ```ts | ||
| import { createSSE, ndjson } from "@solid-primitives/sse"; | ||
|
|
||
| const { data } = createSSE<TickEvent[]>(url, { transform: ndjson }); | ||
| // data() === [{ id: 1, type: "tick" }, { id: 2, type: "tick" }] | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## `lines` | ||
|
|
||
| Split the message data into individual lines, returning a `string[]`. Empty lines are filtered out. Useful for multi-line text events that are not JSON. | ||
|
|
||
| ```ts | ||
| import { createSSE, lines } from "@solid-primitives/sse"; | ||
|
|
||
| const { data } = createSSE<string[]>(url, { transform: lines }); | ||
| // data() === ["line one", "line two"] | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## `number` | ||
|
|
||
| Parse the message data as a number using `Number()` semantics. Handy for streams that emit counters, progress percentages, sensor readings, or prices. | ||
|
|
||
| ```ts | ||
| import { createSSE, number } from "@solid-primitives/sse"; | ||
|
|
||
| const { data } = createSSE<number>(url, { transform: number }); | ||
| // data() === 42 | ||
| ``` | ||
|
|
||
| Note: follows `Number()` coercion — an empty string becomes `0` and non-numeric strings become `NaN`. | ||
|
|
||
| --- | ||
|
|
||
| ## `safe(transform, fallback?)` | ||
|
|
||
| Wraps any transform in a `try/catch`. When the inner transform throws, `safe` returns `fallback` instead of propagating the error. This keeps the stream alive across malformed events. | ||
|
|
||
| ```ts | ||
| import { createSSE, json, number, safe } from "@solid-primitives/sse"; | ||
|
|
||
| // Returns undefined on a bad event instead of throwing | ||
| const { data } = createSSE<MyEvent>(url, { transform: safe(json) }); | ||
|
|
||
| // With an explicit fallback value | ||
| const { data } = createSSE<number>(url, { transform: safe(number, 0) }); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## `pipe(a, b)` | ||
|
|
||
| Composes two transforms into one: the output of `a` is passed as the input of `b`. Useful for building custom transforms from existing primitives without writing anonymous functions. | ||
|
|
||
| ```ts | ||
| import { createSSE, ndjson, json, safe, pipe } from "@solid-primitives/sse"; | ||
|
|
||
| // Parse NDJSON then keep only "tick" rows | ||
| type RawEvent = { type: string }; | ||
| const { data } = createSSE<RawEvent[]>(url, { | ||
| transform: pipe(ndjson<RawEvent>, rows => rows.filter(r => r.type === "tick")), | ||
| }); | ||
|
|
||
| // Safe JSON with a post-processing step | ||
| const { data } = createSSE<string>(url, { | ||
| transform: pipe(safe(json<{ label: string }>), ev => ev?.label ?? ""), | ||
| }); | ||
| ``` |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.