From b3b7bd0f833566579b48d364801a8a1a8ee7bc28 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 10 Feb 2022 17:39:47 +0100 Subject: [PATCH] cache improvement solves #219 --- packages/api/src/controllers/queryHistory.js | 2 +- packages/api/src/utility/socket.js | 4 +- packages/web/src/utility/api.ts | 12 +-- packages/web/src/utility/cache.ts | 102 +++++++++++++++++-- packages/web/src/utility/metadataLoaders.ts | 80 ++------------- 5 files changed, 108 insertions(+), 92 deletions(-) diff --git a/packages/api/src/controllers/queryHistory.js b/packages/api/src/controllers/queryHistory.js index 9d4c3eb0..4fc5d5b7 100644 --- a/packages/api/src/controllers/queryHistory.js +++ b/packages/api/src/controllers/queryHistory.js @@ -48,7 +48,7 @@ module.exports = { async write({ data }) { const fileName = path.join(datadir(), 'query-history.jsonl'); await fs.appendFile(fileName, JSON.stringify(data) + '\n'); - socket.emitChanged('query-history-changed'); + socket.emit('query-history-changed'); return 'OK'; }, }; diff --git a/packages/api/src/utility/socket.js b/packages/api/src/utility/socket.js index 928e24c5..5ebf4deb 100644 --- a/packages/api/src/utility/socket.js +++ b/packages/api/src/utility/socket.js @@ -23,7 +23,7 @@ module.exports = { } }, emitChanged(key) { - this.emit('clean-cache', key); - this.emit(key); + this.emit('changed-cache', key); + // this.emit(key); }, }; diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index f1fabec1..3f34afa1 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -1,11 +1,11 @@ import resolveApi, { resolveApiHeaders } from './resolveApi'; import { writable } from 'svelte/store'; -import { cacheClean } from './cache'; +// import { cacheClean } from './cache'; import getElectron from './getElectron'; // import socket from './socket'; let eventSource; -let cacheCleanerRegistered; +// let cacheCleanerRegistered; function wantEventSource() { if (!eventSource) { @@ -60,10 +60,10 @@ export function apiOn(event: string, handler: Function) { eventSource.addEventListener(event, apiHandlers.get(handler)); } - if (!cacheCleanerRegistered) { - cacheCleanerRegistered = true; - apiOn('clean-cache', reloadTrigger => cacheClean(reloadTrigger)); - } + // if (!cacheCleanerRegistered) { + // cacheCleanerRegistered = true; + // apiOn('clean-cache', reloadTrigger => cacheClean(reloadTrigger)); + // } } export function apiOff(event: string, handler: Function) { diff --git a/packages/web/src/utility/cache.ts b/packages/web/src/utility/cache.ts index 8dd251bf..7fddee08 100644 --- a/packages/web/src/utility/cache.ts +++ b/packages/web/src/utility/cache.ts @@ -1,40 +1,120 @@ +import { apiOn } from './api'; import getAsArray from './getAsArray'; -let cachedByKey = {}; -let cachedPromisesByKey = {}; +const cachedByKey = {}; +const cachedPromisesByKey = {}; const cachedKeysByReloadTrigger = {}; +const subscriptionsByReloadTrigger = {}; +const cacheGenerationByKey = {}; -export function cacheGet(key) { +let cacheGeneration = 0; + +function cacheGet(key) { return cachedByKey[key]; } -export function cacheSet(key, value, reloadTrigger) { - cachedByKey[key] = value; +function addCacheKeyToReloadTrigger(cacheKey, reloadTrigger) { for (const item of getAsArray(reloadTrigger)) { if (!(item in cachedKeysByReloadTrigger)) { cachedKeysByReloadTrigger[item] = []; } - cachedKeysByReloadTrigger[item].push(key); + cachedKeysByReloadTrigger[item].push(cacheKey); } - delete cachedPromisesByKey[key]; } -export function cacheClean(reloadTrigger) { +function cacheSet(cacheKey, value, reloadTrigger, generation) { + cachedByKey[cacheKey] = value; + addCacheKeyToReloadTrigger(cacheKey, reloadTrigger); + delete cachedPromisesByKey[cacheKey]; + cacheGenerationByKey[cacheKey] = generation; +} + +function cacheClean(reloadTrigger) { + cacheGeneration += 1; for (const item of getAsArray(reloadTrigger)) { const keys = cachedKeysByReloadTrigger[item]; if (keys) { for (const key of keys) { delete cachedByKey[key]; delete cachedPromisesByKey[key]; + cacheGenerationByKey[key] = cacheGeneration; } } delete cachedKeysByReloadTrigger[item]; } } -export function getCachedPromise(key, func) { - if (key in cachedPromisesByKey) return cachedPromisesByKey[key]; +function getCachedPromise(reloadTrigger, cacheKey, func) { + if (cacheKey in cachedPromisesByKey) return cachedPromisesByKey[cacheKey]; const promise = func(); - cachedPromisesByKey[key] = promise; + cachedPromisesByKey[cacheKey] = promise; + addCacheKeyToReloadTrigger(cacheKey, reloadTrigger); return promise; } + +function acquireCacheGeneration() { + cacheGeneration += 1; + return cacheGeneration; +} + +function getCacheGenerationForKey(cacheKey) { + return cacheGenerationByKey[cacheKey] || 0; +} + +export async function loadCachedValue(reloadTrigger, cacheKey, func) { + const fromCache = cacheGet(cacheKey); + if (fromCache) { + return fromCache; + } else { + const generation = acquireCacheGeneration(); + try { + const res = await getCachedPromise(reloadTrigger, cacheKey, func); + if (getCacheGenerationForKey(cacheKey) > generation) { + return cacheGet(cacheKey) || res; + } else { + cacheSet(cacheKey, res, reloadTrigger, generation); + return res; + } + } catch (err) { + console.error('Error when using cached promise', err); + cacheClean(cacheKey); + const res = await func(); + cacheSet(cacheKey, res, reloadTrigger, generation); + return res; + } + } +} + +export async function subscribeCacheChange(reloadTrigger, cacheKey, reloadHandler) { + for (const item of getAsArray(reloadTrigger)) { + if (!subscriptionsByReloadTrigger[item]) { + subscriptionsByReloadTrigger[item] = []; + } + subscriptionsByReloadTrigger[item].push(reloadHandler); + } +} + +export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHandler) { + for (const item of getAsArray(reloadTrigger)) { + if (subscriptionsByReloadTrigger[item]) { + subscriptionsByReloadTrigger[item] = subscriptionsByReloadTrigger[item].filter(x => x != reloadHandler); + } + if (subscriptionsByReloadTrigger[item].length == 0) { + delete subscriptionsByReloadTrigger[item]; + } + } +} + +function dispatchCacheChange(reloadTrigger) { + cacheClean(reloadTrigger); + + for (const item of getAsArray(reloadTrigger)) { + if (subscriptionsByReloadTrigger[item]) { + for (const handler of subscriptionsByReloadTrigger[item]) { + handler(); + } + } + } +} + +apiOn('changed-cache', reloadTrigger => dispatchCacheChange(reloadTrigger)); diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts index c6edbef8..016974dd 100644 --- a/packages/web/src/utility/metadataLoaders.ts +++ b/packages/web/src/utility/metadataLoaders.ts @@ -1,7 +1,6 @@ import _ from 'lodash'; -import { cacheGet, cacheSet, getCachedPromise } from './cache'; +import { loadCachedValue, subscribeCacheChange, unsubscribeCacheChange } from './cache'; import stableStringify from 'json-stable-stringify'; -import { cacheClean } from './cache'; import getAsArray from './getAsArray'; import { DatabaseInfo } from 'dbgate-types'; import { derived } from 'svelte/store'; @@ -123,7 +122,7 @@ const appFilesLoader = ({ folder }) => ({ const usedAppsLoader = ({ conid, database }) => ({ url: 'apps/get-used-apps', - params: { }, + params: {}, reloadTrigger: `used-apps-changed`, }); @@ -172,11 +171,7 @@ async function getCore(loader, args) { return res; } - const fromCache = cacheGet(key); - if (fromCache) return fromCache; - const res = await getCachedPromise(key, doLoad); - - cacheSet(key, res, reloadTrigger); + const res = await loadCachedValue(reloadTrigger, key, doLoad); return res; } @@ -187,77 +182,20 @@ function useCore(loader, args) { return { subscribe: onChange => { async function handleReload() { - async function doLoad() { - const resp = await apiCall(url, params); - const res = (transform || (x => x))(resp); - if (onLoaded) onLoaded(res); - return res; - } - - if (cacheKey) { - const fromCache = cacheGet(cacheKey); - if (fromCache) { - onChange(fromCache); - } else { - try { - const res = await getCachedPromise(cacheKey, doLoad); - cacheSet(cacheKey, res, reloadTrigger); - onChange(res); - } catch (err) { - console.error(`Error when using cached promise ${url}`, err); - cacheClean(cacheKey); - const res = await doLoad(); - cacheSet(cacheKey, res, reloadTrigger); - onChange(res); - } - } - } else { - const res = await doLoad(); - onChange(res); - } + const res = await getCore(loader, args); + onChange(res); } - // if (reloadTrigger && !socket) { - // console.error('Socket not available, reloadTrigger not planned'); - // } handleReload(); + if (reloadTrigger) { - for (const item of getAsArray(reloadTrigger)) { - apiOn(item, handleReload); - } + subscribeCacheChange(reloadTrigger, cacheKey, handleReload); return () => { - for (const item of getAsArray(reloadTrigger)) { - apiOff(item, handleReload); - } + unsubscribeCacheChange(reloadTrigger, cacheKey, handleReload); }; } }, }; - - // const useTrack = track => ({ - // subscribe: onChange => { - // onChange('TRACK ' + track); - // if (track) { - // const handle = setInterval(() => onChange('TRACK ' + track + ';' + new Date()), 1000); - // // console.log("ON", track); - // const oldTrack = track; - // return () => { - // clearInterval(handle); - // // console.log("OFF", oldTrack); - // }; - // } - // }, - // }); - - // const res = useFetch({ - // url, - // params, - // reloadTrigger, - // cacheKey, - // transform, - // }); - - // return res; } /** @returns {Promise} */ @@ -439,8 +377,6 @@ export function useAppFolders(args = {}) { return useCore(appFoldersLoader, args); } - - export function getUsedApps(args = {}) { return getCore(usedAppsLoader, args); }