diff --git a/.changeset/sse-solid2-async-reactivity.md b/.changeset/sse-solid2-async-reactivity.md new file mode 100644 index 000000000..9c23f83f2 --- /dev/null +++ b/.changeset/sse-solid2-async-reactivity.md @@ -0,0 +1,68 @@ +--- +"@solid-primitives/sse": minor +--- + +Align `createSSE` with Solid 2.0 async reactivity patterns + +### Breaking changes + +**`pending` removed from `SSEReturn`** + +Use `` for initial load UI and `isPending(() => data())` for stale-while-revalidating. Both are re-exported from this package. + +```tsx +// Before +const { data, pending } = createSSE(url); +

{data()}

+ +// After — declarative initial load +Connecting…

}> +

{data()}

+
+ +// After — stale-while-revalidating (only true once a value exists and new data is pending) + data())}>

Refreshing…

+``` + +**`error` removed from `SSEReturn`** + +Terminal errors (connection CLOSED with no retries left) now propagate through `data()` to ``. Non-terminal errors (browser reconnecting) are still surfaced via `onError` callback. + +```tsx +// Before +const { data, error } = createSSE(url); +

Error: {error()?.type}

+ +// After — single error path via Errored boundary +

Connection failed

}> + Connecting…

}> +

{data()}

+
+
+``` + +**`data` type narrowed from `Accessor` to `Accessor`** + +The `| undefined` loading hole is removed. When `data()` is not ready it throws `NotReadyError` (caught by ``) or the terminal error (caught by ``); it never returns `undefined` due to pending state. + +**SSR stub**: `data()` now throws `NotReadyError` on the server when no `initialValue` is provided (consistent with browser behaviour). Provide `initialValue` for a non-throwing SSR default. + +### New primitives + +**`makeSSEAsyncIterable(url, options?)`** + +Wraps an SSE endpoint as a standard `AsyncIterable`. Each message is one yielded value; terminal errors are thrown. Cleanup runs automatically when the iterator is abandoned. + +```ts +for await (const msg of makeSSEAsyncIterable(url)) { + console.log(msg); +} +``` + +**`createSSEStream(url, options?)`** + +Minimal reactive alternative to `createSSE` — returns only a `data: Accessor` backed by an async iterable. Same `` / `` integration, no `source` / `readyState` / `close` / `reconnect`. + +```ts +const data = createSSEStream<{ msg: string }>(url, { transform: JSON.parse }); +``` diff --git a/packages/sse/README.md b/packages/sse/README.md index 88fc99cc3..2342b8042 100644 --- a/packages/sse/README.md +++ b/packages/sse/README.md @@ -8,10 +8,12 @@ [![version](https://img.shields.io/npm/v/@solid-primitives/sse?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/sse) [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-2.json)](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. +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. Designed for Solid 2.0's async reactivity model. - [`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`. +- [`createSSE`](#createsse) — Reactive primitive. Accepts a reactive URL, integrates with Solid's owner lifecycle, and returns signals for `data` and `readyState`. +- [`makeSSEAsyncIterable`](#makesseasynciterable) — Wraps an SSE endpoint as an `AsyncIterable`. Non-reactive foundation. +- [`createSSEStream`](#createssesstream) — Minimal reactive stream: just a `data` accessor backed by an async iterable. - [`makeSSEWorker`](#running-sse-in-a-worker) — Runs the SSE connection inside a Web Worker or SharedWorker. - [Built-in transformers](#built-in-transformers) — `json`, `ndjson`, `lines`, `number`, `safe`, `pipe`. @@ -70,42 +72,87 @@ Reactive SSE primitive. Connects on creation, closes when the owner is disposed, ```ts import { createSSE, SSEReadyState } from "@solid-primitives/sse"; -const { data, readyState, error, close, reconnect } = createSSE<{ message: string }>( +const { data, readyState, close, reconnect } = createSSE<{ message: string }>( "https://api.example.com/events", { transform: JSON.parse, reconnect: { retries: 3, delay: 2000 }, }, ); +``` + +### Loading and error boundaries + +`data()` integrates with Solid 2.0's async reactivity: + +- **``** — shows fallback while `data()` is pending (before the first message arrives). +- **``** — catches terminal errors (connection CLOSED with no retries left) thrown through `data()`. + +```tsx +import { Loading, Errored } from "solid-js"; +import { createSSE } from "@solid-primitives/sse"; + +const { data, close, reconnect } = createSSE<{ message: string }>( + "https://api.example.com/events", + { transform: JSON.parse }, +); return ( -
- Connecting…

}> -

Latest: {data()?.message ?? "—"}

-
- -

Connection error

+

Connection failed

}> + Connecting…

}> +

Latest: {data().message}

+
+
+); +``` + +Non-terminal errors (while the browser is reconnecting automatically) are surfaced via the `onError` callback only — they don't interrupt the reactive graph. + +### Stale-while-revalidating with `isPending` + +After the first message has arrived, subsequent reconnects (URL change, `reconnect()` call) put the connection back into a pending state. Use `isPending` from Solid to show a subtle "refreshing" indicator without replacing the whole subtree: + +```tsx +import { isPending } from "solid-js"; +import { createSSE } from "@solid-primitives/sse"; + +const { data } = createSSE<{ msg: string }>(url, { transform: JSON.parse }); + +return ( + <> + data())}> +

Refreshing…

- - -
+ Connecting…

}> +

{data().msg}

+
+ ); ``` -### Reactive URL +> **Note:** `isPending` is `false` during the initial `` fallback (no stale value yet). It becomes `true` only when a stale value exists and new data is pending — i.e., after a URL change or reconnect. -When the URL is a signal accessor, the connection is replaced whenever the URL changes: +### Reactive URL with `` -```ts +When the URL is a signal accessor, the connection is replaced whenever the URL changes. Use ``'s `on` prop to re-show the fallback on each URL change: + +```tsx const [userId, setUserId] = createSignal("user-1"); const { data } = createSSE( () => `https://api.example.com/notifications/${userId()}`, { transform: JSON.parse }, ); + +return ( + // on={userId()} re-shows the fallback each time userId changes while pending + Connecting…

}> +

{data().message}

+
+); ``` -Changing `userId()` will close the existing connection and open a new one to the updated URL. +Without `on`, `` keeps showing stale content during revalidation. With `on`, it re-shows the fallback whenever the key changes and a new connection is establishing. ### Options @@ -114,7 +161,7 @@ Changing `userId()` will close the existing connection and open a new one to the | `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 | +| `onError` | `(e: Event) => void` | — | Called on error (terminal and transient) | | `events` | `Record 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` | @@ -129,14 +176,22 @@ Changing `userId()` will close the existing connection and open a new one to the ### Return value -| Property | Type | Description | -| ------------ | ---------------------------------------- | ------------------------------------------------ | -| `source` | `Accessor` | Underlying source instance; `undefined` on SSR | -| `data` | `Accessor` | Latest message data | -| `error` | `Accessor` | Latest error event | -| `readyState` | `Accessor` | `SSEReadyState.CONNECTING` / `.OPEN` / `.CLOSED` | -| `close` | `VoidFunction` | Close the connection | -| `reconnect` | `VoidFunction` | Force-close and reopen | +| Property | Type | Description | +| ------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------- | +| `source` | `Accessor` | Underlying source instance; `undefined` on SSR | +| `data` | `Accessor` | Latest message data; throws `NotReadyError` until first message, terminal errors thereafter | +| `readyState` | `Accessor` | `SSEReadyState.CONNECTING` / `.OPEN` / `.CLOSED` | +| `close` | `VoidFunction` | Close the connection | +| `reconnect` | `VoidFunction` | Force-close and reopen; resets `data` to pending | + +### Initial value + +Provide `initialValue` to skip the pending state entirely — `data()` returns it immediately with no `` fallback needed: + +```ts +const { data } = createSSE(url, { initialValue: [] as string[] }); +// data() === [] immediately, no Loading needed +``` ### `SSEReadyState` @@ -154,6 +209,80 @@ SSEReadyState.CLOSED; // 2 `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. +## `makeSSEAsyncIterable` + +Wraps an SSE endpoint as a standard `AsyncIterable`. Each SSE message becomes one yielded value; terminal errors (connection CLOSED) are thrown by the iterator. Cleanup runs automatically when the iterator is abandoned via `return()`. + +Use this as a non-reactive building block: integrate it with a `for await…of` loop, pass it to your own `createMemo`, or compose it with other async utilities. + +```ts +import { makeSSEAsyncIterable } from "@solid-primitives/sse"; + +const iterable = makeSSEAsyncIterable("https://api.example.com/events"); + +for await (const msg of iterable) { + console.log(msg); +} +``` + +### Definition + +```ts +function makeSSEAsyncIterable( + url: string | URL, + options?: CreateSSEStreamOptions, +): AsyncIterable; + +type CreateSSEStreamOptions = { + withCredentials?: boolean; + onOpen?: (event: Event) => void; + onError?: (event: Event) => void; + transform?: (raw: string) => T; + events?: Record void>; + source?: SSESourceFn; +}; +``` + +## `createSSEStream` + +A minimal reactive alternative to `createSSE` that returns only a `data` accessor. Internally it drives an `AsyncIterable` produced by `makeSSEAsyncIterable`, giving the same `` / `` integration with less API surface. + +Use this when you only need the stream values and don't need access to `source`, `readyState`, `close`, or `reconnect`. + +```ts +import { createSSEStream } from "@solid-primitives/sse"; + +const data = createSSEStream<{ msg: string }>(url, { transform: JSON.parse }); + +return ( +

Connection failed

}> + Connecting…

}> +

{data().msg}

+
+
+); +``` + +Reactive URL is supported — the stream reconnects automatically when the URL signal changes: + +```ts +const [userId, setUserId] = createSignal("user-1"); + +const data = createSSEStream( + () => `https://api.example.com/notifications/${userId()}`, + { transform: JSON.parse }, +); +``` + +### Definition + +```ts +function createSSEStream( + url: MaybeAccessor, + options?: CreateSSEStreamOptions, +): Accessor; +``` + ## Integration with `@solid-primitives/event-bus` Because `bus.emit` matches the `(event: MessageEvent) => void` shape of `onMessage`, you can wire them directly: @@ -214,7 +343,7 @@ return {msg =>

{msg}

}
; ## Built-in transformers -Ready-made `transform` functions for the most common SSE data formats. Pass one as the `transform` option to `createSSE`: +Ready-made `transform` functions for the most common SSE data formats. Pass one as the `transform` option to `createSSE` or `createSSEStream`: ```ts import { createSSE, json } from "@solid-primitives/sse"; @@ -357,7 +486,7 @@ const worker = new Worker(new URL("@solid-primitives/sse/worker-handler", import type: "module", }); -const { data, readyState, error, close, reconnect } = createSSE<{ msg: string }>( +const { data, readyState, close, reconnect } = createSSE<{ msg: string }>( "https://api.example.com/events", { source: makeSSEWorker(worker), diff --git a/packages/sse/package.json b/packages/sse/package.json index 0fd879084..97831f484 100644 --- a/packages/sse/package.json +++ b/packages/sse/package.json @@ -67,7 +67,7 @@ "scripts": { "dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts", "build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts", - "vitest": "vitest -c ../../configs/vitest.config.ts", + "vitest": "vitest -c vitest.config.ts", "test": "pnpm run vitest", "test:ssr": "pnpm run vitest --mode ssr" }, @@ -75,9 +75,9 @@ "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "solid-js": "^1.6.12" + "solid-js": "2.0.0-beta.7" }, "devDependencies": { - "solid-js": "^1.9.7" + "solid-js": "2.0.0-beta.7" } } diff --git a/packages/sse/src/index.ts b/packages/sse/src/index.ts index f6fb9ad0d..190e64d03 100644 --- a/packages/sse/src/index.ts +++ b/packages/sse/src/index.ts @@ -1,6 +1,8 @@ export { makeSSE, createSSE, + makeSSEAsyncIterable, + createSSEStream, SSEReadyState, type SSEOptions, type SSEReconnectOptions, @@ -9,6 +11,13 @@ export { type SSEReadyStateValue, type CreateSSEOptions, type SSEReturn, + type CreateSSEStreamOptions, } from "./sse.js"; export { json, ndjson, lines, number, safe, pipe } from "./transform.js"; + +// Re-export Solid 2.0 async primitives commonly used with SSE primitives: +// - isPending(() => data()) — true during stale-while-revalidating (not initial load) +// - onSettled(() => ...) — runs when the first message arrives +// - NotReadyError — thrown by data() while pending (caught by ) +export { isPending, onSettled, NotReadyError } from "solid-js"; diff --git a/packages/sse/src/sse.ts b/packages/sse/src/sse.ts index 12a626d63..3d6736ced 100644 --- a/packages/sse/src/sse.ts +++ b/packages/sse/src/sse.ts @@ -1,9 +1,8 @@ -import { type Accessor, createComputed, createSignal, onCleanup, untrack } from "solid-js"; +import { onCleanup, createSignal, createTrackedEffect, untrack, NotReadyError } from "solid-js"; +import type { Accessor } from "solid-js"; import { isServer } from "solid-js/web"; import { access, type MaybeAccessor } from "@solid-primitives/utils"; -// ─── ReadyState ─────────────────────────────────────────────────────────────── - /** * Named constants for the SSE connection state, mirroring the `EventSource` * static properties. Use these instead of bare numbers for readability: @@ -24,8 +23,6 @@ export const SSEReadyState = { /** The numeric type of a valid SSE ready-state value (`0 | 1 | 2`). */ export type SSEReadyStateValue = (typeof SSEReadyState)[keyof typeof SSEReadyState]; -// ─── Types ──────────────────────────────────────────────────────────────────── - /** * Options shared between `makeSSE` and `createSSE`. */ @@ -36,7 +33,13 @@ export type SSEOptions = { onOpen?: (event: Event) => void; /** Called on every unnamed `"message"` event */ onMessage?: (event: MessageEvent) => void; - /** Called on error */ + /** + * Called on error. For non-terminal errors (browser is reconnecting, + * `readyState` is still `CONNECTING`) this is purely informational. + * For terminal errors (`readyState` is `CLOSED` with no retries left), + * the error also propagates through the reactive graph so `` + * can catch it without any extra wiring. + */ onError?: (event: Event) => void; /** Handlers for custom named SSE event types, e.g. `{ update: handler }` */ events?: Record void>; @@ -69,7 +72,13 @@ export type SSESourceFn = ( ) => [source: SSESourceHandle, cleanup: VoidFunction]; export type CreateSSEOptions = SSEOptions & { - /** Initial value of the `data` signal before any message arrives */ + /** + * Initial value of the `data` signal before any message arrives. + * + * When provided, `data()` returns this value immediately (no pending state). + * When omitted, `data()` throws `NotReadyError` until the first message + * arrives, integrating with Solid's `` for a loading fallback. + */ initialValue?: T; /** * Transform raw string data from each message event. @@ -98,10 +107,22 @@ export type CreateSSEOptions = SSEOptions & { export type SSEReturn = { /** The underlying source instance. `undefined` on SSR or before first connect. */ source: Accessor; - /** The latest message data, parsed through `transform` if provided. */ - data: Accessor; - /** The latest error event, `undefined` when no error has occurred. */ - error: Accessor; + /** + * The latest message data, parsed through `transform` if provided. + * + * **Pending until the first message arrives** (unless `initialValue` is set). + * Reading this inside a component wrapped with `` will show the + * fallback while the connection is establishing. After the first message the + * signal updates reactively on every subsequent message. + * + * For stale-while-revalidating UI (after reconnect or URL change), use + * `isPending(() => data())` — it is `false` during initial load (handled by + * ``) and `true` only once a stale value exists and new data is pending. + * + * Terminal errors (connection CLOSED with no retries left) are thrown through + * `data()` so `` can catch them without any extra wiring. + */ + data: Accessor; /** * The current connection state. Use `SSEReadyState` for named comparisons: * - `SSEReadyState.CONNECTING` (0) @@ -111,11 +132,18 @@ export type SSEReturn = { readyState: Accessor; /** Close the connection. */ close: VoidFunction; - /** Force-close the current connection and open a new one. */ + /** + * Force-close the current connection and open a new one. + * Resets `data` to pending until the next message arrives. + */ reconnect: VoidFunction; }; -// ─── makeSSE ───────────────────────────────────────────────────────────────── +// Internal sentinel marking "no message received yet". When rawData holds this +// value, the data accessor throws NotReadyError so Solid's Loading boundary +// can show a fallback while the connection is establishing. +const NOT_SET: unique symbol = Symbol(); +type NotSet = typeof NOT_SET; /** * Creates a raw `EventSource` connection without Solid lifecycle management. @@ -162,23 +190,34 @@ export const makeSSE = ( return [source, cleanup]; }; -// ─── createSSE ─────────────────────────────────────────────────────────────── - /** * Creates a reactive SSE (Server-Sent Events) connection that integrates with - * the Solid reactive system and owner lifecycle. + * Solid's async reactivity system and owner lifecycle. * - * - Accepts a reactive URL — reconnects automatically when the URL signal changes - * - Closes the connection on owner disposal via `onCleanup` - * - SSR-safe: returns static stubs on the server + * - `data` is **pending** (throws `NotReadyError`) until the first message + * arrives, enabling `` to show a loading fallback. Provide + * `initialValue` to start with a settled value instead. + * - Terminal errors (CLOSED with no retries) are thrown through `data()` so + * `` can catch them. Non-terminal errors call `onError` only. + * - Accepts a reactive URL — reconnects automatically when the URL signal + * changes, resetting `data` to pending. + * - Closes the connection on owner disposal via `onCleanup`. + * - SSR-safe: returns static stubs on the server. * * ```ts - * const { data, readyState, error, close, reconnect } = createSSE<{ msg: string }>( + * const { data, readyState, close, reconnect } = createSSE<{ msg: string }>( * "https://api.example.com/events", * { transform: JSON.parse, reconnect: { retries: 3, delay: 2000 } }, * ); * - * return

{data()?.msg}

; + * // In JSX — Loading shows fallback while connecting, Errored catches terminal failures: + * return ( + *

Connection failed

}> + * Connecting…

}> + *

{data()?.msg}

+ *
+ *
+ * ); * ``` * * @param url Static URL string or reactive `Accessor` @@ -188,25 +227,51 @@ export const createSSE = ( url: MaybeAccessor, options: CreateSSEOptions = {}, ): SSEReturn => { - // ── SSR stub ────────────────────────────────────────────────────────────── if (isServer) { return { source: () => undefined, - data: () => options.initialValue, - error: () => undefined, + data: + options.initialValue !== undefined + ? () => options.initialValue! + : () => { + throw new NotReadyError("SSE awaiting first message"); + }, readyState: () => SSEReadyState.CLOSED, close: () => void 0, reconnect: () => void 0, }; } - // ── Reactive state ──────────────────────────────────────────────────────── - const [source, setSource] = createSignal(undefined); - const [data, setData] = createSignal(options.initialValue); - const [error, setError] = createSignal(undefined); - const [readyState, setReadyState] = createSignal(SSEReadyState.CONNECTING); + const [source, setSource] = createSignal(undefined, { + ownedWrite: true, + }); + + // rawData holds either the latest message value or the NOT_SET sentinel. + const [rawData, setRawData] = createSignal( + options.initialValue !== undefined ? options.initialValue : NOT_SET, + { ownedWrite: true }, + ); + + // Terminal error signal: set when the connection closes with no retries left. + // data() re-throws this so can catch it — single error path. + const [terminalError, setTerminalError] = createSignal(undefined, { + ownedWrite: true, + }); + + // Computed data signal: throws terminal error (→ Errored boundary) or + // NotReadyError (→ Loading boundary) when not ready. + const [data] = createSignal(() => { + const err = terminalError(); + if (err !== undefined) throw err; + const val = rawData(); + if (val === NOT_SET) throw new NotReadyError("SSE awaiting first message"); + return val; + }); + + const [readyState, setReadyState] = createSignal(SSEReadyState.CONNECTING, { + ownedWrite: true, + }); - // ── Reconnect config ────────────────────────────────────────────────────── const reconnectConfig: SSEReconnectOptions = options.reconnect === true ? { retries: Infinity, delay: 3000 } @@ -224,12 +289,12 @@ export const createSSE = ( } }; - // ── Connection management ───────────────────────────────────────────────── let currentCleanup: VoidFunction | undefined; - /** Open a fresh connection, resetting the retry counter. */ + /** Open a fresh connection, resetting the retry counter and terminal error. */ const connect = (resolvedUrl: string) => { retriesLeft = reconnectConfig.retries ?? 0; + setTerminalError(undefined); _open(resolvedUrl); }; @@ -239,27 +304,29 @@ export const createSSE = ( const handleOpen = (e: Event) => { setReadyState(SSEReadyState.OPEN); - setError(undefined); options.onOpen?.(e); }; const handleMessage = (e: MessageEvent) => { const value = options.transform ? options.transform(e.data as string) : (e.data as T); - setData(() => value); + setRawData(() => value); options.onMessage?.(e); }; const handleError = (e: Event) => { const es = e.target as SSESourceHandle; setReadyState(es.readyState as SSEReadyStateValue); - setError(() => e); options.onError?.(e); - // Only app-level reconnect when the browser has given up (CLOSED). - // When readyState is still CONNECTING the browser is handling retries. - if (es.readyState === SSEReadyState.CLOSED && retriesLeft > 0) { - retriesLeft--; - reconnectTimer = setTimeout(() => _open(resolvedUrl), reconnectConfig.delay ?? 3000); + if (es.readyState === SSEReadyState.CLOSED) { + if (retriesLeft > 0) { + // Browser gave up but we have retries: schedule app-level reconnect. + retriesLeft--; + reconnectTimer = setTimeout(() => _open(resolvedUrl), reconnectConfig.delay ?? 3000); + } else { + // Terminal: no more retries — propagate through Errored boundary. + setTerminalError(() => e); + } } }; @@ -277,7 +344,7 @@ export const createSSE = ( currentCleanup = cleanup; }; - const disconnect = () => { + const close = () => { clearReconnectTimer(); retriesLeft = 0; currentCleanup?.(); @@ -286,44 +353,229 @@ export const createSSE = ( setReadyState(SSEReadyState.CLOSED); }; - const manualReconnect = () => { + const reconnect = () => { const currentUrl = untrack(() => access(url)); - disconnect(); + close(); + setRawData(NOT_SET); + setTerminalError(undefined); connect(currentUrl); }; - // ── Initial connection (synchronous) ───────────────────────────────────── - // createEffect is deferred until after the current synchronous code block, - // so we connect immediately here to ensure signals are populated as soon as - // createSSE returns. connect(untrack(() => access(url))); - // ── Reactive URL handling ───────────────────────────────────────────────── - // Only needed when url is an accessor. `createComputed` runs synchronously - // on creation (unlike `createEffect`, which is deferred), so the reactive - // subscription to `url` is established immediately. The `prevUrl` guard - // prevents a redundant reconnect on the first pass (we already connected). + // createTrackedEffect runs synchronously so the reactive subscription + // to `url` is established immediately. The prevUrl guard prevents a + // redundant reconnect on the first pass. if (typeof url === "function") { let prevUrl = untrack(url); - createComputed(() => { + createTrackedEffect(() => { const resolvedUrl = url(); if (resolvedUrl !== prevUrl) { prevUrl = resolvedUrl; untrack(() => { currentCleanup?.(); currentCleanup = undefined; + setRawData(NOT_SET); + setTerminalError(undefined); + connect(resolvedUrl); }); - connect(resolvedUrl); } }); } - // ── Lifecycle cleanup ───────────────────────────────────────────────────── onCleanup(() => { clearReconnectTimer(); currentCleanup?.(); currentCleanup = undefined; }); - return { source, data, error, readyState, close: disconnect, reconnect: manualReconnect }; + return { source, data, readyState, close, reconnect }; +}; + +/** Options for `makeSSEAsyncIterable` and `createSSEStream`. */ +export type CreateSSEStreamOptions = SSEOptions & { + /** Transform raw string data from each message event. */ + transform?: (raw: string) => T; + /** Custom source factory (defaults to `makeSSE`). */ + source?: SSESourceFn; +}; + +/** + * Wraps an SSE endpoint as an `AsyncIterable`. Each SSE message becomes + * one yielded value. Terminal errors (connection CLOSED) are thrown by the + * iterator. Cleanup (closing the `EventSource`) runs automatically when the + * iterator is abandoned via `return()`. + * + * This is the non-reactive foundation primitive. Use `createSSEStream` if you + * want Solid reactivity, or pass this directly to a `createMemo` that accepts + * async iterables. + * + * ```ts + * const iterable = makeSSEAsyncIterable("https://api.example.com/events"); + * for await (const msg of iterable) { + * console.log(msg); + * } + * ``` + * + * @param url The SSE endpoint URL + * @param options Event handlers and transform + */ +export const makeSSEAsyncIterable = ( + url: string | URL, + options: CreateSSEStreamOptions = {}, +): AsyncIterable => ({ + [Symbol.asyncIterator](): AsyncIterator { + const queue: T[] = []; + let notify: (() => void) | undefined; + let done = false; + let terminalErr: Event | undefined; + + const sourceFn: SSESourceFn = options.source ?? makeSSE; + const [, cleanup] = sourceFn(String(url), { + withCredentials: options.withCredentials, + onOpen: options.onOpen, + onError: (e: Event) => { + const es = e.target as SSESourceHandle; + if (es.readyState === SSEReadyState.CLOSED) { + terminalErr = e; + done = true; + notify?.(); + notify = undefined; + } + options.onError?.(e); + }, + onMessage: (e: MessageEvent) => { + const value = options.transform ? options.transform(e.data as string) : (e.data as T); + queue.push(value); + notify?.(); + notify = undefined; + }, + events: options.events, + }); + + return { + async next(): Promise> { + while (!done && queue.length === 0) { + await new Promise(r => { + notify = r; + }); + } + if (queue.length > 0) return { value: queue.shift()!, done: false }; + if (terminalErr) throw terminalErr; + return { value: undefined as unknown as T, done: true }; + }, + return(): Promise> { + done = true; + notify?.(); + notify = undefined; + cleanup(); + return Promise.resolve({ value: undefined as unknown as T, done: true }); + }, + throw(err?: unknown): Promise> { + done = true; + cleanup(); + return Promise.reject(err); + }, + }; + }, +}); + +/** + * Creates a reactive SSE stream using Solid's async computation model. + * Returns a single `Accessor` backed by an `AsyncIterable` of SSE data values. + * + * Compared to `createSSE`, this is a minimal API: no `source`, `readyState`, + * `close`, or `reconnect` — just the data stream. Use it when you only need + * the values and want the simplest possible integration with ``. + * + * - Suspends (``) until the first message arrives. + * - Reactively reconnects when `url` changes (closes old iterator, starts new one). + * - Terminal errors propagate through the accessor to ``. + * - Owner disposal closes the underlying `EventSource` via `onCleanup`. + * + * ```ts + * const data = createSSEStream<{ msg: string }>(url, { transform: JSON.parse }); + * + * return ( + *

Connection failed

}> + * Connecting…

}> + *

{data().msg}

+ *
+ *
+ * ); + * ``` + * + * @param url Static URL string or reactive `Accessor` + * @param options Transform and event handler options + */ +export const createSSEStream = ( + url: MaybeAccessor, + options: CreateSSEStreamOptions = {}, +): Accessor => { + if (isServer) { + return () => { + throw new NotReadyError("SSE not available on server"); + }; + } + + const [rawData, setRawData] = createSignal(NOT_SET, { ownedWrite: true }); + const [terminalError, setTerminalError] = createSignal(undefined, { + ownedWrite: true, + }); + + const [data] = createSignal(() => { + const err = terminalError(); + if (err !== undefined) throw err; + const val = rawData(); + if (val === NOT_SET) throw new NotReadyError("SSE stream awaiting first message"); + return val; + }); + + let currentReturn: (() => void) | undefined; + + const startStream = (resolvedUrl: string) => { + const iter = makeSSEAsyncIterable(resolvedUrl, options)[Symbol.asyncIterator](); + currentReturn = () => { + iter.return?.(); + }; + + const consume = async () => { + try { + let result = await iter.next(); + while (!result.done) { + setRawData(() => result.value); + result = await iter.next(); + } + } catch (e) { + setTerminalError(() => e as Event); + } + }; + void consume(); + }; + + startStream(untrack(() => access(url))); + + if (typeof url === "function") { + let prevUrl = untrack(url); + createTrackedEffect(() => { + const resolvedUrl = (url as Accessor)(); + if (resolvedUrl !== prevUrl) { + prevUrl = resolvedUrl; + untrack(() => { + currentReturn?.(); + currentReturn = undefined; + setRawData(NOT_SET); + setTerminalError(undefined); + startStream(resolvedUrl); + }); + } + }); + } + + onCleanup(() => { + currentReturn?.(); + currentReturn = undefined; + }); + + return data; }; diff --git a/packages/sse/test/index.test.ts b/packages/sse/test/index.test.ts index 7cd09d3bb..fed6d1647 100644 --- a/packages/sse/test/index.test.ts +++ b/packages/sse/test/index.test.ts @@ -1,7 +1,7 @@ import "./setup"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createRoot, createSignal } from "solid-js"; -import { makeSSE, createSSE, SSEReadyState } from "../src/index.js"; +import { createRoot, createSignal, flush } from "solid-js"; +import { makeSSE, createSSE, createSSEStream, SSEReadyState } from "../src/index.js"; import { MockEventSource } from "./setup.js"; beforeAll(() => vi.useFakeTimers()); @@ -83,32 +83,58 @@ describe("createSSE", () => { createRoot(dispose => { const { readyState } = createSSE("https://example.com/events"); vi.advanceTimersByTime(20); + flush(); expect(readyState()).toBe(SSEReadyState.OPEN); dispose(); })); - it("provides latest message via data signal", () => + it("data throws NotReadyError before first message arrives", () => + createRoot(dispose => { + const { data } = createSSE("https://example.com/events"); + expect(() => data()).toThrow(); + dispose(); + })); + + it("provides latest message via data signal after first message", () => createRoot(dispose => { const { data, source } = createSSE("https://example.com/events"); - expect(data()).toBeUndefined(); vi.advanceTimersByTime(20); + flush(); (source() as unknown as MockEventSource).simulateMessage("hello"); + flush(); expect(data()).toBe("hello"); dispose(); })); + it("updates data on subsequent messages", () => + createRoot(dispose => { + const { data, source } = createSSE("https://example.com/events"); + vi.advanceTimersByTime(20); + flush(); + const mock = source() as unknown as MockEventSource; + mock.simulateMessage("first"); + flush(); + expect(data()).toBe("first"); + mock.simulateMessage("second"); + flush(); + expect(data()).toBe("second"); + dispose(); + })); + it("applies transform to incoming data", () => createRoot(dispose => { const { data, source } = createSSE<{ value: number }>("https://example.com/events", { transform: JSON.parse, }); vi.advanceTimersByTime(20); + flush(); (source() as unknown as MockEventSource).simulateMessage(JSON.stringify({ value: 42 })); + flush(); expect(data()).toEqual({ value: 42 }); dispose(); })); - it("returns initialValue before any message arrives", () => + it("returns initialValue before any message arrives (no pending state)", () => createRoot(dispose => { const { data } = createSSE("https://example.com/events", { initialValue: "loading", @@ -117,30 +143,33 @@ describe("createSSE", () => { dispose(); })); - it("clears error signal on successful open", () => + it("calls onError for non-terminal errors (browser reconnecting)", () => createRoot(dispose => { - const { error, source } = createSSE("https://example.com/events", { + const errors: Event[] = []; + const { source } = createSSE("https://example.com/events", { reconnect: { retries: 1, delay: 50 }, + onError: e => errors.push(e), }); vi.advanceTimersByTime(20); + flush(); (source() as unknown as MockEventSource).simulateError(); - expect(error()).toBeTruthy(); - // reconnect fires after delay; new source opens - vi.advanceTimersByTime(100); - vi.advanceTimersByTime(20); // new source opens - expect(error()).toBeUndefined(); + flush(); + expect(errors.length).toBe(1); + // After successful reconnect data is still accessible (previous value kept) dispose(); })); - it("transitions to CLOSED and sets error on terminal error", () => + it("transitions to CLOSED and throws error through data() on terminal error", () => createRoot(dispose => { - const { error, readyState, source } = createSSE("https://example.com/events", { + const { data, readyState, source } = createSSE("https://example.com/events", { reconnect: false, }); vi.advanceTimersByTime(20); + flush(); (source() as unknown as MockEventSource).simulateError(); + flush(); expect(readyState()).toBe(SSEReadyState.CLOSED); - expect(error()).toBeTruthy(); + expect(() => data()).toThrow(); // propagates to boundary dispose(); })); @@ -151,8 +180,11 @@ describe("createSSE", () => { reconnect: { retries: 5, delay: 50 }, }); vi.advanceTimersByTime(20); + flush(); (source() as unknown as MockEventSource).simulateTransientError(); + flush(); vi.advanceTimersByTime(300); + flush(); // readyState stayed CONNECTING → no new EventSource was created expect(SSEInstances.length).toBe(initialCount + 1); dispose(); @@ -164,10 +196,13 @@ describe("createSSE", () => { reconnect: { retries: 1, delay: 100 }, }); vi.advanceTimersByTime(20); + flush(); const first = source(); (first as unknown as MockEventSource).simulateError(); + flush(); expect(source()).toBe(first); // no change yet vi.advanceTimersByTime(150); + flush(); expect(source()).not.toBe(first); // new connection opened dispose(); })); @@ -178,14 +213,20 @@ describe("createSSE", () => { reconnect: { retries: 1, delay: 50 }, }); vi.advanceTimersByTime(20); + flush(); const first = source(); (first as unknown as MockEventSource).simulateError(); + flush(); vi.advanceTimersByTime(100); // first retry + flush(); const second = source(); expect(second).not.toBe(first); vi.advanceTimersByTime(20); // second opens + flush(); (second as unknown as MockEventSource).simulateError(); + flush(); vi.advanceTimersByTime(200); // no more retries + flush(); expect(source()).toBe(second); // still the same source dispose(); })); @@ -194,32 +235,79 @@ describe("createSSE", () => { createRoot(dispose => { const { readyState, close } = createSSE("https://example.com/events"); vi.advanceTimersByTime(20); + flush(); expect(readyState()).toBe(SSEReadyState.OPEN); close(); + flush(); expect(readyState()).toBe(SSEReadyState.CLOSED); dispose(); })); - it("reconnect() opens a fresh connection", () => + it("reconnect() opens a fresh connection and resets data to pending", () => createRoot(dispose => { - const { source, reconnect } = createSSE("https://example.com/events"); + const { data, source, reconnect } = createSSE("https://example.com/events"); vi.advanceTimersByTime(20); + flush(); const first = source(); + (first as unknown as MockEventSource).simulateMessage("hello"); + flush(); + expect(data()).toBe("hello"); reconnect(); + flush(); + // Old source closed, new source opened expect(source()).not.toBe(first); - expect(first?.readyState).toBe(SSEReadyState.CLOSED); // old one closed + expect(first?.readyState).toBe(SSEReadyState.CLOSED); + // New connection receives a message — data resets properly + vi.advanceTimersByTime(20); + flush(); + (source() as unknown as MockEventSource).simulateMessage("hello-v2"); + flush(); + expect(data()).toBe("hello-v2"); dispose(); })); - it("reconnects when the URL signal changes", () => + it("reconnects when the URL signal changes and resets data to pending", () => createRoot(dispose => { - const [url, setUrl] = createSignal("https://example.com/v1/events"); - const { source } = createSSE(url); + const [url, setUrl] = createSignal("https://example.com/v1/events", { ownedWrite: true }); + const { data, source } = createSSE(url); vi.advanceTimersByTime(20); + flush(); const first = source(); + (first as unknown as MockEventSource).simulateMessage("v1 data"); + flush(); + expect(data()).toBe("v1 data"); setUrl("https://example.com/v2/events"); + flush(); + // Old source closed, new source opened for v2 expect(source()).not.toBe(first); expect(first?.readyState).toBe(SSEReadyState.CLOSED); + // New connection updates data on message + vi.advanceTimersByTime(20); + flush(); + (source() as unknown as MockEventSource).simulateMessage("v2 data"); + flush(); + expect(data()).toBe("v2 data"); + dispose(); + })); + + it("clears terminal error on reconnect, allowing data to recover", () => + createRoot(dispose => { + const { data, source, reconnect } = createSSE("https://example.com/events", { + reconnect: false, + }); + vi.advanceTimersByTime(20); + flush(); + (source() as unknown as MockEventSource).simulateError(); + flush(); + expect(() => data()).toThrow(); // terminal error on first call (no stale cache) + reconnect(); + flush(); + vi.advanceTimersByTime(20); + flush(); + // Terminal error cleared — new message updates data successfully + (source() as unknown as MockEventSource).simulateMessage("recovered"); + flush(); + expect(data()).toBe("recovered"); dispose(); })); @@ -228,9 +316,76 @@ describe("createSSE", () => { createRoot(dispose => { const { source } = createSSE("https://example.com/events"); vi.advanceTimersByTime(20); + flush(); const es = source(); vi.spyOn(es as unknown as MockEventSource, "close").mockImplementation(() => resolve()); dispose(); }), )); }); + +// ── createSSEStream ─────────────────────────────────────────────────────────── + +describe("createSSEStream", () => { + it("data throws NotReadyError before first message arrives", () => + createRoot(dispose => { + const data = createSSEStream("https://example.com/events"); + expect(() => data()).toThrow(); + dispose(); + })); + + it("provides latest message after first message resolves", async () => { + await new Promise(resolve => + createRoot(async dispose => { + const data = createSSEStream("https://example.com/events"); + vi.advanceTimersByTime(20); + // Locate the mock source via SSEInstances + const mock = SSEInstances[SSEInstances.length - 1]!; + mock.simulateMessage("stream-hello"); + // Let the async iterator microtask resolve + await Promise.resolve(); + await Promise.resolve(); + flush(); + expect(data()).toBe("stream-hello"); + dispose(); + resolve(); + }), + ); + }); + + it("applies transform to incoming data", async () => { + await new Promise(resolve => + createRoot(async dispose => { + const data = createSSEStream<{ v: number }>("https://example.com/events", { + transform: JSON.parse, + }); + vi.advanceTimersByTime(20); + const mock = SSEInstances[SSEInstances.length - 1]!; + mock.simulateMessage(JSON.stringify({ v: 7 })); + await Promise.resolve(); + await Promise.resolve(); + flush(); + expect(data()).toEqual({ v: 7 }); + dispose(); + resolve(); + }), + ); + }); + + it("propagates terminal error through data()", async () => { + await new Promise(resolve => + createRoot(async dispose => { + const data = createSSEStream("https://example.com/events"); + vi.advanceTimersByTime(20); + const mock = SSEInstances[SSEInstances.length - 1]!; + mock.simulateError(); // CLOSED → terminal + await Promise.resolve(); + await Promise.resolve(); + flush(); + expect(() => data()).toThrow(); + dispose(); + resolve(); + }), + ); + }); +}); diff --git a/packages/sse/test/server.test.ts b/packages/sse/test/server.test.ts index ab7ea714f..d6a457234 100644 --- a/packages/sse/test/server.test.ts +++ b/packages/sse/test/server.test.ts @@ -1,14 +1,13 @@ import { describe, expect, it } from "vitest"; import { createRoot } from "solid-js"; -import { createSSE } from "../src/index.js"; +import { createSSE, createSSEStream } from "../src/index.js"; describe("SSR", () => { it("returns safe stubs without touching EventSource", () => createRoot(dispose => { const sse = createSSE("https://example.com/events"); expect(sse.source()).toBeUndefined(); - expect(sse.data()).toBeUndefined(); - expect(sse.error()).toBeUndefined(); + expect(() => sse.data()).toThrow(); // throws NotReadyError — no initialValue expect(sse.readyState()).toBe(2); expect(() => sse.close()).not.toThrow(); expect(() => sse.reconnect()).not.toThrow(); @@ -23,4 +22,11 @@ describe("SSR", () => { expect(data()).toBe("loading"); dispose(); })); + + it("createSSEStream throws NotReadyError on server", () => + createRoot(dispose => { + const data = createSSEStream("https://example.com/events"); + expect(() => data()).toThrow(); + dispose(); + })); }); diff --git a/packages/sse/test/worker.test.ts b/packages/sse/test/worker.test.ts index ee635674a..74c7f2851 100644 --- a/packages/sse/test/worker.test.ts +++ b/packages/sse/test/worker.test.ts @@ -1,6 +1,6 @@ import "./setup"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createRoot } from "solid-js"; +import { createRoot, flush } from "solid-js"; import { createSSE, SSEReadyState } from "../src/sse.js"; import { makeSSEWorker, type SSEWorkerMessage, type SSEWorkerTarget } from "../src/worker.js"; @@ -227,6 +227,7 @@ describe("createSSE with worker source", () => { }); const id = (target.sent[0] as Extract).id; target.respond({ type: "open", id }); + flush(); expect(readyState()).toBe(SSEReadyState.OPEN); dispose(); })); @@ -240,6 +241,7 @@ describe("createSSE with worker source", () => { const id = (target.sent[0] as Extract).id; target.respond({ type: "open", id }); target.respond({ type: "message", id, data: "world", eventType: "message" }); + flush(); expect(data()).toBe("world"); dispose(); })); @@ -254,6 +256,7 @@ describe("createSSE with worker source", () => { const id = (target.sent[0] as Extract).id; target.respond({ type: "open", id }); target.respond({ type: "message", id, data: JSON.stringify({ n: 7 }), eventType: "message" }); + flush(); expect(data()).toEqual({ n: 7 }); dispose(); })); @@ -288,12 +291,15 @@ describe("createSSE with worker source", () => { }); const id1 = (target.sent[0] as Extract).id; target.respond({ type: "open", id: id1 }); + flush(); target.respond({ type: "error", id: id1, readyState: SSEReadyState.CLOSED }); + flush(); // Before the reconnect timer fires, only 1 connect expect(target.sent.filter(m => m.type === "connect")).toHaveLength(1); vi.advanceTimersByTime(150); + flush(); // After the delay, a new connect should have been sent expect(target.sent.filter(m => m.type === "connect")).toHaveLength(2); @@ -308,8 +314,10 @@ describe("createSSE with worker source", () => { }); const id = (target.sent[0] as Extract).id; target.respond({ type: "open", id }); + flush(); expect(readyState()).toBe(SSEReadyState.OPEN); close(); + flush(); expect(readyState()).toBe(SSEReadyState.CLOSED); expect(target.sent.some(m => m.type === "disconnect")).toBe(true); dispose(); diff --git a/packages/sse/vitest.config.ts b/packages/sse/vitest.config.ts new file mode 100644 index 000000000..927689e4f --- /dev/null +++ b/packages/sse/vitest.config.ts @@ -0,0 +1,46 @@ +import { defineConfig } from "vitest/config"; +import solidPlugin from "vite-plugin-solid"; + +export default defineConfig(({ mode }) => { + const testSSR = mode === "test:ssr" || mode === "ssr"; + return { + plugins: [ + solidPlugin({ + hot: false, + solid: { generate: testSSR ? "ssr" : "dom", omitNestedClosingTags: false }, + }), + ], + resolve: { + conditions: testSSR + ? ["@solid-primitives/source", "node"] + : ["@solid-primitives/source", "browser", "development"], + alias: { + "solid-js/web": new URL( + testSSR + ? "../../node_modules/.pnpm/@solidjs+web@2.0.0-beta.7_@solidjs+signals@2.0.0-beta.7_solid-js@2.0.0-beta.7/node_modules/@solidjs/web/dist/server.js" + : "../../node_modules/.pnpm/@solidjs+web@2.0.0-beta.7_@solidjs+signals@2.0.0-beta.7_solid-js@2.0.0-beta.7/node_modules/@solidjs/web/dist/web.js", + import.meta.url, + ).pathname, + "@solidjs/web": new URL( + testSSR + ? "../../node_modules/.pnpm/@solidjs+web@2.0.0-beta.7_@solidjs+signals@2.0.0-beta.7_solid-js@2.0.0-beta.7/node_modules/@solidjs/web/dist/server.js" + : "../../node_modules/.pnpm/@solidjs+web@2.0.0-beta.7_@solidjs+signals@2.0.0-beta.7_solid-js@2.0.0-beta.7/node_modules/@solidjs/web/dist/web.js", + import.meta.url, + ).pathname, + }, + }, + test: { + watch: false, + isolate: false, + passWithNoTests: true, + environment: testSSR ? "node" : "jsdom", + transformMode: { web: [/\.[jt]sx$/] }, + ...(testSSR + ? { include: ["test/server.test.{ts,tsx}"] } + : { + include: ["test/*.test.{ts,tsx}"], + exclude: ["test/server.test.{ts,tsx}"], + }), + }, + }; +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ecadfdb95..592544f84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -869,8 +869,8 @@ importers: version: link:../utils devDependencies: solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.7 + version: 2.0.0-beta.7 packages/state-machine: devDependencies: @@ -1048,10 +1048,10 @@ importers: version: link:../packages/utils '@solidjs/meta': specifier: ^0.29.3 - version: 0.29.4(solid-js@1.9.7) + version: 0.29.4(solid-js@2.0.0-experimental.16) '@solidjs/router': specifier: ^0.13.1 - version: 0.13.6(solid-js@1.9.7) + version: 0.13.6(solid-js@2.0.0-experimental.16) clsx: specifier: ^2.0.0 version: 2.1.1 @@ -1078,13 +1078,13 @@ importers: version: 1.77.8 solid-dismiss: specifier: ^1.7.121 - version: 1.8.2(solid-js@1.9.7) + version: 1.8.2(solid-js@2.0.0-experimental.16) solid-icons: specifier: ^1.1.0 - version: 1.1.0(solid-js@1.9.7) + version: 1.1.0(solid-js@2.0.0-experimental.16) solid-tippy: specifier: ^0.2.1 - version: 0.2.1(solid-js@1.9.7)(tippy.js@6.3.7) + version: 0.2.1(solid-js@2.0.0-experimental.16)(tippy.js@6.3.7) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -2042,6 +2042,7 @@ packages: '@graphql-tools/prisma-loader@8.0.4': resolution: {integrity: sha512-hqKPlw8bOu/GRqtYr0+dINAI13HinTVYBDqhwGAPIFmLr5s+qKskzgCiwbsckdrb5LWVFmVZc+UXn80OGiyBzg==} engines: {node: '>=16.0.0'} + deprecated: 'This package was intended to be used with an older versions of Prisma.\nThe newer versions of Prisma has a different approach to GraphQL integration.\nTherefore, this package is no longer needed and has been deprecated and removed.\nLearn more: https://www.prisma.io/graphql' peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -2587,6 +2588,12 @@ packages: peerDependencies: solid-js: ^1.5.3 + '@solidjs/signals@0.11.3': + resolution: {integrity: sha512-udMfutYPOlcxKUmc5+n1QtarsxOiAlC6LJY2TqFyaMwdXgo+reiYUcYGDlOiAPXfCLE0lavZHQ/6GT5pJbXKBA==} + + '@solidjs/signals@2.0.0-beta.7': + resolution: {integrity: sha512-SgK6oQlQZofz82LiEJ2RzT3sbs1lWTqFEtLoWjLsUo/dk1v9EoIFpJJlmvgkXvNugASWG+l1yOHa1a8lPamxug==} + '@solidjs/start@1.1.4': resolution: {integrity: sha512-ma1TBYqoTju87tkqrHExMReM5Z/+DTXSmi30CCTavtwuR73Bsn4rVGqm528p4sL2koRMfAuBMkrhuttjzhL68g==} peerDependencies: @@ -3513,6 +3520,7 @@ packages: dax-sh@0.43.2: resolution: {integrity: sha512-uULa1sSIHgXKGCqJ/pA0zsnzbHlVnuq7g8O2fkHokWFNwEGIhh5lAJlxZa1POG5En5ba7AU4KcBAvGQWMMf8rg==} + deprecated: This package has moved to simply be 'dax' instead of 'dax-sh' db0@0.3.2: resolution: {integrity: sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw==} @@ -4164,11 +4172,12 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} @@ -5892,10 +5901,20 @@ packages: peerDependencies: seroval: ^1.0 + seroval-plugins@1.5.2: + resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + seroval@1.3.2: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} + seroval@1.5.2: + resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} + engines: {node: '>=10'} + serve-placeholder@2.0.2: resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} @@ -6001,6 +6020,12 @@ packages: solid-js@1.9.7: resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} + solid-js@2.0.0-beta.7: + resolution: {integrity: sha512-7JHs+BhLeZXoU+u9dG+eKnyxxfZyGpOuJEBbN/1XbHKO/WhxecdplOAurlg/YDllNWPhsbXqmLR1H2paqSu62g==} + + solid-js@2.0.0-experimental.16: + resolution: {integrity: sha512-zZ1dU7cR0EnvLnrYiRLQbCFiDw5blLdlqmofgLzKUYE1TCMWDcisBlSwz0Ez8l4yXB4adbdhtaYCuynH4xSq9A==} + solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} peerDependencies: @@ -6195,6 +6220,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -6710,6 +6736,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -8576,18 +8603,22 @@ snapshots: dependencies: solid-js: 1.9.7 - '@solidjs/meta@0.29.4(solid-js@1.9.7)': + '@solidjs/meta@0.29.4(solid-js@2.0.0-experimental.16)': dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 - '@solidjs/router@0.13.6(solid-js@1.9.7)': + '@solidjs/router@0.13.6(solid-js@2.0.0-experimental.16)': dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 '@solidjs/router@0.8.4(solid-js@1.9.7)': dependencies: solid-js: 1.9.7 + '@solidjs/signals@0.11.3': {} + + '@solidjs/signals@2.0.0-beta.7': {} + '@solidjs/start@1.1.4(solid-js@1.9.7)(vinxi@0.5.7(@types/node@22.15.31)(db0@0.3.2)(ioredis@5.6.1)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0))': dependencies: '@tanstack/server-functions-plugin': 1.121.0(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(sass@1.77.8)(terser@5.42.0)(tsx@4.20.2)(yaml@2.5.0)) @@ -12441,8 +12472,14 @@ snapshots: dependencies: seroval: 1.3.2 + seroval-plugins@1.5.2(seroval@1.5.2): + dependencies: + seroval: 1.5.2 + seroval@1.3.2: {} + seroval@1.5.2: {} + serve-placeholder@2.0.2: dependencies: defu: 6.1.4 @@ -12557,13 +12594,13 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - solid-dismiss@1.8.2(solid-js@1.9.7): + solid-dismiss@1.8.2(solid-js@2.0.0-experimental.16): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 - solid-icons@1.1.0(solid-js@1.9.7): + solid-icons@1.1.0(solid-js@2.0.0-experimental.16): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 solid-js@1.9.7: dependencies: @@ -12571,6 +12608,20 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.2(seroval@1.3.2) + solid-js@2.0.0-beta.7: + dependencies: + '@solidjs/signals': 2.0.0-beta.7 + csstype: 3.1.3 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + + solid-js@2.0.0-experimental.16: + dependencies: + '@solidjs/signals': 0.11.3 + csstype: 3.1.3 + seroval: 1.5.2 + seroval-plugins: 1.5.2(seroval@1.5.2) + solid-refresh@0.6.3(solid-js@1.9.7): dependencies: '@babel/generator': 7.27.5 @@ -12580,9 +12631,9 @@ snapshots: transitivePeerDependencies: - supports-color - solid-tippy@0.2.1(solid-js@1.9.7)(tippy.js@6.3.7): + solid-tippy@0.2.1(solid-js@2.0.0-experimental.16)(tippy.js@6.3.7): dependencies: - solid-js: 1.9.7 + solid-js: 2.0.0-experimental.16 tippy.js: 6.3.7 solid-transition-group@0.2.3(solid-js@1.9.7):