# jg-storage

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

- **Version:** `1.1.1`
- **Runtimes:** Browser
- **Install:** `npm install jg-storage`
- **On npm:** [jg-storage](https://www.npmjs.com/package/jg-storage)
- **Source:** [packages/jg-storage](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-storage)
- **Recipes:** [jg-storage recipes](https://jg-packages.jesusgraterol.dev/packages/jg-storage/recipes/index.md)

## 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.

## <a id="api-reference"></a>jg-storage API reference

Every export of `jg-storage@1.1.1`, 11 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

- Functions: [`createIndexedDbStore`](#api-createIndexedDbStore), [`createLocalStore`](#api-createLocalStore), [`createMemoryStore`](#api-createMemoryStore), [`createSessionStore`](#api-createSessionStore)
- Constants: [`ERRORS`](#api-ERRORS)
- Types: [`IAsyncStore`](#api-IAsyncStore), [`IRecordId`](#api-IRecordId), [`ISchema`](#api-ISchema), [`IStoreMechanism`](#api-IStoreMechanism), [`IStoreOptions`](#api-IStoreOptions), [`ISyncStore`](#api-ISyncStore)

### <a id="api-functions"></a>Functions

#### <a id="api-createIndexedDbStore"></a>`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.

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

**Parameters**

- `id` `string`: 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_ID`: The store id must hold only letters, digits, underscores, and hyphens.
- `STORE_UNAVAILABLE`: set or del ran while IndexedDB is unavailable, without a fallback.
- Errors from IndexedDB itself, such as a value it can't clone, propagate.

**Example**

```ts
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');
```

#### <a id="api-createLocalStore"></a>`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.

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

**Parameters**

- `id` `string`: 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_ID`: The store id must hold only letters, digits, underscores, and hyphens.
- `FAILED_TO_STRINGIFY_JSON`: set received a value JSON can't represent.
- `FAILED_TO_PARSE_JSON`: get read a value that isn't JSON.
- `STORE_UNAVAILABLE`: set or del ran while localStorage is unavailable, without a fallback.
- Errors from localStorage itself, such as a full quota, propagate.

**Example**

```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'
```

#### <a id="api-createMemoryStore"></a>`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.

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

**Parameters**

- `id` `string`: 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_ID`: The store id must hold only letters, digits, underscores, and hyphens.

**Example**

```ts
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
```

#### <a id="api-createSessionStore"></a>`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.

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

**Parameters**

- `id` `string`: 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_ID`: The store id must hold only letters, digits, underscores, and hyphens.
- `FAILED_TO_STRINGIFY_JSON`: set received a value JSON can't represent.
- `FAILED_TO_PARSE_JSON`: get read a value that isn't JSON.
- `STORE_UNAVAILABLE`: set or del ran while sessionStorage is unavailable, without a fallback.
- Errors from sessionStorage itself, such as a full quota, propagate.

**Example**

```ts
import { createSessionStore } from 'jg-storage';

const wizard = createSessionStore<number>('wizard-step', { fallback: 'memory' });
wizard.set(undefined, 2);
wizard.get(); // 2
wizard.del();
```

### <a id="api-constants"></a>Constants

#### <a id="api-ERRORS"></a>`ERRORS`

*Constant*

the codes for every error thrown by the package

```ts
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" }
```

### <a id="api-types"></a>Types

#### <a id="api-IAsyncStore"></a>`IAsyncStore`

*Type*

a store whose methods return promises: IndexedDB

```ts
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**

- `id: string` (property): the identifier every record key starts with
- `mechanism: IStoreMechanism` (property): the mechanism the store was created for. isCompatible tells whether it's available
- `isCompatible(): Promise<boolean>` (method): whether the mechanism accepts writes, probed on the first call and remembered
- `get(recordId?: IRecordId): Promise<T | undefined>` (method): reads a record, or the root record when recordId is undefined
- `set(recordId: IRecordId, value: T): Promise<void>` (method): writes a record, or the root record when recordId is undefined
- `del(recordId?: IRecordId): Promise<void>` (method): deletes a record, or the root record when recordId is undefined

#### <a id="api-IRecordId"></a>`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

```ts
type IRecordId = undefined | string | number;
```

#### <a id="api-ISchema"></a>`ISchema`

*Type*

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

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

**Members**

- `parse(value: unknown): T` (method)

#### <a id="api-IStoreMechanism"></a>`IStoreMechanism`

*Type*

the mechanism a store keeps its records in

```ts
type IStoreMechanism = 'tempMemory' | 'localStorage' | 'sessionStorage' | 'indexedDB';
```

#### <a id="api-IStoreOptions"></a>`IStoreOptions`

*Type*

the options every store factory accepts

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

**Members**

- `fallback: "memory"` (property): 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
- `schema: ISchema<T>` (property): validates every stored value get reads, such as a Zod schema. Its errors propagate unchanged

#### <a id="api-ISyncStore"></a>`ISyncStore`

*Type*

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

```ts
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**

- `id: string` (property): the identifier every record key starts with
- `mechanism: IStoreMechanism` (property): the mechanism the store was created for. isCompatible tells whether it's available
- `isCompatible(): boolean` (method): whether the mechanism accepts writes, probed on the first call and remembered
- `get(recordId?: IRecordId): T | undefined` (method): reads a record, or the root record when recordId is undefined
- `set(recordId: IRecordId, value: T): void` (method): writes a record, or the root record when recordId is undefined
- `del(recordId?: IRecordId): void` (method): deletes a record, or the root record when recordId is undefined
