Skip to main content
jg-packages

Search

    Browser storage

    jg-storage

    Typed key-value stores over localStorage, sessionStorage, IndexedDB, and memory, for pages and workers alike.

    v1.1.1BrowserESMMIT

    npm install jg-storage

    Usage

    Keep settings in localStorage, with a root record and records by ID:

    ts
    import { createLocalStore } from 'jg-storage';
    
    const settings = createLocalStore<{ theme: 'light' | 'dark' }>('settings');
    settings.set(undefined, { theme: 'dark' }); // stored under the key 'jg-storage:settings'
    settings.get(); // { theme: 'dark' }
    settings.set('draft', { theme: 'light' }); // stored under the key 'jg-storage:settings:draft'
    settings.del('draft');

    Store ids hold only letters, digits, underscores, and hyphens; any other id throws INVALID_STORE_ID. Record ids become strings, so 1 and '1' address the same record.

    Keep records in IndexedDB, from a page or a service worker. They live in the records object store of the jg-storage database:

    ts
    import { createIndexedDbStore } from 'jg-storage';
    
    const devices = createIndexedDbStore<{ token: string }>('devices');
    await devices.set('phone', { token: 'abc' });
    await devices.get('phone'); // { token: 'abc' }

    Validate what you read, and fall back to memory where storage is blocked:

    ts
    import { createSessionStore, ERRORS } from 'jg-storage';
    import { hasErrorCode } from 'jg-exception';
    import { z } from 'zod';
    
    const Step = z.number().int().min(1);
    const wizard = createSessionStore('wizard-step', {
      fallback: 'memory', // without it, set and del throw STORE_UNAVAILABLE
      schema: Step, // validates every stored value get reads; its errors propagate unchanged
    });
    wizard.set(undefined, 2);
    wizard.get(); // 2
    
    try {
      createSessionStore('other').set(undefined, 1);
    } catch (error) {
      if (hasErrorCode(error, ERRORS.STORE_UNAVAILABLE)) {
        // sessionStorage is unavailable in this context
      }
    }

    Every store probes its mechanism on its first call, never when it’s created, so creating stores at module scope is safe during server-side rendering.

    Generated from TSDoc

    API reference

    Every export of [email protected], 11 in all, with its signature, its errors, and an example.

    Functions

    createIndexedDbStore

    function

    Creates a store over IndexedDB, which works in pages and workers alike. Records live in the records object store of the jg-storage database, under [id] for the root record and [id, recordId] for the others. Values are stored with the structured clone algorithm, so dates, maps, and binary data keep their types. IndexedDB is read and probed on the first call, never when the store is created.

    Signature
    createIndexedDbStore<T>(id: string, options?: IStoreOptions<T>): IAsyncStore<T>

    Parameters

    idstring

    The store's identifier: letters, digits, underscores, and hyphens.

    options?IStoreOptions<T>

    fallback: 'memory' keeps records in memory when IndexedDB is unavailable, and schema validates every stored value get reads.

    Defaults to {}.

    Returns

    IAsyncStore<T>

    The store.

    Throws

    • INVALID_STORE_IDThe store id must hold only letters, digits, underscores, and hyphens.
    • STORE_UNAVAILABLEset or del ran while IndexedDB is unavailable, without a fallback.
    • ErrorErrors from IndexedDB itself, such as a value it can't clone, propagate.
    Example
    import { createIndexedDbStore } from 'jg-storage';
    
    const devices = createIndexedDbStore<{ token: string }>('devices');
    await devices.set('phone', { token: 'abc' }); // stored under the key ['devices', 'phone']
    await devices.get('phone'); // { token: 'abc' }
    await devices.del('phone');

    createLocalStore

    function

    Creates a store over localStorage, whose records outlive the page. Values are stored as JSON, under jg-storage:<id> for the root record and jg-storage:<id>:<recordId> for the others. localStorage is read and probed on the first call, never when the store is created.

    Signature
    createLocalStore<T>(id: string, options?: IStoreOptions<T>): ISyncStore<T>

    Parameters

    idstring

    The store's identifier: letters, digits, underscores, and hyphens.

    options?IStoreOptions<T>

    fallback: 'memory' keeps records in memory when localStorage is unavailable, and schema validates every stored value get reads.

    Defaults to {}.

    Returns

    ISyncStore<T>

    The store.

    Throws

    • INVALID_STORE_IDThe store id must hold only letters, digits, underscores, and hyphens.
    • FAILED_TO_STRINGIFY_JSONset received a value JSON can't represent.
    • FAILED_TO_PARSE_JSONget read a value that isn't JSON.
    • STORE_UNAVAILABLEset or del ran while localStorage is unavailable, without a fallback.
    • ErrorErrors from localStorage itself, such as a full quota, propagate.
    Example
    import { createLocalStore } from 'jg-storage';
    
    const settings = createLocalStore<{ theme: 'light' | 'dark' }>('settings');
    settings.set(undefined, { theme: 'dark' }); // stored under the key 'jg-storage:settings'
    settings.get(); // { theme: 'dark' }
    settings.set('draft', { theme: 'light' }); // stored under the key 'jg-storage:settings:draft'

    createMemoryStore

    function

    Creates a store that keeps its records in memory for as long as the store exists. Values are kept as they are, without serializing them, and the store is always available.

    Signature
    createMemoryStore<T>(id: string, options?: IStoreOptions<T>): ISyncStore<T>

    Parameters

    idstring

    The store's identifier: letters, digits, underscores, and hyphens.

    options?IStoreOptions<T>

    schema validates every stored value get reads. fallback has no effect, because memory is always available.

    Defaults to {}.

    Returns

    ISyncStore<T>

    The store.

    Throws

    • INVALID_STORE_IDThe store id must hold only letters, digits, underscores, and hyphens.
    Example
    import { createMemoryStore } from 'jg-storage';
    
    const drafts = createMemoryStore<{ text: string }>('drafts');
    drafts.set('post-1', { text: 'Hello' });
    drafts.get('post-1'); // { text: 'Hello' }
    drafts.del('post-1');
    drafts.get('post-1'); // undefined

    createSessionStore

    function

    Creates a store over sessionStorage, whose records last as long as the browser tab. Values are stored as JSON, under jg-storage:<id> for the root record and jg-storage:<id>:<recordId> for the others. sessionStorage is read and probed on the first call, never when the store is created.

    Signature
    createSessionStore<T>(id: string, options?: IStoreOptions<T>): ISyncStore<T>

    Parameters

    idstring

    The store's identifier: letters, digits, underscores, and hyphens.

    options?IStoreOptions<T>

    fallback: 'memory' keeps records in memory when sessionStorage is unavailable, and schema validates every stored value get reads.

    Defaults to {}.

    Returns

    ISyncStore<T>

    The store.

    Throws

    • INVALID_STORE_IDThe store id must hold only letters, digits, underscores, and hyphens.
    • FAILED_TO_STRINGIFY_JSONset received a value JSON can't represent.
    • FAILED_TO_PARSE_JSONget read a value that isn't JSON.
    • STORE_UNAVAILABLEset or del ran while sessionStorage is unavailable, without a fallback.
    • ErrorErrors from sessionStorage itself, such as a full quota, propagate.
    Example
    import { createSessionStore } from 'jg-storage';
    
    const wizard = createSessionStore<number>('wizard-step', { fallback: 'memory' });
    wizard.set(undefined, 2);
    wizard.get(); // 2
    wizard.del();

    Constants

    ERRORS

    const

    the codes for every error thrown by the package

    Declaration
    const ERRORS: { FAILED_TO_STRINGIFY_JSON: "FAILED_TO_STRINGIFY_JSON"; FAILED_TO_PARSE_JSON: "FAILED_TO_PARSE_JSON"; STORE_UNAVAILABLE: "STORE_UNAVAILABLE"; INVALID_STORE_ID: "INVALID_STORE_ID" }

    Types

    IAsyncStore

    type

    a store whose methods return promises: IndexedDB

    Declaration
    type IAsyncStore<T> = {
        readonly id: string;
        readonly mechanism: IStoreMechanism;
        isCompatible(): Promise<boolean>;
        get(recordId?: IRecordId): Promise<T | undefined>;
        set(recordId: IRecordId, value: T): Promise<void>;
        del(recordId?: IRecordId): Promise<void>;
    };

    Members

    • idproperty
      string

      the identifier every record key starts with

    • mechanismproperty
      IStoreMechanism

      the mechanism the store was created for. isCompatible tells whether it's available

    • isCompatiblemethod
      isCompatible(): Promise<boolean>

      whether the mechanism accepts writes, probed on the first call and remembered

    • getmethod
      get(recordId?: IRecordId): Promise<T | undefined>

      reads a record, or the root record when recordId is undefined

    • setmethod
      set(recordId: IRecordId, value: T): Promise<void>

      writes a record, or the root record when recordId is undefined

    • delmethod
      del(recordId?: IRecordId): Promise<void>

      deletes a record, or the root record when recordId is undefined

    IRecordId

    type

    identifies a record in a store. undefined addresses the store's root record. A number becomes a string, so 1 and '1' address the same record in every mechanism

    Declaration
    type IRecordId = undefined | string | number;

    ISchema

    type

    validates data and returns it typed, which a Zod schema does

    Declaration
    type ISchema<T> = {
        parse(value: unknown): T;
    };

    Members

    • parsemethod
      parse(value: unknown): T

    IStoreMechanism

    type

    the mechanism a store keeps its records in

    Declaration
    type IStoreMechanism = 'tempMemory' | 'localStorage' | 'sessionStorage' | 'indexedDB';

    IStoreOptions

    type

    the options every store factory accepts

    Declaration
    type IStoreOptions<T> = {
        fallback?: 'memory';
        schema?: ISchema<T>;
    };

    Members

    • fallbackproperty
      "memory"

      keeps records in a memory map owned by the store when its mechanism is unavailable. Without it, get returns undefined and set and del throw STORE_UNAVAILABLE

    • schemaproperty
      ISchema<T>

      validates every stored value get reads, such as a Zod schema. Its errors propagate unchanged

    ISyncStore

    type

    a store whose methods return their results directly: localStorage, sessionStorage, and memory

    Declaration
    type ISyncStore<T> = {
        readonly id: string;
        readonly mechanism: IStoreMechanism;
        isCompatible(): boolean;
        get(recordId?: IRecordId): T | undefined;
        set(recordId: IRecordId, value: T): void;
        del(recordId?: IRecordId): void;
    };

    Members

    • idproperty
      string

      the identifier every record key starts with

    • mechanismproperty
      IStoreMechanism

      the mechanism the store was created for. isCompatible tells whether it's available

    • isCompatiblemethod
      isCompatible(): boolean

      whether the mechanism accepts writes, probed on the first call and remembered

    • getmethod
      get(recordId?: IRecordId): T | undefined

      reads a record, or the root record when recordId is undefined

    • setmethod
      set(recordId: IRecordId, value: T): void

      writes a record, or the root record when recordId is undefined

    • delmethod
      del(recordId?: IRecordId): void

      deletes a record, or the root record when recordId is undefined