Plugins and Error Handling
Problem
You need to integrate storage with your app's existing logging, validation, and observability infrastructure. You also need predictable error handling for quota errors, disposed adapters, and IndexedDB migration failures.
Solution
All four adapter factories accept the same optional plugin options at construction time. Each plugin uses a structural interface — pass the real library object directly.
Logger
Pass any object with an error(...) method. Observer notification errors are routed to logger.error. A @vielzeug/rune Logger satisfies the interface directly.
import { createLogger } from '@vielzeug/rune';
import { createIndexedDB, table } from '@vielzeug/vault';
type User = { id: number; name: string };
const schema = { users: table<User>('id') };
const db = createIndexedDB({
name: 'app',
schema,
version: 1,
logger: createLogger('vault'),
});Validators
Pass any object with a parse(value): T method. Validators run before every put, putAll, update, and upsert. A @vielzeug/spell schema satisfies the interface directly.
import { s } from '@vielzeug/spell';
import { createMemory, table } from '@vielzeug/vault';
type User = { id: number; name: string; age: number };
const schema = { users: table<User>('id') };
const db = createMemory({
schema,
validators: {
users: s.object({
id: s.number(),
name: s.string(),
age: s.number().min(0).max(150),
}),
},
});
// throws a spell validation error — nothing is written to storage
await db.put('users', { id: 1, name: 'Alice', age: -5 });Metrics
onMetrics is called after every completed operation with table name, operation name, and duration.
import { createMemory, table } from '@vielzeug/vault';
type User = { id: number; name: string };
const schema = { users: table<User>('id') };
const db = createMemory({
schema,
onMetrics: (event) => {
console.log(`[${event.table}] ${event.operation} — ${event.duration}ms`);
},
});Quota exceeded hook (LocalStorage / SessionStorage)
import { createLocalStorage, table, type VaultQuotaError } from '@vielzeug/vault';
type CacheEntry = { id: string; payload: string };
const schema = { cache: table<CacheEntry>('id') };
const db = createLocalStorage({
name: 'app',
schema,
onQuotaExceeded: (tableName, error: VaultQuotaError) => {
console.warn(`[${String(tableName)}] quota exceeded — dropping write`, error.message);
return 'ignore'; // silently drop the write; use 'throw' to rethrow (default)
},
});IndexedDB migration hook
import { createIndexedDB, table, type MigrationFn } from '@vielzeug/vault';
type User = { id: number; name: string };
const schema = { users: table<User>('id') };
const migrate: MigrationFn = ({ db, oldVersion, tx }) => {
if (oldVersion < 2 && db.objectStoreNames.contains('users')) {
tx.objectStore('users').createIndex('name', 'name', { unique: false });
}
};
const db = createIndexedDB({ name: 'app', migrate, schema, version: 2 });
void db;Error handling
import {
createMemory,
table,
VaultDisposedError,
VaultError,
VaultMigrationError,
VaultQuotaError,
VaultScopeError,
} from '@vielzeug/vault';
type User = { id: number; name: string };
const schema = { users: table<User>('id') };
const db = createMemory({ schema });
try {
await db.put('users', { id: 1, name: 'Alice' });
} catch (err) {
if (err instanceof VaultDisposedError) {
// adapter was disposed before this call
} else if (err instanceof VaultScopeError) {
// batch() accessed an out-of-scope table, or observeMany() received an empty array
} else if (err instanceof VaultQuotaError) {
// LocalStorage / SessionStorage write exceeded quota
} else if (err instanceof VaultMigrationError) {
// IndexedDB onupgradeneeded threw
} else if (err instanceof VaultError) {
// any other vault error
} else {
throw err;
}
}Pitfalls
onMetricsis called after the operation completes. A validator error thrown before the write never reachesonMetrics.- Validators receive the raw value passed to
put— they run before TTL wrapping and before any storage write. A thrown parse error leaves storage unchanged. - The
migratecallback on IndexedDB runs synchronously insideonupgradeneeded. Do not callawaitor open a second transaction inside it — IDB will throw. Errors thrown frommigratesurface asVaultMigrationErroron the first operation. onQuotaExceededreturning'ignore'silently drops the write without throwing. The adapter continues operating normally. Returning'throw'(or not providing the hook) rethrows the originalVaultQuotaError.