Skip to content

Commit ee5bbba

Browse files
committed
Persist and sync across clients text annotations
1 parent 1cb0b20 commit ee5bbba

File tree

5 files changed

+163
-58
lines changed

5 files changed

+163
-58
lines changed

app/app.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ const https = require('https');
1010
const http = require('http');
1111
const morgan = require('morgan');
1212
const nwl = require('neuroweblab');
13+
const { reduce, assign } = require('lodash');
1314
const microdrawWebsocketServer = require('./controller/microdrawWebsocketServer/microdrawWebsocketServer.js');
15+
const { Server: HocuspocusServer } = require('@hocuspocus/server');
1416
const routes = require('./routes/routes');
1517
let port;
1618
let server;
@@ -96,7 +98,6 @@ const start = async function () {
9698
microdrawWebsocketServer.initSocketConnection();
9799
});
98100

99-
100101
// CORS
101102
app.use(function (req, res, next) {
102103
// Website you wish to allow to connect
@@ -156,12 +157,40 @@ const start = async function () {
156157
});
157158
global.authTokenMiddleware = nwl.authTokenMiddleware;
158159

160+
161+
// CRDT backend
162+
163+
const hocuspocusServer = HocuspocusServer.configure({
164+
port: 8081,
165+
async onDisconnect(data) {
166+
const project = await app.db.queryProject({shortname: data.documentName});
167+
const vectorialAnnotations = project.annotations.list.filter((annotation) => annotation.type === 'vectorial');
168+
const textAnnotations = project.annotations.list.filter((annotation) => annotation.type === 'text');
169+
const files = data.document.getArray('files').toJSON();
170+
const newTextAnnotations = textAnnotations.map((annotation) => {
171+
const {name} = annotation;
172+
const valueByFile = reduce(files.map((file) => ({ [file.source]: file[name] })), (result, obj) => assign(result, obj), {});
173+
174+
return {
175+
...annotation,
176+
values: valueByFile
177+
};
178+
});
179+
project.annotations.list = [...newTextAnnotations, ...vectorialAnnotations];
180+
app.db.updateProject(project);
181+
}
182+
}
183+
);
184+
185+
hocuspocusServer.listen();
186+
187+
159188
/* setup GUI routes */
160189
routes(app);
161190

162191

163192
// catch 404 and forward to error handler
164-
app.use(function (req, res, next) {
193+
app.use(function (req) {
165194
console.log('ERROR: File not found', req.url);
166195
// var err = new Error('Not Found'); //, req);
167196
// err.status = 404;

app/views/scripts/components/ProjectPage.vue

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
<TextAnnotations
1010
:extract-keys="extractTextKeys"
1111
:link-prefix="linkPrefix"
12-
:files="files"
12+
:files="store.files"
1313
:selected="selectedFileIndex"
14+
@value-change="valueChange"
1415
@select-file="selectFile"
1516
/>
1617
<VolumeAnnotations
@@ -40,9 +41,9 @@
4041

4142
<script setup>
4243
import { forEach, get, set } from "lodash";
43-
import { initSyncedStore, waitForSync } from "../store/synced";
4444
import useVisualization from "../store/visualization";
45-
import { enableVueBindings } from "@syncedstore/core";
45+
import { HocuspocusProvider } from "@hocuspocus/provider";
46+
import { syncedStore, getYjsDoc, enableVueBindings } from "@syncedstore/core";
4647
import Tools from "./Tools.vue";
4748
import {
4849
Editor,
@@ -53,12 +54,20 @@
5354
} from "nwl-components";
5455
import * as Vue from "vue";
5556
56-
const { store, webrtcProvider, doc } = initSyncedStore(projectInfo.shortname);
57-
const { baseURL } = Vue.inject('config');
58-
5957
// make SyncedStore use Vuejs internally
6058
enableVueBindings(Vue);
61-
59+
60+
const store = syncedStore({ files: [], fragment: "xml" });
61+
const doc = getYjsDoc(store);
62+
63+
const crdtProvider = new HocuspocusProvider({
64+
url: "ws://0.0.0.0:8081", // FIXME
65+
name: projectInfo.shortname,
66+
document: doc
67+
});
68+
69+
const { baseURL } = Vue.inject('config');
70+
6271
const props = defineProps({
6372
project: {
6473
type: Object,
@@ -71,12 +80,7 @@
7180
});
7281
7382
const linkPrefix = `${baseURL}/project/${projectInfo.shortname}?source=`
74-
const files = Vue.ref(projectInfo.files.list);
7583
const selectedFileIndex = projectInfo.files.list.findIndex(file => file.source === props.selectedFile);
76-
doc.getArray("files").observe(() => {
77-
files.value.splice(0, files.value.length);
78-
files.value.push(...store.files);
79-
});
8084
8185
const textAnnotations = projectInfo.annotations.list.filter(anno => anno.type !== 'vectorial');
8286
const volumeAnnotations = projectInfo.annotations.list.filter(anno => anno.type === 'vectorial');
@@ -102,6 +106,10 @@
102106
const keys = new Map();
103107
keys.set("Name", "name");
104108
keys.set("File", "source");
109+
forEach(textAnnotations, (annotation) => {
110+
if (annotation.display)
111+
keys.set(annotation.name, `${annotation.name}`);
112+
});
105113
return keys;
106114
};
107115
@@ -113,19 +121,14 @@
113121
return keys;
114122
};
115123
116-
const syncMicrodraw = () => {
117-
console.log('sync microdraw')
118-
}
119-
120124
const valueChange = (content, index, selector) => {
121125
const sel =
122126
typeof selector === "string" ? [index, selector] : [index, ...selector];
123-
set(store.files, sel, content);
124-
syncMicrodraw();
127+
set(store.files, sel, content);
125128
};
126129
127130
const selectFile = async (file) => {
128-
window.location = `${linkPrefix}${file.source}`
131+
// No-op. We'd rather let user click on a link.
129132
}
130133
131134
const setupKeyDownListeners = () => {
@@ -137,27 +140,23 @@
137140
return;
138141
}
139142
if (selectedTr.previousElementSibling) {
140-
selectedTr.previousElementSibling.click();
143+
selectedTr.previousElementSibling.querySelector('a[href]').click();
141144
}
142145
break;
143146
case "ArrowDown":
144147
if (!selectedTr) {
145148
return;
146149
}
147150
if (selectedTr.nextElementSibling) {
148-
selectedTr.nextElementSibling.click();
151+
selectedTr.nextElementSibling.querySelector('a[href]').click();
149152
}
150153
break;
151154
default:
152155
break;
153156
}
154157
});
155158
};
156-
157-
const delay = (ms) => {
158-
return new Promise((resolve) => setTimeout(resolve, ms));
159-
};
160-
159+
161160
const handleLayoutChange = () => {
162161
Microdraw.resizeAnnotationOverlay();
163162
};
@@ -187,7 +186,18 @@
187186
188187
Vue.onMounted(async () => {
189188
setupKeyDownListeners();
190-
await waitForSync(webrtcProvider);
189+
crdtProvider.on('synced', () => {
190+
console.log('on synced');
191+
if (store.files.length === 0) {
192+
store.files.push(...projectInfo.files.list);
193+
forEach(textAnnotations, (annotation) => {
194+
forEach(annotation.values, (value, source) => {
195+
const file = store.files.find(file => file.source === source);
196+
if (file) file[annotation.name] = value;
197+
});
198+
});
199+
}
200+
});
191201
await initVisualization();
192202
window.addEventListener('resize', handleResize);
193203
});

app/views/scripts/store/synced.js

Lines changed: 0 additions & 25 deletions
This file was deleted.

package-lock.json

Lines changed: 92 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"dev-pages": "webpack --mode development --config webpack.pages.config.js && cp app/views/scripts/dist/*-page.js app/public/js/pages/"
1616
},
1717
"dependencies": {
18-
"@syncedstore/core": "^0.4.1",
18+
"@hocuspocus/provider": "^2.2.3",
19+
"@hocuspocus/server": "^2.2.3",
20+
"@syncedstore/core": "^0.4.3",
1921
"bcrypt": "^3.0.8",
2022
"body-parser": "^1.18.3",
2123
"cross-env": "^7.0.3",

0 commit comments

Comments
 (0)