
Security News
CVE Volume Surges Past 48,000 in 2025 as WordPress Plugin Ecosystem Drives Growth
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.
loro-websocket
Advanced tools
WebSocket client and SimpleServer for syncing CRDTs base on loro-protocol
WebSocket client and a minimal SimpleServer for syncing Loro CRDTs. The client provides connection status events, auto‑reconnect with exponential backoff, latency tracking via ping/pong, safe fragmentation/reassembly for large updates, and seamless room rejoin across reconnects.
pnpm add loro-websocket loro-adaptors loro-protocol
# plus peer dep in your app
pnpm add loro-crdt
// In Node, provide a WebSocket implementation
import { WebSocket } from "ws";
(globalThis as any).WebSocket =
WebSocket as unknown as typeof globalThis.WebSocket;
import { LoroWebsocketClient, ClientStatus } from "loro-websocket";
import { LoroAdaptor } from "loro-adaptors/loro";
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
// React to connection status
const offStatus = client.onStatusChange(s => console.log("status:", s));
await client.waitConnected();
// Join a room with a CRDT adaptor
const adaptor = new LoroAdaptor();
const room = await client.join({ roomId: "demo", crdtAdaptor: adaptor });
// Edit document and sync
const text = adaptor.getDoc().getText("content");
text.insert(0, "Hello, Loro!");
adaptor.getDoc().commit();
await room.destroy();
offStatus();
%ELO (end‑to‑end encrypted Loro) using EloAdaptor:
import { LoroWebsocketClient } from "loro-websocket";
import { EloAdaptor } from "loro-adaptors/loro";
const key = new Uint8Array(32);
key[0] = 1;
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
await client.waitConnected();
const adaptor = new EloAdaptor({
getPrivateKey: async () => ({ keyId: "k1", key }),
});
await client.join({ roomId: "secure-room", crdtAdaptor: adaptor });
adaptor.getDoc().getText("t").insert(0, "secret");
adaptor.getDoc().commit();
new LoroWebsocketClient(options: LoroWebsocketClientOptions)
interface LoroWebsocketClientOptions {
url: string; // WebSocket URL (ws:// or wss://)
pingIntervalMs?: number; // Periodic ping interval (default 30_000ms)
disablePing?: boolean; // Disable periodic pings entirely
onWsClose?: () => void; // Low‑level ws close callback (before status transitions)
}
// Status values (no enums)
const ClientStatus = {
Connecting: "connecting",
Connected: "connected",
Disconnected: "disconnected",
} as const;
type ClientStatusValue = typeof ClientStatus[keyof typeof ClientStatus];
Connection
waitConnected(): Promise<void> resolves once the socket is open.connect(): Promise<void> manually initiate/resume connection; also re‑enables auto‑reconnect after a close().close(): void manually close, wait for buffered frames to flush, and stop auto‑reconnect (status → Disconnected).destroy(): void teardown: remove listeners, stop timers, reject pending waitConnected/ping calls, flush buffers, close socket, status → Disconnected.Status and events
getStatus(): ClientStatusValue returns the current status.onStatusChange(cb): () => void subscribes to status changes; immediately calls cb with the current status; returns unsubscribe.Latency
ping(timeoutMs?: number): Promise<void> sends an app‑level ping and resolves on pong or rejects on timeout.getLatency(): number | undefined returns the last measured ping round‑trip time (ms).onLatency(cb): () => void subscribes to latency updates; if a value exists, emits immediately; returns unsubscribe.Rooms
join({ roomId, crdtAdaptor, auth? }): Promise<LoroWebsocketClientRoom> joins a room for a given CRDT type via its adaptor. Optional auth carries application-defined join metadata (e.g., auth/session tokens) and is forwarded to the server’s authenticate hook.
leave(): Promise<void>, waitForReachingServerVersion(): Promise<void>, destroy(): Promise<void>.Status values
Connecting: initial or manual connect() in progress.Connected: websocket is open and usable.Disconnected: socket closed. Auto‑reconnect keeps retrying unless close()/destroy() stop it.Auto‑reconnect
offline; the client stays in Disconnected until an online event arrives, at which point the next retry fires immediately.close() or destroy() turns off auto‑retry. Invoke connect() later to restart the process with a fresh backoff window.4400–4499 range or a reason of permission_changed / room_closed, the client assumes the server intentionally removed the connection and does not schedule reconnect attempts. This mirrors the Durable Object kick semantics used in @loro-protocol/do.Periodic pings
"ping" every 30s (configurable via pingIntervalMs) and expects a "pong". This keeps the connection alive and measures round‑trip latency.disablePing: true to turn off the timer.On‑demand ping
ping(timeoutMs) to send a ping and await the next pong; rejects on timeout.onLatency(cb) fires whenever a pong is observed and updates getLatency().Join handshake
join() sends a JoinRequest with the adaptor’s CRDT type, version, and optional join metadata (auth bytes).JoinResponseOk, the adaptor reconciles to the server’s version and begins streaming updates.Rejoin after reconnect
JoinRequest for each active room and the adaptor re‑syncs.VersionUnknown, the client retries using adaptor.getAlternativeVersion() or an empty version as a fallback.%ELO, updates that arrive right after join may be buffered briefly to cover backfills that race the join.DocUpdateFragmentHeader + DocUpdateFragment messages and reassembled on the receiver.MAX_MESSAGE_SIZE). No action is required by callers.import { SimpleServer } from "loro-websocket/server";
const server = new SimpleServer({
port: 8787,
authenticate: async (_roomId, _crdt, auth) => {
// join metadata is passed as `auth`; return "read" | "write" | null
return new TextDecoder().decode(auth) === "readonly" ? "read" : "write";
},
onLoadDocument: async (_roomId, _crdt) => null,
onSaveDocument: async (_roomId, _crdt, _data) => {},
saveInterval: 60_000,
});
await server.start();
// Later: await server.stop(); flushes any buffered frames before terminating clients
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
client.onStatusChange(s => console.log("status:", s));
// Later, stop auto‑reconnect
client.close(); // status → Disconnected
// Resume auto‑reconnect
await client.connect(); // status: Connecting → Connected
const off = client.onLatency(ms => console.log("latency:", ms));
await client.ping(2000);
console.log("last RTT:", client.getLatency());
off();
const adaptor = new LoroAdaptor();
adaptor.getDoc().setPeerId(42);
await client.join({
roomId: "project-123",
crdtAdaptor: adaptor,
auth: new TextEncoder().encode("write-token"), // application-defined join payload
});
WebSocket and window online/offline events.ws) via globalThis.WebSocket.globalThis.crypto.subtle); Node 18+ provides it.MIT
FAQs
WebSocket client and SimpleServer for syncing CRDTs base on loro-protocol
The npm package loro-websocket receives a total of 401 weekly downloads. As such, loro-websocket popularity was classified as not popular.
We found that loro-websocket demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.

Security News
Socket CEO Feross Aboukhadijeh joins Insecure Agents to discuss CVE remediation and why supply chain attacks require a different security approach.

Security News
Tailwind Labs laid off 75% of its engineering team after revenue dropped 80%, as LLMs redirect traffic away from documentation where developers discover paid products.