diff --git a/app/app.js b/app/app.js
index 5e64a89..ec60a60 100644
--- a/app/app.js
+++ b/app/app.js
@@ -10,7 +10,9 @@ const https = require('https');
const http = require('http');
const morgan = require('morgan');
const nwl = require('neuroweblab');
+const { reduce, assign } = require('lodash');
const microdrawWebsocketServer = require('./controller/microdrawWebsocketServer/microdrawWebsocketServer.js');
+const { Server: HocuspocusServer } = require('@hocuspocus/server');
const routes = require('./routes/routes');
let port;
let server;
@@ -96,7 +98,6 @@ const start = async function () {
microdrawWebsocketServer.initSocketConnection();
});
-
// CORS
app.use(function (req, res, next) {
// Website you wish to allow to connect
@@ -156,12 +157,41 @@ const start = async function () {
});
global.authTokenMiddleware = nwl.authTokenMiddleware;
+
+ // CRDT backend
+
+ const hocuspocusServer = HocuspocusServer.configure({
+ port: 8081,
+ async onDisconnect(data) {
+ const project = await app.db.queryProject({shortname: data.documentName});
+ const vectorialAnnotations = project.annotations.list.filter((annotation) => annotation.type === 'vectorial');
+ const textAnnotations = project.annotations.list.filter((annotation) => annotation.type === 'text');
+ const files = data.document.getArray('files').toJSON();
+ const newTextAnnotations = textAnnotations.map((annotation) => {
+ const {name} = annotation;
+ const valueByFile = reduce(files.map((file) => ({ [file.source]: file[name] })), (result, obj) => assign(result, obj), {});
+
+ return {
+ ...annotation,
+ values: valueByFile
+ };
+ });
+ project.annotations.list = [...newTextAnnotations, ...vectorialAnnotations];
+ console.log(JSON.stringify(project));
+ app.db.updateProject(project);
+ }
+ }
+ );
+
+ hocuspocusServer.listen();
+
+
/* setup GUI routes */
routes(app);
// catch 404 and forward to error handler
- app.use(function (req, res, next) {
+ app.use(function (req) {
console.log('ERROR: File not found', req.url);
// var err = new Error('Not Found'); //, req);
// err.status = 404;
diff --git a/app/views/scripts/components/ProjectPage.vue b/app/views/scripts/components/ProjectPage.vue
index 92546a8..2df4f74 100644
--- a/app/views/scripts/components/ProjectPage.vue
+++ b/app/views/scripts/components/ProjectPage.vue
@@ -9,8 +9,9 @@
import { forEach, get, set } from "lodash";
- import { initSyncedStore, waitForSync } from "../store/synced";
import useVisualization from "../store/visualization";
- import { enableVueBindings } from "@syncedstore/core";
+ import { HocuspocusProvider } from "@hocuspocus/provider";
+ import { syncedStore, getYjsDoc, enableVueBindings } from "@syncedstore/core";
import Tools from "./Tools.vue";
import {
Editor,
@@ -53,12 +54,20 @@
} from "nwl-components";
import * as Vue from "vue";
- const { store, webrtcProvider, doc } = initSyncedStore(projectInfo.shortname);
- const { baseURL } = Vue.inject('config');
-
// make SyncedStore use Vuejs internally
enableVueBindings(Vue);
-
+
+ const store = syncedStore({ files: [], fragment: "xml" });
+ const doc = getYjsDoc(store);
+
+ const crdtProvider = new HocuspocusProvider({
+ url: "ws://0.0.0.0:8081", // FIXME
+ name: projectInfo.shortname,
+ document: doc
+ });
+
+ const { baseURL } = Vue.inject('config');
+
const props = defineProps({
project: {
type: Object,
@@ -71,12 +80,7 @@
});
const linkPrefix = `${baseURL}/project/${projectInfo.shortname}?source=`
- const files = Vue.ref(projectInfo.files.list);
const selectedFileIndex = projectInfo.files.list.findIndex(file => file.source === props.selectedFile);
- doc.getArray("files").observe(() => {
- files.value.splice(0, files.value.length);
- files.value.push(...store.files);
- });
const textAnnotations = projectInfo.annotations.list.filter(anno => anno.type !== 'vectorial');
const volumeAnnotations = projectInfo.annotations.list.filter(anno => anno.type === 'vectorial');
@@ -102,6 +106,10 @@
const keys = new Map();
keys.set("Name", "name");
keys.set("File", "source");
+ forEach(textAnnotations, (annotation) => {
+ if (annotation.display)
+ keys.set(annotation.name, `${annotation.name}`);
+ });
return keys;
};
@@ -113,19 +121,14 @@
return keys;
};
- const syncMicrodraw = () => {
- console.log('sync microdraw')
- }
-
const valueChange = (content, index, selector) => {
const sel =
typeof selector === "string" ? [index, selector] : [index, ...selector];
- set(store.files, sel, content);
- syncMicrodraw();
+ set(store.files, sel, content);
};
const selectFile = async (file) => {
- window.location = `${linkPrefix}${file.source}`
+ // No-op. We'd rather let user click on a link.
}
const setupKeyDownListeners = () => {
@@ -137,7 +140,7 @@
return;
}
if (selectedTr.previousElementSibling) {
- selectedTr.previousElementSibling.click();
+ selectedTr.previousElementSibling.querySelector('a[href]').click();
}
break;
case "ArrowDown":
@@ -145,7 +148,7 @@
return;
}
if (selectedTr.nextElementSibling) {
- selectedTr.nextElementSibling.click();
+ selectedTr.nextElementSibling.querySelector('a[href]').click();
}
break;
default:
@@ -153,11 +156,7 @@
}
});
};
-
- const delay = (ms) => {
- return new Promise((resolve) => setTimeout(resolve, ms));
- };
-
+
const handleLayoutChange = () => {
Microdraw.resizeAnnotationOverlay();
};
@@ -187,7 +186,18 @@
Vue.onMounted(async () => {
setupKeyDownListeners();
- await waitForSync(webrtcProvider);
+ crdtProvider.on('synced', () => {
+ console.log('on synced');
+ if (store.files.length === 0) {
+ store.files.push(...projectInfo.files.list);
+ forEach(textAnnotations, (annotation) => {
+ forEach(annotation.values, (value, source) => {
+ const file = store.files.find(file => file.source === source);
+ if (file) file[annotation.name] = value;
+ });
+ });
+ }
+ });
await initVisualization();
window.addEventListener('resize', handleResize);
});
diff --git a/app/views/scripts/store/synced.js b/app/views/scripts/store/synced.js
deleted file mode 100644
index 4774f47..0000000
--- a/app/views/scripts/store/synced.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { getYjsValue, syncedStore } from '@syncedstore/core';
-import { WebrtcProvider } from 'y-webrtc';
-
-export const initSyncedStore = (identifier) => {
- const store = syncedStore({ files: [], fragment: 'xml' });
-
- // Get the Yjs document and sync automatically using y-webrtc
- const doc = getYjsValue(store);
- // eslint-disable-next-line
- const webrtcProvider = new WebrtcProvider(identifier, doc);
-
- return { store, webrtcProvider, doc };
-};
-
-// not the prettiest way to poll for sync, but will do for now
-export const waitForSync = (provider) => new Promise((resolve) => {
- const int = setInterval(() => {
- if (provider.room !== null) {
- if (provider.room.synced || provider.room.webrtcConns.size === 0) {
- clearInterval(int);
- resolve();
- }
- }
- }, 500);
-});
diff --git a/package-lock.json b/package-lock.json
index 4c082be..1431aec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,9 @@
"version": "0.0.1",
"license": "GPL-3.0",
"dependencies": {
- "@syncedstore/core": "^0.4.1",
+ "@hocuspocus/provider": "^2.2.3",
+ "@hocuspocus/server": "^2.2.3",
+ "@syncedstore/core": "^0.4.3",
"bcrypt": "^3.0.8",
"body-parser": "^1.18.3",
"cross-env": "^7.0.3",
@@ -113,6 +115,74 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@hocuspocus/common": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@hocuspocus/common/-/common-2.2.3.tgz",
+ "integrity": "sha512-7FxNUGnA8dNgXgve2ranrRtA2px7+K4EAsP21pvv/mbQuYkU4g+ciD32jywZsmshxsPaa/+nr0j62tkDXVjA5Q==",
+ "dependencies": {
+ "lib0": "^0.2.47"
+ }
+ },
+ "node_modules/@hocuspocus/provider": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@hocuspocus/provider/-/provider-2.2.3.tgz",
+ "integrity": "sha512-r/kMzXwJLL1+Vd5SOD7CYqBr9/bK1hm5445gs3/+ss9Wt8zH1JVE2AtK5shNc+3VYpyNb3MAvdZx5OKx9JaSPg==",
+ "dependencies": {
+ "@hocuspocus/common": "^2.2.3",
+ "@lifeomic/attempt": "^3.0.2",
+ "lib0": "^0.2.47",
+ "ws": "^7.5.9"
+ },
+ "peerDependencies": {
+ "y-protocols": "^1.0.5",
+ "yjs": "^13.5.29"
+ }
+ },
+ "node_modules/@hocuspocus/server": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@hocuspocus/server/-/server-2.2.3.tgz",
+ "integrity": "sha512-NFU9vXGo1IHkFluh+rW4IaOKPgxAgovJRL+RwJj6c8oTJSdLpayoUWnNIEMpZ7Jo68wZLMp6Sp3ib0NY98c/WQ==",
+ "dependencies": {
+ "@hocuspocus/common": "^2.2.3",
+ "async-lock": "^1.3.1",
+ "kleur": "^4.1.4",
+ "lib0": "^0.2.47",
+ "uuid": "^9.0.0",
+ "ws": "^8.5.0"
+ },
+ "peerDependencies": {
+ "y-protocols": "^1.0.5",
+ "yjs": "^13.5.29"
+ }
+ },
+ "node_modules/@hocuspocus/server/node_modules/uuid": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/@hocuspocus/server/node_modules/ws": {
+ "version": "8.13.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
+ "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.9.3",
"dev": true,
@@ -183,6 +253,11 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@lifeomic/attempt": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@lifeomic/attempt/-/attempt-3.0.3.tgz",
+ "integrity": "sha512-GlM2AbzrErd/TmLL3E8hAHmb5Q7VhDJp35vIbyPVA5Rz55LZuRr8pwL3qrwwkVNo05gMX1J44gURKb4MHQZo7w=="
+ },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.8",
"license": "BSD-3-Clause",
@@ -1100,6 +1175,11 @@
"version": "3.2.3",
"license": "MIT"
},
+ "node_modules/async-lock": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz",
+ "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ=="
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"license": "MIT"
@@ -4167,6 +4247,14 @@
"node": ">=0.10.0"
}
},
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/kruptein": {
"version": "3.0.3",
"license": "MIT",
@@ -8081,8 +8169,9 @@
"license": "ISC"
},
"node_modules/ws": {
- "version": "7.5.6",
- "license": "MIT",
+ "version": "7.5.9",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
+ "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"engines": {
"node": ">=8.3.0"
},
diff --git a/package.json b/package.json
index d1a8ee6..27c417b 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,9 @@
"dev-pages": "webpack --mode development --config webpack.pages.config.js && cp app/views/scripts/dist/*-page.js app/public/js/pages/"
},
"dependencies": {
- "@syncedstore/core": "^0.4.1",
+ "@hocuspocus/provider": "^2.2.3",
+ "@hocuspocus/server": "^2.2.3",
+ "@syncedstore/core": "^0.4.3",
"bcrypt": "^3.0.8",
"body-parser": "^1.18.3",
"cross-env": "^7.0.3",