Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

createSocketClient

Low-level Socket client over native WebSocket, with explicit URL and contract, validation in both directions and exponential backoff reconnection.

advanced API Since 1.0.0 @meekojs/meeko/framework Source code
ts
import { createSocketClient } from "@meekojs/meeko/framework";

createSocketClient opens a WebSocket to a Socket channel with an explicit URL and contract. It validates every outgoing and incoming event against the contract schemas, automatically reconnects with exponential backoff and exposes its state as a signal. Delivery is at most once: emit rejects when disconnected, without a queue or resend.

It is not tied to a component: close it with close(). Inside a component, use useSocketClient, which closes it on unmount; in an application, you typically use the auto-import useSocket("chat") or socket.chat, which already know each channel's URL and contract.

Usage#

ts
import { createSocketClient } from "@meekojs/meeko/framework";
import type { Schema } from "@meekojs/meeko/server";

const message: Schema<{ text: string }> = {
  "~standard": {
    version: 1,
    validate: (value) => typeof value === "object" && value !== null && typeof (value as { text?: unknown }).text === "string"
      ? { value: value as { text: string } }
      : { issues: [{ message: "Expected { text }" }] },
  },
};
const chatContract = { clientToServer: { message }, serverToClient: { message } };

const chat = createSocketClient("wss://example.com/_meeko/socket/chat?room=general", chatContract, {
  onError: (error) => console.warn("socket", error),
});
const stop = chat.on("message", (data) => console.log(data.text));
// Later, with the socket open:
await chat.emit("message", { text: "Hello" });
stop();
chat.close();

Type#

ts
function createSocketClient<C extends SocketContract>(
  url: string,
  contract: C,
  options?: SocketClientOptions,
): SocketClient<C>

SocketClientOptions

ts
interface SocketClientOptions {
	readonly retryMs?: number
	readonly maxRetryMs?: number
	readonly maxMessageBytes?: number
	readonly webSocketFactory?: (url: string) => WebSocket
	readonly onError?: (error: unknown) => void
}

SocketClient

ts
interface SocketClient<C extends SocketContract> {
  readonly state: ReadonlySignal<SocketClientState>;
  emit<K extends keyof C["clientToServer"] & string>(
    event: K,
    data: SchemaInput<C["clientToServer"][K]>,
  ): Promise<void>;
  on<K extends keyof C["serverToClient"] & string>(
    event: K,
    listener: (data: SchemaOutput<C["serverToClient"][K]>) => void,
  ): () => void;
  close(): void;
}

SocketContract

ts
interface SocketContract<
  ClientToServer extends Events = Events,
  ServerToClient extends Events = Events,
> {
  readonly clientToServer: ClientToServer;
  readonly serverToClient: ServerToClient;
}

Parameters#

Parameter Type Default Description
url text required Channel ws: or wss: URL: <origen>/_meeko/socket/<canal>, with ?room= if the channel uses rooms.
contract SocketContract required { clientToServer, serverToClient }: each event's schema in each direction.
options.retryMs number 500 Initial reconnection delay; doubles on each attempt.
options.maxRetryMs number 30 000 Maximum reconnection delay.
options.maxMessageBytes number 65 536 Largest incoming message; a larger one (or binary message) is discarded and reported.
options.webSocketFactory (url) => WebSocket new WebSocket(url) Creates the socket; for tests or runtimes without a global WebSocket.
options.onError (error) => void console Invalid incoming messages and listener failures.

Return value#

A SocketClient typed by the contract:

Member Description
state Read-only signal: "connecting", "open", "closed" or, since 1.1, terminal "unauthorized": the server rejected the session or authorization (close 4401/4403) and the client does not retry.
emit(event, data) Validates and sends. Rejects with Socket is disconnected if not open, Invalid socket event payload if the schema fails, Unknown socket event: … if the event does not exist and Socket message too large if it exceeds the limit.
on(event, listener) Listens to an already validated server event. Returns the function to stop listening.
close() Closes the connection and stops reconnecting.

The URL is passed unchanged to new WebSocket(url): a URL rejected by the runtime's WebSocket throws its error when creating the client.

Examples#

Wait for it to open before sending#

ts
import { effect } from "@meekojs/meeko";

effect(() => {
  if (chat.state.value === "open") void chat.emit("message", { text: "conectado" });
  if (chat.state.value === "unauthorized") location.assign("/login");
});

state is a signal: an effect (or a component's template) updates on every change.

A test with a fake WebSocket#

ts
import { createSocketClient } from "@meekojs/meeko/framework";
import type { Schema } from "@meekojs/meeko/server";

class FakeSocket extends EventTarget {
  readyState: number = WebSocket.CONNECTING;
  sent: string[] = [];
  send(value: string): void { this.sent.push(value); }
  close(): void { this.readyState = WebSocket.CLOSED; this.dispatchEvent(new Event("close")); }
  open(): void { this.readyState = WebSocket.OPEN; this.dispatchEvent(new Event("open")); }
}

const text: Schema<string> = { "~standard": { version: 1, validate: (value) => typeof value === "string" ? { value } : { issues: [{ message: "text" }] } } };
const peer = new FakeSocket();
const client = createSocketClient("ws://localhost/chat", { clientToServer: { message: text }, serverToClient: { message: text } }, {
  webSocketFactory: () => peer as unknown as WebSocket,
});

await client.emit("message", "early").catch((error: Error) => console.log(error.message)); // Socket is disconnected
peer.open();
await client.emit("message", "hello");
console.log(peer.sent); // [ '{"event":"message","payload":"hello"}' ]
client.close();

Outside a Meeko application#

ts
// An admin dashboard listening for store orders
const orders = createSocketClient(`wss://tienda.example.com/_meeko/socket/orders?room=${storeId}`, ordersContract);
orders.on("created", (order) => addRow(order));
window.addEventListener("pagehide", () => orders.close());

The server checks Origin: an origin different from the application's must be in the runtime's allowedOrigins.

Common mistakes#

Pass the channel name instead of its URL#

Incorrectts
createSocketClient("chat", chatContract);

createSocketClient does not know application channels: "chat" is a relative URL. In Node, the global WebSocket throws a DOMException with message TypeError: Invalid URL; with another scheme, such as ftp:, expected a ws: or wss: url.

Correctts
createSocketClient(`${location.origin.replace(/^http/, "ws")}/_meeko/socket/chat`, chatContract);

Inside an application, useSocket("chat") or socket.chat build the URL and know the contract.

Send before it opens#

Incorrectts
const chat = createSocketClient(url, chatContract);
await chat.emit("message", { text: "hello" });

Rejects with Socket is disconnected: the socket is still "connecting", and the client does not queue messages.

Correctts
const chat = createSocketClient(url, chatContract);
effect(() => { if (chat.state.value === "open") void chat.emit("message", { text: "hello" }); });

Never close it#

Incorrectts
function openChat() {
  const chat = createSocketClient(url, chatContract);
  chat.on("message", render);
}

Each call leaves an open socket that also reconnects automatically after disconnecting: connections accumulate until the page reloads.

Correctts
function openChat() {
  const chat = createSocketClient(url, chatContract);
  chat.on("message", render);
  return () => chat.close();
}

Notes#

  • Browser only (or with webSocketFactory). In Node and SSR there is no global WebSocket to open.
  • Security. Server messages are validated with serverToClient before reaching listeners: an invalid message is discarded and sent to onError.
  • Reconnection. After a disconnect, retries after 500 ms, 1 s, 2 s… up to 30 s. Messages sent meanwhile are rejected; resend what you need when it returns to "open".
  • Authorization. A 4401 or 4403 close (or the hub's session policy 1008) leaves state at "unauthorized" permanently: redirect to sign-in and create a new client afterward.

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/advanced/createsocketclient.md