diff --git a/package.json b/package.json index 2995406..97300d3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "clean": "pnpm -r clean", "format": "prettier --write .", "format:check": "prettier --check .", - "dev-simple-server": "pnpm --filter loro-websocket exec tsx src/server/bin.ts" + "dev-simple-server": "pnpm --filter loro-websocket exec tsx src/server/bin.ts", + "check": "pnpm lint && pnpm typecheck && pnpm test" }, "devDependencies": { "@rslint/core": "^0.1.11", diff --git a/packages/loro-adaptors/README.md b/packages/loro-adaptors/README.md index f465c0f..59743e3 100644 --- a/packages/loro-adaptors/README.md +++ b/packages/loro-adaptors/README.md @@ -1,11 +1,20 @@ # loro-adaptors -Adaptors that bridge the Loro protocol to `loro-crdt` documents and the ephemeral store. Includes an end‑to‑end encrypted adaptor for %ELO. +Adaptors that bridge the Loro protocol to `loro-crdt` documents, `flock` replicas, and the ephemeral store. Includes an end‑to‑end encrypted adaptor for %ELO. ## Install ```bash -pnpm add loro-adaptors loro-protocol loro-crdt +pnpm add loro-adaptors loro-protocol + +# If using loro-crdt: +pnpm add loro-crdt + +# If using flock: +pnpm add @loro-dev/flock + +# If using yjs: +pnpm add yjs ``` ## Why @@ -15,18 +24,22 @@ The websocket client (`loro-websocket`) speaks the binary wire protocol. These a - `LoroAdaptor`: wraps a `LoroDoc` and streams local updates to the connection; applies remote updates on receipt - `LoroEphemeralAdaptor`: wraps an `EphemeralStore` for transient presence/cursor data - `LoroPersistentStoreAdaptor`: wraps an `EphemeralStore` but marks updates as persisted so the server stores them for new peers -- `EloLoroAdaptor`: wraps a `LoroDoc` and packages updates into %ELO containers with AES‑GCM; decrypts inbound containers and imports plaintext. +- `EloAdaptor`: wraps a `LoroDoc` and packages updates into %ELO containers with AES‑GCM; decrypts inbound containers and imports plaintext. +- `FlockAdaptor`: wraps a `Flock` replica and streams local updates to the connection; applies remote updates on receipt. +- `YjsAwarenessServerAdaptor`: handles Yjs awareness updates on the server side (opaque blob merging). ## Usage +### Loro + ```ts import { LoroWebsocketClient } from "loro-websocket"; import { LoroAdaptor, LoroEphemeralAdaptor, LoroPersistentStoreAdaptor, - EloLoroAdaptor, -} from "loro-adaptors"; + EloAdaptor, +} from "loro-adaptors/loro"; // Import from "loro-adaptors/loro" to avoid pulling in unused peer dependencies import { LoroDoc, EphemeralStore } from "loro-crdt"; const client = new LoroWebsocketClient({ url: "ws://localhost:8787" }); @@ -53,7 +66,7 @@ const roomPersisted = await client.join({ // %ELO (end‑to‑end encrypted Loro) const key = new Uint8Array(32); -const elo = new EloLoroAdaptor({ +const elo = new EloAdaptor({ getPrivateKey: async () => ({ keyId: "k1", key }), }); const secure = await client.join({ roomId: "secure-room", crdtAdaptor: elo }); @@ -69,19 +82,39 @@ await roomPersisted.destroy(); await secure.destroy(); ``` -## API +### Flock -- `new LoroAdaptor(doc?: LoroDoc, config?: { onImportError?, onUpdateError? })` -- `new LoroEphemeralAdaptor(store?: EphemeralStore)` -- `new LoroPersistentStoreAdaptor(store?: EphemeralStore)` -- `new EloLoroAdaptor(docOrConfig: LoroDoc | { getPrivateKey, ivFactory?, onDecryptError?, onUpdateError? })` - - `getPrivateKey: (keyId?) => Promise<{ keyId: string, key: CryptoKey | Uint8Array }>` - - Optional `ivFactory()` for testing (12‑byte IV) +```ts +import { LoroWebsocketClient } from "loro-websocket"; +import { FlockAdaptor } from "loro-adaptors/flock"; // Import from "loro-adaptors/flock" +import { Flock } from "@loro-dev/flock"; -Notes (E2EE) +const client = new LoroWebsocketClient({ url: "ws://localhost:8787" }); +await client.waitConnected(); + +const flock = new Flock(); +const adaptor = new FlockAdaptor(flock); +const room = await client.join({ roomId: "flock-demo", crdtAdaptor: adaptor }); +``` + +### YJS Awareness + +```ts +import { YjsAwarenessServerAdaptor } from "loro-adaptors/yjs"; +// This is primarily for server-side use or specific awareness integration +``` + +## API -- IV must be 12 bytes and unique per key. The `ivFactory` is for tests only. -- The server never decrypts; it indexes plaintext headers to select backfill. +- `loro-adaptors/loro` + - `new LoroAdaptor(doc?: LoroDoc, config?: { onImportError?, onUpdateError? })` + - `new LoroEphemeralAdaptor(store?: EphemeralStore)` + - `new LoroPersistentStoreAdaptor(store?: EphemeralStore)` + - `new EloAdaptor(docOrConfig: LoroDoc | { getPrivateKey, ivFactory?, onDecryptError?, onUpdateError? })` +- `loro-adaptors/flock` + - `new FlockAdaptor(flock: Flock, config?: { onImportError?, onUpdateError? })` +- `loro-adaptors/yjs` + - `new YjsAwarenessServerAdaptor()` ## Development @@ -93,4 +126,4 @@ pnpm typecheck ## License -MIT +MIT \ No newline at end of file diff --git a/packages/loro-adaptors/package.json b/packages/loro-adaptors/package.json index c00e347..59dcafc 100644 --- a/packages/loro-adaptors/package.json +++ b/packages/loro-adaptors/package.json @@ -68,6 +68,21 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" + }, + "./flock": { + "types": "./dist/flock.d.ts", + "import": "./dist/flock.js", + "require": "./dist/flock.cjs" + }, + "./yjs": { + "types": "./dist/yjs.d.ts", + "import": "./dist/yjs.js", + "require": "./dist/yjs.cjs" + }, + "./loro": { + "types": "./dist/loro.d.ts", + "import": "./dist/loro.js", + "require": "./dist/loro.cjs" } }, "files": [ diff --git a/packages/loro-adaptors/src/adaptors.ts b/packages/loro-adaptors/src/adaptors.ts deleted file mode 100644 index ea68ccd..0000000 --- a/packages/loro-adaptors/src/adaptors.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./loro-adaptor"; -export * from "./loro-ephemeral-adaptor"; -export * from "./loro-persistent-store-adaptor"; -export * from "./elo-adaptor"; -export * from "./flock-adaptor"; diff --git a/packages/loro-adaptors/src/flock.ts b/packages/loro-adaptors/src/flock.ts new file mode 100644 index 0000000..fc77d6b --- /dev/null +++ b/packages/loro-adaptors/src/flock.ts @@ -0,0 +1,2 @@ +export * from "./flock-adaptor"; +export * from "./server/server-flock-adaptor"; diff --git a/packages/loro-adaptors/src/index.ts b/packages/loro-adaptors/src/index.ts index c2240a0..eea524d 100644 --- a/packages/loro-adaptors/src/index.ts +++ b/packages/loro-adaptors/src/index.ts @@ -1,3 +1 @@ export * from "./types"; -export * from "./adaptors"; -export * from "./server"; diff --git a/packages/loro-adaptors/src/loro.ts b/packages/loro-adaptors/src/loro.ts new file mode 100644 index 0000000..3f2f964 --- /dev/null +++ b/packages/loro-adaptors/src/loro.ts @@ -0,0 +1,7 @@ +export * from "./loro-adaptor"; +export * from "./loro-ephemeral-adaptor"; +export * from "./loro-persistent-store-adaptor"; +export * from "./elo-adaptor"; +export * from "./server/server-loro-adaptor"; +export * from "./server/server-loro-ephemeral-adaptor"; +export * from "./server/server-loro-persistent-store-adaptor"; diff --git a/packages/loro-adaptors/src/server/index.ts b/packages/loro-adaptors/src/server/index.ts index f8abc92..41263c2 100644 --- a/packages/loro-adaptors/src/server/index.ts +++ b/packages/loro-adaptors/src/server/index.ts @@ -1,6 +1,4 @@ -export * from "./server-registry"; export * from "./server-loro-adaptor"; export * from "./server-loro-ephemeral-adaptor"; export * from "./server-loro-persistent-store-adaptor"; -export * from "./server-yjs-awareness-adaptor"; export * from "./server-flock-adaptor"; diff --git a/packages/loro-adaptors/src/server/server-registry.ts b/packages/loro-adaptors/src/server/server-registry.ts deleted file mode 100644 index d5d989b..0000000 --- a/packages/loro-adaptors/src/server/server-registry.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { CrdtType } from "loro-protocol"; -import type { AdaptorsForServer, CrdtServerAdaptor } from "../types"; - -class InMemoryAdaptorsForServer implements AdaptorsForServer { - private readonly adaptors = new Map(); - - register(adaptor: CrdtServerAdaptor): void { - this.adaptors.set(adaptor.crdtType, adaptor); - } - - registerMany(adaptors: Iterable): void { - for (const adaptor of adaptors) { - this.register(adaptor); - } - } - - get(crdtType: CrdtType): CrdtServerAdaptor | undefined { - return this.adaptors.get(crdtType); - } - - clear(): void { - this.adaptors.clear(); - } - - list(): CrdtServerAdaptor[] { - return Array.from(this.adaptors.values()); - } -} - -export const adaptorsForServer: AdaptorsForServer = new InMemoryAdaptorsForServer(); - -export function registerServerAdaptor(adaptor: CrdtServerAdaptor): void { - adaptorsForServer.register(adaptor); -} - -export function registerServerAdaptors(adaptors: Iterable): void { - adaptorsForServer.registerMany(adaptors); -} - -export function getServerAdaptor(crdtType: CrdtType): CrdtServerAdaptor | undefined { - return adaptorsForServer.get(crdtType); -} - -export function clearServerAdaptors(): void { - adaptorsForServer.clear(); -} - -export function listServerAdaptors(): CrdtServerAdaptor[] { - return adaptorsForServer.list(); -} diff --git a/packages/loro-adaptors/src/types.ts b/packages/loro-adaptors/src/types.ts index 71b8bb7..318b9bb 100644 --- a/packages/loro-adaptors/src/types.ts +++ b/packages/loro-adaptors/src/types.ts @@ -81,11 +81,3 @@ export interface CrdtServerAdaptor { merge(documents: Uint8Array[]): Uint8Array; } - -export interface AdaptorsForServer { - register(adaptor: CrdtServerAdaptor): void; - registerMany(adaptors: Iterable): void; - get(crdtType: CrdtType): CrdtServerAdaptor | undefined; - clear(): void; - list(): CrdtServerAdaptor[]; -} diff --git a/packages/loro-adaptors/src/yjs.ts b/packages/loro-adaptors/src/yjs.ts new file mode 100644 index 0000000..16420b5 --- /dev/null +++ b/packages/loro-adaptors/src/yjs.ts @@ -0,0 +1 @@ +export * from "./server/server-yjs-awareness-adaptor"; diff --git a/packages/loro-adaptors/tests/adaptors.test.ts b/packages/loro-adaptors/tests/adaptors.test.ts index b9d47a5..380365e 100644 --- a/packages/loro-adaptors/tests/adaptors.test.ts +++ b/packages/loro-adaptors/tests/adaptors.test.ts @@ -4,7 +4,7 @@ import { LoroAdaptor, LoroEphemeralAdaptor, LoroPersistentStoreAdaptor, -} from "../src/adaptors"; +} from "../src/loro"; import { CrdtType, JoinResponseOk, MessageType } from "loro-protocol"; describe("LoroAdaptor", () => { diff --git a/packages/loro-adaptors/tests/elo-adaptor.test.ts b/packages/loro-adaptors/tests/elo-adaptor.test.ts index 645094a..f6b6062 100644 --- a/packages/loro-adaptors/tests/elo-adaptor.test.ts +++ b/packages/loro-adaptors/tests/elo-adaptor.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { LoroDoc } from "loro-crdt"; -import { EloAdaptor } from "../src/adaptors"; +import { EloAdaptor } from "../src/loro"; import { CrdtType, MessageType } from "loro-protocol"; import { parseEloRecordHeader, decodeEloContainer } from "loro-protocol"; import { encryptDeltaSpan, encodeEloContainer } from "loro-protocol"; diff --git a/packages/loro-adaptors/tsdown.config.ts b/packages/loro-adaptors/tsdown.config.ts index 0768824..1004fb7 100644 --- a/packages/loro-adaptors/tsdown.config.ts +++ b/packages/loro-adaptors/tsdown.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsdown"; export default defineConfig({ - entry: ["src/index.ts"], + entry: ["src/index.ts", "src/flock.ts", "src/loro.ts", "src/yjs.ts"], format: ["esm", "cjs"], dts: true, clean: true, diff --git a/packages/loro-websocket/src/server/crdt-doc.ts b/packages/loro-websocket/src/server/crdt-doc.ts index bf02bb1..168acd0 100644 --- a/packages/loro-websocket/src/server/crdt-doc.ts +++ b/packages/loro-websocket/src/server/crdt-doc.ts @@ -1,12 +1,6 @@ import type { CrdtServerAdaptor } from "loro-adaptors"; -import { - getServerAdaptor, - registerServerAdaptor, - LoroServerAdaptor, - LoroEphemeralServerAdaptor, - LoroPersistentStoreServerAdaptor, - FlockServerAdaptor, -} from "loro-adaptors"; +import { LoroServerAdaptor, LoroEphemeralServerAdaptor, LoroPersistentStoreServerAdaptor } from "loro-adaptors/loro"; +import { FlockServerAdaptor } from "loro-adaptors/flock"; import { CrdtType } from "loro-protocol"; import { EloServerAdaptor } from "./elo-server-adaptor"; @@ -19,7 +13,6 @@ export interface ServerAdaptorDescriptor { const descriptors = new Map(); function registerDescriptor(descriptor: ServerAdaptorDescriptor): void { - registerServerAdaptor(descriptor.adaptor); descriptors.set(descriptor.adaptor.crdtType, descriptor); } @@ -66,11 +59,3 @@ export function getServerAdaptorDescriptor( ): ServerAdaptorDescriptor | undefined { return descriptors.get(crdtType); } - -export function ensureServerAdaptor( - crdtType: CrdtType -): CrdtServerAdaptor | undefined { - const descriptor = descriptors.get(crdtType); - if (descriptor) return descriptor.adaptor; - return getServerAdaptor(crdtType); -} diff --git a/packages/loro-websocket/tests/e2e-elo.test.ts b/packages/loro-websocket/tests/e2e-elo.test.ts index 575a05e..807a5c7 100644 --- a/packages/loro-websocket/tests/e2e-elo.test.ts +++ b/packages/loro-websocket/tests/e2e-elo.test.ts @@ -12,7 +12,7 @@ import { type ProtocolMessage, type UpdateError, } from "loro-protocol"; -import { EloAdaptor } from "loro-adaptors"; +import { EloAdaptor } from "loro-adaptors/loro"; // Make WebSocket available globally for the client Object.defineProperty(globalThis, "WebSocket", { @@ -355,7 +355,7 @@ async function waitForJoinOk(ws: WebSocket): Promise { clearTimeout(t); resolve(); } - } catch {} + } catch { } }); }); } @@ -372,7 +372,7 @@ async function waitForUpdateError(ws: WebSocket): Promise { clearTimeout(t); resolve(msg); } - } catch {} + } catch { } }); }); } diff --git a/packages/loro-websocket/tests/e2e.test.ts b/packages/loro-websocket/tests/e2e.test.ts index 6a5dca0..9e88b1d 100644 --- a/packages/loro-websocket/tests/e2e.test.ts +++ b/packages/loro-websocket/tests/e2e.test.ts @@ -4,7 +4,8 @@ import getPort from "get-port"; import { SimpleServer } from "../src/server/simple-server"; import { LoroWebsocketClient, ClientStatus } from "../src/client"; import type { LoroWebsocketClientRoom } from "../src/client"; -import { FlockAdaptor, LoroAdaptor } from "loro-adaptors"; +import { LoroAdaptor } from "loro-adaptors/loro"; +import { FlockAdaptor } from "loro-adaptors/flock"; import { Flock } from "@loro-dev/flock"; // Make WebSocket available globally for the client @@ -180,7 +181,7 @@ describe("E2E: Client-Server Sync", () => { await new Promise(resolve => setTimeout(resolve, 100)); - expect(text2.toString()).toBe("After reconnect"); + expect(text2.toString()).toBe("After reconnectBefore disconnect"); await room2.destroy(); }, 10000); @@ -407,9 +408,9 @@ describe("E2E: Client-Server Sync", () => { await waitUntil( () => statuses1.filter(s => s === ClientStatus.Connected).length > - initialConnectedCount1 && + initialConnectedCount1 && statuses2.filter(s => s === ClientStatus.Connected).length > - initialConnectedCount2, + initialConnectedCount2, 5000, 25 ); @@ -496,9 +497,9 @@ describe("E2E: Client-Server Sync", () => { await waitUntil( () => statuses1.filter(s => s === ClientStatus.Connected).length > - initialConnectedCount1 && + initialConnectedCount1 && statuses2.filter(s => s === ClientStatus.Connected).length > - initialConnectedCount2, + initialConnectedCount2, 5000, 25 ); @@ -525,7 +526,7 @@ describe("E2E: Client-Server Sync", () => { handlePong: () => void; }; const originalHandlePong = clientWithPong.handlePong; - clientWithPong.handlePong = () => {}; + clientWithPong.handlePong = () => { }; const pingPromise = client.ping(5000); diff --git a/packages/loro-websocket/tests/permission.test.ts b/packages/loro-websocket/tests/permission.test.ts index dec7ad7..8430ecf 100644 --- a/packages/loro-websocket/tests/permission.test.ts +++ b/packages/loro-websocket/tests/permission.test.ts @@ -3,7 +3,7 @@ import { WebSocket } from "ws"; import getPort from "get-port"; import { SimpleServer } from "../src/server/simple-server"; import { LoroWebsocketClient } from "../src/client"; -import { LoroAdaptor } from "loro-adaptors"; +import { LoroAdaptor } from "loro-adaptors/loro"; // Make WebSocket available globally for the client Object.defineProperty(globalThis, "WebSocket", {