src/core/listStore.js

/**
 * @file
 * @brief Browser-safe list store: once-only depletion and alias resolution behind the injected loader.
 */

import _ from "lodash";
import { keywordAlias, artistAlias } from "../helpers/aliases.js";
import { isGatedList } from "../gatedLists.js";

// Loader-backed port of helpers/listFiles.js's pull/depletion logic, made
// data-source-agnostic: the working list copies come from `loader.readListLines`
// (fs in Node, bundled ?raw text in the browser) instead of reading files here.
// Keeps the once-only depletion + alias resolution behaviour of the original.
/**
 * Create a loader-backed list store with the original once-only depletion + alias behaviour.
 * @param {object} loader Data loader (`readListLines`, `listNames`).
 * @returns {{pull: Function, reset: Function}} The store API.
 */
export function createListStore(loader) {
  const lists = {};
  const artists = {};

  function isArtistName(settings, name) {
    return name == settings.artistFilename || name.includes("artist");
  }

  function bucket(settings, name) {
    return isArtistName(settings, name) ? artists : lists;
  }

  function ensureLoaded(settings, name) {
    const target = bucket(settings, name);
    if (target[name] === undefined) {
      const lines = loader.readListLines(name, settings.includeAdult);
      target[name] = lines ? [...lines] : [];
    }
    return target;
  }

  function resolveName(settings, name) {
    if (name == keywordAlias && String(settings.keywordsFilename) != "false")
      return settings.keywordsFilename;
    if (name == artistAlias && String(settings.artistFilename) != "false")
      return settings.artistFilename;
    if (name == keywordAlias && String(settings.keywordsFilename) == "false")
      return _.sample(
        loader
          .listNames()
          .filter((n) => !isArtistName(settings, n) && (settings.includeAdult || !isGatedList(n))),
      );
    if (name == artistAlias && String(settings.artistFilename) == "false")
      return _.sample(
        loader
          .listNames()
          .filter((n) => isArtistName(settings, n) && (settings.includeAdult || !isGatedList(n))),
      );
    return name;
  }

  function reload(settings, name) {
    const target = bucket(settings, name);
    const lines = loader.readListLines(name, settings.includeAdult);
    target[name] = lines ? [...lines] : [];
    return target[name];
  }

  /**
   * Pull a random entry from list `name` (alias-resolved), with once-only depletion.
   * @param {object} settings The merged settings.
   * @param {string} name The list name or alias.
   * @returns {string} A random entry, or "".
   */
  function pull(settings, name) {
    name = resolveName(settings, name);
    if (name === undefined || name === null) return "";

    if (isArtistName(settings, name) && !settings.includeArtist) return "";
    if (isGatedList(name) && !settings.includeAdult) return "";

    const target = ensureLoaded(settings, name);
    let list = target[name];

    if (list.length <= 0) list = reload(settings, name);
    if (list.length <= 0) return "";

    const index = _.random(0, list.length - 1);
    const entry = list[index];

    if (settings.listEntriesUsedOnce) list.splice(index, 1);
    if (list.length <= 0) reload(settings, name);

    return entry;
  }

  /**
   * Clear depletion state — call once per generated prompt so each draws from a full set.
   * @returns {void}
   */
  // Clear depletion state — call once per generated prompt so each prompt draws
  // from a full set of list entries.
  function reset() {
    for (const k of Object.keys(lists)) delete lists[k];
    for (const k of Object.keys(artists)) delete artists[k];
  }

  return { pull, reset };
}