Rebasely Beta: data may be purged

examples/live-notes Public

TypeScript · 107 lines · 3.4 KiB · at 99dd857

// The editor page: a textarea bound to one note over a WebSocket. The note id comes from the// URL fragment, so /#groceries edits the note "groceries".import { Session, moveCaret } from "./session.ts";import type { Editor, ServerMessage } from "../src/protocol.ts";const NOTE_ID = /^[A-Za-z0-9_-]{1,64}$/;const area = document.querySelector<HTMLTextAreaElement>("#note")!;const status = document.querySelector<HTMLElement>("#status")!;const title = document.querySelector<HTMLElement>("#title")!;const people = document.querySelector<HTMLUListElement>("#here")!;function noteId(): string {  const id = decodeURIComponent(location.hash.slice(1));  return NOTE_ID.test(id) ? id : "scratch";}let socket: WebSocket | null = null;let retry = 0;// Who is here, from the room's latest presence message, and when each was last seen typing.let here: Editor[] = [];let me: string | null = null;const typing = new Map<string, number>();const TYPING_MS = 1500;function renderPeople(): void {  const now = Date.now();  people.replaceChildren(    ...here.map((editor) => {      const li = document.createElement("li");      li.style.setProperty("--color", editor.color);      li.textContent = editor.id === me ? `${editor.name} (you)` : editor.name;      if ((typing.get(editor.id) ?? 0) > now) li.classList.add("typing");      return li;    }),  );}function sawTyping(id: string): void {  typing.set(id, Date.now() + TYPING_MS);  renderPeople();  setTimeout(renderPeople, TYPING_MS + 50);}function connect(): void {  const id = noteId();  title.textContent = id;  document.title = `${id} · live-notes`;  area.disabled = true;  status.textContent = "Connecting";  const scheme = location.protocol === "https:" ? "wss" : "ws";  const ws = new WebSocket(`${scheme}://${location.host}/notes/${encodeURIComponent(id)}/ws`);  const session = new Session((msg) => ws.send(JSON.stringify(msg)));  socket = ws;  ws.addEventListener("message", (event) => {    const msg = JSON.parse(String(event.data)) as ServerMessage;    if (msg.type === "presence") {      here = msg.here;      if (msg.you) me = msg.you.id;      renderPeople();      return;    }    const { selectionStart, selectionEnd } = area;    const applied = session.receive(msg);    if (applied === "reset") {      area.value = session.text;      area.disabled = false;      area.focus();      retry = 0;      status.textContent = msg.type === "snapshot" && msg.reason ? `Edit undone: ${msg.reason}` : `Version ${session.version}`;    } else if (applied) {      area.value = session.text;      area.setSelectionRange(moveCaret(selectionStart, applied), moveCaret(selectionEnd, applied));      if (msg.type === "op") sawTyping(msg.by);    }    if (msg.type !== "snapshot") status.textContent = session.pending ? "Saving" : `Version ${session.version}`;  });  area.oninput = () => {    session.edit(area.value);    status.textContent = "Saving";  };  ws.addEventListener("close", () => {    if (socket !== ws) return;    area.disabled = true;    here = [];    renderPeople();    // Back off to at most 10 seconds between attempts.    const wait = Math.min(10_000, 500 * 2 ** retry++);    status.textContent = "Offline, reconnecting";    setTimeout(connect, wait);  });}window.addEventListener("hashchange", () => {  const old = socket;  socket = null;  old?.close();  connect();});connect();