Rebasely Beta: data may be purged

examples/live-notes Public

TypeScript · 133 lines · 4.3 KiB · at 99dd857

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), String(i));    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);  });});