Files
plainleaf/client/spaces/sync.test.ts
T

285 lines
9.5 KiB
TypeScript

import { expect, test } from "vitest";
import { SpaceSync, SyncSnapshot } from "./sync.ts";
import { sleep } from "@silverbulletmd/silverbullet/lib/async";
import { MemoryKvPrimitives } from "../data/memory_kv_primitives.ts";
import { DataStoreSpacePrimitives } from "./datastore_space_primitives.ts";
test("Test sync with no filtering", async () => {
const primary = new DataStoreSpacePrimitives(new MemoryKvPrimitives());
const secondary = new DataStoreSpacePrimitives(new MemoryKvPrimitives());
const snapshot = new SyncSnapshot();
const sync = new SpaceSync(primary, secondary, {
conflictResolver: SpaceSync.primaryConflictResolver,
isSyncCandidate: () => true, // Sync everything always
});
console.log("Write one page to primary");
await primary.writeFile("index.md", stringToBytes("Hello"));
expect((await secondary.fetchFileList()).length).toEqual(0);
console.log("Initial sync ops", await doSync());
expect((await secondary.fetchFileList()).length).toEqual(1);
expect((await secondary.readFile("index.md")).data).toEqual(
stringToBytes("Hello"),
);
// Should be a no-op
let ops = await doSync();
expect(ops).toEqual(0);
expect(snapshot.nonSyncedFiles.size).toEqual(0);
// Now let's make a change on the secondary
await secondary.writeFile("index.md", stringToBytes("Hello!!"));
await secondary.writeFile("test.md", stringToBytes("Test page"));
// And sync it
ops = await doSync();
expect(ops).toEqual(2);
expect(snapshot.nonSyncedFiles.size).toEqual(0);
expect((await primary.fetchFileList()).length).toEqual(2);
expect((await secondary.fetchFileList()).length).toEqual(2);
expect((await primary.readFile("index.md")).data).toEqual(
stringToBytes("Hello!!"),
);
// Let's make some random edits on both ends
await primary.writeFile("index.md", stringToBytes("1"));
await primary.writeFile("index2.md", stringToBytes("2"));
await secondary.writeFile("index3.md", stringToBytes("3"));
await secondary.writeFile("index4.md", stringToBytes("4"));
await doSync();
expect((await primary.fetchFileList()).length).toEqual(5);
expect((await secondary.fetchFileList()).length).toEqual(5);
ops = await doSync();
expect(ops).toEqual(0);
console.log("Deleting pages");
// Delete some pages
await primary.deleteFile("index.md");
await primary.deleteFile("index3.md");
await doSync();
expect((await primary.fetchFileList()).length).toEqual(3);
expect((await secondary.fetchFileList()).length).toEqual(3);
// No-op
ops = await doSync();
expect(ops).toEqual(0);
await secondary.deleteFile("index4.md");
await primary.deleteFile("index2.md");
await doSync();
// Just "test" left
expect((await primary.fetchFileList()).length).toEqual(1);
expect((await secondary.fetchFileList()).length).toEqual(1);
// No-op
ops = await doSync();
expect(ops).toEqual(0);
await secondary.writeFile("index.md", stringToBytes("I'm back"));
await doSync();
expect((await primary.readFile("index.md")).data).toEqual(
stringToBytes("I'm back"),
);
// Cause a conflict
console.log("Introducing a conflict now");
await primary.writeFile("index.md", stringToBytes("Hello 1"));
await secondary.writeFile("index.md", stringToBytes("Hello 2"));
await doSync();
// Sync conflicting copy back
await doSync();
// Verify that primary won
expect((await primary.readFile("index.md")).data).toEqual(
stringToBytes("Hello 1"),
);
expect((await secondary.readFile("index.md")).data).toEqual(
stringToBytes("Hello 1"),
);
// test + index + index.conflicting copy
expect((await primary.fetchFileList()).length).toEqual(3);
expect((await secondary.fetchFileList()).length).toEqual(3);
// Introducing a fake conflict (same content, so not really conflicting)
await primary.writeFile("index.md", stringToBytes("Hello 1"));
await secondary.writeFile("index.md", stringToBytes("Hello 1"));
await doSync();
await doSync();
// test + index + index.md + previous index.conflicting copy but nothing more
expect((await primary.fetchFileList()).length).toEqual(3);
async function doSync() {
await sleep(10);
const r = await sync.syncFiles(snapshot);
await sleep(10);
return r;
}
});
test("Test sync with filtering", async () => {
const primary = new DataStoreSpacePrimitives(new MemoryKvPrimitives());
const secondary = new DataStoreSpacePrimitives(new MemoryKvPrimitives());
const snapshot = new SyncSnapshot();
let sync = new SpaceSync(primary, secondary, {
conflictResolver: SpaceSync.primaryConflictResolver,
isSyncCandidate: (path) => path.endsWith(".md"), // Only sync .md files
});
console.log(
"Write one non-sync file on the primary, which SHOULD sync to the secondary",
);
await primary.writeFile("index.txt", stringToBytes("Hello"));
expect((await secondary.fetchFileList()).length).toEqual(0);
let ops = await doSync();
expect(ops).toEqual(1);
// Note: this number should be 0 because the file DOES have a local representation so it's not technically non-synced
expect(snapshot.nonSyncedFiles.size).toEqual(0);
expect((await secondary.fetchFileList()).length).toEqual(1);
expect((await secondary.readFile("index.txt")).data).toEqual(
stringToBytes("Hello"),
);
console.log("Updating on secondary");
await secondary.writeFile("index.txt", stringToBytes("Hello Updated"));
ops = await doSync();
expect(ops).toEqual(1);
expect(snapshot.nonSyncedFiles.size).toEqual(1);
try {
await primary.getFileMeta("index.md");
expect(
false,
"Local file should have been deleted locally since it's out of date",
).toBeTruthy();
} catch {
// Expected
}
console.log("Deleting remote files");
await secondary.deleteFile("index.txt");
ops = await doSync();
expect(ops).toEqual(1);
expect(snapshot.nonSyncedFiles.size).toEqual(0);
console.log("Creating a remote non-synced file");
await secondary.writeFile("index2.txt", stringToBytes("Hello 2"));
ops = await doSync();
expect(ops).toEqual(0); // No-op, metadata only
expect(snapshot.nonSyncedFiles.size).toEqual(1);
ops = await doSync();
expect(ops).toEqual(0); // No-op, metadata only
expect(snapshot.nonSyncedFiles.size).toEqual(1);
await primary.writeFile("index2.txt", stringToBytes("Hello local"));
ops = await doSync();
expect(ops).toEqual(1);
expect(snapshot.nonSyncedFiles.size).toEqual(0);
console.log("Getting into a state with some synced and non-synced files");
await secondary.writeFile("index.md", stringToBytes("This will sync"));
await secondary.writeFile("index.txt", stringToBytes("This will not sync"));
await secondary.writeFile("index2.txt", stringToBytes("This will not sync"));
ops = await doSync();
expect(ops).toEqual(2);
expect(snapshot.nonSyncedFiles.size).toEqual(2);
// Check file listings on both ends
expect((await secondary.fetchFileList()).length).toEqual(3);
expect((await primary.fetchFileList()).length).toEqual(1);
////////////
// Now let's start another sync session, but now wanting to sync everything
console.log("Going to switch to syncing everything now");
sync = new SpaceSync(primary, secondary, {
conflictResolver: SpaceSync.primaryConflictResolver,
isSyncCandidate: () => true,
});
ops = await doSync();
// This should pull 2 files from remote to local
expect(ops).toEqual(2);
expect((await primary.fetchFileList()).length).toEqual(3);
expect((await secondary.fetchFileList()).length).toEqual(3);
console.log("And now to syncing nothing");
sync = new SpaceSync(primary, secondary, {
conflictResolver: SpaceSync.primaryConflictResolver,
isSyncCandidate: () => false,
});
ops = await doSync();
// This should delete 3 files from the primary
expect(ops).toEqual(3);
// Leaving nothing on primary
expect((await primary.fetchFileList()).length).toEqual(0);
// And everything unchanged on secondary
expect((await secondary.fetchFileList()).length).toEqual(3);
// Ok, now we're going to sync everything again
sync = new SpaceSync(primary, secondary, {
conflictResolver: SpaceSync.primaryConflictResolver,
isSyncCandidate: () => true,
});
// await secondary.
ops = await doSync();
expect(ops).toEqual(3);
expect((await primary.fetchFileList()).length).toEqual(3);
expect((await secondary.fetchFileList()).length).toEqual(3);
async function doSync() {
await sleep(10);
const r = await sync.syncFiles(snapshot);
return r;
}
});
test("Local push sync", async () => {
const primary = new DataStoreSpacePrimitives(new MemoryKvPrimitives());
const secondary = new DataStoreSpacePrimitives(new MemoryKvPrimitives());
const snapshot = new SyncSnapshot();
const sync = new SpaceSync(primary, secondary, {
conflictResolver: SpaceSync.primaryConflictResolver,
isSyncCandidate: (path) => path.endsWith(".md"), // Only sync .md files
});
console.log(
"Write one non-sync file on the primary, which SHOULD sync to the secondary",
);
const operations = await sync.syncFiles(snapshot);
expect(operations).toEqual(0);
await primary.writeFile("index.md", stringToBytes("Hello"));
expect(1).toEqual(await sync.syncSingleFile("index.md", snapshot));
expect((await secondary.readFile("index.md")).data).toEqual(
stringToBytes("Hello"),
);
console.log("Let's write a new file on primary that is not a sync candidate");
await primary.writeFile("test.txt", stringToBytes("Hello"));
expect(1).toEqual(await sync.syncSingleFile("test.txt", snapshot));
expect(snapshot.nonSyncedFiles.size).toEqual(0);
});
function stringToBytes(s: string): Uint8Array {
return new TextEncoder().encode(s);
}