Skip to content

Commit

Permalink
Persist and sync across clients text annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
denishov committed Aug 1, 2023
1 parent 1cb0b20 commit ee5bbba
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 58 deletions.
33 changes: 31 additions & 2 deletions app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -96,7 +98,6 @@ const start = async function () {
microdrawWebsocketServer.initSocketConnection();
});


// CORS
app.use(function (req, res, next) {
// Website you wish to allow to connect
Expand Down Expand Up @@ -156,12 +157,40 @@ 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];
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;
Expand Down
64 changes: 37 additions & 27 deletions app/views/scripts/components/ProjectPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
<TextAnnotations
:extract-keys="extractTextKeys"
:link-prefix="linkPrefix"
:files="files"
:files="store.files"
:selected="selectedFileIndex"
@value-change="valueChange"
@select-file="selectFile"
/>
<VolumeAnnotations
Expand Down Expand Up @@ -40,9 +41,9 @@

<script setup>
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,
Expand All @@ -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,
Expand All @@ -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');
Expand All @@ -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;
};
Expand All @@ -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 = () => {
Expand All @@ -137,27 +140,23 @@
return;
}
if (selectedTr.previousElementSibling) {
selectedTr.previousElementSibling.click();
selectedTr.previousElementSibling.querySelector('a[href]').click();
}
break;
case "ArrowDown":
if (!selectedTr) {
return;
}
if (selectedTr.nextElementSibling) {
selectedTr.nextElementSibling.click();
selectedTr.nextElementSibling.querySelector('a[href]').click();
}
break;
default:
break;
}
});
};
const delay = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const handleLayoutChange = () => {
Microdraw.resizeAnnotationOverlay();
};
Expand Down Expand Up @@ -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);
});
Expand Down
25 changes: 0 additions & 25 deletions app/views/scripts/store/synced.js

This file was deleted.

95 changes: 92 additions & 3 deletions package-lock.json

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

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit ee5bbba

Please sign in to comment.