diff --git a/src/protocol.ts b/src/protocol.ts index 24320f0..bf45488 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -6,10 +6,17 @@ export type ClientMessage = | { type: "update"; data: number[] } // yjs update as byte array | { type: "awareness"; data: number[] }; +export type AwarenessState = { + clientId: number; + cursor?: { line: number; col: number }; + selection?: { startLine: number; startCol: number; endLine: number; endCol: number }; + name?: string; +}; + export type ServerMessage = | { type: "sync"; data: number[] } // full yjs state | { type: "update"; data: number[] } - | { type: "awareness"; data: number[] } + | { type: "awareness"; data: AwarenessState } | { type: "peers"; count: number } | { type: "error"; message: string }; diff --git a/src/session.test.ts b/src/session.test.ts index 3750ed9..26703e1 100644 --- a/src/session.test.ts +++ b/src/session.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import * as Y from "yjs"; -import { getOrCreateSession, Session, type WsData } from "./session"; +import { getOrCreateSession, Session, type Client, type WsData } from "./session"; describe("Session", () => { test("creates yjs doc on init", () => { @@ -57,3 +57,26 @@ describe("getOrCreateSession", () => { expect(s1).not.toBe(s2); }); }); + +describe("awareness", () => { + test("broadcasts awareness to other clients", () => { + const session = new Session("test-room"); + const sent1: unknown[] = []; + const sent2: unknown[] = []; + + const client1 = { ws: { send: (m: string) => sent1.push(JSON.parse(m)) } } as Client; + const client2 = { ws: { send: (m: string) => sent2.push(JSON.parse(m)) } } as Client; + + session.join(client1); + session.join(client2); + + const awareness = { clientId: 1, cursor: { line: 5, col: 10 } }; + session.broadcastAwareness(client1, awareness); + + // client1 should NOT receive their own awareness + expect(sent1.filter(m => m.type === "awareness")).toHaveLength(0); + // client2 should receive it + expect(sent2.filter(m => m.type === "awareness")).toHaveLength(1); + expect(sent2.find(m => m.type === "awareness")?.data).toEqual(awareness); + }); +}); diff --git a/src/session.ts b/src/session.ts index b68b8e0..b4a6ba2 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,6 +1,6 @@ import type { ServerWebSocket } from "bun"; import * as Y from "yjs"; -import { encode } from "./protocol"; +import { encode, type AwarenessState } from "./protocol"; export interface WsData { room: string | null; @@ -84,6 +84,19 @@ export class Session { } } } + + broadcastAwareness(sender: Client, state: AwarenessState): void { + const message = encode({ type: "awareness", data: state }); + for (const client of this.clients) { + if (client !== sender) { + try { + client.ws.send(message); + } catch { + // client disconnected, ignore + } + } + } + } } // room name -> session