Skip to content

Commit 2eeea76

Browse files
authored
fix!: enforce to use string for room-id (#27)
* fix: parsing roomId err in simple server * refactor!: enforce that roomId must be a utf8 string in the protocol This can make the behavior more explicit and easy to understand. It can also make the overall app easier to inspect and maintain * fix: check length with bytes * fix: type err about elo * fix: update rust crates to reflect the changes on the protocol
1 parent 72a4fd5 commit 2eeea76

File tree

29 files changed

+132
-157
lines changed

29 files changed

+132
-157
lines changed

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pnpm clean
3838
- **packages/loro-protocol**: Protocol types and binary encoders/decoders, bytes utilities, and `%ELO` container/crypto helpers (TypeScript). Key files: `src/{protocol,encoding,bytes,e2ee}.ts` with tests under `src/`.
3939
- **packages/loro-websocket**: WebSocket client and a `SimpleServer` (TypeScript).
4040
- Features: message fragmentation/reassembly (≤256 KiB), connection‑scoped keepalive frames (`"ping"/"pong"` text), permission hooks, optional persistence hooks.
41-
- **packages/loro-adaptors**: Adaptors that connect the WebSocket client to `loro-crdt` (`LoroAdaptor`, `LoroEphemeralAdaptor`) and `%ELO` (`EloLoroAdaptor`).
41+
- **packages/loro-adaptors**: Adaptors that connect the WebSocket client to `loro-crdt` (`LoroAdaptor`, `LoroEphemeralAdaptor`) and `%ELO` (`EloAdaptor`).
4242
- **examples/excalidraw-example**: React demo using `SimpleServer`; syncs a Loro doc and ephemeral presence.
4343
- **rust/**: Rust workspace mirroring the TS packages:
4444
- `rust/loro-protocol`: Encoder/decoder parity with JS (snapshot tests included).

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@ Tip: For a working reference, see `packages/loro-websocket/src/e2e.test.ts` whic
6969

7070
`%ELO` adds end‑to‑end encryption to Loro sync. The server never decrypts; it indexes plaintext headers only to support backfill and routing. Clients encrypt/decrypt using AES‑GCM with a 12‑byte IV and the exact encoded header bytes as AAD.
7171

72-
- TypeScript: use `EloLoroAdaptor` from `loro-adaptors` + `LoroWebsocketClient`.
72+
- TypeScript: use `EloAdaptor` from `loro-adaptors` + `LoroWebsocketClient`.
7373
- Provide a `getPrivateKey()` hook that resolves `{ keyId, key }` (Web Crypto CryptoKey or Uint8Array).
7474
- The adaptor packages updates into `%ELO` containers and decrypts incoming ones, applying to its internal `LoroDoc`.
7575

7676
Example (Node 18+):
7777

7878
```ts
7979
import { LoroWebsocketClient } from "loro-websocket/client";
80-
import { EloLoroAdaptor } from "loro-adaptors/loro";
80+
import { EloAdaptor } from "loro-adaptors/loro";
8181
import { WebSocket } from "ws";
8282
(globalThis as any).WebSocket =
8383
WebSocket as unknown as typeof globalThis.WebSocket;
@@ -89,7 +89,7 @@ const key = new Uint8Array([
8989

9090
const client = new LoroWebsocketClient({ url: "ws://localhost:8787" });
9191
await client.waitConnected();
92-
const adaptor = new EloLoroAdaptor({
92+
const adaptor = new EloAdaptor({
9393
getPrivateKey: async () => ({ keyId: "k1", key }),
9494
});
9595
const room = await client.join({ roomId: "elo-room", crdtAdaptor: adaptor });

llms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ Adaptors bridge the client to actual CRDT state:
183183

184184
- `LoroAdaptor` – Loro document (`const adaptor = new LoroAdaptor()`).
185185
- `LoroEphemeralAdaptor` – transient presence (`%EPH`).
186-
- `EloLoroAdaptor``%ELO` encrypted Loro with `getPrivateKey()`.
186+
- `EloAdaptor``%ELO` encrypted Loro with `getPrivateKey()`.
187187

188188
### 3.2 Constructor Options
189189

packages/loro-adaptors/src/elo-adaptor.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import {
1616
} from "loro-protocol";
1717

1818
// Minimal placeholder to avoid requiring DOM lib in this package
19-
interface CryptoKey {}
19+
interface CryptoKey { }
2020

21-
export interface EloLoroAdaptorConfig {
21+
export interface EloAdaptorConfig {
2222
getPrivateKey: (
2323
keyId?: string
2424
) => Promise<{ keyId: string; key: CryptoKey | Uint8Array }>;
@@ -36,7 +36,7 @@ export class EloAdaptor implements CrdtDocAdaptor {
3636
private doc: LoroDoc;
3737
private ctx?: CrdtAdaptorContext;
3838
private destroyed = false;
39-
private config: EloLoroAdaptorConfig;
39+
private config: EloAdaptorConfig;
4040
private localUpdateUnsubscribe?: () => void;
4141
private initServerVersion?: VersionVector;
4242
private hasReachedServerVersion = false;
@@ -48,15 +48,15 @@ export class EloAdaptor implements CrdtDocAdaptor {
4848
private lastSentVV?: Record<string, number>;
4949

5050
// Overloads to allow (config) or (doc, config)
51-
constructor(doc: LoroDoc, config: EloLoroAdaptorConfig);
52-
constructor(config: EloLoroAdaptorConfig);
51+
constructor(doc: LoroDoc, config: EloAdaptorConfig);
52+
constructor(config: EloAdaptorConfig);
5353
constructor(
54-
docOrConfig: LoroDoc | EloLoroAdaptorConfig,
55-
maybeConfig?: EloLoroAdaptorConfig
54+
docOrConfig: LoroDoc | EloAdaptorConfig,
55+
maybeConfig?: EloAdaptorConfig
5656
) {
5757
if (docOrConfig instanceof LoroDoc) {
5858
this.doc = docOrConfig;
59-
this.config = maybeConfig as EloLoroAdaptorConfig;
59+
this.config = maybeConfig as EloAdaptorConfig;
6060
} else {
6161
this.doc = new LoroDoc();
6262
this.config = docOrConfig;

packages/loro-adaptors/tests/elo-adaptor.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function hexToBytes(hex: string): Uint8Array {
1515
return out;
1616
}
1717

18-
describe("EloLoroAdaptor — snapshot join", () => {
18+
describe("EloAdaptor — snapshot join", () => {
1919
let doc: LoroDoc;
2020
let adaptor: EloAdaptor;
2121
const key = hexToBytes(KEY_HEX);
@@ -60,7 +60,7 @@ describe("EloLoroAdaptor — snapshot join", () => {
6060
});
6161
});
6262

63-
describe("EloLoroAdaptor — apply snapshot update", () => {
63+
describe("EloAdaptor — apply snapshot update", () => {
6464
it("applies a snapshot container and updates the doc", async () => {
6565
const key = hexToBytes(KEY_HEX);
6666
// Create a source doc with content
@@ -99,7 +99,7 @@ describe("EloLoroAdaptor — apply snapshot update", () => {
9999
});
100100
});
101101

102-
describe("EloLoroAdaptor — apply delta update", () => {
102+
describe("EloAdaptor — apply delta update", () => {
103103
it("applies a delta container update and updates the doc", async () => {
104104
const key = hexToBytes(KEY_HEX);
105105
// Create source update plaintext via standard update export

packages/loro-protocol/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { encode, decode, CrdtType, MessageType } from "loro-protocol";
2727
const join = encode({
2828
type: MessageType.JoinRequest,
2929
crdt: CrdtType.Loro,
30-
roomId: new TextEncoder().encode("room-1"),
30+
roomId: "room-1",
3131
auth: new Uint8Array(),
3232
version: new Uint8Array(),
3333
});

packages/loro-protocol/src/encoding.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,13 @@ export function encode(message: ProtocolMessage): Uint8Array {
8888
const crdtBytes = new TextEncoder().encode(message.crdt);
8989
writer.pushBytes(crdtBytes);
9090

91-
// Write room ID as varBytes
92-
if (typeof message.roomId === "string") {
93-
if (message.roomId.length > MAX_ROOM_ID_LENGTH) {
94-
throw new Error("Room ID too long");
95-
}
96-
writer.pushVarString(message.roomId);
97-
} else {
98-
if (message.roomId.length > MAX_ROOM_ID_LENGTH) {
99-
throw new Error("Room ID too long");
100-
}
101-
writer.pushVarBytes(message.roomId);
91+
// Write room ID as varString
92+
const roomIdBytes = new TextEncoder().encode(message.roomId);
93+
if (roomIdBytes.byteLength > MAX_ROOM_ID_LENGTH) {
94+
throw new Error("Room ID too long");
10295
}
96+
writer.pushVarBytes(roomIdBytes);
97+
10398

10499
// Write message type
105100
writer.pushByte(message.type);
@@ -209,10 +204,7 @@ export function decode(data: Uint8Array): ProtocolMessage {
209204
}
210205

211206
// Read room ID
212-
const roomId = reader.readVarBytes();
213-
if (roomId.length > 128) {
214-
throw new Error("Room ID exceeds maximum length of 128 bytes");
215-
}
207+
const roomId = reader.readVarString();
216208

217209
// Read message type
218210
const typeByte = reader.readByte();

packages/loro-protocol/src/protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const UpdateErrorCode = {
6666
export type UpdateErrorCode =
6767
(typeof UpdateErrorCode)[keyof typeof UpdateErrorCode];
6868

69-
export type RoomId = string | Uint8Array;
69+
export type RoomId = string;
7070

7171
export interface MessageBase {
7272
crdt: CrdtType;
Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`encoding snapshots > DocUpdate multiple updates 1`] = `"0x25594a53040102030403030301020304040506070108"`;
3+
exports[`encoding snapshots > DocUpdate multiple updates 1`] = `"0x25594a5309726f6f6d2d3132333403030301020304040506070108"`;
44

5-
exports[`encoding snapshots > DocUpdateFragment 1`] = `"0x254c4f5204010203040500000000000000000320000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"`;
5+
exports[`encoding snapshots > DocUpdateFragment 1`] = `"0x254c4f5209726f6f6d2d313233340500000000000000000320000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"`;
66

7-
exports[`encoding snapshots > DocUpdateFragmentHeader 1`] = `"0x254c4f52040102030404ff112233445566770a80c03e"`;
7+
exports[`encoding snapshots > DocUpdateFragmentHeader 1`] = `"0x254c4f5209726f6f6d2d3132333404ff112233445566770a80c03e"`;
88

9-
exports[`encoding snapshots > JoinError app error with appCode 1`] = `"0x25594a530401020304027f1a4170706c69636174696f6e207370656369666963206572726f720e71756f74615f6578636565646564"`;
9+
exports[`encoding snapshots > JoinError app error with appCode 1`] = `"0x25594a5309726f6f6d2d31323334027f1a4170706c69636174696f6e207370656369666963206572726f720e71756f74615f6578636565646564"`;
1010

11-
exports[`encoding snapshots > JoinError auth failed 1`] = `"0x254550480401020304020213496e76616c69642063726564656e7469616c73"`;
11+
exports[`encoding snapshots > JoinError auth failed 1`] = `"0x2545504809726f6f6d2d31323334020213496e76616c69642063726564656e7469616c73"`;
1212

13-
exports[`encoding snapshots > JoinError version unknown with receiver version 1`] = `"0x254c4f52040102030402011056657273696f6e206d69736d61746368026364"`;
13+
exports[`encoding snapshots > JoinError version unknown with receiver version 1`] = `"0x254c4f5209726f6f6d2d3132333402011056657273696f6e206d69736d61746368026364"`;
1414

15-
exports[`encoding snapshots > JoinRequest 1`] = `"0x254c4f52040102030400030a141e0328323c"`;
15+
exports[`encoding snapshots > JoinRequest 1`] = `"0x254c4f5209726f6f6d2d3132333400030a141e0328323c"`;
1616

17-
exports[`encoding snapshots > JoinResponseOk read 1`] = `"0x25594a530401020304010472656164030b162100"`;
17+
exports[`encoding snapshots > JoinResponseOk read 1`] = `"0x25594a5309726f6f6d2d31323334010472656164030b162100"`;
1818

19-
exports[`encoding snapshots > JoinResponseOk write + extra 1`] = `"0x254c4f52040102030401057772697465022c3703424d58"`;
19+
exports[`encoding snapshots > JoinResponseOk write + extra 1`] = `"0x254c4f5209726f6f6d2d3132333401057772697465022c3703424d58"`;
2020

21-
exports[`encoding snapshots > Leave 1`] = `"0x254c4f52040102030407"`;
21+
exports[`encoding snapshots > Leave 1`] = `"0x254c4f5209726f6f6d2d3132333407"`;
2222

23-
exports[`encoding snapshots > UpdateError app error with appCode 1`] = `"0x254c4f520401020304067f10437573746f6d20617070206572726f720f637573746f6d5f636f64655f313233"`;
23+
exports[`encoding snapshots > UpdateError app error with appCode 1`] = `"0x254c4f5209726f6f6d2d31323334067f10437573746f6d20617070206572726f720f637573746f6d5f636f64655f313233"`;
2424

25-
exports[`encoding snapshots > UpdateError fragment timeout with batchId 1`] = `"0x255941570401020304060710467261676d656e742074696d656f75740100000000000000"`;
25+
exports[`encoding snapshots > UpdateError fragment timeout with batchId 1`] = `"0x2559415709726f6f6d2d31323334060710467261676d656e742074696d656f75740100000000000000"`;
2626

27-
exports[`encoding snapshots > UpdateError permission denied 1`] = `"0x254c4f5204010203040603134e6f207772697465207065726d697373696f6e"`;
27+
exports[`encoding snapshots > UpdateError permission denied 1`] = `"0x254c4f5209726f6f6d2d313233340603134e6f207772697465207065726d697373696f6e"`;

packages/loro-protocol/tests/encoding.snap.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
// TODO: REVIEW ensure binary snapshots stay stable across changes to protocol
2020

2121
describe("encoding snapshots", () => {
22-
const roomId = new Uint8Array([1, 2, 3, 4]);
22+
const roomId = "room-1234";
2323

2424
it("JoinRequest", () => {
2525
const msg: JoinRequest = {

0 commit comments

Comments
 (0)