File size: 3,389 Bytes
f6678ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
 * Persistence Tests (Section 2 of TESTS.md)
 *
 * Tests debouncedSave, local file read/write, and flushAll.
 * Uses a temporary directory to avoid polluting real data.
 */
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "fs";
import { mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import * as Y from "yjs";
import { setDataDir, getDataDir, docPath } from "../src/utils.js";
import { debouncedSave, resetSaveTimers } from "../src/create-app.js";

let tmpDir: string;

beforeEach(() => {
  tmpDir = mkdtempSync(join(tmpdir(), "collab-test-"));
  mkdirSync(tmpDir, { recursive: true });
  setDataDir(tmpDir);
});

afterEach(() => {
  resetSaveTimers();
  setDataDir(undefined);
  try {
    rmSync(tmpDir, { recursive: true, force: true });
  } catch {}
});

function makeDoc(text: string): Y.Doc {
  const ydoc = new Y.Doc();
  const fragment = ydoc.getXmlFragment("default");
  const el = new Y.XmlElement("paragraph");
  el.insert(0, [new Y.XmlText(text)]);
  fragment.insert(0, [el]);
  return ydoc;
}

describe("2.1 Local persistence", () => {
  it("2.1.1 debouncedSave writes .yjs file after debounce", async () => {
    const ydoc = makeDoc("Hello world");

    debouncedSave("test-doc", ydoc);

    // File should NOT exist immediately (debounce is 2s)
    const p = docPath("test-doc");
    expect(existsSync(p)).toBe(false);

    // Wait for debounce to fire
    await new Promise((r) => setTimeout(r, 2500));

    expect(existsSync(p)).toBe(true);
    const buf = readFileSync(p);
    expect(buf.length).toBeGreaterThan(0);

    // Verify it's valid Yjs binary by applying it to a new doc
    const restored = new Y.Doc();
    Y.applyUpdate(restored, new Uint8Array(buf));
    const fragment = restored.getXmlFragment("default");
    expect(fragment.length).toBeGreaterThan(0);
  });

  it("2.1.2 docPath reads from configured DATA_DIR", () => {
    // Write a .yjs file manually, then verify docPath points to it
    const ydoc = makeDoc("Existing content");
    const state = Y.encodeStateAsUpdate(ydoc);
    const p = docPath("existing");
    writeFileSync(p, Buffer.from(state));

    // Read it back and verify content
    expect(existsSync(p)).toBe(true);
    const buf = readFileSync(p);
    const restored = new Y.Doc();
    Y.applyUpdate(restored, new Uint8Array(buf));
    const fragment = restored.getXmlFragment("default");
    expect(fragment.length).toBeGreaterThan(0);
  });

  it("2.1.3 debounce collapses rapid saves into one write", async () => {
    const ydoc = makeDoc("Version 1");

    // Trigger 3 rapid saves
    debouncedSave("rapid", ydoc);

    const ydoc2 = makeDoc("Version 2");
    debouncedSave("rapid", ydoc2);

    const ydoc3 = makeDoc("Version 3");
    debouncedSave("rapid", ydoc3);

    const p = docPath("rapid");

    // Nothing written yet
    expect(existsSync(p)).toBe(false);

    // Wait for the single debounced write
    await new Promise((r) => setTimeout(r, 2500));

    expect(existsSync(p)).toBe(true);

    // The file should contain the last version (ydoc3)
    const buf = readFileSync(p);
    const restored = new Y.Doc();
    Y.applyUpdate(restored, new Uint8Array(buf));
    const fragment = restored.getXmlFragment("default");
    expect(fragment.length).toBeGreaterThan(0);
  });
});