285 lines
9.5 KiB
TypeScript
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);
|
|
}
|