Added syncSvc
This commit is contained in:
parent
02a4696b40
commit
8f743dd1b5
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
export default () => ({
|
||||
type: 'content',
|
||||
state: {},
|
||||
text: '\n',
|
||||
properties: {},
|
||||
discussions: {},
|
||||
comments: {},
|
||||
updated: 0,
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
export default () => ({
|
||||
type: 'file',
|
||||
name: '',
|
||||
folderId: null,
|
||||
contentId: null,
|
||||
|
@ -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/';
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
}, {
|
||||
|
@ -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
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 {
|
||||
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);
|
||||
},
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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 = {
|
||||
|
@ -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 = {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user