All files persistent.ts

100% Statements 62/62
100% Branches 16/16
100% Functions 29/29
100% Lines 51/51

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107          141x 141x 517x                       349x     344x 344x     344x 344x 344x 617x 617x   7x 7x     344x 274x   344x 344x 189x             141x   196x 194x 193x 193x 65x 19x 19x   46x         123x 122x 122x 114x 114x 114x   8x 8x 8x 8x           14x 14x 14x 14x 6x 6x 6x         8x 7x 7x       8x 7x 7x 24x              
import { createStore } from "idb-keyval";
import { metadata, type StorageBackend, type Stored, scope, stored } from "./storage";
 
/** @internal */
export function persistentStorage(namespace: string, schemaVersion: number): StorageBackend {
  const useStore = createStore("@oomol-lab/resource-cache", "records");
  const prefix = `${scope(namespace)}:${schemaVersion}:`;
  const identity = (key: string, part: string) => prefix + JSON.stringify([key, part]);
 
  // All callbacks run synchronously within IDB events. Host code and network waits
  // never run inside a transaction. Synchronous write failures explicitly abort it.
  function transaction<R>(
    mode: IDBTransactionMode,
    work: (
      store: IDBObjectStore,
      read: <T>(request: IDBRequest<T>, fn: (value: T) => void) => void,
      result: (value: R) => void,
    ) => void,
  ): Promise<R> {
    return useStore(
      mode,
      (store) =>
        new Promise<R>((resolve, reject) => {
          const tx = store.transaction;
          let value: R;
          let failure: unknown;
          tx.oncomplete = () => resolve(value);
          tx.onabort = () => reject(failure ?? tx.error);
          const guard = (fn: () => void) => {
            try {
              fn();
            } catch (error) {
              failure = error;
              tx.abort();
            }
          };
          const read = <T>(request: IDBRequest<T>, fn: (value: T) => void) => {
            request.onsuccess = () => guard(() => fn(request.result));
          };
          guard(() =>
            work(store, read, (result) => {
              value = result;
            }),
          );
        }),
    );
  }
 
  return {
    read(key, version) {
      return transaction<Stored | undefined>("readonly", (store, read, result) => {
        read(store.get(identity(key, "meta")), (raw) => {
          const meta = metadata(raw);
          if (!meta) return;
          if (meta.version === version) {
            result({ meta });
            return;
          }
          read(store.get(identity(key, "body")), (body) => result(stored(meta, body)));
        });
      });
    },
    write(key, entry, modified) {
      return transaction<boolean>("readwrite", (store, read, result) => {
        const { data, ...meta } = entry;
        if (modified) {
          store.put(meta, identity(key, "meta"));
          store.put({ version: meta.version, data }, identity(key, "body"));
          result(true);
        } else {
          read(store.get(identity(key, "meta")), (raw) => {
            const matches = metadata(raw)?.version === meta.version;
            if (matches) store.put(meta, identity(key, "meta"));
            result(matches);
          });
        }
      });
    },
    invalidate(key) {
      return transaction<Stored | undefined>("readwrite", (store, read, result) => {
        read(store.get(identity(key, "meta")), (raw) => {
          const meta = metadata(raw);
          if (!meta) return;
          meta.invalidated = true;
          store.put(meta, identity(key, "meta"));
          read(store.get(identity(key, "body")), (body) => result(stored(meta, body)));
        });
      });
    },
    remove(key) {
      return transaction<void>("readwrite", (store) => {
        store.delete(identity(key, "meta"));
        store.delete(identity(key, "body"));
      });
    },
    clear() {
      return transaction<void>("readwrite", (store, read) => {
        read(store.getAllKeys(), (keys) => {
          for (const key of keys) {
            if (typeof key === "string" && key.startsWith(prefix)) store.delete(key);
          }
        });
      });
    },
  };
}