From 8f743dd1b578c2c64fd4020fb81fc211ee42d039 Mon Sep 17 00:00:00 2001 From: benweet Date: Fri, 28 Jul 2017 08:40:24 +0100 Subject: [PATCH] Added syncSvc --- src/components/App.vue | 7 ++- src/components/Layout.vue | 10 ++++ src/components/NavigationBar.vue | 8 ++- src/data/emptyContent.js | 2 + src/data/emptyFile.js | 1 + src/data/{sample.md => welcomeFile.md} | 0 src/index.js | 1 + src/services/editorEngineSvc.js | 6 +-- src/services/editorSvc.js | 33 ++++++------ src/services/localDbSvc.js | 70 +++++++++++++------------- src/services/syncSvc.js | 48 ++++++++++++++++++ src/services/utils.js | 6 ++- src/store/index.js | 2 + src/store/modules/contents.js | 6 +-- src/store/modules/files.js | 16 ++++-- src/store/modules/moduleTemplate.js | 24 ++++++--- 16 files changed, 170 insertions(+), 70 deletions(-) rename src/data/{sample.md => welcomeFile.md} (100%) create mode 100644 src/services/syncSvc.js diff --git a/src/components/App.vue b/src/components/App.vue index 242f2b73..2b01a1db 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -1,5 +1,5 @@ @@ -11,6 +11,11 @@ export default { components: { Layout, }, + computed: { + loading() { + return !this.$store.getters['contents/current'].id; + }, + }, }; diff --git a/src/components/Layout.vue b/src/components/Layout.vue index a4dd32ed..f0664718 100644 --- a/src/components/Layout.vue +++ b/src/components/Layout.vue @@ -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; + } } diff --git a/src/components/NavigationBar.vue b/src/components/NavigationBar.vue index 7fb80f7a..7ae77b5b 100644 --- a/src/components/NavigationBar.vue +++ b/src/components/NavigationBar.vue @@ -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; } diff --git a/src/data/emptyContent.js b/src/data/emptyContent.js index fc89d65e..4a298adb 100644 --- a/src/data/emptyContent.js +++ b/src/data/emptyContent.js @@ -1,7 +1,9 @@ export default () => ({ + type: 'content', state: {}, text: '\n', properties: {}, discussions: {}, comments: {}, + updated: 0, }); diff --git a/src/data/emptyFile.js b/src/data/emptyFile.js index b0d96108..75dab695 100644 --- a/src/data/emptyFile.js +++ b/src/data/emptyFile.js @@ -1,4 +1,5 @@ export default () => ({ + type: 'file', name: '', folderId: null, contentId: null, diff --git a/src/data/sample.md b/src/data/welcomeFile.md similarity index 100% rename from src/data/sample.md rename to src/data/welcomeFile.md diff --git a/src/index.js b/src/index.js index e8a832e7..700f85f2 100644 --- a/src/index.js +++ b/src/index.js @@ -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/'; diff --git a/src/services/editorEngineSvc.js b/src/services/editorEngineSvc.js index 971f181f..99ecdc8b 100644 --- a/src/services/editorEngineSvc.js +++ b/src/services/editorEngineSvc.js @@ -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); }, }; diff --git a/src/services/editorSvc.js b/src/services/editorSvc.js index e3282fc9..3deb2048 100644 --- a/src/services/editorSvc.js +++ b/src/services/editorSvc.js @@ -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(); } }, { diff --git a/src/services/localDbSvc.js b/src/services/localDbSvc.js index 488b41f9..0bc83ef8 100644 --- a/src/services/localDbSvc.js +++ b/src/services/localDbSvc.js @@ -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() { - const storeItemMap = {}; - [ - store.state.files, - ].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap)); - const tx = this.connection.createTx(); - this.readAll(storeItemMap, tx, () => this.writeAll(storeItemMap, tx)); - } + return new Promise((resolve) => { + const storeItemMap = {}; + [ + store.state.contents, + store.state.files, + ].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap)); + 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; + }, +}; diff --git a/src/services/syncSvc.js b/src/services/syncSvc.js new file mode 100644 index 00000000..a8bb3024 --- /dev/null +++ b/src/services/syncSvc.js @@ -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); diff --git a/src/services/utils.js b/src/services/utils.js index 4b1dd55b..ac09abf8 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -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); }, }; diff --git a/src/store/index.js b/src/store/index.js index 93497307..0e5a7bcc 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -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, diff --git a/src/store/modules/contents.js b/src/store/modules/contents.js index b9586763..172a01d7 100644 --- a/src/store/modules/contents.js +++ b/src/store/modules/contents.js @@ -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 = { diff --git a/src/store/modules/files.js b/src/store/modules/files.js index 9d33d44f..afa09b4b 100644 --- a/src/store/modules/files.js +++ b/src/store/modules/files.js @@ -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 = { diff --git a/src/store/modules/moduleTemplate.js b/src/store/modules/moduleTemplate.js index 94676342..1577ee55 100644 --- a/src/store/modules/moduleTemplate.js +++ b/src/store/modules/moduleTemplate.js @@ -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) {