diff --git a/.changeset/websocket-solid-2-async.md b/.changeset/websocket-solid-2-async.md
new file mode 100644
index 000000000..c7a971f09
--- /dev/null
+++ b/.changeset/websocket-solid-2-async.md
@@ -0,0 +1,32 @@
+---
+"@solid-primitives/websocket": major
+---
+
+Upgrade to Solid.js 2.0 (`^2.0.0-beta.7`) and add async-reactive message primitives.
+
+**Breaking changes**
+
+- Peer dependency is now `solid-js@^2.0.0-beta.7`. All `createEffect` examples in docs now use the Solid 2.0 split form: `createEffect(compute, effect)`.
+
+**New: `createWSMessage`**
+
+Reactive `Accessor` for the most recently received WebSocket message. Cleans up its event listener on owner disposal via `onCleanup`.
+
+```ts
+const message = createWSMessage(ws);
+return
{message()}
;
+```
+
+> Note: uses a signal internally, so under burst conditions only the final message before a flush is tracked by effects. For every-message processing, use the planned `wsMessageIterable` / `createWSData` primitives.
+
+**`createWSState` signal fix**
+
+Internal signal now uses `{ ownedWrite: true }` to suppress the Solid 2.0 dev-mode `SIGNAL_WRITE_IN_OWNED_SCOPE` diagnostic, which would fire if `ws.close()` is called from inside a component body or reactive computation.
+
+**Planned for next minor: async message primitives**
+
+The following are designed and documented but not yet implemented, based on Solid 2.0's `createMemo(AsyncIterable)` model:
+
+- `wsMessageIterable` — buffered `AsyncIterable` that never drops burst messages; works with `makeReconnectingWS`
+- `createWSData` — async memo over `wsMessageIterable`; suspends `` until first message; integrates with `isPending` and `latest`
+- `createWSStore` — reactive store driven by WS messages as draft-mutation patches via `createStore(fn, seed)`
diff --git a/packages/websocket/CHANGELOG.md b/packages/websocket/CHANGELOG.md
index d123f3f90..04bb848db 100644
--- a/packages/websocket/CHANGELOG.md
+++ b/packages/websocket/CHANGELOG.md
@@ -1,5 +1,14 @@
# @solid-primitives/websocket
+## 2.0.0-beta.0
+
+### Major Changes
+
+- Upgrade to Solid.js 2.0 (`^2.0.0-beta.7`).
+- `createWSState`: signal now uses `ownedWrite: true` to suppress dev-mode warnings when `ws.close()` is called from within a component or effect.
+- New primitive `createWSMessage`: reactive signal containing the latest received WebSocket message. Cleans up its event listener automatically on owner disposal.
+- Updated all JSDoc examples to use the Solid 2.0 split `createEffect(compute, effect)` form.
+
## 1.3.2
### Patch Changes
diff --git a/packages/websocket/README.md b/packages/websocket/README.md
index 44ccd0248..85cc66076 100644
--- a/packages/websocket/README.md
+++ b/packages/websocket/README.md
@@ -4,150 +4,279 @@
# @solid-primitives/websocket
+[](https://bundlephobia.com/package/@solid-primitives/websocket)
+[](https://www.npmjs.com/package/@solid-primitives/websocket)
[](https://github.com/solidjs-community/solid-primitives#contribution-process)
-Primitive to help establish, maintain and operate a websocket connection.
+- [**Docs**](https://primitives.solidjs.community/docs/websocket)
-- `makeWS` - sets up a web socket connection with a buffered send
-- `createWS` - sets up a web socket connection that disconnects on cleanup
-- `createWSState` - creates a reactive signal containing the readyState of a websocket
-- `makeReconnectingWS` - sets up a web socket connection that reconnects if involuntarily closed
-- `createReconnectingWS` - sets up a reconnecting web socket connection that disconnects on cleanup
-- `makeHeartbeatWS` - wraps a reconnecting web socket to send a heart beat and reconnect if the answer fails
+Primitives to help establish, maintain, and operate WebSocket connections in Solid.
-All of them return a WebSocket instance extended with a `message` prop containing an accessor for the last received message for convenience and the ability to receive messages to send before the connection is opened.
+## Connection primitives
-## How to use it
+- [`makeWS`](#makews) — raw WebSocket with a buffered send queue (manual cleanup)
+- [`createWS`](#createws) — same, but closes on owner disposal
+- [`createWSState`](#createwsstate) — reactive `readyState` signal (`0`–`3`)
+- [`makeReconnectingWS`](#makereconnectingws) — auto-reconnects on involuntary close (manual cleanup)
+- [`createReconnectingWS`](#createreconnectingws) — same, but closes on owner disposal
+- [`makeHeartbeatWS`](#makeheartbeatws) — wraps a reconnecting WS with a heartbeat/pong watchdog
+
+### Message primitives
+
+- [`createWSMessage`](#createwsmessage) — reactive signal for the **latest** received message
+- [`wsMessageIterable`](#wsmessageiterable) — buffered `AsyncIterable` over WS messages
+- [`createWSData`](#createwsdata) — async memo compatible with ``, `isPending`, and `latest`
+- [`createWSStore`](#createwsstore) — reactive store driven by WS message patches
+
+---
+
+## Connection primitives
+
+### `makeWS`
+
+Sets up a WebSocket with a buffered send queue. Messages sent before the connection opens are queued and flushed on `open`. Does **not** close on cleanup — use `createWS` for that.
+
+```ts
+const ws = makeWS("ws://localhost:5000");
+createEffect(
+ () => serverMessage(),
+ msg => ws.send(msg),
+);
+onCleanup(() => ws.close());
+```
+
+### `createWS`
+
+Same as `makeWS`, but registers `ws.close()` with `onCleanup`.
+
+```ts
+const ws = createWS("ws://localhost:5000");
+createEffect(
+ () => serverMessage(),
+ msg => ws.send(msg),
+);
+```
+
+### `createWSState`
+
+Returns a reactive `Accessor<0 | 1 | 2 | 3>` tracking the WebSocket's `readyState`.
```ts
const ws = createWS("ws://localhost:5000");
const state = createWSState(ws);
-const states = ["Connecting", "Connected", "Disconnecting", "Disconnected"];
-ws.send("it works");
-createEffect(on(ws.message, msg => console.log(msg), { defer: true }));
-return
;
+```
+
+### `createWSMessage`
+
+Returns a reactive `Accessor` that holds the **most recently received** message. Starts as `undefined`.
+
+```ts
+const ws = createWS("ws://localhost:5000");
+const message = createWSMessage(ws);
+
+return
Last message: {message()}
;
+```
+
+> **Note — "latest wins" semantics.** `createWSMessage` uses a signal internally. In Solid 2.0, signal writes are batched: if two messages arrive before the reactive flush, only the second is seen by effects. This is fine for "current state" displays, but if your protocol can burst messages and you need to process every one, use [`wsMessageIterable`](#wsmessageiterable) or [`createWSData`](#createwsdata) instead.
+
+### `makeReconnectingWS`
+
+Returns a `WebSocket`-shaped proxy that transparently opens a new underlying connection whenever the server closes it involuntarily.
+
+```ts
+const ws = makeReconnectingWS("ws://localhost:5000", undefined, { delay: 3000, retries: Infinity });
+createEffect(
+ () => serverMessage(),
+ msg => ws.send(msg),
);
-// with the primitives starting with `make...`, one needs to manually clean up:
-socket.send("this will reconnect if connection fails");
+onCleanup(() => ws.close());
```
-### Definitions
+### `createReconnectingWS`
+
+Same as `makeReconnectingWS`, but closes on owner disposal.
+
+### `makeHeartbeatWS`
+
+Wraps a `ReconnectingWebSocket` to send a periodic heartbeat. If no response arrives within `wait` ms the connection is force-reconnected.
```ts
-/** Arguments of the primitives */
-type WSProps = [url: string, protocols?: string | string[]];
-type WSMessage = string | ArrayBufferLike | ArrayBufferView | Blob;
-type WSReadyState = WebSocket.CONNECTING | WebSocket.OPEN | WebSocket.CLOSING | WebSocket.CLOSED;
-type WSEventMap = {
- close: CloseEvent;
- error: Event;
- message: MessageEvent;
- open: Event;
-};
-type ReconnectingWebSocket = WebSocket & {
- reconnect: () => void;
- // ws.send.before is meant to be used by heartbeat
- send: ((msg: WSMessage) => void) & { before: () => void };
-};
-type WSHeartbeatOptions = {
- /**
- * Heartbeat message being sent to the server in order to validate the connection
- * @default "ping"
- */
- message?: WSMessage;
- /**
- * The time between messages being sent in milliseconds
- * @default 1000
- */
- interval?: number;
- /**
- * The time after the heartbeat message being sent to wait for the next message in milliseconds
- * @default 1500
- */
- wait?: number;
-};
+const ws = makeHeartbeatWS(createReconnectingWS("ws://localhost:5000"), {
+ message: "ping",
+ interval: 1000,
+ wait: 1500,
+});
+```
+
+---
+
+## Async message primitives
+
+These three primitives leverage Solid's async reactivity — `createMemo` with `AsyncIterable`, `` boundaries, `isPending`, and `latest` — to provide a more powerful and correct model for WebSocket data.
+
+### `wsMessageIterable`
+
+```ts
+function wsMessageIterable(ws: WebSocket): AsyncIterable
```
-If you want to use the messages as a signal, have a look at the [`event-listener`](../event-listener/README.md) package:
+The foundational building block. Returns a buffered `AsyncIterable` over a WebSocket's message stream. Messages that arrive while a consumer is busy are queued; none are dropped. The event listener is removed automatically when the iterator's `return()` method is called (Solid does this on memo disposal).
```ts
-import { createWS } from "@solid-primitives/websocket";
-import { createEventSignal } from "@solid-primitives/event-listener";
+// Compose freely with any Solid 2.0 async primitive:
+const latestQuote = createMemo(async function* () {
+ for await (const raw of wsMessageIterable(ws)) {
+ yield JSON.parse(raw) as Quote;
+ }
+});
+```
-const ws = createWS("ws://localhost:5000");
-const messageEvent = createEventSignal(ws, "message");
-const message = () => messageEvent().data;
+Works correctly with `makeReconnectingWS` — event listeners are re-attached to each new underlying connection, so the iterable survives reconnects transparently.
+
+**Why this doesn't drop messages:** Unlike `createWSMessage`, each yielded value triggers its own reactive flush. Messages that arrive while an earlier one is being processed are buffered and drained in order, so no message is skipped.
+
+### `createWSData`
+
+```ts
+function createWSData(
+ ws: WebSocket,
+ options?: { transform?: (msg: T) => U },
+): Accessor
```
-Otherwise, you can simply use the message event to get message.data:
+An async memo wrapping `wsMessageIterable`. Suspends the nearest `` boundary until the first message arrives; subsequent updates work with `isPending` and `latest`. The optional `transform` is applied to each raw message before the memo value is updated.
+
+```tsx
+const ws = createReconnectingWS("wss://prices.example.com");
+const price = createWSData(ws, { transform: JSON.parse });
+
+return (
+ Waiting for first quote…
+ Socket auto-opens after 600 ms. "Close" sends a clean handshake (1000); "Force Drop"
+ simulates an involuntary disconnect (1006). Refresh the panel to reset.
+
+
+ );
+ },
+});
+
+// ─── createWSMessage ──────────────────────────────────────────────────────────
+
+const DEMO_MESSAGES = [
+ '{"type":"update","value":42}',
+ "Hello from the server!",
+ '{"type":"ping"}',
+ "Status: all systems nominal",
+ '{"type":"data","items":[1,2,3]}',
+];
+
+export const WSMessageStory = meta.story({
+ name: "Latest received message",
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "`createWSMessage(ws)` returns a reactive `Accessor` holding the **most recently received** message, starting as `undefined`. Because it is backed by a signal, only the last message in a reactive flush is observed — use `wsMessageIterable` when every message must be processed.",
+ },
+ },
+ },
+ render: () => {
+ const mock = new SimulatedWS();
+ const message = createWSMessage(mock as unknown as WebSocket);
+ const state = createWSState(mock as unknown as WebSocket);
+ let msgIdx = 0;
+
+ setTimeout(() => mock.simOpen(), 300);
+
+ return (
+
+
+ "Drop" simulates an involuntary close (code 1006) — the proxy reconnects after 1 s.
+ "Close" sends a clean close (code 1000) which permanently stops reconnecting.
+
+
+ );
+ },
+});
+
+// ─── makeHeartbeatWS ──────────────────────────────────────────────────────────
+
+export const HeartbeatWSStory = meta.story({
+ name: "Heartbeat watchdog",
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "`makeHeartbeatWS(rws, options?)` wraps a `ReconnectingWebSocket` with a ping/pong watchdog. After each received message it schedules a ping in `interval` ms; before each send it starts a pong-timeout — if no message arrives within `wait` ms it calls `ws.reconnect()`. Toggle **Suppress pong** to watch the timeout expire and the connection auto-heal.",
+ },
+ },
+ },
+ render: () => {
+ let pongEnabled = true;
+ let connCount = 0;
+ const [pongOn, setPongOn] = createSignal(true);
+ const [reconnects, setReconnects] = createSignal(0);
+ const [log, setLog] = createSignal<{ label: string; time: string }[]>([]);
+
+ const logEntry = (label: string) =>
+ setLog(prev => [{ label, time: ts() }, ...prev].slice(0, 7));
+
+ installMockWS(ws => {
+ // Intercept _send (captured by makeWS before it replaces ws.send)
+ // so we can detect pings and auto-pong
+ const origSend = ws.send.bind(ws);
+ ws.send = (data: any) => {
+ origSend(data);
+ logEntry(`↑ ${String(data)}`);
+ if (pongEnabled) {
+ setTimeout(() => {
+ ws.simReceive("pong");
+ logEntry("↓ pong");
+ }, 150);
+ }
+ };
+ setTimeout(() => {
+ connCount++;
+ ws.simOpen();
+ if (connCount === 1) logEntry("connected");
+ else {
+ setReconnects(r => r + 1);
+ logEntry("↻ reconnected");
+ }
+ }, 200);
+ });
+
+ const rws = createReconnectingWS("ws://simulated", undefined, { delay: 500, retries: 20 });
+ const ws = makeHeartbeatWS(rws as ReconnectingWebSocket, { interval: 2000, wait: 3000 });
+ const state = createWSState(ws as unknown as WebSocket);
+
+ const togglePong = () => {
+ pongEnabled = !pongEnabled;
+ setPongOn(pongEnabled);
+ logEntry(pongEnabled ? "pong enabled" : "pong suppressed — timeout in ~3 s");
+ };
+
+ return (
+
+
makeHeartbeatWS
+
+
+ {STATE_LABELS[state()]}
+
+ {reconnects()}
+
+
+
+
+
+
+
+
+
+
+ Ping every 2 s; pong timeout 3 s. Suppress the pong and watch the timeout trigger an
+ automatic reconnect.
+