Skip to content

Commit

Permalink
working prototype with basic dumb merge
Browse files Browse the repository at this point in the history
  • Loading branch information
sdumetz committed Dec 4, 2023
1 parent 1907bad commit 54191de
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 55 deletions.
7 changes: 6 additions & 1 deletion source/client/components/CVDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default class CVDocument extends CRenderGraph

protected titles: Dictionary<string> = {};
protected meta: CVMeta = null;
protected ref_id :number = -1;

protected static readonly ins = {
dumpJson: types.Event("Document.DumpJSON"),
Expand Down Expand Up @@ -173,6 +174,7 @@ export default class CVDocument extends CRenderGraph
*/
openDocument(documentData: IDocument, assetPath?: string, mergeParent?: boolean | NVNode | NVScene)
{
console.time("openDocument");
if (ENV_DEVELOPMENT) {
console.log("CVDocument.openDocument - assetPath: %s, mergeParent: %s", assetPath, mergeParent);
}
Expand Down Expand Up @@ -215,6 +217,8 @@ export default class CVDocument extends CRenderGraph
this.outs.assetPath.setValue(assetPath);
this.name = this.getMainComponent(CVAssetManager).getAssetName(assetPath);
}
this.ref_id = documentData.asset.id;
console.timeEnd("openDocument");
}

appendModel(assetPath: string, quality?: EDerivativeQuality | string, parent?: NVNode | NVScene) : CVModel2
Expand Down Expand Up @@ -266,7 +270,8 @@ export default class CVDocument extends CRenderGraph
type: CVDocument.mimeType,
version: CVDocument.version,
generator: "Voyager",
copyright: "(c) Smithsonian Institution. All rights reserved."
copyright: "(c) Smithsonian Institution. All rights reserved.",
id: this.ref_id,
},
scene: 0,
scenes: [],
Expand Down
1 change: 1 addition & 0 deletions source/client/schema/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface IDocumentAsset
version: string;
copyright?: string;
generator?: string;
id?: number;
}

export interface IScene
Expand Down
37 changes: 0 additions & 37 deletions source/server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion source/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
},
"dependencies": {
"body-parser": "^1.20.1",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"express": "^4.17.1",
"express-rate-limit": "^7.1.2",
Expand Down
17 changes: 10 additions & 7 deletions source/server/routes/scenes/get/document.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from "path";

import { getUserId, getVfs } from "../../../utils/locals.js";
import { getVfs } from "../../../utils/locals.js";
import { Request, Response } from "express";
import { createHash } from "crypto";

Expand All @@ -14,12 +13,16 @@ export default async function handleGetDocument(req :Request, res :Response){
let scene = await vfs.getScene(scene_name);
let f = await vfs.getDoc(scene.id);

let hash = createHash("sha256").update(f.data).digest("base64url");
let data = Buffer.from(f.data);
//Use this to know the client's document generation if he submits a change
res.cookie("docID", `${f.id}`, {sameSite: "strict", path: path.dirname(req.originalUrl)});
let doc = JSON.parse(f.data);
//Inject document id to know the client's reference document if he submits a change
doc.asset.id = f.id;

res.set("ETag", `W/${hash}`);
let data = Buffer.from(JSON.stringify(doc));

let hash = createHash("sha256").update(data).digest("base64url");


res.set("ETag", hash);
res.set("Last-Modified", f.mtime.toUTCString());
if(req.fresh){
return res.status(304).send("Not Modified");
Expand Down
2 changes: 0 additions & 2 deletions source/server/routes/scenes/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Router } from "express";

import bodyParser from "body-parser";
import cookieParser from "cookie-parser";

import {handlePropfind} from "./propfind.js";
import {handlePutFile, handlePutDocument} from "./put/index.js";
Expand Down Expand Up @@ -38,7 +37,6 @@ router.get("/:scene/:file(*.svx.json)", wrap(handleGetDocument));
router.put("/:scene/:file(*.svx.json)",
canWrite,
bodyParser.json({type:["application/si-dpo-3d.document+json", "application/json"]}),
cookieParser(),
wrap(handlePutDocument)
);
router.copy("/:scene/:file(*.svx.json)", canWrite, wrap(handleCopyDocument));
Expand Down
10 changes: 4 additions & 6 deletions source/server/routes/scenes/put/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,16 @@ export default async function handlePutDocument(req :Request, res :Response){
const uid = getUserId(req);
const {scene} = req.params;
const newDoc = req.body;

const refId = parseInt(req.cookies["docID"] ?? "0");
const refId = newDoc?.asset?.id;
if(!refId) return await overwritePutDocument(req, res);

else delete newDoc.asset.id; //Don't write this to DB

await getVfs(req).isolate(async (tr)=>{
// perform a diff of the document with the reference one
const {id: scene_id} = await tr.getScene(scene);
const {data: currentDocString} = await tr.getDoc(scene_id);
const currentDoc = JSON.parse(currentDocString);
const {data:refDocString} = await tr.getDocById(refId);
const {data: refDocString} = await tr.getDocById(refId);
const refDoc = JSON.parse(refDocString);

const docDiff = merge.diff(refDoc, newDoc);
Expand All @@ -59,10 +58,9 @@ export default async function handlePutDocument(req :Request, res :Response){
}
console.log("Merge changes : ", JSON.stringify(docDiff));
const mergedDoc = merge.apply(currentDoc, docDiff);
let s = JSON.stringify(mergedDoc, null, 2);
let s = JSON.stringify(mergedDoc);
if(s == "{}") throw new BadRequestError(`Invalid json document`);
let id = await tr.writeDoc(s, scene, uid);
res.cookie("docID", `${id}`, {sameSite: "strict", path: path.dirname(req.originalUrl)});
res.status(204).send();
})

Expand Down
76 changes: 75 additions & 1 deletion source/server/utils/merge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ describe("merge.diff()", function(){
expect(diff({a:["foo", "bar", "baz"]}, {a:["foo", "baz"]})).to.deep.equal({a:{1: "baz", 2: DELETE_KEY}});
});

it("handle deep changes", function(){
expect(diff({a:[{v:"foo"}, {v:"bar"}]}, {a:[{v:"foo"}, {v:"baz"}]})).to.deep.equal({a:{1:{v:"baz"}}});
});

it("throws when diffing an array with an object", function(){
expect(()=>diff({a:[]}, {a:{}})).to.throw("Can't diff an array with an object");
});
Expand Down Expand Up @@ -87,16 +91,86 @@ describe("merge.apply()", function(){
it("merges updated arrays", function(){
expect(apply({a:[1,2]}, {a:{0:1, 1:3}})).to.deep.equal({a:[1,3]});
});


});


describe("three-way merge", function(){
// This is the real point of this module
/* This is the real point of this module
* For consistency, tests should follow the following naming scheme:
* a *ref* as the common source,
* a *current* as the document that have been saved in-between
* a *next* as the document that is being saved now
*/

it("string update", function(){
/** @TODO could try to do string splicing for finer results */
const ref = {greet:"Hello", name:"World"};
const current = {greet:"Hi", name:"World"};
const next = {greet:"Hello", name:"Universe"};
expect(apply(current, diff(ref, next))).to.deep.equal({greet:"Hi", name:"Universe"});
});

it.skip("ignores camera translation/rotation changes", function(){
//This is a potential optimization
//The camera node's position can safely be ignored as it will get reset on load, in favor of `scene.setups[].navigation`
});

describe.skip("conflict resolution", function(){
//Test only cases where "smart" conflict resolution is possible
describe("by ID", function(){
//Here it would be possible to differentiate annotations or articles by their id
//thus detecting a double-push and resolving it
const A1 = { "id": "mMzG2tLjmIst", "titles": { "EN": "A1"} }
const A2 = { "id": "FiIaONzRIbL4", "titles": { "EN": "A2"} }
const A3 = { "id": "rJpCltJjxyyL", "titles": { "EN": "A3"} }
it("handle double array push (annotations)", function(){
const ref = {annotations: [A1], title: "foo"};
const current = {annotations: [A1,A2]};
const next = {annotations: [A1,A3]};

const d = diff(ref, next);
expect(d).to.deep.equal({annotations: {1: A3}});
expect(apply(current, d)).to.deep.equal({annotations: [A1,A2,A3], title: "foo"});
});

it.skip("handle double array push (articles)", function(){
const ref = {articles: [A1], title: "foo"};
const current = {articles: [A1,A2]};
const next = {articles: [A1,A3]};
expect(apply(current, diff(ref, next))).to.deep.equal({articles: [A1,A2,A3], title: "foo"});
});
})

describe.skip("by name", function(){

it("reassign scene nodes appropriately", function(){
//Differentiate nodes by their names
//However, we also need to handle possible conflict in `scenes[x].nodes` if we reorder nodes.
const N1 = { "name": "n1"};
const N2 = { "name": "n2"};
const N3 = { "name": "n3"};
const ref = {nodes: [N1], scenes:[{nodes:[0]}] };
const current = {nodes: [N1,N2], scenes:[{nodes:[0, 1]}]};
const next = {nodes: [N1,N3], scenes:[{nodes:[0, 1]}]};

expect(apply(current, diff(ref, next))).to.deep.equal({nodes: [N1,N2,N3], scenes:[{nodes:[0, 1, 2]}]});
//FIXME a node's children might also be affected.
});

it("reassign nodes children appropriately", function(){

});

["lights", "cameras", "models"].forEach(type=>{
it(`reassign ${type} appropriately`, function(){
//Lights have no property that could be used to differentiate them
//We can only suppose that if someone adds a node, the light attached to this node is unique to this one and should stay with it.
});
});
});


});
});

0 comments on commit 54191de

Please sign in to comment.