From 7e29a2d2d1cc7c2139fd7e2a60e40de2e291276a Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sat, 18 Apr 2026 22:17:43 -0400 Subject: [PATCH 1/3] Minor cleanup and review --- packages/sse/package.json | 6 +- packages/sse/src/index.ts | 6 ++ packages/sse/src/sse.ts | 131 ++++++++++++++++++++++--------- packages/sse/test/index.test.ts | 82 ++++++++++++++++--- packages/sse/test/server.test.ts | 6 +- packages/sse/test/worker.test.ts | 10 ++- packages/sse/vitest.config.ts | 46 +++++++++++ pnpm-lock.yaml | 65 +++++++++++---- 8 files changed, 283 insertions(+), 69 deletions(-) create mode 100644 packages/sse/vitest.config.ts diff --git a/packages/sse/package.json b/packages/sse/package.json index 0fd879084..73813ee63 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-experimental.16" }, "devDependencies": { - "solid-js": "^1.9.7" + "solid-js": "2.0.0-experimental.16" } } diff --git a/packages/sse/src/index.ts b/packages/sse/src/index.ts index f6fb9ad0d..38cf1d252 100644 --- a/packages/sse/src/index.ts +++ b/packages/sse/src/index.ts @@ -12,3 +12,9 @@ export { } from "./sse.js"; export { json, ndjson, lines, number, safe, pipe } from "./transform.js"; + +// Re-export Solid 2.0 async primitives commonly used with createSSE: +// - isPending(data) — true while awaiting the first SSE message +// - 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..f5dadc3ac 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`. */ @@ -69,7 +66,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,7 +101,17 @@ 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. */ + /** + * 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. + * + * Use `pending()` to check the pending state imperatively, and + * `onSettled(() => ...)` to react when the first value arrives. + */ data: Accessor; /** The latest error event, `undefined` when no error has occurred. */ error: Accessor; @@ -109,13 +122,27 @@ export type SSEReturn = { * - `SSEReadyState.CLOSED` (2) */ readyState: Accessor; - /** Close the connection. */ + /** + * `true` until the first message arrives (or after `reconnect()` / URL + * change until the next message). Use this for imperative pending checks; + * use `` for declarative loading UI (it catches the `NotReadyError` + * that `data()` throws while pending). + */ + pending: Accessor; + /** Close the connection. Resets `data` to pending on the next `reconnect()`. */ 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 Suspense 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,15 +189,17 @@ 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 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. + * - 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 }>( @@ -178,7 +207,12 @@ export const makeSSE = ( * { transform: JSON.parse, reconnect: { retries: 3, delay: 2000 } }, * ); * - * return

{data()?.msg}

; + * // In JSX — Suspense shows fallback while connecting: + * return ( + * Connecting…

}> + *

{data()?.msg}

+ *
+ * ); * ``` * * @param url Static URL string or reactive `Accessor` @@ -195,6 +229,7 @@ export const createSSE = ( data: () => options.initialValue, error: () => undefined, readyState: () => SSEReadyState.CLOSED, + pending: () => options.initialValue === undefined, close: () => void 0, reconnect: () => void 0, }; @@ -202,11 +237,35 @@ export const createSSE = ( // ── Reactive state ──────────────────────────────────────────────────────── const [source, setSource] = createSignal(undefined); - const [data, setData] = createSignal(options.initialValue); + + // rawData holds either the latest message value or the NOT_SET sentinel. + // The cast to `Exclude | typeof NOT_SET` selects overload 2 of + // createSignal (plain value, not compute function). NOT_SET is a unique symbol + // so it's never a Function; for initialValue, SSE data types are never functions. + const [rawData, setRawData] = createSignal( + (options.initialValue !== undefined ? options.initialValue : NOT_SET) as + | Exclude + | typeof NOT_SET, + ); + + // A computed signal: throws NotReadyError when rawData is NOT_SET so that + // shows a fallback while awaiting the first message. After the + // first message it updates reactively on every subsequent message. + const [data] = createSignal(() => { + const val = rawData(); + if (val === NOT_SET) throw new NotReadyError("SSE awaiting first message"); + return val as T | undefined; + }); + const [error, setError] = createSignal(undefined); const [readyState, setReadyState] = createSignal(SSEReadyState.CONNECTING); - // ── Reconnect config ────────────────────────────────────────────────────── + // Explicit pending flag — true until the first message arrives (or after + // reconnect). The `data` computed throws NotReadyError for , but + // Solid isPending() can't detect the initial STATUS_UNINITIALIZED + // state, so we expose this for imperative checks. + const [pending, setPending] = createSignal(options.initialValue === undefined); + const reconnectConfig: SSEReconnectOptions = options.reconnect === true ? { retries: Infinity, delay: 3000 } @@ -245,7 +304,8 @@ export const createSSE = ( const handleMessage = (e: MessageEvent) => { const value = options.transform ? options.transform(e.data as string) : (e.data as T); - setData(() => value); + setRawData(() => value); + setPending(false); options.onMessage?.(e); }; @@ -277,7 +337,7 @@ export const createSSE = ( currentCleanup = cleanup; }; - const disconnect = () => { + const close = () => { clearReconnectTimer(); retriesLeft = 0; currentCleanup?.(); @@ -286,44 +346,43 @@ export const createSSE = ( setReadyState(SSEReadyState.CLOSED); }; - const manualReconnect = () => { + const reconnect = () => { const currentUrl = untrack(() => access(url)); - disconnect(); + close(); + // Reset to pending so Suspense shows a fallback during reconnect. + setRawData(NOT_SET); + setPending(true); 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; + // Reset to pending — new connection, new loading state. + setRawData(NOT_SET); + setPending(true); + connect(resolvedUrl); }); - connect(resolvedUrl); } }); } - // ── Lifecycle cleanup ───────────────────────────────────────────────────── onCleanup(() => { clearReconnectTimer(); currentCleanup?.(); currentCleanup = undefined; }); - return { source, data, error, readyState, close: disconnect, reconnect: manualReconnect }; + return { source, data, error, readyState, pending, close, reconnect }; }; diff --git a/packages/sse/test/index.test.ts b/packages/sse/test/index.test.ts index 7cd09d3bb..9bf4ffd05 100644 --- a/packages/sse/test/index.test.ts +++ b/packages/sse/test/index.test.ts @@ -1,6 +1,6 @@ import "./setup"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createRoot, createSignal } from "solid-js"; +import { createRoot, createSignal, flush } from "solid-js"; import { makeSSE, createSSE, SSEReadyState } from "../src/index.js"; import { MockEventSource } from "./setup.js"; @@ -83,36 +83,65 @@ 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 is pending before first message arrives", () => createRoot(dispose => { - const { data, source } = createSSE("https://example.com/events"); - expect(data()).toBeUndefined(); + const { data, pending } = createSSE("https://example.com/events"); + expect(pending()).toBe(true); + expect(() => data()).toThrow(); + dispose(); + })); + + it("provides latest message via data signal after first message", () => + createRoot(dispose => { + const { data, source, pending } = createSSE("https://example.com/events"); vi.advanceTimersByTime(20); + flush(); (source() as unknown as MockEventSource).simulateMessage("hello"); + flush(); + expect(pending()).toBe(false); 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", { + const { data, pending } = createSSE("https://example.com/events", { initialValue: "loading", }); + expect(pending()).toBe(false); expect(data()).toBe("loading"); dispose(); })); @@ -123,11 +152,15 @@ describe("createSSE", () => { reconnect: { retries: 1, delay: 50 }, }); vi.advanceTimersByTime(20); + flush(); (source() as unknown as MockEventSource).simulateError(); + flush(); expect(error()).toBeTruthy(); // reconnect fires after delay; new source opens vi.advanceTimersByTime(100); + flush(); vi.advanceTimersByTime(20); // new source opens + flush(); expect(error()).toBeUndefined(); dispose(); })); @@ -138,7 +171,9 @@ describe("createSSE", () => { reconnect: false, }); vi.advanceTimersByTime(20); + flush(); (source() as unknown as MockEventSource).simulateError(); + flush(); expect(readyState()).toBe(SSEReadyState.CLOSED); expect(error()).toBeTruthy(); dispose(); @@ -151,8 +186,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 +202,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 +219,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,30 +241,44 @@ 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, pending, 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(); + expect(pending()).toBe(true); // pending again after reconnect expect(source()).not.toBe(first); expect(first?.readyState).toBe(SSEReadyState.CLOSED); // old one closed 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 { data, source, pending } = 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(); + expect(pending()).toBe(true); // pending for new URL expect(source()).not.toBe(first); expect(first?.readyState).toBe(SSEReadyState.CLOSED); dispose(); @@ -228,6 +289,7 @@ 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(); diff --git a/packages/sse/test/server.test.ts b/packages/sse/test/server.test.ts index ab7ea714f..3d1bd6abe 100644 --- a/packages/sse/test/server.test.ts +++ b/packages/sse/test/server.test.ts @@ -7,9 +7,10 @@ describe("SSR", () => { createRoot(dispose => { const sse = createSSE("https://example.com/events"); expect(sse.source()).toBeUndefined(); - expect(sse.data()).toBeUndefined(); + expect(sse.data()).toBeUndefined(); // SSR returns undefined, not pending expect(sse.error()).toBeUndefined(); expect(sse.readyState()).toBe(2); + expect(sse.pending()).toBe(true); // no initialValue → pending expect(() => sse.close()).not.toThrow(); expect(() => sse.reconnect()).not.toThrow(); dispose(); @@ -17,10 +18,11 @@ describe("SSR", () => { it("exposes initialValue in SSR data stub", () => createRoot(dispose => { - const { data } = createSSE("https://example.com/events", { + const { data, pending } = createSSE("https://example.com/events", { initialValue: "loading", }); expect(data()).toBe("loading"); + expect(pending()).toBe(false); 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..65854a03f --- /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-experimental.16_solid-js@2.0.0-experimental.16/node_modules/@solidjs/web/dist/server.js" + : "../../node_modules/.pnpm/@solidjs+web@2.0.0-experimental.16_solid-js@2.0.0-experimental.16/node_modules/@solidjs/web/dist/web.js", + import.meta.url, + ).pathname, + "@solidjs/web": new URL( + testSSR + ? "../../node_modules/.pnpm/@solidjs+web@2.0.0-experimental.16_solid-js@2.0.0-experimental.16/node_modules/@solidjs/web/dist/server.js" + : "../../node_modules/.pnpm/@solidjs+web@2.0.0-experimental.16_solid-js@2.0.0-experimental.16/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..bd5ce25e9 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-experimental.16 + version: 2.0.0-experimental.16 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 @@ -2587,6 +2587,9 @@ packages: peerDependencies: solid-js: ^1.5.3 + '@solidjs/signals@0.11.3': + resolution: {integrity: sha512-udMfutYPOlcxKUmc5+n1QtarsxOiAlC6LJY2TqFyaMwdXgo+reiYUcYGDlOiAPXfCLE0lavZHQ/6GT5pJbXKBA==} + '@solidjs/start@1.1.4': resolution: {integrity: sha512-ma1TBYqoTju87tkqrHExMReM5Z/+DTXSmi30CCTavtwuR73Bsn4rVGqm528p4sL2koRMfAuBMkrhuttjzhL68g==} peerDependencies: @@ -5892,10 +5895,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 +6014,9 @@ packages: solid-js@1.9.7: resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} + 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: @@ -8576,18 +8592,20 @@ 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/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 +12459,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 +12581,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 +12595,13 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.2(seroval@1.3.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 +12611,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): From ce9b27a881ea264d30ba6f866df367f85233ebd4 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sun, 19 Apr 2026 21:14:51 -0400 Subject: [PATCH 2/3] Experimental improvement for Solid 2.0 --- .changeset/sse-solid2-async-reactivity.md | 68 +++++ packages/sse/README.md | 183 +++++++++++-- packages/sse/src/index.ts | 9 +- packages/sse/src/sse.ts | 308 +++++++++++++++++----- packages/sse/test/index.test.ts | 143 ++++++++-- packages/sse/test/server.test.ts | 16 +- 6 files changed, 604 insertions(+), 123 deletions(-) create mode 100644 .changeset/sse-solid2-async-reactivity.md 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/src/index.ts b/packages/sse/src/index.ts index 38cf1d252..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,12 +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 createSSE: -// - isPending(data) — true while awaiting the first SSE message +// 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 ) +// - 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 f5dadc3ac..0d1b87706 100644 --- a/packages/sse/src/sse.ts +++ b/packages/sse/src/sse.ts @@ -33,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>; @@ -71,7 +77,7 @@ export type CreateSSEOptions = SSEOptions & { * * 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. + * arrives, integrating with Solid's `` for a loading fallback. */ initialValue?: T; /** @@ -105,16 +111,18 @@ export type SSEReturn = { * 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 + * 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. * - * Use `pending()` to check the pending state imperatively, and - * `onSettled(() => ...)` to react when the first value arrives. + * 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 latest error event, `undefined` when no error has occurred. */ - error: Accessor; + data: Accessor; /** * The current connection state. Use `SSEReadyState` for named comparisons: * - `SSEReadyState.CONNECTING` (0) @@ -122,14 +130,7 @@ export type SSEReturn = { * - `SSEReadyState.CLOSED` (2) */ readyState: Accessor; - /** - * `true` until the first message arrives (or after `reconnect()` / URL - * change until the next message). Use this for imperative pending checks; - * use `` for declarative loading UI (it catches the `NotReadyError` - * that `data()` throws while pending). - */ - pending: Accessor; - /** Close the connection. Resets `data` to pending on the next `reconnect()`. */ + /** Close the connection. */ close: VoidFunction; /** * Force-close the current connection and open a new one. @@ -139,7 +140,7 @@ export type SSEReturn = { }; // Internal sentinel marking "no message received yet". When rawData holds this -// value, the data accessor throws NotReadyError so Solid's Suspense boundary +// 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; @@ -191,27 +192,31 @@ export const makeSSE = ( /** * Creates a reactive SSE (Server-Sent Events) connection that integrates with - * Solid async reactivity system and owner lifecycle. + * Solid's async reactivity system and owner lifecycle. * * - `data` is **pending** (throws `NotReadyError`) until the first message - * arrives, enabling `` to show a loading fallback. Provide + * 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 } }, * ); * - * // In JSX — Suspense shows fallback while connecting: + * // In JSX — Loading shows fallback while connecting, Errored catches terminal failures: * return ( - * Connecting…

}> - *

{data()?.msg}

- *
+ *

Connection failed

}> + * Connecting…

}> + *

{data()?.msg}

+ *
+ *
* ); * ``` * @@ -222,50 +227,44 @@ 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, - pending: () => options.initialValue === undefined, close: () => void 0, reconnect: () => void 0, }; } - // ── Reactive state ──────────────────────────────────────────────────────── const [source, setSource] = createSignal(undefined); // rawData holds either the latest message value or the NOT_SET sentinel. - // The cast to `Exclude | typeof NOT_SET` selects overload 2 of - // createSignal (plain value, not compute function). NOT_SET is a unique symbol - // so it's never a Function; for initialValue, SSE data types are never functions. const [rawData, setRawData] = createSignal( - (options.initialValue !== undefined ? options.initialValue : NOT_SET) as - | Exclude - | typeof NOT_SET, + options.initialValue !== undefined ? options.initialValue : NOT_SET, ); - // A computed signal: throws NotReadyError when rawData is NOT_SET so that - // shows a fallback while awaiting the first message. After the - // first message it updates reactively on every subsequent message. - const [data] = createSignal(() => { + // 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); + + // 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 as T | undefined; + return val; }); - const [error, setError] = createSignal(undefined); const [readyState, setReadyState] = createSignal(SSEReadyState.CONNECTING); - // Explicit pending flag — true until the first message arrives (or after - // reconnect). The `data` computed throws NotReadyError for , but - // Solid isPending() can't detect the initial STATUS_UNINITIALIZED - // state, so we expose this for imperative checks. - const [pending, setPending] = createSignal(options.initialValue === undefined); - const reconnectConfig: SSEReconnectOptions = options.reconnect === true ? { retries: Infinity, delay: 3000 } @@ -283,12 +282,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); }; @@ -298,28 +297,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); setRawData(() => value); - setPending(false); 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); + } } }; @@ -349,9 +349,8 @@ export const createSSE = ( const reconnect = () => { const currentUrl = untrack(() => access(url)); close(); - // Reset to pending so Suspense shows a fallback during reconnect. setRawData(NOT_SET); - setPending(true); + setTerminalError(undefined); connect(currentUrl); }; @@ -369,9 +368,8 @@ export const createSSE = ( untrack(() => { currentCleanup?.(); currentCleanup = undefined; - // Reset to pending — new connection, new loading state. setRawData(NOT_SET); - setPending(true); + setTerminalError(undefined); connect(resolvedUrl); }); } @@ -384,5 +382,191 @@ export const createSSE = ( currentCleanup = undefined; }); - return { source, data, error, readyState, pending, close, reconnect }; + 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); + const [terminalError, setTerminalError] = createSignal(undefined); + + 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 9bf4ffd05..4f770ef03 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, flush } from "solid-js"; -import { makeSSE, createSSE, SSEReadyState } from "../src/index.js"; +import { makeSSE, createSSE, createSSEStream, SSEReadyState } from "../src/index.js"; import { MockEventSource } from "./setup.js"; beforeAll(() => vi.useFakeTimers()); @@ -88,22 +88,20 @@ describe("createSSE", () => { dispose(); })); - it("data is pending before first message arrives", () => + it("data throws NotReadyError before first message arrives", () => createRoot(dispose => { - const { data, pending } = createSSE("https://example.com/events"); - expect(pending()).toBe(true); + 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, pending } = createSSE("https://example.com/events"); + const { data, source } = createSSE("https://example.com/events"); vi.advanceTimersByTime(20); flush(); (source() as unknown as MockEventSource).simulateMessage("hello"); flush(); - expect(pending()).toBe(false); expect(data()).toBe("hello"); dispose(); })); @@ -138,36 +136,32 @@ describe("createSSE", () => { it("returns initialValue before any message arrives (no pending state)", () => createRoot(dispose => { - const { data, pending } = createSSE("https://example.com/events", { + const { data } = createSSE("https://example.com/events", { initialValue: "loading", }); - expect(pending()).toBe(false); expect(data()).toBe("loading"); 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(); flush(); - expect(error()).toBeTruthy(); - // reconnect fires after delay; new source opens - vi.advanceTimersByTime(100); - flush(); - vi.advanceTimersByTime(20); // new source opens - flush(); - expect(error()).toBeUndefined(); + 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); @@ -175,7 +169,7 @@ describe("createSSE", () => { (source() as unknown as MockEventSource).simulateError(); flush(); expect(readyState()).toBe(SSEReadyState.CLOSED); - expect(error()).toBeTruthy(); + expect(() => data()).toThrow(); // propagates to boundary dispose(); })); @@ -251,7 +245,7 @@ describe("createSSE", () => { it("reconnect() opens a fresh connection and resets data to pending", () => createRoot(dispose => { - const { data, source, pending, reconnect } = createSSE("https://example.com/events"); + const { data, source, reconnect } = createSSE("https://example.com/events"); vi.advanceTimersByTime(20); flush(); const first = source(); @@ -260,16 +254,22 @@ describe("createSSE", () => { expect(data()).toBe("hello"); reconnect(); flush(); - expect(pending()).toBe(true); // pending again after reconnect + // 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 and resets data to pending", () => createRoot(dispose => { const [url, setUrl] = createSignal("https://example.com/v1/events"); - const { data, source, pending } = createSSE(url); + const { data, source } = createSSE(url); vi.advanceTimersByTime(20); flush(); const first = source(); @@ -278,9 +278,36 @@ describe("createSSE", () => { expect(data()).toBe("v1 data"); setUrl("https://example.com/v2/events"); flush(); - expect(pending()).toBe(true); // pending for new URL + // 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(); })); @@ -296,3 +323,69 @@ describe("createSSE", () => { }), )); }); + +// ── 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 3d1bd6abe..d6a457234 100644 --- a/packages/sse/test/server.test.ts +++ b/packages/sse/test/server.test.ts @@ -1,16 +1,14 @@ 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(); // SSR returns undefined, not pending - expect(sse.error()).toBeUndefined(); + expect(() => sse.data()).toThrow(); // throws NotReadyError — no initialValue expect(sse.readyState()).toBe(2); - expect(sse.pending()).toBe(true); // no initialValue → pending expect(() => sse.close()).not.toThrow(); expect(() => sse.reconnect()).not.toThrow(); dispose(); @@ -18,11 +16,17 @@ describe("SSR", () => { it("exposes initialValue in SSR data stub", () => createRoot(dispose => { - const { data, pending } = createSSE("https://example.com/events", { + const { data } = createSSE("https://example.com/events", { initialValue: "loading", }); expect(data()).toBe("loading"); - expect(pending()).toBe(false); + dispose(); + })); + + it("createSSEStream throws NotReadyError on server", () => + createRoot(dispose => { + const data = createSSEStream("https://example.com/events"); + expect(() => data()).toThrow(); dispose(); })); }); From a93d284364a780a8f0b7695680812cf6f6327144 Mon Sep 17 00:00:00 2001 From: David Di Biase <1168397+davedbase@users.noreply.github.com> Date: Sun, 19 Apr 2026 22:09:08 -0400 Subject: [PATCH 3/3] Updating to beta 7 --- packages/sse/package.json | 4 ++-- packages/sse/src/sse.ts | 19 ++++++++++++++----- packages/sse/test/index.test.ts | 2 +- packages/sse/vitest.config.ts | 8 ++++---- pnpm-lock.yaml | 26 +++++++++++++++++++++++--- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/packages/sse/package.json b/packages/sse/package.json index 73813ee63..97831f484 100644 --- a/packages/sse/package.json +++ b/packages/sse/package.json @@ -75,9 +75,9 @@ "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "solid-js": "2.0.0-experimental.16" + "solid-js": "2.0.0-beta.7" }, "devDependencies": { - "solid-js": "2.0.0-experimental.16" + "solid-js": "2.0.0-beta.7" } } diff --git a/packages/sse/src/sse.ts b/packages/sse/src/sse.ts index 0d1b87706..3d6736ced 100644 --- a/packages/sse/src/sse.ts +++ b/packages/sse/src/sse.ts @@ -242,16 +242,21 @@ export const createSSE = ( }; } - const [source, setSource] = createSignal(undefined); + 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); + const [terminalError, setTerminalError] = createSignal(undefined, { + ownedWrite: true, + }); // Computed data signal: throws terminal error (→ Errored boundary) or // NotReadyError (→ Loading boundary) when not ready. @@ -263,7 +268,9 @@ export const createSSE = ( return val; }); - const [readyState, setReadyState] = createSignal(SSEReadyState.CONNECTING); + const [readyState, setReadyState] = createSignal(SSEReadyState.CONNECTING, { + ownedWrite: true, + }); const reconnectConfig: SSEReconnectOptions = options.reconnect === true @@ -511,8 +518,10 @@ export const createSSEStream = ( }; } - const [rawData, setRawData] = createSignal(NOT_SET); - const [terminalError, setTerminalError] = createSignal(undefined); + const [rawData, setRawData] = createSignal(NOT_SET, { ownedWrite: true }); + const [terminalError, setTerminalError] = createSignal(undefined, { + ownedWrite: true, + }); const [data] = createSignal(() => { const err = terminalError(); diff --git a/packages/sse/test/index.test.ts b/packages/sse/test/index.test.ts index 4f770ef03..fed6d1647 100644 --- a/packages/sse/test/index.test.ts +++ b/packages/sse/test/index.test.ts @@ -268,7 +268,7 @@ describe("createSSE", () => { it("reconnects when the URL signal changes and resets data to pending", () => createRoot(dispose => { - const [url, setUrl] = createSignal("https://example.com/v1/events"); + const [url, setUrl] = createSignal("https://example.com/v1/events", { ownedWrite: true }); const { data, source } = createSSE(url); vi.advanceTimersByTime(20); flush(); diff --git a/packages/sse/vitest.config.ts b/packages/sse/vitest.config.ts index 65854a03f..927689e4f 100644 --- a/packages/sse/vitest.config.ts +++ b/packages/sse/vitest.config.ts @@ -17,14 +17,14 @@ export default defineConfig(({ mode }) => { alias: { "solid-js/web": new URL( testSSR - ? "../../node_modules/.pnpm/@solidjs+web@2.0.0-experimental.16_solid-js@2.0.0-experimental.16/node_modules/@solidjs/web/dist/server.js" - : "../../node_modules/.pnpm/@solidjs+web@2.0.0-experimental.16_solid-js@2.0.0-experimental.16/node_modules/@solidjs/web/dist/web.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/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-experimental.16_solid-js@2.0.0-experimental.16/node_modules/@solidjs/web/dist/server.js" - : "../../node_modules/.pnpm/@solidjs+web@2.0.0-experimental.16_solid-js@2.0.0-experimental.16/node_modules/@solidjs/web/dist/web.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/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, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd5ce25e9..592544f84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -869,8 +869,8 @@ importers: version: link:../utils devDependencies: solid-js: - specifier: 2.0.0-experimental.16 - version: 2.0.0-experimental.16 + specifier: 2.0.0-beta.7 + version: 2.0.0-beta.7 packages/state-machine: devDependencies: @@ -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 @@ -2590,6 +2591,9 @@ packages: '@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: @@ -3516,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==} @@ -4167,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==} @@ -6014,6 +6020,9 @@ 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==} @@ -6211,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==} @@ -6726,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==} @@ -8606,6 +8617,8 @@ snapshots: '@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)) @@ -12595,6 +12608,13 @@ 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