Source: storage.js

/**
 * @license MIT
 * Copyright © 2025 Steve Butler (henspace.com)
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the “Software”),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */


/**
 * @module hcje/storage
 * @description
 * Functions for managing [localStorage]{@link https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage}.
 *
 * All keys in the storage are prefixed to prevent clashes with other apps on the same domain. 
 *
 * The keys used for storing items are created as follows:
 *
 * + **storageKeyPrefix_variantId_itemKey**
 */

/** Unique ID added to the key. This is typically used for variants of a game and is used along with the
 * [storageKeyPrefix]{@link module:hcje/storage~storageKeyPrefix} to create the full prefix for all stored items.
 * @type {string}
 * @private*/
let variantId = '';

/** Storage prefix. This should be unique for the game. 
 * @type {string}
 * @private
 */
let storageKeyPrefix;


/**
 * Get the storage prefix. This created from the [storageKeyPrefix]{@link module:hcje/storage~storageKeyPrefix} and
 * [variantId]{@link module:hcje/storage~variantId}.
 * @returns {string}
 * @throws {Error} if [storageKeyPrefix]{@link module:hcje/storage~storageKeyPrefix} has not been set.
 * @private
 */
function getKeyPrefix() {
  if (!storageKeyPrefix) {
    throw new Error('The storageKeyPrefix has not been set. Check module hcje/storage, setStorageKeyPrefix()');
  }
  return `${storageKeyPrefix}_${variantId}`;
}

/**
 * Convert item key to the full storage key. 
 * @param {string} itemKey - Specific item key. This is not modified but is added as a suffix to application's
 * key prefix.
 * @see module:hcje/storage~getKeyPrefix
 * @returns {string}
 * @private
 */ 
function getFullKey(itemKey) {
  return `${getKeyPrefix()}_${itemKey}`;
}


/**
 * Set the primary storage prefix. All keys begin with this prefix. Non-word characters are replaced with an underscore.
 * This function must be called prior to using the storage or an exception will be thrown when attempts are made to 
 * access the storage.
 * @param {string} prefix - Prefix for keys. 
 */
export function setStorageKeyPrefix(prefix) {
  storageKeyPrefix = prefix.replace(/\W+/g, '_');
  console.debug(`Storage key prefix set to "${storageKeyPrefix}"`);
}

/**
 * Set the variant ID. This is added to the key just after the primary storage prefix.
 * Non word characters are replaced with an underscore.
 * @param {string} [id = ''] - Variant id.
 */
export function setVariantId(id = '') {
  variantId = id.replace(/\W+/g, '_');

  console.debug(`Storage variant id set to ${variantId}`);
}

/**
 * Store an item.
 * @param {string} key - Item key. NB this will be modified before storage by prefixing with the primary storage key
 * prefix and  variant ID; see [setStorageKeyPrefix]{@link module:hcje/storage.setStorageKeyPrefix} and 
 * [setVariantId]{@link module:hcje/storage.setVariantId}.
 * @param {*} value - Value to store. This is stringified before storage.
 */
export function setItem(key, value) {
  return localStorage.setItem(getFullKey(key), JSON.stringify(value));
}

/**
 * Get item from localStorage. The data is assumed to be json and is parsed prior to returning.
 * If the value cannot be parsed, it is removed from storage.
 * @param {string} key - Item key. NB this will be modified before storage by prefixing with the primary storage key
 * prefix and  variant ID; see [setStorageKeyPrefix]{@link module:hcje/storage.setStorageKeyPrefix} and 
 * [setVariantId]{@link module:hcje/storage.setVariantId}.
 * @returns {*} Undefined on error or non-existence.
 */
export function getItem(key) {
  const safeKey = getFullKey(key);
  try {
    const json = localStorage.getItem(safeKey);
    if (json) {
      return JSON.parse(json);
   }
  } catch (error) {
    localStorage.removeItem(safeKey);
    console.error(`Failed to parse ${safeKey} so item removed: ${error.message}`);
  }
  return undefined;
}

/**
 * Remove item from local storage.
 * @param {string} key - Item key. NB this will be modified before storage by prefixing with the primary storage key
 * prefix and  variant ID; see [setStorageKeyPrefix]{@link module:hcje/storage.setStorageKeyPrefix} and 
 * [setVariantId]{@link module:hcje/storage.setVariantId}.
 */
export function removeItem(key) {
  return localStorage.removeItem(getFullKey(key));
}

/**
 * Clear all storage associated with this application. Note only keys associated with this app are removed.
 * That is keys that begin with the current storage key prefix and variant id; see 
 * [setStorageKeyPrefix]{@link module:hcje/storage.setStorageKeyPrefix} and 
 * [setVariantId]{@link module:hcje/storage.setVariantId}.
 */
export function clear() {
  const keyPrefix = getKeyPrefix();
  const appKeys = [];
  for (let index = 0; index < localStorage.length; index++) {
    const key = localStorage.key(index);
    if (key.startsWith(keyPrefix)) {
      appKeys.push(key);
    }
  }


  for (const key of appKeys) {
    localStorage.removeItem(key);
    console.debug(`Removed ${key} from localStorage.`);
  }
}