diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index f183e0876..d457d92f2 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -1,11 +1,11 @@ -/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/ +/*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/ require('./snippetbar.less'); const React = require('react'); const createClass = require('create-react-class'); const _ = require('lodash'); const cx = require('classnames'); -import { getHistoryItems, historyExists } from '../../utils/versionHistory.js'; +import { loadHistory } from '../../utils/versionHistory.js'; //Import all themes const ThemeSnippets = {}; @@ -50,30 +50,47 @@ const Snippetbar = createClass({ renderer : this.props.renderer, themeSelector : false, snippets : [], - historyExists : false + showHistory : false, + historyExists : false, + historyItems : [] }; }, - componentDidMount : async function() { + componentDidMount : async function(prevState) { const snippets = this.compileSnippets(); this.setState({ snippets : snippets }); }, - componentDidUpdate : async function(prevProps) { + componentDidUpdate : async function(prevProps, prevState) { if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) { this.setState({ snippets : this.compileSnippets() }); }; - if(historyExists(this.props.brew) != this.state.historyExists){ + // Update history list if it has changed + const checkHistoryItems = await loadHistory(this.props.brew); + + // If all items have the noData property, there is no saved data + const checkHistoryExists = !checkHistoryItems.every((historyItem)=>{ + return historyItem?.noData; + }); + if(prevState.historyExists != checkHistoryExists){ this.setState({ - historyExists : !this.state.historyExists + historyExists : checkHistoryExists }); - }; - + } + + // If any history items have changed, update the list + if(checkHistoryExists && checkHistoryItems.some((historyItem, index)=>{ + return index >= prevState.historyItems.length || !_.isEqual(historyItem, prevState.historyItems[index]); + })){ + this.setState({ + historyItems : checkHistoryItems + }); + } }, mergeCustomizer : function(oldValue, newValue, key) { @@ -151,12 +168,18 @@ const Snippetbar = createClass({ return this.props.updateBrew(item); }, + toggleHistoryMenu : function(){ + this.setState({ + showHistory : !this.state.showHistory + }); + }, + renderHistoryItems : function() { - const historyItems = getHistoryItems(this.props.brew); + if(!this.state.historyExists) return; return
- {_.map(historyItems, (item, index)=>{ - if(!item.savedAt) return; + {_.map(this.state.historyItems, (item, index)=>{ + if(item.noData || !item.savedAt) return; const saveTime = new Date(item.savedAt); const diffMs = new Date() - saveTime; @@ -197,9 +220,10 @@ const Snippetbar = createClass({ } return
-
+
- {this.state.historyExists && this.renderHistoryItems() } + { this.state.showHistory && this.renderHistoryItems() }
diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 79bb01aa2..fcc43e81a 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -228,8 +228,8 @@ const EditPage = createClass({ htmlErrors : Markdown.validate(prevState.brew.text) })); - updateHistory(this.state.brew); - versionHistoryGarbageCollection(); + await updateHistory(this.state.brew); + await versionHistoryGarbageCollection(); const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); diff --git a/client/homebrew/utils/versionHistory.js b/client/homebrew/utils/versionHistory.js index ad7c6102e..a23af844a 100644 --- a/client/homebrew/utils/versionHistory.js +++ b/client/homebrew/utils/versionHistory.js @@ -1,8 +1,10 @@ +import * as IDB from 'idb-keyval/dist/index.js'; + export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY'; export const HISTORY_SLOTS = 5; // History values in minutes -const DEFAULT_HISTORY_SAVE_DELAYS = { +const HISTORY_SAVE_DELAYS = { '0' : 0, '1' : 2, '2' : 10, @@ -10,29 +12,30 @@ const DEFAULT_HISTORY_SAVE_DELAYS = { '4' : 12 * 60, '5' : 2 * 24 * 60 }; +// const HISTORY_SAVE_DELAYS = { +// '0' : 0, +// '1' : 1, +// '2' : 2, +// '3' : 3, +// '4' : 4, +// '5' : 5 +// }; -const DEFAULT_GARBAGE_COLLECT_DELAY = 28 * 24 * 60; - -const HISTORY_SAVE_DELAYS = global.config?.historyData?.HISTORY_SAVE_DELAYS ?? DEFAULT_HISTORY_SAVE_DELAYS; -const GARBAGE_COLLECT_DELAY = global.config?.historyData?.GARBAGE_COLLECT_DELAY ?? DEFAULT_GARBAGE_COLLECT_DELAY; +const HB_DB = 'HOMEBREWERY-DB'; +const HB_STORE = 'HISTORY'; +const GARBAGE_COLLECT_DELAY = 28 * 24 * 60; +// const GARBAGE_COLLECT_DELAY = 10; function getKeyBySlot(brew, slot){ + // Return a string representing the key for this brew and history slot return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`; }; -function getVersionBySlot(brew, slot){ - // Read stored brew data - // - If it exists, parse data to object - // - If it doesn't exist, pass default object - const key = getKeyBySlot(brew, slot); - const storedVersion = localStorage.getItem(key); - const output = storedVersion ? JSON.parse(storedVersion) : { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true }; - return output; -}; - -function updateStoredBrew(brew, slot = 0) { +function parseBrewForStorage(brew, slot = 0) { + // Strip out unneeded object properties + // Returns an array of [ key, brew ] const archiveBrew = { title : brew.title, text : brew.text, @@ -46,44 +49,55 @@ function updateStoredBrew(brew, slot = 0) { archiveBrew.expireAt.setMinutes(archiveBrew.expireAt.getMinutes() + HISTORY_SAVE_DELAYS[slot]); const key = getKeyBySlot(brew, slot); - localStorage.setItem(key, JSON.stringify(archiveBrew)); + + return [key, archiveBrew]; } - -export function historyExists(brew){ - return Object.keys(localStorage) - .some((key)=>{ - return key.startsWith(`${HISTORY_PREFIX}-${brew.shareId}`); - }); +// Create a custom IDB store +async function createHBStore(){ + return await IDB.createStore(HB_DB, HB_STORE); } -export function loadHistory(brew){ - const history = {}; +export async function loadHistory(brew){ + const DEFAULT_HISTORY_ITEM = { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true }; - // Load data from local storage to History object + const historyKeys = []; + + // Create array of all history keys for (let i = 1; i <= HISTORY_SLOTS; i++){ - history[i] = getVersionBySlot(brew, i); + historyKeys.push(getKeyBySlot(brew, i)); }; - return history; + // Load all keys from IDB at once + const dataArray = await IDB.getMany(historyKeys, await createHBStore()); + return dataArray.map((data)=>{ return data ?? DEFAULT_HISTORY_ITEM; }); } -export function updateHistory(brew) { - const history = loadHistory(brew); +export async function updateHistory(brew) { + const history = await loadHistory(brew); // Walk each version position - for (let slot = HISTORY_SLOTS; slot > 0; slot--){ + for (let slot = HISTORY_SLOTS - 1; slot >= 0; slot--){ const storedVersion = history[slot]; // If slot has expired, update all lower slots and break if(new Date() >= new Date(storedVersion.expireAt)){ - for (let updateSlot = slot - 1; updateSlot>0; updateSlot--){ + + // Create array of arrays : [ [key1, value1], [key2, value2], ..., [keyN, valueN] ] + // to pass to IDB.setMany + const historyUpdate = []; + + for (let updateSlot = slot; updateSlot > 0; updateSlot--){ // Move data from updateSlot to updateSlot + 1 - !history[updateSlot]?.noData && updateStoredBrew(history[updateSlot], updateSlot + 1); + if(!history[updateSlot - 1]?.noData) { + historyUpdate.push(parseBrewForStorage(history[updateSlot - 1], updateSlot + 1)); + } }; // Update the most recent brew - updateStoredBrew(brew, 1); + historyUpdate.push(parseBrewForStorage(brew, 1)); + + await IDB.setMany(historyUpdate, await createHBStore()); // Break out of data checks because we found an expired value break; @@ -91,26 +105,15 @@ export function updateHistory(brew) { }; }; -export function getHistoryItems(brew){ - const historyArray = []; +export async function versionHistoryGarbageCollection(){ - for (let i = 1; i <= HISTORY_SLOTS; i++){ - historyArray.push(getVersionBySlot(brew, i)); - } + const entries = await IDB.entries(await createHBStore()); - return historyArray; -}; - -export function versionHistoryGarbageCollection(){ - Object.keys(localStorage) - .filter((key)=>{ - return key.startsWith(HISTORY_PREFIX); - }) - .forEach((key)=>{ - const collectAt = new Date(JSON.parse(localStorage.getItem(key)).savedAt); - collectAt.setMinutes(collectAt.getMinutes() + GARBAGE_COLLECT_DELAY); - if(new Date() > collectAt){ - localStorage.removeItem(key); - } - }); + for (const [key, value] of entries){ + const expireAt = new Date(value.savedAt); + expireAt.setMinutes(expireAt.getMinutes() + GARBAGE_COLLECT_DELAY); + if(new Date() > expireAt){ + await IDB.del(key, await createHBStore()); + }; + }; }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3283076de..4e076609e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "express-async-handler": "^1.2.0", "express-static-gzip": "2.1.8", "fs-extra": "11.2.0", + "idb-keyval": "^6.2.1", "js-yaml": "^4.1.0", "jwt-simple": "^0.5.6", "less": "^3.13.1", @@ -7363,6 +7364,11 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz", + "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", diff --git a/package.json b/package.json index 486d54639..f8007c608 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "express-async-handler": "^1.2.0", "express-static-gzip": "2.1.8", "fs-extra": "11.2.0", + "idb-keyval": "^6.2.1", "js-yaml": "^4.1.0", "jwt-simple": "^0.5.6", "less": "^3.13.1", @@ -139,4 +140,4 @@ "stylelint-config-recommended": "^14.0.1", "supertest": "^7.0.0" } -} \ No newline at end of file +} diff --git a/server/app.js b/server/app.js index 17ee3d2d9..df723b2c0 100644 --- a/server/app.js +++ b/server/app.js @@ -481,8 +481,7 @@ const renderPage = async (req, res)=>{ const configuration = { local : isLocalEnvironment, publicUrl : config.get('publicUrl') ?? '', - environment : nodeEnv, - history : config.get('historyConfig') ?? {} + environment : nodeEnv }; const props = { version : require('./../package.json').version,