Added syncSvc

This commit is contained in:
benweet 2017-07-28 08:40:24 +01:00
parent 02a4696b40
commit 8f743dd1b5
16 changed files with 170 additions and 70 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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;
} }

View File

@ -1,7 +1,9 @@
export default () => ({ export default () => ({
type: 'content',
state: {}, state: {},
text: '\n', text: '\n',
properties: {}, properties: {},
discussions: {}, discussions: {},
comments: {}, comments: {},
updated: 0,
}); });

View File

@ -1,4 +1,5 @@
export default () => ({ export default () => ({
type: 'file',
name: '', name: '',
folderId: null, folderId: null,
contentId: null, contentId: null,

View File

@ -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/';

View File

@ -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);
}, },
}; };

View File

@ -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();
} }
}, { }, {

View File

@ -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() {
return new Promise((resolve) => {
const storeItemMap = {}; const storeItemMap = {};
[ [
store.state.contents,
store.state.files, store.state.files,
].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap)); ].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap));
const tx = this.connection.createTx(); this.connection.createTx((tx) => {
this.readAll(storeItemMap, tx, () => this.writeAll(storeItemMap, 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
View 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);

View File

@ -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);
}, },
}; };

View File

@ -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,

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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) {