From 73a8259ccb8eaaf63d107b267b51fee991a6ffcd Mon Sep 17 00:00:00 2001 From: Jack Greenlee Date: Wed, 27 Sep 2023 16:03:11 -0400 Subject: [PATCH] add storage.ts, to replace KVStore storage.ts is replacing storage.js which had the KVStore service inside it. storage.ts will provide a set of functions performing the same duties as KVStorage's functions did. - `get` -> `storageGet` - `set` -> `storageSet` - `remove` -> `storageRemove` - `getDirect` -> `storageGetDirect` - `syncAllWebAndNativeValues` -> `storageSyncLocalAndNative` storage.ts will still use the "BEMUserCache" Cordova plugin in exactly the same way that KVStore did. However, instead of using the `angular-local-storage` package as a wrapper around localStorage, we will use it directly. A couple functions were added ('localStorageSet` and `localStorageGet`) to facilitate using localStorage directly - localStorage requires us to stringify objects before storing, and parse them on retrieval. Other than these substitutions, and being rewritten in modern JS with some typings, the logic is exactly the same as it was in KVStore. --To facilitate this change, storage.js is temporarily renamed to ngStorage.js so that it doesn't conflict with the storage.ts filename. ngStorage.js will be removed soon after it is not used anymore. --- www/index.js | 2 +- www/js/plugin/{storage.js => ngStorage.js} | 0 www/js/plugin/storage.ts | 182 +++++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) rename www/js/plugin/{storage.js => ngStorage.js} (100%) create mode 100644 www/js/plugin/storage.ts diff --git a/www/index.js b/www/index.js index 55cb233b5..39304165c 100644 --- a/www/index.js +++ b/www/index.js @@ -31,4 +31,4 @@ import './js/control/uploadService.js'; import './js/metrics-factory.js'; import './js/metrics-mappings.js'; import './js/plugin/logger.ts'; -import './js/plugin/storage.js'; +import './js/plugin/ngStorage.js'; diff --git a/www/js/plugin/storage.js b/www/js/plugin/ngStorage.js similarity index 100% rename from www/js/plugin/storage.js rename to www/js/plugin/ngStorage.js diff --git a/www/js/plugin/storage.ts b/www/js/plugin/storage.ts new file mode 100644 index 000000000..04ca3d539 --- /dev/null +++ b/www/js/plugin/storage.ts @@ -0,0 +1,182 @@ +import { getAngularService } from "../angular-react-helper"; +import { displayErrorMsg, logDebug } from "./logger"; + +const mungeValue = (key, value) => { + let store_val = value; + if (typeof value != "object") { + // Should this be {"value": value} or {key: value}? + store_val = {}; + store_val[key] = value; + } + return store_val; +} + +/* + * If a non-JSON object was munged for storage, unwrap it. + */ +const unmungeValue = (key, retData) => { + if (retData?.[key]) { + // it must have been a simple data type that we munged upfront + return retData[key]; + } else { + // it must have been an object + return retData; + } +} + +const localStorageSet = (key: string, value: {[k: string]: any}) => { + localStorage.setItem(key, JSON.stringify(value)); +} + +const localStorageGet = (key: string) => { + const value = localStorage.getItem(key); + if (value) { + return JSON.parse(value); + } else { + return null; + } +} + +/* We redundantly store data in both local and native storage. This function checks + both for a value. If a value is present in only one, it copies it to the other and returns it. + If a value is present in both, but they are different, it copies the native value to + local storage and returns it. */ +function getUnifiedValue(key) { + let ls_stored_val = localStorageGet(key); + return window['cordova'].plugins.BEMUserCache.getLocalStorage(key, false).then((uc_stored_val) => { + logDebug(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, + ls_stored_val = ${JSON.stringify(ls_stored_val)}.`); + + /* compare stored values by stringified JSON equality, not by == or ===. + for objects, == or === only compares the references, not the contents of the objects */ + if (JSON.stringify(ls_stored_val) == JSON.stringify(uc_stored_val)) { + logDebug("local and native values match, already synced"); + return uc_stored_val; + } else { + // the values are different + if (ls_stored_val == null) { + console.assert(uc_stored_val != null, "uc_stored_val should be non-null"); + logDebug(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, + ls_stored_val = ${JSON.stringify(ls_stored_val)}. + copying native ${key} to local...`); + localStorageSet(key, uc_stored_val); + return uc_stored_val; + } else if (uc_stored_val == null) { + console.assert(ls_stored_val != null); + /* + * Backwards compatibility ONLY. Right after the first + * update to this version, we may have a local value that + * is not a JSON object. In that case, we want to munge it + * before storage. Remove this after a few releases. + */ + ls_stored_val = mungeValue(key, ls_stored_val); + displayErrorMsg(`Local ${key} found, native ${key} missing, writing ${key} to native`); + logDebug(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, + ls_stored_val = ${JSON.stringify(ls_stored_val)}. + copying local ${key} to native...`); + return window['cordova'].plugins.BEMUserCache.putLocalStorage(key, ls_stored_val).then(() => { + // we only return the value after we have finished writing + return ls_stored_val; + }); + } + console.assert(ls_stored_val != null && uc_stored_val != null, + "ls_stored_val =" + JSON.stringify(ls_stored_val) + + "uc_stored_val =" + JSON.stringify(uc_stored_val)); + displayErrorMsg(`Local ${key} found, native ${key} found, but different, + writing ${key} to local`); + logDebug(`for key ${key}, uc_stored_val = ${JSON.stringify(uc_stored_val)}, + ls_stored_val = ${JSON.stringify(ls_stored_val)}. + copying native ${key} to local...`); + localStorageSet(key, uc_stored_val); + return uc_stored_val; + } + }); +} + +export function storageSet(key: string, value: any) { + const storeVal = mungeValue(key, value); + /* + * How should we deal with consistency here? Have the threads be + * independent so that there is greater chance that one will succeed, + * or the local only succeed if native succeeds. I think parallel is + * better for greater robustness. + */ + localStorageSet(key, storeVal); + return window['cordova'].plugins.BEMUserCache.putLocalStorage(key, storeVal); +} + +export function storageGet(key: string) { + return getUnifiedValue(key).then((retData) => unmungeValue(key, retData)); +} + +export function storageRemove(key: string) { + localStorage.removeItem(key); + return window['cordova'].plugins.BEMUserCache.removeLocalStorage(key); +} + +export function storageClear({ local, native }: { local?: boolean, native?: boolean }) { + if (local) localStorage.clear(); + if (native) return window['cordova'].plugins.BEMUserCache.clearAll(); + return Promise.resolve(); +} + +export function storageGetDirect(key: string) { + // will run in background, we won't wait for the results + getUnifiedValue(key); + return unmungeValue(key, localStorageGet(key)); +} + +function findMissing(fromKeys, toKeys) { + const foundKeys = []; + const missingKeys = []; + fromKeys.forEach((fk) => { + if (toKeys.includes(fk)) { + foundKeys.push(fk); + } else { + missingKeys.push(fk); + } + }); + return [foundKeys, missingKeys]; +} + +export function storageSyncLocalAndNative() { + const ClientStats = getAngularService('ClientStats'); + console.log("STORAGE_PLUGIN: Called syncAllWebAndNativeValues "); + const syncKeys = window['cordova'].plugins.BEMUserCache.listAllLocalStorageKeys().then((nativeKeys) => { + console.log("STORAGE_PLUGIN: native plugin returned"); + const webKeys = Object.keys(localStorage); + // I thought about iterating through the lists and copying over + // only missing values, etc but `getUnifiedValue` already does + // that, and we don't need to copy it + // so let's just find all the missing values and read them + logDebug("STORAGE_PLUGIN: Comparing web keys " + webKeys + " with " + nativeKeys); + let [foundNative, missingNative] = findMissing(webKeys, nativeKeys); + let [foundWeb, missingWeb] = findMissing(nativeKeys, webKeys); + logDebug("STORAGE_PLUGIN: Found native keys " + foundNative + " missing native keys " + missingNative); + logDebug("STORAGE_PLUGIN: Found web keys " + foundWeb + " missing web keys " + missingWeb); + const allMissing = missingNative.concat(missingWeb); + logDebug("STORAGE_PLUGIN: Syncing all missing keys " + allMissing); + allMissing.forEach(getUnifiedValue); + if (allMissing.length != 0) { + ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { + "type": "local_storage_mismatch", + "allMissingLength": allMissing.length, + "missingWebLength": missingWeb.length, + "missingNativeLength": missingNative.length, + "foundWebLength": foundWeb.length, + "foundNativeLength": foundNative.length, + "allMissing": allMissing, + }).then(logDebug("Logged missing keys to client stats")); + } + }); + const listAllKeys = window['cordova'].plugins.BEMUserCache.listAllUniqueKeys().then((nativeKeys) => { + logDebug("STORAGE_PLUGIN: For the record, all unique native keys are " + nativeKeys); + if (nativeKeys.length == 0) { + ClientStats.addReading(ClientStats.getStatKeys().MISSING_KEYS, { + "type": "all_native", + }).then(logDebug("Logged all missing native keys to client stats")); + } + }); + + return Promise.all([syncKeys, listAllKeys]); +}