|
| 1 | +# loro-websocket |
| 2 | + |
| 3 | +WebSocket client and a minimal SimpleServer for syncing Loro CRDTs. Supports message fragmentation/reassembly (≤256 KiB), connection‑scoped keepalive ("ping"/"pong"), permission hooks, optional persistence hooks, and routing of %ELO end‑to‑end encrypted updates. |
| 4 | + |
| 5 | +## Install |
| 6 | + |
| 7 | +```bash |
| 8 | +pnpm add loro-websocket loro-adaptors loro-protocol |
| 9 | +# plus peer dep in your app |
| 10 | +pnpm add loro-crdt |
| 11 | +``` |
| 12 | + |
| 13 | +## Client |
| 14 | + |
| 15 | +```ts |
| 16 | +// In Node, provide a WebSocket implementation |
| 17 | +import { WebSocket } from "ws"; |
| 18 | +(globalThis as any).WebSocket = WebSocket as unknown as typeof globalThis.WebSocket; |
| 19 | + |
| 20 | +import { LoroWebsocketClient } from "loro-websocket"; |
| 21 | +import { createLoroAdaptor } from "loro-adaptors"; |
| 22 | + |
| 23 | +const client = new LoroWebsocketClient({ url: "ws://localhost:8787" }); |
| 24 | +await client.waitConnected(); |
| 25 | + |
| 26 | +const adaptor = createLoroAdaptor({ peerId: 1 }); |
| 27 | +const room = await client.join({ roomId: "demo", crdtAdaptor: adaptor }); |
| 28 | + |
| 29 | +// Edit |
| 30 | +const text = adaptor.getDoc().getText("content"); |
| 31 | +text.insert(0, "Hello, Loro!"); |
| 32 | +adaptor.getDoc().commit(); |
| 33 | + |
| 34 | +await room.destroy(); |
| 35 | +``` |
| 36 | + |
| 37 | +%ELO (end‑to‑end encrypted Loro) using `EloLoroAdaptor`: |
| 38 | + |
| 39 | +```ts |
| 40 | +import { LoroWebsocketClient } from "loro-websocket"; |
| 41 | +import { EloLoroAdaptor } from "loro-adaptors"; |
| 42 | + |
| 43 | +const key = new Uint8Array(32); key[0] = 1; |
| 44 | +const client = new LoroWebsocketClient({ url: "ws://localhost:8787" }); |
| 45 | +await client.waitConnected(); |
| 46 | + |
| 47 | +const adaptor = new EloLoroAdaptor({ getPrivateKey: async () => ({ keyId: "k1", key }) }); |
| 48 | +const room = await client.join({ roomId: "secure-room", crdtAdaptor: adaptor }); |
| 49 | + |
| 50 | +adaptor.getDoc().getText("t").insert(0, "secret"); |
| 51 | +adaptor.getDoc().commit(); |
| 52 | +``` |
| 53 | + |
| 54 | +## SimpleServer |
| 55 | + |
| 56 | +```ts |
| 57 | +import { SimpleServer } from "loro-websocket/server"; |
| 58 | + |
| 59 | +const server = new SimpleServer({ |
| 60 | + port: 8787, |
| 61 | + authenticate: async (_roomId, _crdt, auth) => { |
| 62 | + // return "read" | "write" | null |
| 63 | + return new TextDecoder().decode(auth) === "readonly" ? "read" : "write"; |
| 64 | + }, |
| 65 | + onLoadDocument: async (_roomId, _crdt) => null, |
| 66 | + onSaveDocument: async (_roomId, _crdt, _data) => {}, |
| 67 | + saveInterval: 60_000, |
| 68 | +}); |
| 69 | +await server.start(); |
| 70 | +``` |
| 71 | + |
| 72 | +## Behavior |
| 73 | + |
| 74 | +- Fragmentation: oversize `DocUpdate` payloads are split into `DocUpdateFragmentHeader` + `DocUpdateFragment` frames and reassembled. |
| 75 | +- Keepalive: text frames "ping" and "pong" are connection‑scoped and bypass the envelope. |
| 76 | +- Permissions: pass an `authenticate` hook to return `"read" | "write" | null` per join. |
| 77 | +- Persistence: optionally `onLoadDocument`/`onSaveDocument` snapshots for `%LOR` documents. |
| 78 | +- %ELO: server indexes plaintext headers to backfill encrypted deltas; ciphertext is not decrypted. |
| 79 | + |
| 80 | +## Node/Web Compatibility |
| 81 | + |
| 82 | +- Client works in browsers (native WebSocket) and Node (supply `globalThis.WebSocket`). |
| 83 | +- %ELO crypto relies on Web Crypto via `loro-protocol` helpers (Node 18+ provides it). |
| 84 | + |
| 85 | +## License |
| 86 | + |
| 87 | +MIT |
| 88 | + |
0 commit comments