1
0

Sync: no conflict when only directive bodies differ

This commit is contained in:
Zef Hemel 2023-01-16 18:55:35 +01:00
parent e4a4467547
commit d23846cdbf
4 changed files with 98 additions and 27 deletions

View File

@ -1,4 +1,4 @@
import { SpaceSync, SyncStatusItem } from "./sync.ts"; import { removeDirectiveBody, SpaceSync, SyncStatusItem } from "./sync.ts";
import { DiskSpacePrimitives } from "./disk_space_primitives.ts"; import { DiskSpacePrimitives } from "./disk_space_primitives.ts";
import { assertEquals } from "../../test_deps.ts"; import { assertEquals } from "../../test_deps.ts";
@ -100,11 +100,23 @@ Deno.test("Test store", async () => {
await primary.writeFile("index", "utf8", "Hello 1"); await primary.writeFile("index", "utf8", "Hello 1");
await secondary.writeFile("index", "utf8", "Hello 1"); await secondary.writeFile("index", "utf8", "Hello 1");
// And two more files with different bodies, but only within a query directive — shouldn't conflict
await primary.writeFile(
"index.md",
"utf8",
"Hello\n<!-- #query page -->\nHello 1\n<!-- /query -->",
);
await secondary.writeFile(
"index.md",
"utf8",
"Hello\n<!-- #query page -->\nHello 2\n<!-- /query -->",
);
await doSync(); await doSync();
await doSync(); await doSync();
// test + index + previous index.conflicting copy but nothing more // test + index + index.md + previous index.conflicting copy but nothing more
assertEquals((await primary.fetchFileList()).length, 3); assertEquals((await primary.fetchFileList()).length, 4);
console.log("Bringing a third device in the mix"); console.log("Bringing a third device in the mix");
@ -141,3 +153,23 @@ function sleep(ms = 10): Promise<void> {
setTimeout(resolve, ms); setTimeout(resolve, ms);
}); });
} }
Deno.test("Remove directive bodies", () => {
assertEquals(
removeDirectiveBody(`<!-- #query page -->
This is a body
bla bla bla
<!-- /query -->
Hello
<!-- #include [[test]] -->
This is a body
<!-- /include -->
`),
`<!-- #query page -->
<!-- /query -->
Hello
<!-- #include [[test]] -->
<!-- /include -->
`,
);
});

View File

@ -1,4 +1,6 @@
import { LogEntry } from "../../plugos/sandbox.ts"; import { renderToText, replaceNodesMatching } from "../../plug-api/lib/tree.ts";
import buildMarkdown from "../markdown_parser/parser.ts";
import { parse } from "../markdown_parser/parse_tree.ts";
import type { FileMeta } from "../types.ts"; import type { FileMeta } from "../types.ts";
import { SpacePrimitives } from "./space_primitives.ts"; import { SpacePrimitives } from "./space_primitives.ts";
@ -34,7 +36,7 @@ export class SpaceSync {
primarySpace: SpacePrimitives, primarySpace: SpacePrimitives,
secondarySpace: SpacePrimitives, secondarySpace: SpacePrimitives,
logger: Logger, logger: Logger,
) => Promise<void>, ) => Promise<number>,
): Promise<number> { ): Promise<number> {
let operations = 0; let operations = 0;
this.logger.log("info", "Fetching snapshot from primary"); this.logger.log("info", "Fetching snapshot from primary");
@ -202,7 +204,7 @@ export class SpaceSync {
name, name,
); );
if (conflictResolver) { if (conflictResolver) {
await conflictResolver( operations += await conflictResolver(
name, name,
this.snapshot, this.snapshot,
this.primary, this.primary,
@ -214,7 +216,6 @@ export class SpaceSync {
`Sync conflict for ${name} with no conflict resolver specified`, `Sync conflict for ${name} with no conflict resolver specified`,
); );
} }
operations++;
} else { } else {
// Nothing needs to happen // Nothing needs to happen
} }
@ -235,7 +236,7 @@ export class SpaceSync {
primary: SpacePrimitives, primary: SpacePrimitives,
secondary: SpacePrimitives, secondary: SpacePrimitives,
logger: Logger, logger: Logger,
): Promise<void> { ): Promise<number> {
logger.log("info", "Starting conflict resolution for", name); logger.log("info", "Starting conflict resolution for", name);
const filePieces = name.split("."); const filePieces = name.split(".");
const fileNameBase = filePieces.slice(0, -1).join("."); const fileNameBase = filePieces.slice(0, -1).join(".");
@ -243,28 +244,50 @@ export class SpaceSync {
const pageData1 = await primary.readFile(name, "arraybuffer"); const pageData1 = await primary.readFile(name, "arraybuffer");
const pageData2 = await secondary.readFile(name, "arraybuffer"); const pageData2 = await secondary.readFile(name, "arraybuffer");
let byteWiseMatch = true; if (name.endsWith(".md")) {
const arrayBuffer1 = new Uint8Array(pageData1.data as ArrayBuffer); logger.log("info", "File is markdown, using smart conflict resolution");
const arrayBuffer2 = new Uint8Array(pageData2.data as ArrayBuffer); // Let's use a smartert check for markdown files, ignoring directive bodies
if (arrayBuffer1.byteLength !== arrayBuffer2.byteLength) { const pageText1 = removeDirectiveBody(
byteWiseMatch = false; new TextDecoder().decode(pageData1.data as Uint8Array),
} );
if (byteWiseMatch) { const pageText2 = removeDirectiveBody(
// Byte-wise comparison new TextDecoder().decode(pageData2.data as Uint8Array),
for (let i = 0; i < arrayBuffer1.byteLength; i++) { );
if (arrayBuffer1[i] !== arrayBuffer2[i]) { if (pageText1 === pageText2) {
byteWiseMatch = false; logger.log(
break; "info",
} "Files are the same (eliminating the directive bodies), no conflict",
} );
// Byte wise they're still the same, so no confict
if (byteWiseMatch) {
logger.log("info", "Files are the same, no conflict");
snapshot.set(name, [ snapshot.set(name, [
pageData1.meta.lastModified, pageData1.meta.lastModified,
pageData2.meta.lastModified, pageData2.meta.lastModified,
]); ]);
return; return 0;
}
} else {
let byteWiseMatch = true;
const arrayBuffer1 = new Uint8Array(pageData1.data as ArrayBuffer);
const arrayBuffer2 = new Uint8Array(pageData2.data as ArrayBuffer);
if (arrayBuffer1.byteLength !== arrayBuffer2.byteLength) {
byteWiseMatch = false;
}
if (byteWiseMatch) {
// Byte-wise comparison
for (let i = 0; i < arrayBuffer1.byteLength; i++) {
if (arrayBuffer1[i] !== arrayBuffer2[i]) {
byteWiseMatch = false;
break;
}
}
// Byte wise they're still the same, so no confict
if (byteWiseMatch) {
logger.log("info", "Files are the same, no conflict");
snapshot.set(name, [
pageData1.meta.lastModified,
pageData2.meta.lastModified,
]);
return 0;
}
} }
} }
const revisionFileName = filePieces.length === 1 const revisionFileName = filePieces.length === 1
@ -303,9 +326,25 @@ export class SpaceSync {
); );
snapshot.set(name, [pageData1.meta.lastModified, writeMeta.lastModified]); snapshot.set(name, [pageData1.meta.lastModified, writeMeta.lastModified]);
return 1;
} }
syncCandidates(files: FileMeta[]): FileMeta[] { syncCandidates(files: FileMeta[]): FileMeta[] {
return files.filter((f) => !f.name.startsWith("_plug/")); return files.filter((f) => !f.name.startsWith("_plug/"));
} }
} }
const markdownLanguage = buildMarkdown([]);
export function removeDirectiveBody(text: string): string {
// Parse
const tree = parse(markdownLanguage, text);
// Remove bodies
replaceNodesMatching(tree, (node) => {
if (node.type === "DirectiveBody") {
return null;
}
});
// Turn back into text
return renderToText(tree);
}

View File

@ -414,7 +414,6 @@ functions:
path: ./stats.ts:statsCommand path: ./stats.ts:statsCommand
command: command:
name: "Stats: Show" name: "Stats: Show"
key: "Shift-Alt-s"
# Cloud pages # Cloud pages
readPageCloud: readPageCloud:

View File

@ -9,6 +9,7 @@ functions:
path: sync.ts:syncCommand path: sync.ts:syncCommand
command: command:
name: "Sync: Sync" name: "Sync: Sync"
key: "Shift-Alt-s"
wipeAndSyncCommand: wipeAndSyncCommand:
path: sync.ts:localWipeAndSyncCommand path: sync.ts:localWipeAndSyncCommand