src/lib/storage/core.ts

Total Symbols
5
Lines of Code
105
Avg Complexity
1.4
Avg Coverage
91.5%

File Relationships

graph LR updateStorageAtomically["updateStorageAtomically"] setToStorage["setToStorage"] withTimeout["withTimeout"] updateStorageAtomically -->|calls| setToStorage updateStorageAtomically -->|calls| withTimeout click updateStorageAtomically "../symbols/bf2f57323401fa98.html" click setToStorage "../symbols/c4192294ba99b656.html" click withTimeout "../symbols/fc7affbaf585b37b.html"

Symbols by Kind

function 4
type 1

All Symbols

Name Kind Visibility Status Lines Signature
StorageKey type exported- 24-24 type StorageKey
getFromStorage function exported- 38-44 getFromStorage( key: string, defaultValue: T, ): : Promise<T>
setToStorage function exported- 51-53 setToStorage(key: string, value: T): : Promise<void>
withTimeout function - 55-64 withTimeout(promise: Promise<T>, ms: number): : Promise<T>
updateStorageAtomically function exported- 77-104 updateStorageAtomically( key: StorageKey, defaultValue: T, updater: (current: T) => T, ): : Promise<T>

Full Source

/**
 * Core storage utilities — low-level wrappers over chrome.storage.local
 * with atomic update support and write queue per key.
 */

import { createLogger } from "@/lib/logger";

const log = createLogger("Storage");

/**
 * Canonical storage key constants used across all storage modules.
 * Maps logical names to the actual `chrome.storage.local` keys.
 */
export const STORAGE_KEYS = {
  RULES: "fill_all_rules",
  SAVED_FORMS: "fill_all_saved_forms",
  SETTINGS: "fill_all_settings",
  IGNORED_FIELDS: "fill_all_ignored_fields",
  FIELD_CACHE: "fill_all_field_cache",
  DEMO_FLOWS: "fill_all_demo_flows",
} as const;

/** Union type of all valid storage key values. */
export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];

const MAX_FIELD_CACHE_ENTRIES = 100;
const WRITE_TIMEOUT_MS = 30_000;
const writeQueues = new Map<StorageKey, Promise<void>>();

export { MAX_FIELD_CACHE_ENTRIES };

/**
 * Reads a value from `chrome.storage.local`.
 * @param key - Storage key to retrieve
 * @param defaultValue - Value returned when the key does not exist
 * @returns The stored value or `defaultValue`
 */
export async function getFromStorage<T>(
  key: string,
  defaultValue: T,
): Promise<T> {
  const result = await chrome.storage.local.get(key);
  return (result[key] as T) ?? defaultValue;
}

/**
 * Writes a value to `chrome.storage.local`.
 * @param key - Storage key to write
 * @param value - Value to persist
 */
export async function setToStorage<T>(key: string, value: T): Promise<void> {
  await chrome.storage.local.set({ [key]: value });
}

async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  let timer: ReturnType<typeof setTimeout>;
  const timeout = new Promise<never>((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`Storage write timeout (${ms}ms)`)),
      ms,
    );
  });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

/**
 * Atomically reads, transforms, and writes a storage key.
 *
 * Uses a per-key write queue to prevent race conditions when multiple
 * async operations modify the same key concurrently.
 *
 * @param key - Storage key to update
 * @param defaultValue - Default value if key does not exist yet
 * @param updater - Pure function that receives current value and returns the next
 * @returns The new value after the update
 */
export async function updateStorageAtomically<T>(
  key: StorageKey,
  defaultValue: T,
  updater: (current: T) => T,
): Promise<T> {
  const previous = writeQueues.get(key) ?? Promise.resolve();
  let nextValue = defaultValue;

  const currentWrite = previous.then(async () => {
    const current = await getFromStorage<T>(key, defaultValue);
    nextValue = updater(current);
    await setToStorage(key, nextValue);
  });

  const guardedWrite = withTimeout(currentWrite, WRITE_TIMEOUT_MS).catch(
    (err) => {
      log.warn(`Atomic update for key "${key}" failed:`, err);
    },
  );

  writeQueues.set(
    key,
    guardedWrite.then(() => {}),
  );

  await currentWrite;
  return nextValue;
}