@manyducks.co/chatter
v2.1.4
Published
Two-way websocket RPC for Bun
Downloads
33
Readme
Chatter
A fast, two-way RPC system built on Bun WebSockets.
How it Works
Chatter simplifies WebSockets by making it feel like you're calling local functions. You define a procedure in a shared file. This is like creating a function signature—it specifies the name, what data it takes, and what it returns. You can then implement this procedure on either the client, the server, or both. Chatter handles all the complex network communication for you behind the scenes. You can just call an async function. It runs the implementation on the other end of the line and returns a response.
Because this is a two-way system, both the client and the server can call procedures on the other. This allows you to build real-time apps where the server can push updates to the client, and the client can call functions on the server.
Installation
bun install @manyducks.co/chatterYou will also need to install your preferred schema library. Chatter uses Standard Schema, meaning it's compatible with any validation library compatible with those types. We test and use Chatter with Zod, but Valibot, ArkType, Yup, Joi and many others should work too.
bun install zod
bun install valibot
...Live Counter Example
This example shows how to set up a realtime counter. Clients can connect to the server and alter the counter's value. All connected users will see the number change in real time.
1. Define a Procedure
Create a shared file to define your procedures. By convention, procedures are named in SCREAMING_SNAKE_CASE.
// src/procedures.ts
import { createProc } from "@manyducks.co/chatter";
import { z } from "zod";
export const GET_COUNT = createProc({
name: "get_count",
returns: z.number(), // what the function returns
// this proc doesn't take anything, so there is no 'takes' schema.
});
export const UPDATE_COUNT = createProc({
name: "update_count",
takes: z.number(),
returns: z.number(),
});2. Set up the Server
Create a Bun server and implement both procedures.
// src/server.ts
import { createServer } from "@manyducks.co/chatter";
import { GET_COUNT, UPDATE_COUNT } from "./procedures";
const chatter = createServer({
onOpen: (connection) => {
// Subscribe all new connections to the counter topic.
// This allows them to receive broadcasts directed to this topic.
connection.subscribe("counter");
},
});
// We'll store the current value in memory on the server.
let currentValue = 0;
chatter.on(GET_COUNT, () => {
// Simply returns the current count to the caller.
return currentValue;
});
chatter.on(UPDATE_COUNT, (amount, connection) => {
// Update the stored value.
currentValue += amount;
// Broadcast the same event to every connection subscribed to the `counter` topic.
// Connection is the client who called this proc, so broadcast() will exclude them by default.
connection.broadcast("counter", UPDATE_COUNT, amount);
// And return the new value back to the caller.
return currentValue;
});
// Start the Bun server
Bun.serve({
port: 3000,
fetch(req, server) {
// Upgrade to a websocket connection.
return chatter.upgrade(req, server);
},
// Chatter handles the websockets
websocket: chatter.websocket,
});
console.log("Chatter server listening on port 3000");3. Call from the Client
Finally, create a client that connects to the server and calls and responds to procedures.
// src/client.ts
import { createClient } from "@manyducks.co/chatter";
import { GET_COUNT, UPDATE_COUNT } from "./procedures";
const chatter = createClient({
url: "ws://localhost:3000",
});
let currentValue = 0;
// The client implements UPDATE_COUNT so the server can broadcast changes by other users.
chatter.on(UPDATE_COUNT, (amount) => {
currentValue += amount;
});
chatter.onStateChange(async () => {
// Load current value from server as soon as we connect.
if (chatter.isConnected) {
currentValue = await chatter.call(GET_COUNT);
}
});
// Implement some methods the UI can use to change the value.
async function increment(amount = 1) {
// Tell the server to update and then set our local state to the resulting value.
currentValue = await chatter.call(UPDATE_COUNT, amount);
}
async function decrement(amount = 1) {
currentValue = await chatter.call(UPDATE_COUNT, -amount);
}API
createProc(options)
Creates a new remote procedure. This function defines the procedure's signature and name, but the actual implementation needs to be handled separately on the client and/or server using chatter.on().
Parameters:
options: An object with the following properties:name: A string that uniquely identifies the procedure.takes(optional): The schema for the data passed to the procedure. If not passed,nullwill be substituted.returns(optional): The schema for the data returned from the procedure. If not passed,nullwill be substituted.
Example:
import { createProc } from "@manyducks.co/chatter";
import { z } from "zod";
export const ADD_NUMBERS = createProc({
name: "add_numbers",
takes: z.object({ a: z.number(), b: z.number() }),
returns: z.number(),
});
export const GREET_USER = createProc({
name: "greet_user",
takes: z.object({ name: z.string() }),
// `returns` is not needed if the function does not return a value
});Client API
createClient(options)
Initializes the client-side Chatter instance.
Parameters:
options: An object with the following properties:url: The Chatter server URL.verbose(optional): Set totrueto enable extra logging.pingInterval(optional): The number of seconds to wait before sending a ping message. Pings are used to keep the connection alive. The default is 30 seconds.
Example:
import { createClient } from "@manyducks.co/chatter";
const client = createClient({
url: "ws://localhost:3000",
verbose: true, // Enable debug logging
pingInterval: 15, // Send a ping every 15 seconds
});Returns: A ChatterClient instance.
Properties
client.state
The current connection state. This is an enum called ConnectionState. Possible values are Disconnected, Connecting, and Connected.
client.isConnected
true if the socket is open and ready. Shorthand for client.state === ConnectionState.Connected.
client.clientId
A persistent, unique UUID for the client, stored in local storage.
client.sessionId
An ephemeral UUID to identify this particular session. Stored in memory and regenerated if the page is reloaded.
Methods
client.onStateChange(callback)
Registers a function to be called when the connection state changes. Returns a function to unsubscribe the callback.
Example:
client.onStateChange((state) => {
if (state === ConnectionState.Connected) {
console.log("Connected to the server\!");
} else if (state === ConnectionState.Disconnected) {
console.log("Disconnected from the server.");
}
});client.on(proc, handler)
Registers a handler to be called when a procedure is invoked on this side of the connection.
Parameters:
proc: The procedure created withcreateProc.handler: A function that receives the incoming data and the connection object.
Example:
import { GREET_USER } from "./procedures";
client.on(GREET_USER, (name) => {
console.log(`Hello, ${name}!`);
});client.call(proc, data)
Calls a procedure on the server and returns its response.
Parameters:
proc: The procedure created withcreateProc.data: The data to send, which must match the procedure'stakesschema.
Returns: A Promise that resolves with the procedure's return value.
Example:
const sum = await client.call(ADD_NUMBERS, { a: 5, b: 10 });
console.log(`The sum is ${sum}`); // The sum is 15Server API
createServer(options?)
Initializes the server-side Chatter instance.
Parameters:
options(optional): An object with the following properties:getConnectionData: A function that runs after a client connects. The return value is added as thedatafield to the connection object.onOpen: A function that runs after a connection is initialized, but before it starts receiving events.onClose: A function that runs after a connection is closed.
Example:
import { createServer } from "@manyducks.co/chatter";
const server = createServer({
onOpen: (connection) => {
console.log(`Connection opened with client ${connection.clientId}`);
},
onClose: (connection, code, reason) => {
console.log(`Connection closed with client ${connection.clientId}`);
},
getConnectionData: async (info) => {
// This is where you would fetch data for the client, e.g., from a database.
// The resulting data is stored on the connection's `data` object.
const userData = {
username: "user" + Math.floor(Math.random() * 100),
};
return userData;
},
});Returns: A ChatterServer instance.
Methods
server.on(proc, handler)
Registers a handler to be called when a procedure is invoked on this side of the connection.
Example:
import { ADD_NUMBERS } from "./procedures";
server.on(ADD_NUMBERS, (data) => {
return data.a + data.b;
});server.broadcast(topics, proc, input?, options?)
Calls a procedure on all clients subscribed to a given topic or set of topics.
Parameters:
topics: A string or iterable of strings representing the topics to broadcast to.proc: The procedure to call.input(optional): The data to send. Must match the procedure'stakesschema.options(optional): An object that can specify a list of connections to exclude from the broadcast.
Example:
// Broadcast a welcome message to all clients in the "general" chat
server.broadcast("general", SEND_MESSAGE, { text: "Hello everyone!" });server.find(where)
Finds the first connection that matches the given criteria.
Parameters:
where: A function that takes a connection object and returns a truthy value for a match.
Returns: A ServerConnection instance or undefined.
Example:
const user = server.find((conn) => conn.data.username === "user123");
user.call(GREET_USER, "user123");server.filter(where)
Finds all connections that match the given criteria.
Parameters:
where: A function that takes a connection object and returns a truthy value for a match.
Returns: An array of ServerConnection instances.
Example:
const loggedInUsers = server.filter((conn) => conn.data.isLoggedIn);
console.log(`Found ${loggedInUsers.length} logged in users.`);Server Connection API
The connection object is the second argument passed to a procedure handler function. This object represents an open socket connection to a single client. This section details the methods and properties available on that object.
Example:
server.on(MY_PROCEDURE, (data, connection) => {
// `connection` is the object with the methods listed below
connection.subscribe("some-topic");
});Properties
connection.data
Custom data associated with the connection. This data is set in the getConnectionData function of the createServer options.
Methods
connection.call(proc, data)
Calls a procedure on the client and returns its response.
Parameters:
proc: The procedure created withcreateProc.data: The data to send, which must match the procedure'stakesschema.
Returns: A Promise that resolves with the procedure's return value.
Example:
// Send a message to a client and wait for a response
const response = await connection.call(GREET_USER, "Bob");
console.log(response);connection.broadcast(topics, proc, input?, options?)
Calls a procedure on all clients subscribed to a given topic or set of topics. By default, this connection is excluded.
Parameters:
topics: A string or iterable of strings representing the topics to broadcast to.proc: The procedure to call.input(optional): The data to send.options(optional): An object that can specify if this connection should be included in the broadcast.
Example:
// Broadcast a message to all other clients subscribed to the same topic
connection.broadcast("chat-room", CHAT_MESSAGE, { text: "Hello!" });connection.subscribe(topic)
Subscribes this connection to a topic.
Parameters:
topic: The topic to subscribe to.
Example:
connection.subscribe("chat-room-1");connection.unsubscribe(topic)
Unsubscribes this connection from a topic.
Parameters:
topic: The topic to unsubscribe from.
Example:
connection.unsubscribe("chat-room-1");connection.isSubscribed(topic)
Checks if the connection is subscribed to a topic.
Parameters:
topic: The topic to check.
Returns: A boolean.
Example:
if (connection.isSubscribed("chat-room-1")) {
// This user will get broadcasts for "chat-room-1"
}