Storage
@aetherAssembly/core provides a unified key-value storage abstraction built around a single StorageAdapter interface. All three built-in adapters implement the same async contract, so you can swap them without changing call sites.
The StorageAdapter interface
interface StorageAdapter {
get<T>(key: string): Promise<T | null>
set<T>(key: string, value: T): Promise<void>
delete(key: string): Promise<void>
clear(): Promise<void>
keys(): Promise<string[]>
}
| Method | Behaviour |
|---|---|
get<T>(key) | Returns the stored value cast to T, or null if the key doesn't exist. Never throws for a missing key. |
set<T>(key, value) | Writes value under key. Values must be JSON-serialisable. Overwrites silently. |
delete(key) | Removes the key. No-ops if the key doesn't exist. |
clear() | Removes all keys owned by this adapter instance. If scoped (prefix or specific object store), only those keys are removed. |
keys() | Returns all key names owned by this adapter. Strips any internal prefix — callers see the same keys they passed to set. |
All methods are async. Even LocalStorageAdapter, which wraps a synchronous API, returns Promises so callers can treat all adapters uniformly.
IDBAdapter
Backed by IndexedDB via the idb library. The right choice for any significant data in a browser — IndexedDB is not subject to the 5–10 MB localStorage quota and doesn't block the main thread.
import { IDBAdapter } from '@aetherAssembly/core'
const storage = new IDBAdapter('my-app', 'items')
| Param | Type | Description |
|---|---|---|
dbName | string | IndexedDB database name. Use a name unique to your app. |
storeName | string | Object store name within that database. Created automatically on first open. |
The constructor immediately begins opening the database and stores the resulting promise. Every method awaits that promise before operating — so you can construct the adapter and start calling methods right away; the first call will wait for the DB to be ready. The database opens at version 1. If you need multiple object stores, create separate IDBAdapter instances (one per store).
await storage.set('theme', 'dark')
const theme = await storage.get<string>('theme') // 'dark'
await storage.keys() // ['theme']
await storage.delete('theme')
await storage.clear()
Use in Attyre
const adapter = new IDBAdapter('attyre', 'items')
export async function getItems() {
const keys = await adapter.keys()
return Promise.all(keys.map(k => adapter.get(k)))
}
export async function saveItems(items) {
await adapter.clear()
await Promise.all(items.map(item => adapter.set(item.id, item)))
}
LocalStorageAdapter
Wraps localStorage with the StorageAdapter interface. Values are JSON-serialised on write and parsed on read.
import { LocalStorageAdapter } from '@aetherAssembly/core'
const storage = new LocalStorageAdapter() // no prefix
const scoped = new LocalStorageAdapter('myapp') // keys stored as "myapp.<key>"
| Param | Type | Default | Description |
|---|---|---|---|
prefix | string | '' | Optional prefix applied to all keys. get/set/delete/keys always use the unprefixed key. |
- If
localStorageis unavailable (SSR, Web Workers, storage blocked), all methods silently no-op.getreturnsnull,keysreturns[]. clear()only removes keys matching this instance's prefix — it won't touch other apps sharing the samelocalStorage.QuotaExceededErroronsetis swallowed silently.
Use it for small, non-critical data where IndexedDB would be overkill — settings, flags, small caches. Before It's Gone re-exports it for settings storage:
// @before-its-gone/core re-export
export { LocalStorageAdapter }
export function createLocalStorageAdapter() {
return new LocalStorageAdapter()
}
MemoryAdapter
Map-backed in-memory adapter. Zero side effects — data is lost when the instance is garbage collected. Use it in unit tests instead of mocking IDBAdapter or LocalStorageAdapter.
import { MemoryAdapter } from '@aetherAssembly/core'
const storage = new MemoryAdapter()
// full StorageAdapter interface, no side effects
import { describe, it, expect, beforeEach } from 'vitest'
import { MemoryAdapter } from '@aetherAssembly/core'
describe('my feature', () => {
let storage: MemoryAdapter
beforeEach(() => { storage = new MemoryAdapter() })
it('stores and retrieves a value', async () => {
await storage.set('key', { hello: 'world' })
expect(await storage.get('key')).toEqual({ hello: 'world' })
})
})
Writing a custom adapter
Implement StorageAdapter and follow the contract above. A minimal sessionStorage example:
import type { StorageAdapter } from '@aetherAssembly/core'
export class SessionStorageAdapter implements StorageAdapter {
constructor(private readonly prefix = '') {}
private key(k: string) {
return this.prefix ? `${this.prefix}.${k}` : k
}
async get<T>(key: string): Promise<T | null> {
const raw = sessionStorage.getItem(this.key(key))
if (raw === null) return null
try { return JSON.parse(raw) as T } catch { return null }
}
async set<T>(key: string, value: T): Promise<void> {
sessionStorage.setItem(this.key(key), JSON.stringify(value))
}
async delete(key: string): Promise<void> {
sessionStorage.removeItem(this.key(key))
}
async clear(): Promise<void> {
for (const k of await this.keys()) sessionStorage.removeItem(this.key(k))
}
async keys(): Promise<string[]> {
const prefixDot = this.prefix ? `${this.prefix}.` : ''
const result: string[] = []
for (let i = 0; i < sessionStorage.length; i++) {
const k = sessionStorage.key(i)
if (k === null) continue
result.push(prefixDot && k.startsWith(prefixDot) ? k.slice(prefixDot.length) : k)
}
return result
}
}
Migration pattern
When migrating from one backend to another (e.g. localStorage → IDB), do it as a one-time operation on startup guarded by a flag:
const MIGRATION_FLAG = 'myapp_idb_migrated_v1'
const idb = new IDBAdapter('myapp', 'items')
const ls = new LocalStorageAdapter('myapp')
async function migrate() {
if (localStorage.getItem(MIGRATION_FLAG)) return
const keys = await ls.keys()
await Promise.all(
keys.map(async k => {
const value = await ls.get(k)
if (value !== null) await idb.set(k, value)
})
)
await ls.clear()
localStorage.setItem(MIGRATION_FLAG, '1')
}
Set the flag directly in localStorage (not through the adapter) so it survives the migration itself and is always fast to check on subsequent launches. Use a versioned flag name (_v1, _v2) so future migrations can target a new version independently.
attyre_idb_migrated_v1 flag is set.