208 lines · 7.3 KB
import * as decoding from "lib0/decoding";
import * as encoding from "lib0/encoding";
import * as awarenessProtocol from "y-protocols/awareness";
import * as syncProtocol from "y-protocols/sync";
import * as Y from "yjs";
import {
  CLOSE_DOWNGRADED,
  CLOSE_REAUTH,
  CLOSE_REVOKED,
  EDITOR_WS_PROTOCOL,
  FILES_MAP,
  MESSAGE_AWARENESS,
  MESSAGE_SYNC,
  textKey,
} from "../lib/editor-protocol";
import type { ClientControl, FileMeta, RoomControl } from "../lib/editor-protocol";

export type RoomEvents = {
  onControl(message: RoomControl): void;
  onState(state: "connecting" | "online" | "offline" | "revoked"): void;
  /** Asked for a fresh token after the current one expired or write access changed. */
  refreshToken(): Promise<string>;
};

const MIN_BACKOFF_MS = 500;
const MAX_BACKOFF_MS = 10_000;
const OPEN_TIMEOUT_MS = 15_000;

/**
 * Yjs client for the EditorRoom: one Y.Doc for the whole branch, y-protocols sync and awareness over a
 * single WebSocket, reconnecting with backoff. Edits made while offline merge on reconnect (CRDT).
 */
export class RoomClient {
  readonly doc = new Y.Doc();
  readonly awareness = new awarenessProtocol.Awareness(this.doc);
  readonly files: Y.Map<FileMeta> = this.doc.getMap<FileMeta>(FILES_MAP);
  writable = false;
  head = "";
  private socket: WebSocket | null = null;
  private backoff = MIN_BACKOFF_MS;