Rebasely Beta: data may be purged

examples/live-notes Public

TypeScript · 71 lines · 2.6 KiB · at 2c15282

// The sequencer for one note. Every edit to a note passes through its room, one at a time, so// the room decides the order and every editor applies the same ops in that order.//// Room knows nothing about Durable Objects or sockets: it reads and writes through a NoteStore// and answers each message with an Outcome. NoteRoom (note-room.ts) supplies SQLite storage and// does the sending, and the tests supply an in-memory store.import { OpError, apply, isNoop, transformPast, type Op } from "./ops.ts";import { parseClientMessage, type ServerMessage } from "./protocol.ts";// How many recent ops the room keeps. An editor whose base version is older than that can't be// caught up op by op and gets a fresh snapshot instead.export const LOG_SIZE = 200;export type Note = { text: string; version: number };export interface NoteStore {  // The saved note, or { text: "", version: 0 } for one nobody has written yet.  load(): Note;  // The logged ops after `version`, oldest first. The room only asks for the last LOG_SIZE.  opsSince(version: number): Op[];  // Saves the note at its new version together with the op that produced it, and trims the  // log to the last LOG_SIZE ops. Must be atomic.  commit(note: Note, op: Op): void;}// What to send after a message: `reply` goes back to the sender, `broadcast` to every other// editor of the note.export type Outcome = { reply: ServerMessage; broadcast?: ServerMessage };export class Room {  #store: NoteStore;  #note: Note;  constructor(store: NoteStore) {    this.#store = store;    this.#note = store.load();  }  get note(): Note {    return this.#note;  }  snapshot(): ServerMessage {    return { type: "snapshot", text: this.#note.text, version: this.#note.version };  }  receive(raw: string): Outcome {    const msg = parseClientMessage(raw);    if (!msg) return { reply: { type: "error", message: "expected an op message" } };    const { text, version } = this.#note;    // A base from the future, or one older than the log reaches, can't be transformed.    if (msg.base > version || version - msg.base > LOG_SIZE) return { reply: this.snapshot() };    const op = transformPast(msg.op, this.#store.opsSince(msg.base));    if (isNoop(op)) return { reply: { type: "ack", version } };    let next: string;    try {      next = apply(text, op);    } catch (e) {      if (e instanceof OpError) return { reply: this.snapshot() };      throw e;    }    const note = { text: next, version: version + 1 };    this.#store.commit(note, op);    this.#note = note;    return { reply: { type: "ack", version: note.version }, broadcast: { type: "op", version: note.version, op } };  }}