Added syncSvc
This commit is contained in:
parent
02a4696b40
commit
8f743dd1b5
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app" class="app" v-bind:class="{'app--loading': loading}">
|
||||||
<layout></layout>
|
<layout></layout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -11,6 +11,11 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Layout,
|
Layout,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
loading() {
|
||||||
|
return !this.$store.getters['contents/current'].id;
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -161,5 +161,15 @@ export default {
|
|||||||
.layout__panel--navigation-bar {
|
.layout__panel--navigation-bar {
|
||||||
/* navigationBarHeight */
|
/* navigationBarHeight */
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout__panel--button-bar,
|
||||||
|
.layout__panel--status-bar,
|
||||||
|
.layout__panel--side-bar,
|
||||||
|
.layout__panel--navigation-bar {
|
||||||
|
.app--loading & > * {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -117,9 +117,14 @@ export default {
|
|||||||
this.titleInputElt.style.width = `${width}px`;
|
this.titleInputElt.style.width = `${width}px`;
|
||||||
};
|
};
|
||||||
|
|
||||||
adjustWidth();
|
|
||||||
this.titleInputElt.addEventListener('keyup', adjustWidth);
|
this.titleInputElt.addEventListener('keyup', adjustWidth);
|
||||||
this.titleInputElt.addEventListener('input', adjustWidth);
|
this.titleInputElt.addEventListener('input', adjustWidth);
|
||||||
|
this.$store.watch(
|
||||||
|
() => this.$store.getters['files/current'].name,
|
||||||
|
adjustWidth, {
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
this.titleInputElt.addEventListener('mouseenter', () => {
|
this.titleInputElt.addEventListener('mouseenter', () => {
|
||||||
this.titleHover = true;
|
this.titleHover = true;
|
||||||
});
|
});
|
||||||
@ -137,7 +142,6 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #2c2c2c;
|
|
||||||
padding: 4px 15px 0;
|
padding: 4px 15px 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
|
type: 'content',
|
||||||
state: {},
|
state: {},
|
||||||
text: '\n',
|
text: '\n',
|
||||||
properties: {},
|
properties: {},
|
||||||
discussions: {},
|
discussions: {},
|
||||||
comments: {},
|
comments: {},
|
||||||
|
updated: 0,
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
|
type: 'file',
|
||||||
name: '',
|
name: '',
|
||||||
folderId: null,
|
folderId: null,
|
||||||
contentId: null,
|
contentId: null,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './components/App';
|
import App from './components/App';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
import './services/syncSvc';
|
||||||
import './extensions/';
|
import './extensions/';
|
||||||
import './services/optional';
|
import './services/optional';
|
||||||
import './icons/';
|
import './icons/';
|
||||||
|
@ -102,7 +102,7 @@ export default {
|
|||||||
clEditor.on('contentChanged', (text) => {
|
clEditor.on('contentChanged', (text) => {
|
||||||
store.dispatch('contents/patchCurrent', { text });
|
store.dispatch('contents/patchCurrent', { text });
|
||||||
syncDiscussionMarkers();
|
syncDiscussionMarkers();
|
||||||
const content = store.getters('contents/current');
|
const content = store.getters['contents/current'];
|
||||||
if (!isChangePatch) {
|
if (!isChangePatch) {
|
||||||
previousPatchableText = currentPatchableText;
|
previousPatchableText = currentPatchableText;
|
||||||
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||||
@ -123,7 +123,7 @@ export default {
|
|||||||
clEditor.addMarker(newDiscussionMarker1);
|
clEditor.addMarker(newDiscussionMarker1);
|
||||||
},
|
},
|
||||||
initClEditor(opts, reinit) {
|
initClEditor(opts, reinit) {
|
||||||
const content = store.getters('contents/current');
|
const content = store.getters['contents/current'];
|
||||||
if (content) {
|
if (content) {
|
||||||
const options = Object.assign({}, opts);
|
const options = Object.assign({}, opts);
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ export default {
|
|||||||
this.lastExternalChange = Date.now();
|
this.lastExternalChange = Date.now();
|
||||||
}
|
}
|
||||||
syncDiscussionMarkers();
|
syncDiscussionMarkers();
|
||||||
const content = store.getters('contents/current');
|
const content = store.getters['contents/current'];
|
||||||
return clEditor.setContent(content.text, isExternal);
|
return clEditor.setContent(content.text, isExternal);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -42,8 +42,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
tocElt: null,
|
tocElt: null,
|
||||||
// Other objects
|
// Other objects
|
||||||
pagedownEditor: null,
|
pagedownEditor: null,
|
||||||
options: {},
|
options: null,
|
||||||
prismGrammars: {},
|
prismGrammars: null,
|
||||||
converter: null,
|
converter: null,
|
||||||
parsingCtx: null,
|
parsingCtx: null,
|
||||||
conversionCtx: null,
|
conversionCtx: null,
|
||||||
@ -172,17 +172,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
|
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
|
||||||
*/
|
*/
|
||||||
initClEditor() {
|
initClEditor() {
|
||||||
const contentId = store.getters['contents/current'].id;
|
|
||||||
if (contentId !== lastContentId) {
|
|
||||||
reinitClEditor = true;
|
|
||||||
instantPreview = true;
|
|
||||||
lastContentId = contentId;
|
|
||||||
}
|
|
||||||
if (!contentId) {
|
|
||||||
// This means the content is not loaded yet
|
|
||||||
editorEngineSvc.clEditor.toggleEditable(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const options = {
|
const options = {
|
||||||
sectionHighlighter: section => Prism.highlight(
|
sectionHighlighter: section => Prism.highlight(
|
||||||
section.text, this.prismGrammars[section.data]),
|
section.text, this.prismGrammars[section.data]),
|
||||||
@ -193,6 +182,9 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
};
|
};
|
||||||
editorEngineSvc.initClEditor(options, reinitClEditor);
|
editorEngineSvc.initClEditor(options, reinitClEditor);
|
||||||
editorEngineSvc.clEditor.toggleEditable(true);
|
editorEngineSvc.clEditor.toggleEditable(true);
|
||||||
|
const contentId = store.getters['contents/current'].id;
|
||||||
|
// Switch off the editor when no content is loaded
|
||||||
|
editorEngineSvc.clEditor.toggleEditable(!!contentId);
|
||||||
reinitClEditor = false;
|
reinitClEditor = false;
|
||||||
this.restoreScrollPosition();
|
this.restoreScrollPosition();
|
||||||
},
|
},
|
||||||
@ -717,14 +709,27 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
|
|
||||||
|
// Watch file content properties changes
|
||||||
store.watch(
|
store.watch(
|
||||||
() => store.getters['contents/current'].properties,
|
() => store.getters['contents/current'].properties,
|
||||||
(properties) => {
|
(properties) => {
|
||||||
const options = properties && extensionSvc.getOptions(properties, true);
|
// Track ID changes at the same time
|
||||||
|
const contentId = store.getters['contents/current'].id;
|
||||||
|
let initClEditor = false;
|
||||||
|
if (contentId !== lastContentId) {
|
||||||
|
reinitClEditor = true;
|
||||||
|
instantPreview = true;
|
||||||
|
lastContentId = contentId;
|
||||||
|
initClEditor = true;
|
||||||
|
}
|
||||||
|
const options = extensionSvc.getOptions(properties, true);
|
||||||
if (JSON.stringify(options) !== JSON.stringify(editorSvc.options)) {
|
if (JSON.stringify(options) !== JSON.stringify(editorSvc.options)) {
|
||||||
editorSvc.options = options;
|
editorSvc.options = options;
|
||||||
editorSvc.initPrism();
|
editorSvc.initPrism();
|
||||||
editorSvc.initConverter();
|
editorSvc.initConverter();
|
||||||
|
initClEditor = true;
|
||||||
|
}
|
||||||
|
if (initClEditor) {
|
||||||
editorSvc.initClEditor();
|
editorSvc.initClEditor();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
@ -96,21 +96,26 @@ class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Storage {
|
export default {
|
||||||
constructor() {
|
lastTx: 0,
|
||||||
this.lastTx = 0;
|
updatedMap: Object.create(null),
|
||||||
this.updatedMap = Object.create(null);
|
connection: new Connection(),
|
||||||
this.connection = new Connection();
|
|
||||||
}
|
|
||||||
|
|
||||||
sync() {
|
sync() {
|
||||||
const storeItemMap = {};
|
return new Promise((resolve) => {
|
||||||
[
|
const storeItemMap = {};
|
||||||
store.state.files,
|
[
|
||||||
].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap));
|
store.state.contents,
|
||||||
const tx = this.connection.createTx();
|
store.state.files,
|
||||||
this.readAll(storeItemMap, tx, () => this.writeAll(storeItemMap, tx));
|
].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap));
|
||||||
}
|
this.connection.createTx((tx) => {
|
||||||
|
this.readAll(storeItemMap, tx, () => {
|
||||||
|
this.writeAll(storeItemMap, tx);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
readAll(storeItemMap, tx, cb) {
|
readAll(storeItemMap, tx, cb) {
|
||||||
let resetMap;
|
let resetMap;
|
||||||
@ -130,7 +135,15 @@ class Storage {
|
|||||||
const itemsToDelete = [];
|
const itemsToDelete = [];
|
||||||
index.openCursor(range).onsuccess = (event) => {
|
index.openCursor(range).onsuccess = (event) => {
|
||||||
const cursor = event.target.result;
|
const cursor = event.target.result;
|
||||||
if (!cursor) {
|
if (cursor) {
|
||||||
|
const item = cursor.value;
|
||||||
|
items.push(item);
|
||||||
|
// Remove old deleted markers
|
||||||
|
if (!item.updated && tx.txCounter - item.tx > deletedMarkerMaxAge) {
|
||||||
|
itemsToDelete.push(item);
|
||||||
|
}
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
itemsToDelete.forEach((item) => {
|
itemsToDelete.forEach((item) => {
|
||||||
dbStore.delete(item.id);
|
dbStore.delete(item.id);
|
||||||
});
|
});
|
||||||
@ -146,15 +159,8 @@ class Storage {
|
|||||||
items.forEach(item => this.readDbItem(item, storeItemMap));
|
items.forEach(item => this.readDbItem(item, storeItemMap));
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
const item = cursor.value;
|
|
||||||
items.push(item);
|
|
||||||
// Remove old deleted markers
|
|
||||||
if (!item.updated && tx.txCounter - item.tx > deletedMarkerMaxAge) {
|
|
||||||
itemsToDelete.push(item);
|
|
||||||
}
|
|
||||||
cursor.continue();
|
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
|
|
||||||
writeAll(storeItemMap, tx) {
|
writeAll(storeItemMap, tx) {
|
||||||
this.lastTx = tx.txCounter;
|
this.lastTx = tx.txCounter;
|
||||||
@ -191,7 +197,7 @@ class Storage {
|
|||||||
this.updatedMap[item.id] = item.updated;
|
this.updatedMap[item.id] = item.updated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
readDbItem(dbItem, storeItemMap) {
|
readDbItem(dbItem, storeItemMap) {
|
||||||
const existingStoreItem = storeItemMap[dbItem.id];
|
const existingStoreItem = storeItemMap[dbItem.id];
|
||||||
@ -206,17 +212,11 @@ class Storage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.updatedMap[dbItem.id] !== dbItem.updated) {
|
} else if (this.updatedMap[dbItem.id] !== dbItem.updated) {
|
||||||
const storeItem = {
|
this.updatedMap[dbItem.id] = dbItem.updated;
|
||||||
...dbItem,
|
storeItemMap[dbItem.id] = dbItem;
|
||||||
tx: undefined,
|
|
||||||
};
|
|
||||||
this.updatedMap[storeItem.id] = storeItem.updated;
|
|
||||||
storeItemMap[storeItem.id] = storeItem;
|
|
||||||
// Put object in the store
|
// Put object in the store
|
||||||
const prefix = getStorePrefixFromType(storeItem.type);
|
const prefix = getStorePrefixFromType(dbItem.type);
|
||||||
store.commit(`${prefix}/setItem`, storeItem);
|
store.commit(`${prefix}/setItem`, dbItem);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Storage;
|
|
||||||
|
48
src/services/syncSvc.js
Normal file
48
src/services/syncSvc.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import localDbSvc from './localDbSvc';
|
||||||
|
import store from '../store';
|
||||||
|
import welcomeFile from '../data/welcomeFile.md';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
|
const ifNoId = cb => (obj) => {
|
||||||
|
if (obj.id) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch file changing
|
||||||
|
store.watch(
|
||||||
|
() => store.getters['files/current'].id,
|
||||||
|
() => Promise.resolve(store.getters['files/current'])
|
||||||
|
// If current file has no ID, get the most recent file
|
||||||
|
.then(ifNoId(() => store.getters['files/mostRecent']))
|
||||||
|
// If no ID, load the DB (we're booting)
|
||||||
|
.then(ifNoId(() => localDbSvc.sync()
|
||||||
|
// Retry
|
||||||
|
.then(() => store.getters['files/current'])
|
||||||
|
.then(ifNoId(() => store.getters['files/mostRecent'])),
|
||||||
|
))
|
||||||
|
// Finally create a new file
|
||||||
|
.then(ifNoId(() => {
|
||||||
|
const contentId = utils.uid();
|
||||||
|
store.commit('contents/setItem', {
|
||||||
|
id: contentId,
|
||||||
|
text: welcomeFile,
|
||||||
|
});
|
||||||
|
const fileId = utils.uid();
|
||||||
|
store.commit('files/setItem', {
|
||||||
|
id: fileId,
|
||||||
|
name: 'Welcome file',
|
||||||
|
contentId,
|
||||||
|
});
|
||||||
|
return store.state.files.itemMap[fileId];
|
||||||
|
}))
|
||||||
|
.then((currentFile) => {
|
||||||
|
store.commit('files/patchItem', { id: currentFile.id });
|
||||||
|
store.commit('files/setCurrentId', currentFile.id);
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.setInterval(() => localDbSvc.sync(), 1200);
|
@ -6,6 +6,10 @@ const array = new Uint32Array(20);
|
|||||||
export default {
|
export default {
|
||||||
uid() {
|
uid() {
|
||||||
crypto.getRandomValues(array);
|
crypto.getRandomValues(array);
|
||||||
return array.map(value => alphabet[value % radix]).join('');
|
return array.cl_map(value => alphabet[value % radix]).join('');
|
||||||
|
},
|
||||||
|
setInterval(func, interval) {
|
||||||
|
const randomizedInterval = Math.floor((1 + ((Math.random() - 0.5) * 0.1)) * interval);
|
||||||
|
setInterval(() => func(), randomizedInterval);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import createLogger from 'vuex/dist/logger';
|
import createLogger from 'vuex/dist/logger';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
|
import contents from './modules/contents';
|
||||||
import files from './modules/files';
|
import files from './modules/files';
|
||||||
import layout from './modules/layout';
|
import layout from './modules/layout';
|
||||||
import editor from './modules/editor';
|
import editor from './modules/editor';
|
||||||
@ -11,6 +12,7 @@ const debug = process.env.NODE_ENV !== 'production';
|
|||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
|
contents,
|
||||||
files,
|
files,
|
||||||
layout,
|
layout,
|
||||||
editor,
|
editor,
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import emptyContent from '../../data/emptyContent';
|
import empty from '../../data/emptyContent';
|
||||||
|
|
||||||
const module = moduleTemplate();
|
const module = moduleTemplate(empty);
|
||||||
|
|
||||||
module.getters = {
|
module.getters = {
|
||||||
...module.getters,
|
...module.getters,
|
||||||
current: (state, getters, rootState, rootGetters) =>
|
current: (state, getters, rootState, rootGetters) =>
|
||||||
state.itemMap[rootGetters['files/current'].contentId] || emptyContent(),
|
state.itemMap[rootGetters['files/current'].contentId] || empty(),
|
||||||
};
|
};
|
||||||
|
|
||||||
module.actions = {
|
module.actions = {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import defaultFile from '../../data/emptyFile';
|
import empty from '../../data/emptyFile';
|
||||||
|
|
||||||
const module = moduleTemplate();
|
const module = moduleTemplate(empty);
|
||||||
|
|
||||||
module.state = {
|
module.state = {
|
||||||
...module.state,
|
...module.state,
|
||||||
@ -10,7 +10,17 @@ module.state = {
|
|||||||
|
|
||||||
module.getters = {
|
module.getters = {
|
||||||
...module.getters,
|
...module.getters,
|
||||||
current: state => state.itemMap[state.currentId] || defaultFile(),
|
current: state => state.itemMap[state.currentId] || empty(),
|
||||||
|
itemsByUpdated: (state, getters) =>
|
||||||
|
getters.items.slice().sort((file1, file2) => file2.updated - file1.updated),
|
||||||
|
mostRecent: (state, getters) => getters.itemsByUpdated[0] || empty(),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.mutations = {
|
||||||
|
...module.mutations,
|
||||||
|
setCurrentId(state, value) {
|
||||||
|
state.currentId = value;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.actions = {
|
module.actions = {
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
export default () => ({
|
export default empty => ({
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
itemMap: {},
|
itemMap: {},
|
||||||
},
|
},
|
||||||
getters: {},
|
getters: {
|
||||||
|
items: state => Object.keys(state.itemMap).map(key => state.itemMap[key]),
|
||||||
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setItem(state, item) {
|
setItem(state, value) {
|
||||||
|
const item = Object.assign(empty(), value);
|
||||||
|
if (!item.id) {
|
||||||
|
item.id = utils.uid();
|
||||||
|
}
|
||||||
|
if (!item.updated) {
|
||||||
|
item.updated = Date.now();
|
||||||
|
}
|
||||||
Vue.set(state.itemMap, item.id, item);
|
Vue.set(state.itemMap, item.id, item);
|
||||||
},
|
},
|
||||||
patchItem(state, patch) {
|
patchItem(state, patch) {
|
||||||
const item = state.itemMap[patch.id];
|
const item = state.itemMap[patch.id];
|
||||||
if (item) {
|
if (item) {
|
||||||
Vue.set(state.itemMap, item.id, {
|
Object.assign(item, patch);
|
||||||
...item,
|
item.updated = Date.now(); // Trigger sync
|
||||||
...patch,
|
Vue.set(state.itemMap, item.id, item);
|
||||||
updated: Date.now(), // Trigger sync
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteItem(state, id) {
|
deleteItem(state, id) {
|
||||||
|
Loading…
Reference in New Issue
Block a user