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>
<div id="app">
<div id="app" class="app" v-bind:class="{'app--loading': loading}">
<layout></layout>
</div>
</template>
@ -11,6 +11,11 @@ export default {
components: {
Layout,
},
computed: {
loading() {
return !this.$store.getters['contents/current'].id;
},
},
};
</script>

View File

@ -161,5 +161,15 @@ export default {
.layout__panel--navigation-bar {
/* navigationBarHeight */
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>

View File

@ -117,9 +117,14 @@ export default {
this.titleInputElt.style.width = `${width}px`;
};
adjustWidth();
this.titleInputElt.addEventListener('keyup', adjustWidth);
this.titleInputElt.addEventListener('input', adjustWidth);
this.$store.watch(
() => this.$store.getters['files/current'].name,
adjustWidth, {
immediate: true,
});
this.titleInputElt.addEventListener('mouseenter', () => {
this.titleHover = true;
});
@ -137,7 +142,6 @@ export default {
position: absolute;
width: 100%;
height: 100%;
background-color: #2c2c2c;
padding: 4px 15px 0;
overflow: hidden;
}

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
import App from './components/App';
import store from './store';
import './services/syncSvc';
import './extensions/';
import './services/optional';
import './icons/';

View File

@ -102,7 +102,7 @@ export default {
clEditor.on('contentChanged', (text) => {
store.dispatch('contents/patchCurrent', { text });
syncDiscussionMarkers();
const content = store.getters('contents/current');
const content = store.getters['contents/current'];
if (!isChangePatch) {
previousPatchableText = currentPatchableText;
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
@ -123,7 +123,7 @@ export default {
clEditor.addMarker(newDiscussionMarker1);
},
initClEditor(opts, reinit) {
const content = store.getters('contents/current');
const content = store.getters['contents/current'];
if (content) {
const options = Object.assign({}, opts);
@ -156,7 +156,7 @@ export default {
this.lastExternalChange = Date.now();
}
syncDiscussionMarkers();
const content = store.getters('contents/current');
const content = store.getters['contents/current'];
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,
// Other objects
pagedownEditor: null,
options: {},
prismGrammars: {},
options: null,
prismGrammars: null,
converter: null,
parsingCtx: 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
*/
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 = {
sectionHighlighter: section => Prism.highlight(
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.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;
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.getters['contents/current'].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)) {
editorSvc.options = options;
editorSvc.initPrism();
editorSvc.initConverter();
initClEditor = true;
}
if (initClEditor) {
editorSvc.initClEditor();
}
}, {

View File

@ -96,21 +96,26 @@ class Connection {
}
}
class Storage {
constructor() {
this.lastTx = 0;
this.updatedMap = Object.create(null);
this.connection = new Connection();
}
export default {
lastTx: 0,
updatedMap: Object.create(null),
connection: new Connection(),
sync() {
return new Promise((resolve) => {
const storeItemMap = {};
[
store.state.contents,
store.state.files,
].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap));
const tx = this.connection.createTx();
this.readAll(storeItemMap, tx, () => this.writeAll(storeItemMap, tx));
}
this.connection.createTx((tx) => {
this.readAll(storeItemMap, tx, () => {
this.writeAll(storeItemMap, tx);
resolve();
});
});
});
},
readAll(storeItemMap, tx, cb) {
let resetMap;
@ -130,7 +135,15 @@ class Storage {
const itemsToDelete = [];
index.openCursor(range).onsuccess = (event) => {
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) => {
dbStore.delete(item.id);
});
@ -146,15 +159,8 @@ class Storage {
items.forEach(item => this.readDbItem(item, storeItemMap));
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) {
this.lastTx = tx.txCounter;
@ -191,7 +197,7 @@ class Storage {
this.updatedMap[item.id] = item.updated;
}
}
}
},
readDbItem(dbItem, storeItemMap) {
const existingStoreItem = storeItemMap[dbItem.id];
@ -206,17 +212,11 @@ class Storage {
}
}
} else if (this.updatedMap[dbItem.id] !== dbItem.updated) {
const storeItem = {
...dbItem,
tx: undefined,
};
this.updatedMap[storeItem.id] = storeItem.updated;
storeItemMap[storeItem.id] = storeItem;
this.updatedMap[dbItem.id] = dbItem.updated;
storeItemMap[dbItem.id] = dbItem;
// Put object in the store
const prefix = getStorePrefixFromType(storeItem.type);
store.commit(`${prefix}/setItem`, storeItem);
const prefix = getStorePrefixFromType(dbItem.type);
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 {
uid() {
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 Vue from 'vue';
import Vuex from 'vuex';
import contents from './modules/contents';
import files from './modules/files';
import layout from './modules/layout';
import editor from './modules/editor';
@ -11,6 +12,7 @@ const debug = process.env.NODE_ENV !== 'production';
const store = new Vuex.Store({
modules: {
contents,
files,
layout,
editor,

View File

@ -1,12 +1,12 @@
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,
current: (state, getters, rootState, rootGetters) =>
state.itemMap[rootGetters['files/current'].contentId] || emptyContent(),
state.itemMap[rootGetters['files/current'].contentId] || empty(),
};
module.actions = {

View File

@ -1,7 +1,7 @@
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,
@ -10,7 +10,17 @@ module.state = {
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 = {

View File

@ -1,23 +1,31 @@
import Vue from 'vue';
import utils from '../../services/utils';
export default () => ({
export default empty => ({
namespaced: true,
state: {
itemMap: {},
},
getters: {},
getters: {
items: state => Object.keys(state.itemMap).map(key => state.itemMap[key]),
},
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);
},
patchItem(state, patch) {
const item = state.itemMap[patch.id];
if (item) {
Vue.set(state.itemMap, item.id, {
...item,
...patch,
updated: Date.now(), // Trigger sync
});
Object.assign(item, patch);
item.updated = Date.now(); // Trigger sync
Vue.set(state.itemMap, item.id, item);
}
},
deleteItem(state, id) {