examples/live-notes Public
import assert from "node:assert/strict";import { describe, test } from "node:test";import { Session, moveCaret } from "../client/session.ts";import { apply, del, insert } from "../src/ops.ts";import type { ClientMessage, ServerMessage } from "../src/protocol.ts";import { Room } from "../src/room.ts";import { MemoryStore } from "./helpers/memory-store.ts";import { randomOp, rng } from "./helpers/random.ts";// A room and some editors joined by in-order queues, delivered whenever the test says so.class Network { room = new Room(new MemoryStore()); editors: { session: Session; up: ClientMessage[]; down: ServerMessage[] }[] = []; join(): Session { const up: ClientMessage[] = []; const session = new Session((msg) => up.push(msg)); session.receive(this.room.snapshot()); this.editors.push({ session, up, down: [] }); return session; } // Delivers the oldest frame from editor `i` to the room. deliverUp(i: number): void { const from = this.editors[i]; const msg = from.up.shift(); if (!msg) return; const out = this.room.receive(JSON.stringify(msg)); from.down.push(out.reply); if (out.broadcast) for (const e of this.editors) if (e !== from) e.down.push(out.broadcast); } // Delivers the oldest frame from the room to editor `i`. deliverDown(i: number): void { const msg = this.editors[i].down.shift(); if (msg) this.editors[i].session.receive(msg); } drain(): void { while (this.editors.some((e) => e.up.length || e.down.length)) { for (let i = 0; i < this.editors.length; i++) { this.deliverUp(i); this.deliverDown(i); } } }}describe("Session", () => { test("sends one op at a time and folds what is typed meanwhile", () => { const net = new Network(); const a = net.join(); a.edit("h"); a.edit("hi"); a.edit("hi!"); assert.equal(net.editors[0].up.length, 1); assert.equal(a.pending, 2); net.deliverUp(0); net.deliverDown(0); assert.deepEqual(net.editors[0].up, [{ type: "op", base: 1, op: insert(1, "i!") }]); net.drain(); assert.equal(a.pending, 0); assert.deepEqual(net.room.note, { text: "hi!", version: 2 }); }); test("moves a remote op past its own pending edit", () => { const net = new Network(); const a = net.join(); const b = net.join(); a.edit("milk"); net.drain(); // Both type at once: a at the start, b at the end. a.edit("oat milk"); b.edit("milk!"); net.deliverUp(1); net.deliverUp(0); // a learns of b's edit before its own ack. net.deliverDown(0); assert.equal(a.text, "oat milk!"); net.drain(); assert.equal(b.text, "oat milk!"); assert.equal(net.room.note.text, "oat milk!"); }); test("a snapshot replaces the text and drops pending edits", () => { const a = new Session(() => {}); a.edit("draft"); assert.equal(a.receive({ type: "snapshot", text: "saved", version: 9 }), "reset"); assert.equal(a.text, "saved"); assert.equal(a.version, 9); assert.equal(a.pending, 0); }); test("editors typing at random converge on the room's text", () => { for (let seed = 1; seed <= 40; seed++) { const next = rng(seed); const net = new Network(); const sessions = [net.join(), net.join(), net.join()]; for (let step = 0; step < 300; step++) { const i = Math.floor(next() * sessions.length); const roll = next(); if (roll < 0.4) sessions[i].edit(apply(sessions[i].text, randomOp(sessions[i].text, next))); else if (roll < 0.7) net.deliverUp(i); else net.deliverDown(i); } net.drain(); for (const s of sessions) { assert.equal(s.text, net.room.note.text, `seed ${seed}`); assert.equal(s.version, net.room.note.version, `seed ${seed}`); } } });});describe("moveCaret", () => { test("stays put for edits after it", () => { assert.equal(moveCaret(3, insert(5, "abc")), 3); assert.equal(moveCaret(3, del(3, 2)), 3); }); test("moves with edits before it", () => { assert.equal(moveCaret(5, insert(1, "ab")), 7); assert.equal(moveCaret(5, del(1, 2)), 3); }); test("lands at the start of a deleted range it was inside", () => { assert.equal(moveCaret(5, del(2, 6)), 2); }); test("text inserted at the caret goes after it", () => { assert.equal(moveCaret(4, insert(4, "x")), 4); });});