@vielzeug/codex 2.2.9 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "apiSource": "import { fail, prependIssuePath } from './errors';\nimport { createParseContext } from './messages';\n\nexport type {\n AnySchema,\n CheckContext,\n FlatError,\n FlatErrorFirst,\n Infer,\n InferInput,\n InferOutput,\n InferSchemaMode,\n Issue,\n JsonSchema,\n MergeSchemaModes,\n MessageFn,\n Messages,\n ParseContext,\n ParseResult,\n SchemaDescriptor,\n SchemaMode,\n SchemaWalker,\n ValidateFn,\n ValidateResult,\n} from './core';\nexport {\n ErrorCode,\n PipeSchema,\n Schema,\n SpellDefinitionError,\n SpellError,\n SpellValidationError,\n schemaMode,\n} from './core';\nexport type { DeepPartial } from './messages';\nexport { s } from './s';\n\n/** Error helpers and immutable parse-context creation are secondary operations. */\nexport const diagnostics = {\n createParseContext,\n fail,\n prependIssuePath,\n};\n",
3
3
  "docs": {
4
4
  "index": "---\ntitle: Spell — Schema validation for TypeScript\ndescription: Schema validation with explicit sync/async checks, portable definitions, JSON Schema export, and tree-shakeable entry points.\npackage: spell\ncategory: validation\nkeywords: [schema, validation, parsing, json-schema, locale, typescript, descriptors]\nrelated: [forge, courier, vault]\nexports:\n [s, Schema, PipeSchema, SpellValidationError, SpellDefinitionError, ErrorCode, diagnostics, './json', './predicates']\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"spell\" />\n\n## Why Spell?\n\nSpell keeps runtime validation, static inference, and portable definitions in one API. Use `s` for schema construction; import JSON conversion and predicates from dedicated subpaths.\n\nThis example shows the difference between manual branching and a single reusable schema.\n\n```ts\n// Before\nfunction parseUserBefore(value: unknown) {\n if (typeof value !== 'object' || value === null) throw new Error('Expected object');\n\n const candidate = value as Record<string, unknown>;\n\n if (typeof candidate.email !== 'string' || !candidate.email.includes('@')) {\n throw new Error('Expected valid email');\n }\n\n if (typeof candidate.role !== 'string' || !['admin', 'editor', 'viewer'].includes(candidate.role)) {\n throw new Error('Expected valid role');\n }\n\n return {\n email: candidate.email,\n role: candidate.role,\n };\n}\n\n// After\nimport { s } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n});\n\nconst user = User.parse({ email: 'ada@example.com', role: 'admin' });\n```\n\n| Feature | Spell | Zod | Yup |\n| ----------------- | --------------------------------------------------------------------------- | -------------------------------------------- | -------------------------------------------- |\n| Bundle size | <PackageInfo package=\"spell\" type=\"size\" /> | ~62 kB | ~14 kB |\n| Type inference | <ore-icon name=\"check\" size=\"16\"></ore-icon> `Infer<T>` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Coercion API | <ore-icon name=\"check\" size=\"16\"></ore-icon> `s.coerce.*` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Async validation | <ore-icon name=\"check\" size=\"16\"></ore-icon> `.checkAsync()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Error flattening | <ore-icon name=\"check\" size=\"16\"></ore-icon> `flatten()` + `flattenFirst()` | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Partial |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Spell when** you want a fluent schema API with strong TypeScript inference, structured errors, and no third-party runtime dependencies.\n\n**Consider alternatives when** you are already standardized on another validator ecosystem and migration cost outweighs the API benefits.\n\n</div>\n\n## Installation\n\nUse your workspace package manager to add Spell.\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/spell\n```\n\n```sh [npm]\nnpm install @vielzeug/spell\n```\n\n```sh [yarn]\nyarn add @vielzeug/spell\n```\n\n:::\n\n## Quick Start\n\nStart with a schema, then parse unknown input and use the inferred output type everywhere else.\n\n```ts\nimport { s, type Infer } from '@vielzeug/spell';\n\nconst User = s\n .object({\n email: s.string().email(),\n name: s.string().min(1),\n role: s.enum(['admin', 'editor', 'viewer'] as const),\n })\n .relaxed(); // allow extra keys — omit for strict-mode (default)\n\ntype User = Infer<typeof User>;\n\nconst payload: unknown = {\n email: 'ada@example.com',\n name: 'Ada',\n role: 'admin',\n team: 'platform',\n};\n\nconst result = User.safeParse(payload);\n\nif (!result.success) throw result.error;\nconst user = result.data;\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Namespace and tree-shakeable schema builders.\n- Sync and async parsing with `parse()`, `safeParse()`, `parseAsync()`, and `safeParseAsync()`.\n- Explicit `check()` and `checkAsync()` rules; sync parsing never skips an async check.\n- Wrapper modes for `optional`, `nullable`, `nullish`, `default`, `catch`, and `required`.\n- Frozen declarative definitions through `definition()` and JSON Schema export via `fromDefinition()` from `@vielzeug/spell/json`.\n- Grouped `diagnostics` and `predicates` utilities keep schema construction focused.\n- Ordered union parsing produces the same selected branch in sync and async modes.\n- Structured errors with direct path lookup, flattened views, and best-match union diagnostics.\n- Object parsing is hardened against prototype-pollution-style keys.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Forge](/forge/) — typed form state that uses Spell schemas as its validation layer\n- [Courier](/courier/) — HTTP client for validating request and response payloads at service boundaries\n- [Vault](/vault/) — unified storage API that accepts Spell schemas to type-gate persisted data\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Spell — API Reference\ndescription: Reference for Spell schema builders, parsing, diagnostics, and tooling exports.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ----------------------- | ------------------------------- | ---------------------------------- | ---------------------------------------------------- |\n| `s` | Creates schemas | Sync or async, depending on checks | `checkAsync()` requires async parsing |\n| `Schema` / `PipeSchema` | Base schema abstractions | Sync or async | Use `Infer` rather than assuming input equals output |\n| `diagnostics` | Parse-context and error helpers | Sync | Context is per parse/request, not global |\n| `SpellValidationError` | Validation failure details | Sync/async parse failures | Use `safeParse()` to handle it as a result |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------------------- | ----------------------------------------------- |\n| `@vielzeug/spell` | Schema builders, errors, types, and diagnostics |\n| `@vielzeug/spell/json` | Convert portable definitions to JSON Schema |\n| `@vielzeug/spell/predicates` | Standalone format and type predicates |\n\n```ts\nimport { diagnostics, s, type Infer } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\nimport { isEmail } from '@vielzeug/spell/predicates';\n```\n\n## `s`\n\nAll builders live under `s`.\n\n| Builder | Purpose |\n| ----------------------------------------------------------------- | -------------------------- |\n| `string`, `number`, `boolean`, `bigint`, `date` | Primitive values |\n| `literal`, `enum`, `null`, `undefined`, `unknown`, `any`, `never` | Exact and universal values |\n| `array`, `tuple`, `set`, `map`, `record`, `object` | Collections |\n| `union`, `intersect`, `discriminatedUnion`, `lazy` | Composition |\n| `coerce.*` | Coercing primitive schemas |\n\n```ts\nconst User = s.object({\n email: s.string().email(),\n id: s.string().uuid(),\n role: s.enum(['admin', 'member'] as const),\n});\n\ntype User = Infer<typeof User>;\n```\n\nObject schemas reject unknown keys. Use `.relaxed()` to retain extras.\n\n## Parsing\n\nEvery schema provides:\n\n```ts\nschema.parse(value, context?); // Output or SpellValidationError\nschema.safeParse(value, context?); // ParseResult<Output>\nschema.parseAsync(value, context?); // Promise<Output>\nschema.safeParseAsync(value, context?); // Promise<ParseResult<Output>>\nschema.is(value); // value is Output\nschema.assert(value, label?); // assertion\n```\n\n`parse()` and `safeParse()` are available on synchronous schemas. Calling `checkAsync()` returns an async-only schema, where TypeScript exposes only `parseAsync()` and `safeParseAsync()`. That async-only mode propagates through compositional schemas when a child is asynchronous.\n\n## Custom Checks\n\n`check()` is synchronous. `checkAsync()` is asynchronous. Do not return a Promise from `check()`.\n\n```ts\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((value, context) => {\n if (value.password !== value.confirm) {\n context.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n\nconst AvailableEmail = s\n .string()\n .email()\n .checkAsync(async (value) => {\n return (await emailAvailable(value)) || 'Email is already registered';\n });\n```\n\n`CheckContext.addIssue()` takes `{ code, message, params?, path? }`. Paths are relative to current schema.\n\n## Modifiers and Transforms\n\n```ts\ns.string().optional();\ns.string().nullable();\ns.string().nullish();\ns.string().required();\ns.string().default('guest');\ns.string().catch('guest');\ns.string()\n .trim()\n .transform((value) => value.toLowerCase());\ns.string().pipe(s.string().slug());\ns.string().label('User name');\n```\n\n`default()`, `catch()`, preprocessors, transforms, and checks are runtime behavior. They cannot become portable definitions.\n\n## Definitions and JSON Schema\n\n`definition()` is only for schemas containing declarative structure. It returns frozen data and throws `SpellDefinitionError` when runtime behavior is present.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n});\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n```\n\nNo implicit schema-to-JSON conversion exists. Make definition boundary explicit.\n\n## Diagnostics\n\n`diagnostics` contains pure helpers and immutable parse-context creation.\n\n```ts\nimport { diagnostics, s } from '@vielzeug/spell';\n\nconst context = diagnostics.createParseContext({\n object: { invalidKeys: () => 'Unsupported field' },\n});\n\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'ada@example.com', extra: true }, context);\n\nif (!result.success) {\n const messages = result.error.messagesAt('email');\n console.log(messages);\n}\n```\n\n`diagnostics.fail(code, message, params?)` and `diagnostics.prependIssuePath(issues, segment)` support custom parser implementations.\n\n## Errors\n\n- `SpellError` — base class. Use `SpellError.is(error)` for cross-boundary narrowing.\n- `SpellValidationError` — validation failure with `issues`, `bestMatch()`, `messagesAt()`, `flatten()`, and `flattenFirst()`.\n- `SpellDefinitionError` — schema cannot create portable definition.\n\n```ts\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'invalid' });\n\nif (!result.success) {\n const { fieldErrors, formErrors } = result.error.flatten();\n console.log(fieldErrors, formErrors);\n}\n```\n\n## Types\n\n### Core schema types\n\n```ts\ntype SchemaMode = 'async' | 'sync';\n\ntype AnySchema<Output = unknown, Input = Output, Mode extends SchemaMode = SchemaMode> = SchemaSurface<\n Output,\n Input,\n Mode\n>;\n\ntype SchemaSurface<Output = unknown, Input = Output, Mode extends SchemaMode = SchemaMode> = {\n _parseFullAsync(value: unknown, ctx?: ParseContext): Promise<{ data: unknown; issues: Issue[] }>;\n _parseFullSync(value: unknown, ctx?: ParseContext): { data: unknown; issues: Issue[] };\n definition(): SchemaDescriptor;\n isOptional: boolean;\n optional(): SchemaSurface<Output | undefined, Input | undefined, Mode>;\n required(): SchemaSurface<Exclude<Output, undefined>, Exclude<Input, undefined>, Mode>;\n readonly [schemaInput]: Input;\n readonly [schemaMode]: Mode;\n readonly [schemaOutput]: Output;\n walk<R>(visitor: SchemaWalker<R>): R | null;\n};\n```\n\n`schemaMode` is the public symbol marking a schema's parsing capability.\n\n### Inference types\n\n```ts\ntype InferOutput<T> =\n T extends Schema<infer Output, unknown, SchemaMode>\n ? Output\n : T extends { readonly [schemaOutput]: infer Output }\n ? Output\n : never;\ntype InferInput<T> = T extends { readonly [schemaInput]: infer Input } ? Input : unknown;\ntype Infer<T> = InferOutput<T>;\ntype InferSchemaMode<T> = T extends { readonly [schemaMode]: infer Mode extends SchemaMode } ? Mode : never;\ntype MergeSchemaModes<Modes extends SchemaMode> = 'async' extends Modes ? 'async' : 'sync';\n```\n\n### Parse result and issues\n\n```ts\ntype ParseResult<T> = { data: T; success: true } | { error: SpellValidationError; success: false };\n\ntype Issue =\n | { code: 'custom'; message: string; params?: Record<string, unknown>; path: (string | number)[] }\n | { code: 'invalid_base64'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_date'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_duration'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_enum'; message: string; params: { values: readonly unknown[] }; path: (string | number)[] }\n | { code: 'invalid_finite'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_integer'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_keys'; message: string; params: { keys: string[] }; path: (string | number)[] }\n | { code: 'invalid_length'; message: string; params: { exact: number }; path: (string | number)[] }\n | { code: 'invalid_literal'; message: string; params: { expected: unknown }; path: (string | number)[] }\n | { code: 'invalid_multiple_of'; message: string; params: { step: number | bigint }; path: (string | number)[] }\n | { code: 'invalid_safe'; message: string; params?: undefined; path: (string | number)[] }\n | {\n code: 'invalid_string';\n message: string;\n params: { format?: string; includes?: string; pattern?: string; prefix?: string; suffix?: string };\n path: (string | number)[];\n }\n | { code: 'invalid_type'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_union'; message: string; params: { errors: Issue[][] }; path: (string | number)[] }\n | { code: 'invalid_unique'; message: string; params: { unique: true }; path: (string | number)[] }\n | { code: 'invalid_url'; message: string; params: { format: string }; path: (string | number)[] }\n | {\n code: 'invalid_variant';\n message: string;\n params: { discriminator: string; expected: string[] };\n path: (string | number)[];\n }\n | {\n code: 'too_big';\n message: string;\n params: { exclusive?: boolean; max: number | bigint | Date };\n path: (string | number)[];\n }\n | {\n code: 'too_small';\n message: string;\n params: { exclusive?: boolean; min: number | bigint | Date };\n path: (string | number)[];\n }\n | { code: string & {}; message: string; params?: Record<string, unknown>; path: (string | number)[] };\n```\n\n`ErrorCode` is a const object mapping each issue code to its string literal.\n\n### Validation contracts\n\n```ts\ntype ParseContext = { messages: Messages };\n\ntype ValidateFn = (value: unknown, ctx?: ParseContext) => Issue[] | null | Promise<Issue[] | null>;\n\ntype CheckContext = {\n addIssue: (issue: {\n code: string;\n message: string;\n params?: Record<string, unknown>;\n path?: (string | number)[];\n }) => void;\n};\n\ntype ValidateResult = boolean | null | undefined | string;\n```\n\n### Messages\n\n```ts\ntype MessageFn<Ctx extends Record<string, unknown> = Record<string, unknown>> = string | ((ctx: Ctx) => string);\n\ntype Messages = {\n array: { length: (ctx: { exact: number; value: unknown[] }) => string; max: (ctx: { max: number; value: unknown[] }) => string; min: (ctx: { min: number; value: unknown[] }) => string; nonEmpty: () => string; type: () => string; unique: () => string };\n bigint: { max: (ctx: { max: bigint; value: bigint }) => string; min: (ctx: { min: bigint; value: bigint }) => string; multipleOf: (ctx: { step: bigint; value: bigint }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; type: () => string };\n boolean: { type: () => string };\n check: { default: () => string };\n date: { max: (ctx: { max: Date; value: Date }) => string; min: (ctx: { min: Date; value: Date }) => string; type: () => string };\n enum: { invalid: (ctx: { values: readonly unknown[] }) => string };\n instanceof: { type: (ctx: { className: string }) => string };\n literal: { expected: (ctx: { expected: unknown }) => string };\n map: { max: (ctx: { max: number; value: Map<unknown, unknown> }) => string; min: (ctx: { min: number; value: Map<unknown, unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Map<unknown, unknown> }) => string; type: () => string };\n never: { invalid: () => string };\n number: { finite: () => string; int: () => string; max: (ctx: { max: number; value: number }) => string; min: (ctx: { min: number; value: number }) => string; multipleOf: (ctx: { step: number; value: number }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; safe: () => string; type: () => string };\n object: { invalidKeys: (ctx: { keys: string[] }) => string; type: () => string };\n set: { max: (ctx: { max: number; value: Set<unknown> }) => string; min: (ctx: { min: number; value: Set<unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Set<unknown> }) => string; type: () => string };\n string: { base64: () => string; base64url: () => string; cuid: () => string; cuid2: () => string; date: () => string; dateTime: () => string; duration: () => string; email: () => string; emoji: () => string; endsWith: (ctx: { suffix: string; value: string }) => string; hex: () => string; hexColor: () => string; includes: (ctx: { substr: string; value: string }) => string; ip: () => string; jwt: () => string; length: (ctx: { exact: number; value: string }) => string; max: (ctx: { max: number; value: string }) => string; min: (ctx: { min: number; value: string }) => string; nanoid: () => string; nonEmpty: () => string; numeric: () => string; regex: (ctx: { value: string }) => string; semver: () => string; slug: () => string; startsWith: (ctx: { prefix: string; value: string }) => string; time: () => string; type: () => string; ulid: () => string; url: () => string; uuid: () => string };\n tuple: { length: (ctx: { exact: number }) => string; min: (ctx: { min: number }) => string; type: () => string };\n union: { invalid: () => string };\n variant: { invalidDiscriminator: (ctx: { discriminator: string; expected: string[] }) => string; type: () => string };\n};\n\ntype DeepPartial<T> = {\n [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];\n};\n```\n\n### Descriptor and JSON Schema\n\n```ts\ntype SchemaDescriptor = BaseDescriptor &\n (\n | { kind: 'any' | 'unknown' | 'never' | 'boolean' | 'bigint' | 'date' | 'lazy' }\n | { className: string; kind: 'instanceof' }\n | { contentEncoding?: string; format?: string; kind: 'string'; maxLength?: number; minLength?: number; pattern?: string | null }\n | { exclusiveMaximum?: number; exclusiveMinimum?: number; kind: 'number'; maximum?: number; minimum?: number; multipleOf?: number; typeHint?: 'integer' }\n | { kind: 'literal'; value: string | number | boolean | null | undefined }\n | { kind: 'enum'; values: readonly (string | number)[] }\n | { items: SchemaDescriptor; kind: 'array'; maxItems?: number; minItems?: number }\n | { items: SchemaDescriptor[]; kind: 'tuple'; rest: SchemaDescriptor | null }\n | { fields: Record<string, SchemaDescriptor>; kind: 'object'; strict: boolean }\n | { key: SchemaDescriptor; kind: 'record'; value: SchemaDescriptor }\n | { items: SchemaDescriptor; kind: 'set' }\n | { key: SchemaDescriptor; kind: 'map'; value: SchemaDescriptor }\n | { branches: SchemaDescriptor[]; kind: 'union' | 'intersect' }\n | { branches: Record<string, SchemaDescriptor>; discriminator: string; kind: 'variant' }\n | { from: SchemaDescriptor; kind: 'pipe'; to: SchemaDescriptor }\n );\n\ntype JsonSchema = Record<string, unknown>;\n```\n\n### Schema walker\n\n```ts\ntype SchemaWalker<R> = {\n array?: <T extends AnySchema, Mode extends SchemaMode>(schema: ArraySchema<T, Mode>, item: R | null) => R;\n bigint?: <Input, Mode extends SchemaMode>(schema: BigIntSchema<Input, Mode>) => R;\n boolean?: <Input, Mode extends SchemaMode>(schema: BooleanSchema<Input, Mode>) => R;\n date?: <Input, Mode extends SchemaMode>(schema: DateSchema<Input, Mode>) => R;\n enum?: <T extends EnumValues, Mode extends SchemaMode>(schema: EnumSchema<T, Mode>) => R;\n instanceof?: <T, Mode extends SchemaMode>(schema: InstanceOfSchema<T, Mode>) => R;\n intersect?: <T extends readonly AnySchema[], Mode extends SchemaMode>(schema: IntersectSchema<T, Mode>, branches: (R | null)[]) => R;\n lazy?: <T, Input, Mode extends SchemaMode>(schema: LazySchema<T, Input, Mode>) => R;\n literal?: <T extends string | number | boolean | null | undefined, Mode extends SchemaMode>(schema: LiteralSchema<T, Mode>) => R;\n map?: <K extends AnySchema, V extends AnySchema, Mode extends SchemaMode>(schema: MapSchema<K, V, Mode>, key: R | null, value: R | null) => R;\n never?: <Mode extends SchemaMode>(schema: NeverSchema<Mode>) => R;\n number?: <Input, Mode extends SchemaMode>(schema: NumberSchema<Input, Mode>) => R;\n object?: <T extends ObjectShape, Mode extends SchemaMode>(schema: ObjectSchema<T, Mode>, fields: Record<string, R | null>) => R;\n pipe?: <To extends AnySchema, From extends AnySchema, Mode extends SchemaMode>(schema: PipeSchema<To, From, Mode>, from: R | null, to: R | null) => R;\n record?: <K extends AnySchema, V extends AnySchema, Mode extends SchemaMode>(schema: RecordSchema<K, V, Mode>, key: R | null, value: R | null) => R;\n set?: <T extends AnySchema, Mode extends SchemaMode>(schema: SetSchema<T, Mode>, item: R | null) => R;\n string?: <Input, Mode extends SchemaMode>(schema: StringSchema<Input, Mode>) => R;\n tuple?: <T extends TupleSchemas, Rest extends AnySchema | null, Mode extends SchemaMode>(schema: TupleSchema<T, Rest, Mode>, items: (R | null)[], rest: R | null) => R;\n union?: <T extends readonly AnySchema[], Mode extends SchemaMode>(schema: UnionSchema<T, Mode>, branches: (R | null)[]) => R;\n unknown?: (schema: AnySchema) => R;\n variant?: <K extends string, M extends Record<string, ObjectSchema<any, any>>, Mode extends SchemaMode>(schema: VariantSchema<K, M, Mode>, branches: Record<string, R | null>) => R;\n};\n```\n\n### Error helpers\n\n```ts\ntype FlatError = { messages: string[]; path: (string | number)[] };\ntype FlatErrorFirst = { message: string; path: (string | number)[] };\n```\n",
5
+ "api": "---\ntitle: Spell — API Reference\ndescription: Reference for Spell schema builders, parsing, diagnostics, and tooling exports.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ----------------------- | ------------------------------- | ---------------------------------- | ---------------------------------------------------- |\n| `s` | Creates schemas | Sync or async, depending on checks | `checkAsync()` requires async parsing |\n| `Schema` / `PipeSchema` | Base schema abstractions | Sync or async | Use `Infer` rather than assuming input equals output |\n| `diagnostics` | Parse-context and error helpers | Sync | Context is per parse/request, not global |\n| `SpellValidationError` | Validation failure details | Sync/async parse failures | Use `safeParse()` to handle it as a result |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------------------- | ----------------------------------------------- |\n| `@vielzeug/spell` | Schema builders, errors, types, and diagnostics |\n| `@vielzeug/spell/json` | Convert portable definitions to JSON Schema |\n| `@vielzeug/spell/predicates` | Standalone format and type predicates |\n\n```ts\nimport { diagnostics, s, type Infer } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\nimport { isEmail } from '@vielzeug/spell/predicates';\n```\n\n## `s`\n\nAll builders live under `s`.\n\n| Builder | Purpose |\n| ----------------------------------------------------------------- | -------------------------- |\n| `string`, `number`, `boolean`, `bigint`, `date` | Primitive values |\n| `literal`, `enum`, `null`, `undefined`, `unknown`, `any`, `never` | Exact and universal values |\n| `array`, `tuple`, `set`, `map`, `record`, `object` | Collections |\n| `union`, `intersect`, `discriminatedUnion`, `lazy` | Composition |\n| `coerce.*` | Coercing primitive schemas |\n\n```ts\nconst User = s.object({\n email: s.string().email(),\n id: s.string().uuid(),\n role: s.enum(['admin', 'member'] as const),\n});\n\ntype User = Infer<typeof User>;\n```\n\nObject schemas reject unknown keys. Use `.relaxed()` to retain extras.\n\n## Parsing\n\nEvery schema provides:\n\n```ts\nschema.parse(value, context?); // Output or SpellValidationError\nschema.safeParse(value, context?); // ParseResult<Output>\nschema.parseAsync(value, context?); // Promise<Output>\nschema.safeParseAsync(value, context?); // Promise<ParseResult<Output>>\nschema.is(value); // value is Output\nschema.assert(value, label?); // assertion\n```\n\n`parse()` and `safeParse()` are available on synchronous schemas. Calling `checkAsync()` returns an async-only schema, where TypeScript exposes only `parseAsync()` and `safeParseAsync()`. That async-only mode propagates through compositional schemas when a child is asynchronous.\n\n## Custom Checks\n\n`check()` is synchronous. `checkAsync()` is asynchronous. Do not return a Promise from `check()`.\n\n```ts\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((value, context) => {\n if (value.password !== value.confirm) {\n context.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n\nconst AvailableEmail = s\n .string()\n .email()\n .checkAsync(async (value) => {\n return (await emailAvailable(value)) || 'Email is already registered';\n });\n```\n\n`CheckContext.addIssue()` takes `{ code, message, params?, path? }`. Paths are relative to current schema.\n\n## Modifiers and Transforms\n\n```ts\ns.string().optional();\ns.string().nullable();\ns.string().nullish();\ns.string().required();\ns.string().default('guest');\ns.string().catch('guest');\ns.string()\n .trim()\n .transform((value) => value.toLowerCase());\ns.string().pipe(s.string().slug());\ns.string().label('User name');\n```\n\n`default()`, `catch()`, preprocessors, transforms, and checks are runtime behavior. They cannot become portable definitions.\n\n## Definitions and JSON Schema\n\n`definition()` is only for schemas containing declarative structure. It returns frozen data and throws `SpellDefinitionError` when runtime behavior is present.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s.object({\n id: s.string().uuid(),\n name: s.string().min(1),\n});\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n```\n\nNo implicit schema-to-JSON conversion exists. Make definition boundary explicit.\n\n## Diagnostics\n\n`diagnostics` contains pure helpers and immutable parse-context creation.\n\n```ts\nimport { diagnostics, s } from '@vielzeug/spell';\n\nconst context = diagnostics.createParseContext({\n object: { invalidKeys: () => 'Unsupported field' },\n});\n\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'ada@example.com', extra: true }, context);\n\nif (!result.success) {\n const messages = result.error.messagesAt('email');\n console.log(messages);\n}\n```\n\n`diagnostics.fail(code, message, params?)` and `diagnostics.prependIssuePath(issues, segment)` support custom parser implementations.\n\n## Errors\n\n- `SpellError` — base class. Use `instanceof SpellError` for cross-boundary narrowing.\n- `SpellValidationError` — validation failure with `issues`, `bestMatch()`, `messagesAt()`, `flatten()`, and `flattenFirst()`.\n- `SpellDefinitionError` — schema cannot create portable definition.\n\n```ts\nconst result = s.object({ email: s.string().email() }).safeParse({ email: 'invalid' });\n\nif (!result.success) {\n const { fieldErrors, formErrors } = result.error.flatten();\n console.log(fieldErrors, formErrors);\n}\n```\n\n## Types\n\n### Core schema types\n\n```ts\ntype SchemaMode = 'async' | 'sync';\n\ntype AnySchema<Output = unknown, Input = Output, Mode extends SchemaMode = SchemaMode> = SchemaSurface<\n Output,\n Input,\n Mode\n>;\n\ntype SchemaSurface<Output = unknown, Input = Output, Mode extends SchemaMode = SchemaMode> = {\n _parseFullAsync(value: unknown, ctx?: ParseContext): Promise<{ data: unknown; issues: Issue[] }>;\n _parseFullSync(value: unknown, ctx?: ParseContext): { data: unknown; issues: Issue[] };\n definition(): SchemaDescriptor;\n isOptional: boolean;\n optional(): SchemaSurface<Output | undefined, Input | undefined, Mode>;\n required(): SchemaSurface<Exclude<Output, undefined>, Exclude<Input, undefined>, Mode>;\n readonly [schemaInput]: Input;\n readonly [schemaMode]: Mode;\n readonly [schemaOutput]: Output;\n walk<R>(visitor: SchemaWalker<R>): R | null;\n};\n```\n\n`schemaMode` is the public symbol marking a schema's parsing capability.\n\n### Inference types\n\n```ts\ntype InferOutput<T> =\n T extends Schema<infer Output, unknown, SchemaMode>\n ? Output\n : T extends { readonly [schemaOutput]: infer Output }\n ? Output\n : never;\ntype InferInput<T> = T extends { readonly [schemaInput]: infer Input } ? Input : unknown;\ntype Infer<T> = InferOutput<T>;\ntype InferSchemaMode<T> = T extends { readonly [schemaMode]: infer Mode extends SchemaMode } ? Mode : never;\ntype MergeSchemaModes<Modes extends SchemaMode> = 'async' extends Modes ? 'async' : 'sync';\n```\n\n### Parse result and issues\n\n```ts\ntype ParseResult<T> = { data: T; success: true } | { error: SpellValidationError; success: false };\n\ntype Issue =\n | { code: 'custom'; message: string; params?: Record<string, unknown>; path: (string | number)[] }\n | { code: 'invalid_base64'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_date'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_duration'; message: string; params: { format: string }; path: (string | number)[] }\n | { code: 'invalid_enum'; message: string; params: { values: readonly unknown[] }; path: (string | number)[] }\n | { code: 'invalid_finite'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_integer'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_keys'; message: string; params: { keys: string[] }; path: (string | number)[] }\n | { code: 'invalid_length'; message: string; params: { exact: number }; path: (string | number)[] }\n | { code: 'invalid_literal'; message: string; params: { expected: unknown }; path: (string | number)[] }\n | { code: 'invalid_multiple_of'; message: string; params: { step: number | bigint }; path: (string | number)[] }\n | { code: 'invalid_safe'; message: string; params?: undefined; path: (string | number)[] }\n | {\n code: 'invalid_string';\n message: string;\n params: { format?: string; includes?: string; pattern?: string; prefix?: string; suffix?: string };\n path: (string | number)[];\n }\n | { code: 'invalid_type'; message: string; params?: undefined; path: (string | number)[] }\n | { code: 'invalid_union'; message: string; params: { errors: Issue[][] }; path: (string | number)[] }\n | { code: 'invalid_unique'; message: string; params: { unique: true }; path: (string | number)[] }\n | { code: 'invalid_url'; message: string; params: { format: string }; path: (string | number)[] }\n | {\n code: 'invalid_variant';\n message: string;\n params: { discriminator: string; expected: string[] };\n path: (string | number)[];\n }\n | {\n code: 'too_big';\n message: string;\n params: { exclusive?: boolean; max: number | bigint | Date };\n path: (string | number)[];\n }\n | {\n code: 'too_small';\n message: string;\n params: { exclusive?: boolean; min: number | bigint | Date };\n path: (string | number)[];\n }\n | { code: string & {}; message: string; params?: Record<string, unknown>; path: (string | number)[] };\n```\n\n`ErrorCode` is a const object mapping each issue code to its string literal.\n\n### Validation contracts\n\n```ts\ntype ParseContext = { messages: Messages };\n\ntype ValidateFn = (value: unknown, ctx?: ParseContext) => Issue[] | null | Promise<Issue[] | null>;\n\ntype CheckContext = {\n addIssue: (issue: {\n code: string;\n message: string;\n params?: Record<string, unknown>;\n path?: (string | number)[];\n }) => void;\n};\n\ntype ValidateResult = boolean | null | undefined | string;\n```\n\n### Messages\n\n```ts\ntype MessageFn<Ctx extends Record<string, unknown> = Record<string, unknown>> = string | ((ctx: Ctx) => string);\n\ntype Messages = {\n array: { length: (ctx: { exact: number; value: unknown[] }) => string; max: (ctx: { max: number; value: unknown[] }) => string; min: (ctx: { min: number; value: unknown[] }) => string; nonEmpty: () => string; type: () => string; unique: () => string };\n bigint: { max: (ctx: { max: bigint; value: bigint }) => string; min: (ctx: { min: bigint; value: bigint }) => string; multipleOf: (ctx: { step: bigint; value: bigint }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; type: () => string };\n boolean: { type: () => string };\n check: { default: () => string };\n date: { max: (ctx: { max: Date; value: Date }) => string; min: (ctx: { min: Date; value: Date }) => string; type: () => string };\n enum: { invalid: (ctx: { values: readonly unknown[] }) => string };\n instanceof: { type: (ctx: { className: string }) => string };\n literal: { expected: (ctx: { expected: unknown }) => string };\n map: { max: (ctx: { max: number; value: Map<unknown, unknown> }) => string; min: (ctx: { min: number; value: Map<unknown, unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Map<unknown, unknown> }) => string; type: () => string };\n never: { invalid: () => string };\n number: { finite: () => string; int: () => string; max: (ctx: { max: number; value: number }) => string; min: (ctx: { min: number; value: number }) => string; multipleOf: (ctx: { step: number; value: number }) => string; negative: () => string; nonNegative: () => string; nonPositive: () => string; positive: () => string; safe: () => string; type: () => string };\n object: { invalidKeys: (ctx: { keys: string[] }) => string; type: () => string };\n set: { max: (ctx: { max: number; value: Set<unknown> }) => string; min: (ctx: { min: number; value: Set<unknown> }) => string; nonEmpty: () => string; size: (ctx: { exact: number; value: Set<unknown> }) => string; type: () => string };\n string: { base64: () => string; base64url: () => string; cuid: () => string; cuid2: () => string; date: () => string; dateTime: () => string; duration: () => string; email: () => string; emoji: () => string; endsWith: (ctx: { suffix: string; value: string }) => string; hex: () => string; hexColor: () => string; includes: (ctx: { substr: string; value: string }) => string; ip: () => string; jwt: () => string; length: (ctx: { exact: number; value: string }) => string; max: (ctx: { max: number; value: string }) => string; min: (ctx: { min: number; value: string }) => string; nanoid: () => string; nonEmpty: () => string; numeric: () => string; regex: (ctx: { value: string }) => string; semver: () => string; slug: () => string; startsWith: (ctx: { prefix: string; value: string }) => string; time: () => string; type: () => string; ulid: () => string; url: () => string; uuid: () => string };\n tuple: { length: (ctx: { exact: number }) => string; min: (ctx: { min: number }) => string; type: () => string };\n union: { invalid: () => string };\n variant: { invalidDiscriminator: (ctx: { discriminator: string; expected: string[] }) => string; type: () => string };\n};\n\ntype DeepPartial<T> = {\n [K in keyof T]?: T[K] extends Record<string, unknown> ? DeepPartial<T[K]> : T[K];\n};\n```\n\n### Descriptor and JSON Schema\n\n```ts\ntype SchemaDescriptor = BaseDescriptor &\n (\n | { kind: 'any' | 'unknown' | 'never' | 'boolean' | 'bigint' | 'date' | 'lazy' }\n | { className: string; kind: 'instanceof' }\n | { contentEncoding?: string; format?: string; kind: 'string'; maxLength?: number; minLength?: number; pattern?: string | null }\n | { exclusiveMaximum?: number; exclusiveMinimum?: number; kind: 'number'; maximum?: number; minimum?: number; multipleOf?: number; typeHint?: 'integer' }\n | { kind: 'literal'; value: string | number | boolean | null | undefined }\n | { kind: 'enum'; values: readonly (string | number)[] }\n | { items: SchemaDescriptor; kind: 'array'; maxItems?: number; minItems?: number }\n | { items: SchemaDescriptor[]; kind: 'tuple'; rest: SchemaDescriptor | null }\n | { fields: Record<string, SchemaDescriptor>; kind: 'object'; strict: boolean }\n | { key: SchemaDescriptor; kind: 'record'; value: SchemaDescriptor }\n | { items: SchemaDescriptor; kind: 'set' }\n | { key: SchemaDescriptor; kind: 'map'; value: SchemaDescriptor }\n | { branches: SchemaDescriptor[]; kind: 'union' | 'intersect' }\n | { branches: Record<string, SchemaDescriptor>; discriminator: string; kind: 'variant' }\n | { from: SchemaDescriptor; kind: 'pipe'; to: SchemaDescriptor }\n );\n\ntype JsonSchema = Record<string, unknown>;\n```\n\n### Schema walker\n\n```ts\ntype SchemaWalker<R> = {\n array?: <T extends AnySchema, Mode extends SchemaMode>(schema: ArraySchema<T, Mode>, item: R | null) => R;\n bigint?: <Input, Mode extends SchemaMode>(schema: BigIntSchema<Input, Mode>) => R;\n boolean?: <Input, Mode extends SchemaMode>(schema: BooleanSchema<Input, Mode>) => R;\n date?: <Input, Mode extends SchemaMode>(schema: DateSchema<Input, Mode>) => R;\n enum?: <T extends EnumValues, Mode extends SchemaMode>(schema: EnumSchema<T, Mode>) => R;\n instanceof?: <T, Mode extends SchemaMode>(schema: InstanceOfSchema<T, Mode>) => R;\n intersect?: <T extends readonly AnySchema[], Mode extends SchemaMode>(schema: IntersectSchema<T, Mode>, branches: (R | null)[]) => R;\n lazy?: <T, Input, Mode extends SchemaMode>(schema: LazySchema<T, Input, Mode>) => R;\n literal?: <T extends string | number | boolean | null | undefined, Mode extends SchemaMode>(schema: LiteralSchema<T, Mode>) => R;\n map?: <K extends AnySchema, V extends AnySchema, Mode extends SchemaMode>(schema: MapSchema<K, V, Mode>, key: R | null, value: R | null) => R;\n never?: <Mode extends SchemaMode>(schema: NeverSchema<Mode>) => R;\n number?: <Input, Mode extends SchemaMode>(schema: NumberSchema<Input, Mode>) => R;\n object?: <T extends ObjectShape, Mode extends SchemaMode>(schema: ObjectSchema<T, Mode>, fields: Record<string, R | null>) => R;\n pipe?: <To extends AnySchema, From extends AnySchema, Mode extends SchemaMode>(schema: PipeSchema<To, From, Mode>, from: R | null, to: R | null) => R;\n record?: <K extends AnySchema, V extends AnySchema, Mode extends SchemaMode>(schema: RecordSchema<K, V, Mode>, key: R | null, value: R | null) => R;\n set?: <T extends AnySchema, Mode extends SchemaMode>(schema: SetSchema<T, Mode>, item: R | null) => R;\n string?: <Input, Mode extends SchemaMode>(schema: StringSchema<Input, Mode>) => R;\n tuple?: <T extends TupleSchemas, Rest extends AnySchema | null, Mode extends SchemaMode>(schema: TupleSchema<T, Rest, Mode>, items: (R | null)[], rest: R | null) => R;\n union?: <T extends readonly AnySchema[], Mode extends SchemaMode>(schema: UnionSchema<T, Mode>, branches: (R | null)[]) => R;\n unknown?: (schema: AnySchema) => R;\n variant?: <K extends string, M extends Record<string, ObjectSchema<any, any>>, Mode extends SchemaMode>(schema: VariantSchema<K, M, Mode>, branches: Record<string, R | null>) => R;\n};\n```\n\n### Error helpers\n\n```ts\ntype FlatError = { messages: string[]; path: (string | number)[] };\ntype FlatErrorFirst = { message: string; path: (string | number)[] };\n```\n",
6
6
  "usage": "---\ntitle: Spell — Usage Guide\ndescription: Learn how to build schemas, compose wrappers, customize locales, and integrate spell with other Vielzeug packages.\n---\n\n[[toc]]\n\n## Basic Usage\n\nStart with `safeParse()` when you want explicit success and failure branches.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Signup = s.object({\n email: s.string().email(),\n password: s.string().min(12),\n referralCode: s.string().optional(),\n});\n\nconst result = Signup.safeParse({\n email: 'ada@example.com',\n password: 'horse-battery-staple',\n});\n\nif (!result.success) {\n console.error(result.error.issues);\n} else {\n console.log(result.data.email);\n}\n```\n\nUse `parse()` when invalid input should throw immediately. Use `safeParse()` when invalid input is part of normal control flow.\n\n## Building Schemas\n\nUse the namespace form when readability matters more than bundle trimming.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Article = s.object({\n id: s.string().uuid(),\n title: s.string().trim().min(1).max(120),\n slug: s.string().slug(),\n tags: s.array(s.string().min(1)).default(() => []),\n meta: s\n .object({\n published: s.boolean(),\n publishedAt: s.date().nullable(),\n })\n .relaxed(),\n});\n```\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Todo = s.object({\n done: s.boolean(),\n tags: s.array(s.string().min(1)).default(() => []),\n title: s.string().min(1),\n});\n```\n\nObject schemas reject unknown keys by default. Call `.relaxed()` when you need to preserve extra properties.\n\nCall `.defaults()` to get a fully default-filled object without providing any input. Every required field must have a `.default()` set, or a `SpellValidationError` is thrown. Call `.partialDefaults()` when only some fields have defaults — fields without a default are silently omitted instead of throwing.\n\n```ts\nconst Config = s.object({\n host: s.string().default('localhost'),\n port: s.number().default(3000),\n});\n\nConfig.defaults(); // { host: 'localhost', port: 3000 }\n\nconst Form = s.object({ name: s.string(), role: s.string().default('viewer') });\nForm.partialDefaults(); // { role: 'viewer' }\n```\n\n## Wrapper Modes, Defaults, and Fallbacks\n\nChain wrappers to describe missing values and recovery rules without losing schema metadata.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst DisplayName = s.string().trim().min(2).label('Display name').optional().default('Guest').nullable();\n\nDisplayName.parse(undefined); // 'Guest'\nDisplayName.parse(null); // null\nDisplayName.description; // 'Display name'\n```\n\nCall `.required()` to remove `undefined` without removing `null`.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst NullableButRequired = s.string().optional().nullable().required();\n\nNullableButRequired.parse('Ada');\nNullableButRequired.parse(null);\n// NullableButRequired.parse(undefined); // throws\n```\n\nUse `.catch()` when you want a fallback output after validation fails.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Port = s.number().int().min(1).max(65535).catch(3000);\n\nPort.parse('not-a-number'); // 3000\n```\n\n## Custom Validation\n\nUse `check()` for synchronous domain rules and `checkAsync()` for asynchronous rules. Sync parsing rejects schemas with asynchronous checks.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\n// Boolean shorthand: return false to fail with default message\nconst EvenNumber = s.number().check((n) => n % 2 === 0);\n\n// String shorthand: return the message as a string\nconst Username = s\n .string()\n .min(3)\n .check((v) => !v.startsWith('_') || 'Cannot start with underscore');\n\n// Multiple issues via ctx.addIssue()\nconst Signup = s.object({ confirm: s.string(), password: s.string() }).check((v, ctx) => {\n if (v.password !== v.confirm) {\n ctx.addIssue({ code: 'custom', message: 'Passwords must match', path: ['confirm'] });\n }\n});\n```\n\n`checkAsync()` returns an async-only schema: TypeScript exposes `parseAsync()` and `safeParseAsync()` but not `parse()` or `safeParse()`. This mode survives fluent modifiers and propagates through nested arrays, objects, unions, intersections, tuples, maps, records, sets, lazy schemas, pipelines, and `s.discriminatedUnion(...)` branches. Sync parsing also fails at runtime instead of accepting an unchecked value.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst takenEmails = new Set(['ada@example.com']);\n\nconst AccountEmail = s\n .string()\n .email()\n .checkAsync(async (value, ctx) => {\n if (takenEmails.has(value)) {\n ctx.addIssue({ code: 'custom', message: 'Email is already taken', path: [] });\n }\n });\n\n// Async checks require parseAsync\nawait AccountEmail.parseAsync('grace@example.com');\n```\n\nUse `check()` for predicate-only rules too. Return `true` on success or message on failure.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst PositivePrice = s.number().check((value) => value > 0 || 'Must be positive');\nPositivePrice.parse(9.99);\n```\n\n## Strings, Numbers, and Safe Regex Usage\n\nUse schema helpers for common string and number constraints instead of hand-written predicates.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Password = s.string().min(12).regex(/[A-Z]/).regex(/[0-9]/);\nconst Price = s.number().nonNegative().multipleOf(0.01);\nconst LaunchWindow = s.date().min(new Date('2025-01-01T00:00:00.000Z'));\n```\n\nSpell strips stateful `/g` and `/y` flags from `regex()` patterns before validation. Repeated parses stay deterministic even when the original regular expression is reused.\n\n## Coercion and Transforms\n\nUse coercion when input arrives as strings, query parameters, or form values.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst Query = s.object({\n draft: s.coerce.boolean().default(false),\n limit: s.coerce.number().int().positive().default(20),\n publishedAt: s.coerce.date().nullable(),\n search: s.coerce.string().trim().min(1).optional(),\n});\n\nconst parsed = Query.parse({\n draft: 'true',\n limit: '50',\n publishedAt: '2025-04-01T12:00:00.000Z',\n search: ' vielzeug ',\n});\n```\n\nUse `transform()` or `pipe()` after validation when downstream code needs a different output shape.\n\n```ts\nimport { s } from '@vielzeug/spell';\n\nconst TrimmedTags = s.array(s.string().trim().min(1)).transform((tags) => tags.map((tag) => tag.toLowerCase()));\nconst Slug = s.string().trim().min(1).pipe(s.string().slug());\n```\n\n## Introspection, Round-Trips, and JSON Schema\n\nUse declarative definitions when schemas need to cross process boundaries or feed tooling.\n\n```ts\nimport { s } from '@vielzeug/spell';\nimport { fromDefinition } from '@vielzeug/spell/json';\n\nconst Product = s\n .object({\n id: s.string().uuid(),\n name: s.string().min(1),\n price: s.number().positive().multipleOf(0.01),\n })\n .label('Product');\n\nconst definition = Product.definition();\nconst jsonSchema = fromDefinition(definition);\n\nProduct.parse({ id: '550e8400-e29b-41d4-a716-446655440000', name: 'Keyboard', price: 129.99 });\nconsole.log(jsonSchema.title);\n```\n\nDefinitions are frozen serializable snapshots of declarative schema structure. Use `definition()` and `fromDefinition()` for external tooling. Schemas with runtime checks, transforms, defaults, catches, or preprocessors intentionally have no definition.\n\n## Messages\n\nSpell has no mutable process-wide configuration. Build one parse context per request, locale, or form, then pass it explicitly.\n\n```ts\nimport { diagnostics, s } from '@vielzeug/spell';\n\nconst User = s.object({ email: s.string().email() });\nconst german = diagnostics.createParseContext({\n object: { invalidKeys: () => 'Keine unbekannten Felder erlaubt' },\n});\n\nUser.safeParse({ email: 'ada@example.com', extra: true }, german);\n```\n\nInternal development warnings always use `console.warn` in development builds. Route application diagnostics in application code instead of mutating library-wide logger state.\n\n## Working with Validation Errors\n\nUse `SpellValidationError` helpers when you need UI-ready error structures.\n\n```ts\nimport { s, SpellValidationError } from '@vielzeug/spell';\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({\n name: s.string().min(2),\n }),\n});\n\nconst result = User.safeParse({ email: 'nope', profile: { name: '' } });\n\nif (!result.success && result.error instanceof SpellValidationError) {\n const profileErrors = result.error.messagesAt('profile', 'name');\n console.log(profileErrors);\n}\n```\n\nUse `bestMatch()` on a union failure when you want the branch that came closest to succeeding. Pass a specific `invalid_union` issue when one validation produced multiple union failures.\n\n## Schema Traversal with walk()\n\nUse `walk()` to inspect or transform a schema tree without importing internal implementation classes.\n\n```ts\nimport { s, type SchemaWalker } from '@vielzeug/spell';\n\nconst fields: string[] = [];\n\nconst collectFields: SchemaWalker<void> = {\n object(schema) {\n for (const [key, child] of Object.entries(schema.shape)) {\n fields.push(key);\n child.walk(collectFields);\n }\n },\n unknown() {},\n};\n\nconst User = s.object({\n email: s.string().email(),\n profile: s.object({ name: s.string() }),\n});\n\nUser.walk(collectFields);\nconsole.log(fields); // ['email', 'profile', 'name']\n```\n\n`walk()` dispatches by `schema.kind`. If no handler matches and no `unknown` fallback is provided, `walk()` returns `null`. Add an `unknown` handler to capture any kind not explicitly listed in your visitor.\n\n## Framework Integration\n\nSpell works anywhere you can call a function before state enters your app.\n\n::: code-group\n\n```tsx [React]\nimport { s } from '@vielzeug/spell';\n\nconst SearchParams = s\n .object({\n page: s.coerce.number().int().positive().default(1),\n q: s.string().trim().optional(),\n })\n .relaxed();\n\nexport function SearchPage({ rawParams }: { rawParams: unknown }) {\n const params = SearchParams.parse(rawParams);\n\n return (\n <div>\n {params.q ?? 'All results'} — page {params.page}\n </div>\n );\n}\n```\n\n```ts [Vue]\nimport { computed, ref } from 'vue';\nimport { s } from '@vielzeug/spell';\n\nconst Settings = s.object({\n locale: s.string().min(2),\n compact: s.coerce.boolean().default(false),\n});\n\nconst raw = ref<unknown>({ locale: 'en', compact: 'true' });\nconst settings = computed(() => Settings.parse(raw.value));\n```\n\n:::\n\nUse `safeParse()` at event boundaries and `parse()` inside trusted data flows.\n\n## Working with Other Vielzeug Libraries\n\nUse Spell as the validation layer and let other packages focus on transport, forms, or storage.\n\n```ts\nimport { createForm } from '@vielzeug/forge';\nimport { customValidator } from '@vielzeug/forge/spell';\nimport { createCourier } from '@vielzeug/courier';\nimport { s } from '@vielzeug/spell';\n\nconst Profile = s.object({\n displayName: s.string().min(2),\n newsletter: s.boolean(),\n});\n\nconst form = createForm({\n initialValues: {\n displayName: '',\n newsletter: false,\n },\n validate: customValidator(Profile),\n});\n\nconst courier = createCourier({ baseUrl: '/api' });\nconst profile = Profile.parse(await courier.get('/profile'));\n```\n\nUse Spell definitions with `@vielzeug/codex` or other tooling when you need generated docs or external schema consumers.\n\n## Best Practices\n\n- Keep schemas close to the boundary where unknown data enters your app.\n- Use `s` consistently for construction; use explicit `/json` and `/predicates` subpaths for tooling.\n- Use `.default(() => value)` for mutable defaults such as arrays, objects, `Map`, and `Set`.\n- Call `.required()` when you want to remove `undefined` but keep `null` semantics intact.\n- Use `check()` with a `ctx` argument when you need `ctx.addIssue()`; return a message for simple predicate failures.\n- Use `checkAsync()` and `parseAsync()` for every asynchronous domain rule.\n- Build a parse context per request or test; never rely on mutable process-wide configuration.\n- Use `definition()` with `fromDefinition()` from `@vielzeug/spell/json` for external tooling.\n",
7
7
  "examples": "---\ntitle: Spell — Examples\ndescription: Practical examples and recipes for spell.\n---\n\n## Examples\n\n- [Validating API Payloads](./examples/api.md)\n- [Form-Safe Parsing](./examples/forms.md)\n- [Async Business Rules](./examples/async.md)\n- [Schema Introspection and Round-Trips](./examples/introspection.md)\n- [Unions, Intersections, and Variants](./examples/unions.md)\n- [Schema Traversal with walk()](./examples/walk.md)\n"
8
8
  },
@@ -1,9 +1,9 @@
1
1
  {
2
- "apiSource": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';\nexport { scheduleExpiredPrune } from './prune';\nexport type { QueryBuilder } from './query';\nexport { isExpired, ttl } from './ttl';\nexport type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';\nexport { table } from './types';\n",
2
+ "apiSource": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';\nexport type { QueryBuilder } from './query';\nexport { isExpired, ttl } from './ttl';\nexport type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';\nexport { table } from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: Vault — Typed storage\ndescription: Typed browser storage and opt-in driver-neutral SQLite with portable keys, TTL, observation, and transactions.\npackage: vault\ncategory: Storage\nkeywords: [storage, indexeddb, localstorage, sessionstorage, sqlite, ttl, browser, node, deno]\nrelated: [courier, forge, ripple]\nexports: [table, ttl, scheduleExpiredPrune, isExpired, createMemory, createLocalStorage, createSessionStorage, createIndexedDB, createSQLite]\nenvironments: [browser, node, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"vault\" />\n\n## Why Vault?\n\nVault gives browser and SQLite persistence one typed schema while keeping backend guarantees explicit. Use `VaultStore` for portable CRUD and observation; choose IndexedDB or the opt-in SQLite subpath when you need atomic transactions or lazy iteration.\n\n```ts\n// Before\nlocalStorage.setItem('theme', JSON.stringify({ value: 'dark' }));\nconst theme = JSON.parse(localStorage.getItem('theme') ?? '{}').value;\n\n// After\nawait store.put('preferences', { id: 'theme', value: 'dark' });\nconst theme = await store.get('preferences', 'theme');\n```\n\n| Feature | Vault | Raw Web Storage | Dexie |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"vault\" type=\"size\" /> | Browser built-in | Extra dependency |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed schema and keys | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Portable Memory/Web Storage API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | IndexedDB only |\n| Explicit atomic transactions | IndexedDB capability | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Driver-neutral SQLite | Opt-in subpath | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Vault when** you need typed browser persistence or application-owned SQLite with one portable CRUD API and explicit storage capabilities.\n\n**Consider raw Web Storage when** you only persist one or two unstructured values. **Consider Dexie when** you need a broader IndexedDB ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/vault\n```\n\n```sh [npm]\nnpm install @vielzeug/vault\n```\n\n```sh [yarn]\nyarn add @vielzeug/vault\n```\n\n:::\n\n## Quick Start\n\nDefine a schema, create a portable store, and dispose it with its owner.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<{ id: string; theme: 'dark' | 'light' }>('id') },\n});\n\ntry {\n await store.put('preferences', { id: 'theme', theme: 'dark' });\n console.log(await store.get('preferences', 'theme'));\n} finally {\n await store.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `table()` defines typed records with portable string or number keys.\n- `/memory`, `/local-storage`, and `/session-storage` return portable `VaultStore` instances without loading other adapters.\n- `observe()` emits current and changed table snapshots.\n- `ttl` creates validated expiration durations.\n- `/indexeddb` returns `IndexedDbVaultStore` with `batch()` and `iterate()`.\n- `createSQLite()` is an opt-in, driver-neutral subpath for Node, Bun, and Deno SQLite drivers.\n- `/indexeddb` also exports `defineMigration()` for schema upgrades.\n- `scheduleExpiredPrune()` removes stale TTL entries on an owned schedule.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Forge](../forge/index.md) saves and restores form drafts through Vault stores.\n- [Ripple](../ripple/index.md) owns application state that can persist through Vault.\n- [Courier](../courier/index.md) can populate persistent cache data.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: Vault — API Reference\ndescription: Reference for Vault schemas, adapter entry points, storage capabilities, SQLite drivers, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createMemory()` | In-memory portable store | Async API | Import from `/memory` |\n| `createLocalStorage()` / `createSessionStorage()` | Web Storage-backed portable stores | Async API | Available only where the corresponding Web API exists |\n| `createIndexedDB()` | Browser transactions and cursor iteration | Async API | Import from `/indexeddb` |\n| `createSQLite()` | Driver-neutral SQLite store | Async API over a synchronous driver | Import from `/sqlite` |\n| `table()` | Typed record schema | Sync | The key field must be a string or finite number |\n| `ttl` | Valid expiration durations | Sync | Durations must be positive |\n| `scheduleExpiredPrune()` | Periodic TTL cleanup | Sync setup, async work | Pass `disposalSignal` to auto-cancel |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/vault` | Adapter-free schemas, TTL, errors, pruning, queries, and shared types |\n| `@vielzeug/vault/memory` | `createMemory` |\n| `@vielzeug/vault/local-storage` | `createLocalStorage` |\n| `@vielzeug/vault/session-storage` | `createSessionStorage` |\n| `@vielzeug/vault/indexeddb` | `createIndexedDB`, `defineMigration`, migrations, and IndexedDB-only types |\n| `@vielzeug/vault/sqlite` | `createSQLite`, the SQLite driver protocol types, and `TransactionContext` |\n\n## Schemas and TTL\n\n### `table()`\n\n```ts\nfunction table<T extends object, Key extends keyof T & string = keyof T & string>(\n key: Key & (T[Key] extends VaultKey ? unknown : never),\n options?: { defaultTtl?: number; indexes?: readonly (keyof T & string)[] },\n): SchemaEntry<T, Key>;\n```\n\nDefines a typed table and its primary-key field.\n\n| Parameter | Description |\n| --- | --- |\n| `key` | A record field whose values are `string` or finite `number` keys |\n| `options.defaultTtl` | Per-table default TTL in milliseconds |\n| `options.indexes` | IndexedDB secondary index fields |\n\n**Returns:** A `SchemaEntry` describing the table.\n\n```ts\nimport { table, ttl } from '@vielzeug/vault';\n\nconst users = table<{ id: number; email: string }>('id', {\n indexes: ['email'],\n defaultTtl: ttl.days(7),\n});\n```\n\n---\n\n### `ttl`\n\n```ts\nconst ttl: {\n days(n: number): number;\n hours(n: number): number;\n minutes(n: number): number;\n ms(n: number): number;\n seconds(n: number): number;\n};\n```\n\nCreates a finite, positive duration in milliseconds for writes and table defaults.\n\n**Returns:** `number`.\n\n```ts\nimport { ttl } from '@vielzeug/vault';\n\nconst cacheLifetime = ttl.minutes(5);\n```\n\n---\n\n### `isExpired()`\n\n```ts\nfunction isExpired(expiresAt: number | undefined): boolean;\n```\n\nReports whether an expiration timestamp has passed.\n\n**Returns:** `true` when `expiresAt` is defined and no later than the current time.\n\n```ts\nimport { isExpired } from '@vielzeug/vault';\n\nif (isExpired(record.expiresAt)) console.log('expired');\n```\n\n## Factories\n\nAll factory options accept `schema`, plus optional `validators`, `logger`, and `onMetrics`. The root entry does not export any factory.\n\n### `createMemory()`\n\n```ts\nfunction createMemory<S extends AnySchema>(options: {\n name?: string;\n schema: S;\n} & BaseAdapterOptions<S>): VaultStore<S>;\n```\n\nCreates an in-memory portable store. A `name` enables same-origin `BroadcastChannel` observation between memory stores when the platform provides it.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `name` | Optional shared memory-store namespace |\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createMemory } from '@vielzeug/vault/memory';\n\nconst store = createMemory({ schema: { users: table<{ id: number; name: string }>('id') } });\n```\n\n---\n\n### `createLocalStorage()`\n\n```ts\nfunction createLocalStorage<S extends AnySchema>(options: {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n schema: S;\n} & BaseAdapterOptions<S>): VaultStore<S>;\n```\n\nCreates a namespaced `localStorage` store.\n\n| Parameter | Description |\n| --- | --- |\n| `name` | Required storage namespace |\n| `onQuotaExceeded` | Handles a Web Storage quota error; returning `'ignore'` drops that write |\n| `schema` | Tables created by `table()` |\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({ name: 'app', schema: { settings: table<{ id: string }>('id') } });\n```\n\n---\n\n### `createSessionStorage()`\n\n```ts\nfunction createSessionStorage<S extends AnySchema>(options: {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n schema: S;\n} & BaseAdapterOptions<S>): VaultStore<S>;\n```\n\nCreates a namespaced `sessionStorage` store. Its options and return type match `createLocalStorage()`.\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createSessionStorage } from '@vielzeug/vault/session-storage';\n\nconst store = createSessionStorage({ name: 'checkout', schema: { cart: table<{ id: string }>('id') } });\n```\n\n---\n\n### `createIndexedDB()`\n\n```ts\nfunction createIndexedDB<S extends AnySchema>(options: {\n migrate?: MigrationFn;\n name: string;\n schema: S;\n version?: number;\n} & BaseAdapterOptions<S>): IndexedDbVaultStore<S>;\n```\n\nCreates an IndexedDB store with atomic batches, lazy cursor iteration, and optional schema migrations.\n\n| Parameter | Description |\n| --- | --- |\n| `name` | Required database name |\n| `schema` | Tables and IndexedDB secondary indexes |\n| `version` | Positive schema version; defaults to `1` |\n| `migrate` | Synchronous upgrade callback for version changes |\n\n**Returns:** `IndexedDbVaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst store = createIndexedDB({ name: 'app', schema: { users: table<{ id: number }>('id') } });\n```\n\n---\n\n### `createSQLite()`\n\n```ts\nfunction createSQLite<S extends AnySchema>(options: SQLiteVaultOptions<S>): SQLiteVaultStore<S>;\n```\n\nCreates a namespaced SQLite store with atomic batches and keyset-paginated iteration. It accepts an application-provided positional-parameter driver and never opens or imports a runtime driver.\n\n| Parameter | Description |\n| --- | --- |\n| `database` | Caller-provided `SQLiteDatabase` connection |\n| `name` | Namespace within the connection |\n| `schema`, `validators`, `logger`, `onMetrics` | Shared factory options |\n| `closeOnDispose` | Closes the connection during disposal; defaults to `false` |\n\n**Returns:** `SQLiteVaultStore<S>`.\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst store = createSQLite({\n database: new DatabaseSync(':memory:'),\n name: 'tests',\n schema: { users: table<{ id: number; name: string }>('id') },\n});\n```\n\nNode `DatabaseSync`, Bun `Database`, and Deno `jsr:@db/sqlite` `Database` satisfy the protocol. Values must be JSON-compatible plain objects. During a `batch()` callback, calls on every Vault store sharing that connection reject; use `tx.*` instead.\n\n## Store Capabilities\n\n### `VaultStore`\n\n```ts\ninterface VaultStore<S extends AnySchema> {\n clear<K extends keyof S & string>(table: K): Promise<void>;\n count<K extends keyof S & string>(table: K): Promise<number>;\n delete<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n deleteMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<number>;\n entries<K extends keyof S & string>(table: K): Promise<Array<[KeyOf<S, K>, RecordOf<S, K>]>>;\n get<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<RecordOf<S, K> | undefined>;\n getAll<K extends keyof S & string>(table: K): Promise<RecordOf<S, K>[]>;\n getMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<Array<RecordOf<S, K> | undefined>>;\n getOrDefault<K extends keyof S & string>(table: K, key: KeyOf<S, K>, defaultFn: () => RecordOf<S, K>, ttl?: number): Promise<RecordOf<S, K>>;\n has<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n isEmpty<K extends keyof S & string>(table: K): Promise<boolean>;\n keys<K extends keyof S & string>(table: K, filter?: (record: RecordOf<S, K>) => boolean): Promise<KeyOf<S, K>[]>;\n put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: number): Promise<void>;\n putAll<K extends keyof S & string>(table: K, values: RecordOf<S, K>[], ttl?: number): Promise<void>;\n query<K extends keyof S & string>(table: K): QueryBuilder<RecordOf<S, K>>;\n update<K extends keyof S & string>(table: K, key: KeyOf<S, K>, changes: Partial<RecordOf<S, K>>, ttl?: number): Promise<RecordOf<S, K> | undefined>;\n upsert<K extends keyof S & string>(table: K, key: KeyOf<S, K>, fn: (existing: RecordOf<S, K> | undefined) => RecordOf<S, K>, ttl?: number): Promise<RecordOf<S, K>>;\n pruneExpired(): Promise<Record<keyof S & string, number>>;\n debug(): Promise<DebugInfo<S>>;\n observe<K extends keyof S & string>(table: K, listener: Observer<RecordOf<S, K>>, options?: { immediate?: boolean; signal?: AbortSignal }): Unsubscribe;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.asyncDispose](): Promise<void>;\n}\n```\n\nThe portable store API is returned by every factory. `observe()` emits the current table snapshot by default and then emits after mutations.\n\n---\n\n### `batch()`\n\n```ts\ninterface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n}\n```\n\nRuns a scoped atomic callback. `IndexedDbVaultStore` and `SQLiteVaultStore` provide it.\n\n| Parameter | Description |\n| --- | --- |\n| `tables` | Tables the transaction may access |\n| `fn` | Async callback that uses only the supplied `tx` context |\n\n**Returns:** The callback result after commit.\n\n```ts\nawait store.batch(['users'], async (tx) => {\n await tx.put('users', { id: 1, name: 'Ada' });\n});\n```\n\n---\n\n### `iterate()`\n\n```ts\ninterface IterableVaultStore<S extends AnySchema> extends VaultStore<S> {\n iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;\n}\n```\n\nLazily yields table records. `IndexedDbVaultStore` uses a cursor; `SQLiteVaultStore` uses keyset pagination.\n\n**Returns:** An `AsyncIterable` of records.\n\n```ts\nfor await (const user of store.iterate('users')) console.log(user);\n```\n\n## Queries, Pruning, and Migrations\n\n### `QueryBuilder`\n\n```ts\ninterface QueryBuilder<T extends object, N extends T = T> {\n between(field: string, lower: number | string, upper: number | string): QueryBuilder<T, N>;\n count(): Promise<number>;\n delete(): Promise<number>;\n equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T & Record<K, V>>;\n exists(): Promise<boolean>;\n filter(fn: (value: N, index: number, array: N[]) => boolean): QueryBuilder<T, N>;\n first(): Promise<N | undefined>;\n limit(n: number): QueryBuilder<T, N>;\n offset(n: number): QueryBuilder<T, N>;\n orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T, N>;\n startsWith(field: keyof T, prefix: string, options?: { ignoreCase?: boolean }): QueryBuilder<T, N>;\n toArray(): Promise<N[]>;\n}\n```\n\nBuilds a lazy table query. `count()` ignores `limit()`, `offset()`, and `orderBy()` — it always returns the full filtered-set size.\n\n```ts\nconst page = await store.query('users').startsWith('name', 'A').orderBy('name').limit(20).toArray();\n```\n\n---\n\n### `scheduleExpiredPrune()`\n\n```ts\nfunction scheduleExpiredPrune<S extends AnySchema>(\n adapter: Pick<VaultStore<S>, 'pruneExpired'>,\n options: {\n interval: number;\n onError?: (error: unknown) => void;\n signal?: AbortSignal;\n },\n): () => void;\n```\n\nSchedules `pruneExpired()` at a finite, positive interval. Pass `signal: store.disposalSignal` to auto-cancel when the store is torn down.\n\n**Returns:** A stop function.\n\n```ts\nimport { scheduleExpiredPrune, ttl } from '@vielzeug/vault';\n\nconst stop = scheduleExpiredPrune(store, {\n interval: ttl.hours(1),\n signal: store.disposalSignal,\n});\nstop();\n```\n\n---\n\n### `defineMigration()`\n\n```ts\nfunction defineMigration(steps: MigrationStep[]): MigrationFn;\n```\n\nBuilds an idempotent IndexedDB migration callback from schema-change steps.\n\n**Returns:** An IndexedDB `MigrationFn`.\n\n```ts\nimport { defineMigration } from '@vielzeug/vault/indexeddb';\n\nconst migrate = defineMigration([{ field: 'email', table: 'users', type: 'addIndex' }]);\n```\n\n## Types\n\n```ts\ntype VaultKey = number | string;\ntype Unsubscribe = () => void;\ntype Observer<T> = (records: T[]) => void;\ntype AnySchema = Record<string, {\n defaultTtl?: number;\n indexes?: readonly string[];\n key: string;\n}>;\ntype SchemaEntry<T extends object, Key extends keyof T & string = keyof T & string> =\n T[Key] extends VaultKey ? {\n defaultTtl?: number;\n indexes?: readonly (keyof T & string)[];\n key: Key;\n } : never;\ntype RecordOf<S extends AnySchema, K extends keyof S> =\n S[K] extends SchemaEntry<infer R, infer _Key> ? R : never;\ntype KeyOf<S extends AnySchema, K extends keyof S> =\n Extract<S[K] extends SchemaEntry<infer R, infer Key> ? R[Key] : never, VaultKey>;\n```\n\n```ts\ntype BaseAdapterOptions<S extends AnySchema> = {\n logger?: VaultLogger;\n onMetrics?: (event: MetricsEvent) => void;\n schema: S;\n validators?: TableValidators<S>;\n};\n\ntype VaultLogger = {\n error(message: string, context?: Error | Record<string, unknown>): void;\n};\n\ntype RecordValidator<T> = {\n parse(value: unknown): T;\n};\n\ntype TableValidators<S extends AnySchema> = {\n [K in keyof S]?: RecordValidator<RecordOf<S, K>>;\n};\n\ntype MetricsEvent = {\n duration: number;\n operation: 'batch' | 'clear' | 'count' | 'delete' | 'deleteMany' | 'entries' | 'get' | 'getAll' |\n 'getMany' | 'getOrDefault' | 'has' | 'isEmpty' | 'keys' | 'put' | 'putAll' | 'query' |\n 'queryDelete' | 'update' | 'upsert';\n table: string;\n};\n\ntype DebugStats = { expiredCount: number; recordCount: number };\ntype DebugInfo<S extends AnySchema> = { tables: Array<{ name: keyof S & string } & DebugStats> };\n```\n\n```ts\ninterface IndexedDbVaultStore<S extends AnySchema>\n extends TransactionalVaultStore<S>, IterableVaultStore<S> {}\n\ntype MigrationContext = {\n db: IDBDatabase;\n newVersion: number | null;\n oldVersion: number;\n tx: IDBTransaction;\n};\n\ntype MigrationFn = (ctx: MigrationContext) => void;\n\ntype MigrationStep =\n | { field: string; table: string; type: 'addIndex' }\n | { field: string; table: string; type: 'removeIndex' }\n | { name: string; type: 'addTable' }\n | { name: string; type: 'removeTable' };\n```\n\nImport `MigrationContext`, `MigrationFn`, and `MigrationStep` from `@vielzeug/vault/indexeddb`.\n\n```ts\ntype SQLiteParameter = null | number | string;\n\ninterface SQLiteStatement {\n all(...parameters: SQLiteParameter[]): readonly Record<string, unknown>[];\n finalize?(): void;\n get(...parameters: SQLiteParameter[]): Record<string, unknown> | undefined;\n run(...parameters: SQLiteParameter[]): unknown;\n}\n\ninterface SQLiteDatabase {\n close?(): void;\n exec(sql: string): void;\n prepare(sql: string): SQLiteStatement;\n}\n\ntype SQLiteVaultOptions<S extends AnySchema> = BaseAdapterOptions<S> & {\n closeOnDispose?: boolean;\n database: SQLiteDatabase;\n name: string;\n};\n\ninterface SQLiteVaultStore<S extends AnySchema>\n extends TransactionalVaultStore<S>, IterableVaultStore<S> {}\n```\n\n`TransactionContext` has the same CRUD, query, and TTL methods as `VaultStore`, narrowed to the tables declared in `batch()`. Import it from `@vielzeug/vault/indexeddb` or `@vielzeug/vault/sqlite`.\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `VaultError` | Any Vault-originated validation, serialization, storage, or query error |\n| `VaultDisposedError` | An operation after the store or observer hub is disposed |\n| `VaultScopeError` | An IndexedDB transaction accesses a table outside its declared batch scope |\n| `VaultQuotaError` | A LocalStorage or SessionStorage write exceeds the browser quota |\n| `VaultMigrationError` | An IndexedDB migration callback throws |\n\nEvery listed error extends `VaultError`.\n",
6
- "usage": "---\ntitle: Vault — Usage Guide\ndescription: Persist typed browser or SQLite data, observe table snapshots, and use atomic transactions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a portable store with one schema and write a typed row.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\ninterface Preference {\n id: string;\n theme: 'dark' | 'light';\n}\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n\nawait store.put('preferences', { id: 'theme', theme: 'dark' });\nconsole.log(await store.get('preferences', 'theme'));\n```\n\n## Create a Portable Store\n\nMemory, LocalStorage, and SessionStorage return `VaultStore`. They share portable string/number keys, CRUD methods, queries, TTL, and `observe()`. Vault keeps record values and expiry metadata separate; the physical storage layout is adapter-specific.\n\nThe root entry is adapter-free. Import `createMemory` from `@vielzeug/vault/memory`, `createLocalStorage` from `@vielzeug/vault/local-storage`, or `createSessionStorage` from `@vielzeug/vault/session-storage`. Import each adapter from its focused subpath so unused backends stay out of the bundle.\n\nUse a new storage name when upgrading from Vault 1. Old key and envelope formats are not read by Vault 2.\n\n```ts\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n```\n\n## Read and Change Records\n\nUse `update()` for an existing row and `upsert()` when the row may not exist.\n\n```ts\nconst updated = await store.update('preferences', 'theme', { theme: 'light' });\n\nawait store.upsert('preferences', 'locale', (current) => ({\n id: 'locale',\n theme: current?.theme ?? 'dark',\n}));\n\nconsole.log(updated);\n```\n\n`update()` returns `undefined` for a missing key. `upsert()` always writes the record returned by its callback.\n\n## Query Records\n\nBuild a query from a table, then finish it with a terminal method. `count()` ignores pagination, which makes it suitable for page controls.\n\n```ts\nconst query = store.query('preferences').startsWith('id', 'theme');\nconst preferences = await query.orderBy('id').limit(10).toArray();\nconst total = await query.count();\n\nconsole.log({ preferences, total });\n```\n\nMemory and Web Storage queries scan the table. IndexedDB can use declared secondary indexes, while SQLite pushes primary-key equality, range, and case-sensitive prefix filters to the database.\n\n## Use TTL and Pruning\n\nUse `ttl.*` helpers for expiring rows. Schedule pruning when stale rows can accumulate without reads.\n\n```ts\nimport { scheduleExpiredPrune, ttl } from '@vielzeug/vault';\n\nawait store.put('preferences', { id: 'temporary', theme: 'dark' }, ttl.hours(1));\nconst stopPrune = scheduleExpiredPrune(store, {\n interval: ttl.hours(6),\n signal: store.disposalSignal,\n});\n\nstopPrune();\n```\n\n## Observe a Table\n\nUse `observe()` for current and future snapshots. Tie subscription lifetime to an `AbortSignal` when a component or request owns it.\n\n```ts\nconst controller = new AbortController();\n\nstore.observe('preferences', (preferences) => {\n console.log(preferences);\n}, { signal: controller.signal });\n\ncontroller.abort();\n```\n\n## Use IndexedDB for Browser Transactions\n\nChoose IndexedDB when browser storage needs multiple writes to commit together or cursor iteration.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst db = createIndexedDB({\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait db.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nOnly await `tx.*` operations inside a batch callback. Do not await timers, fetches, or other external asynchronous work; IndexedDB can commit an inactive transaction.\n\n## Use SQLite Outside the Browser\n\nImport SQLite from the opt-in subpath so the browser root stays free of runtime drivers. Vault never opens a connection or configures its SQLite process behavior for you.\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst database = new DatabaseSync('app.db', { timeout: 5_000 });\nconst store = createSQLite({\n database,\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait store.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nNode's `node:sqlite` API is experimental. Bun's `bun:sqlite` `Database` satisfies the same positional `exec()` and `prepare()` contract; configure WAL from your application when the deployment needs it. Deno does not include SQLite, but `jsr:@db/sqlite`'s `Database` satisfies the same contract when its FFI, filesystem, and environment permissions are granted.\n\nSQLite stores serialize all access through the injected connection. `batch()` starts `BEGIN IMMEDIATE` and rolls back callback failures. While its callback runs, calls on any store sharing that connection reject rather than waiting behind the transaction; use `tx.*` instead. The underlying drivers are synchronous, so move large scans and writes to a worker or isolate when event-loop latency matters.\n\n## Store SQLite Values and Observe Changes\n\nSQLite accepts JSON-compatible plain-object records only. Circular values, `bigint`, dates, class instances, functions, and non-finite numbers are rejected before writing. Number and string primary keys remain distinct.\n\n`observe()` sees mutations written through Vault stores sharing the same injected connection after a commit. It cannot detect direct SQL changes, writes from another process, or writes through another connection. The connection belongs to the caller by default; use `closeOnDispose: true` only when the store owns it.\n\n## Handle IndexedDB Schema Migrations\n\nDeclare IndexedDB indexes in the schema. Use `migrate` only for IndexedDB version upgrades and mirror Vault’s fixed `value.<field>` index path.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB, type MigrationFn } from '@vielzeug/vault/indexeddb';\n\nconst schema = { users: table<{ id: number; name: string }>('id', { indexes: ['name'] }) };\nconst migrate: MigrationFn = ({ db, oldVersion, tx }) => {\n if (oldVersion < 2 && db.objectStoreNames.contains('users')) {\n tx.objectStore('users').createIndex('name', 'value.name');\n }\n};\n\ncreateIndexedDB({ name: 'app-v2', migrate, schema, version: 2 });\n```\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const [rows, setRows] = useState<RecordOf<S, K>[]>([]);\n\n useEffect(() => store.observe(table, setRows), [store, table]);\n return rows;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const rows = shallowRef<RecordOf<S, K>[]>([]);\n const stop = store.observe(table, (next) => (rows.value = next));\n\n onUnmounted(stop);\n return rows;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function tableStore<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n return readable<RecordOf<S, K>[]>([], (set) => store.observe(table, set));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple signals as application state and persist selected changes through Vault writes.\n\n## Best Practices\n\n- Define one schema per storage namespace.\n- Use string or finite-number primary keys only.\n- Choose a new namespace for Vault 1 storage unless you migrate it yourself.\n- Use `observe()` for table snapshots.\n- Use IndexedDB or SQLite for atomic work.\n- Keep external asynchronous work outside `batch()` callbacks.\n- Use `ttl.*` instead of raw durations.\n- Keep SQLite scans and writes off latency-sensitive event loops, and dispose stores with their owner.\n- Dispose stores when their owner ends.\n",
4
+ "index": "---\ntitle: Vault — Typed storage\ndescription: Typed browser storage and opt-in driver-neutral SQLite with portable keys, TTL, observation, and transactions.\npackage: vault\ncategory: Storage\nkeywords: [storage, indexeddb, localstorage, sessionstorage, sqlite, ttl, browser, node, deno]\nrelated: [courier, forge, ripple]\nexports: [table, ttl, isExpired, createMemory, createLocalStorage, createSessionStorage, createIndexedDB, createSQLite, defineMigration]\nenvironments: [browser, node, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"vault\" />\n\n## Why Vault?\n\nVault gives browser and SQLite persistence one typed schema while keeping backend guarantees explicit. Use `VaultStore` for portable CRUD and observation; choose IndexedDB or the opt-in SQLite subpath when you need atomic transactions or lazy iteration.\n\n```ts\n// Before\nlocalStorage.setItem('theme', JSON.stringify({ value: 'dark' }));\nconst theme = JSON.parse(localStorage.getItem('theme') ?? '{}').value;\n\n// After\nawait store.put('preferences', { id: 'theme', value: 'dark' });\nconst theme = await store.get('preferences', 'theme');\n```\n\n| Feature | Vault | Raw Web Storage | Dexie |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"vault\" type=\"size\" /> | Browser built-in | Extra dependency |\n| Runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Typed schema and keys | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Application-defined | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Portable Memory/Web Storage API | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"triangle-alert\" size=\"16\"></ore-icon> | IndexedDB only |\n| Explicit atomic transactions | IndexedDB capability | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Driver-neutral SQLite | Opt-in subpath | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Vault when** you need typed browser persistence or application-owned SQLite with one portable CRUD API and explicit storage capabilities.\n\n**Consider raw Web Storage when** you only persist one or two unstructured values. **Consider Dexie when** you need a broader IndexedDB ecosystem.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/vault\n```\n\n```sh [npm]\nnpm install @vielzeug/vault\n```\n\n```sh [yarn]\nyarn add @vielzeug/vault\n```\n\n:::\n\n## Quick Start\n\nDefine a schema, create a portable store, and dispose it with its owner.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<{ id: string; theme: 'dark' | 'light' }>('id') },\n});\n\ntry {\n await store.put('preferences', { id: 'theme', theme: 'dark' });\n console.log(await store.get('preferences', 'theme'));\n} finally {\n await store.dispose();\n}\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- `table()` defines typed records with portable string or number keys.\n- `/memory`, `/local-storage`, and `/session-storage` return portable `VaultStore` instances without loading other adapters.\n- `observe()` emits current and changed table snapshots.\n- `ttl` creates validated expiration durations.\n- `/indexeddb` returns `TransactionalVaultStore` with `batch()` and `iterate()`.\n- `createSQLite()` is an opt-in, driver-neutral subpath for Node, Bun, and Deno SQLite drivers.\n- `/indexeddb` also exports `defineMigration()` for schema upgrades.\n- `pruneExpired()` removes stale TTL entries on demand.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Forge](../forge/index.md) saves and restores form drafts through Vault stores.\n- [Ripple](../ripple/index.md) owns application state that can persist through Vault.\n- [Courier](../courier/index.md) can populate persistent cache data.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: Vault — API Reference\ndescription: Reference for Vault schemas, adapter entry points, storage capabilities, SQLite drivers, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createMemory()` | In-memory portable store | Async API | Import from `/memory` |\n| `createLocalStorage()` / `createSessionStorage()` | Web Storage-backed portable stores | Async API | Available only where the corresponding Web API exists |\n| `createIndexedDB()` | Browser transactions and cursor iteration | Async API | Import from `/indexeddb` |\n| `createSQLite()` | Driver-neutral SQLite store | Async API over a synchronous driver | Import from `/sqlite` |\n| `defineMigration()` | Declarative IndexedDB schema upgrade | Sync | Import from `/indexeddb` |\n| `table()` | Typed record schema | Sync | The key field must be a string or finite number |\n| `ttl` | Valid expiration durations | Sync | Durations must be positive |\n| `isExpired()` | Check an expiration timestamp | Sync | Returns `false` when no expiry is set |\n\n## Package Entry Points\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/vault` | Adapter-free schemas, TTL, errors, pruning, queries, and shared types |\n| `@vielzeug/vault/memory` | `createMemory` |\n| `@vielzeug/vault/local-storage` | `createLocalStorage` |\n| `@vielzeug/vault/session-storage` | `createSessionStorage` |\n| `@vielzeug/vault/indexeddb` | `createIndexedDB`, `defineMigration`, migrations, and IndexedDB-only types |\n| `@vielzeug/vault/sqlite` | `createSQLite`, the SQLite driver protocol types, and `TransactionContext` |\n\n## Schemas and TTL\n\n### `table()`\n\n```ts\nfunction table<T extends object, Key extends keyof T & string = keyof T & string>(\n key: Key & (T[Key] extends VaultKey ? unknown : never),\n options?: { defaultTtl?: number; indexes?: readonly (keyof T & string)[] },\n): SchemaEntry<T, Key>;\n```\n\nDefines a typed table and its primary-key field.\n\n| Parameter | Description |\n| --- | --- |\n| `key` | A record field whose values are `string` or finite `number` keys |\n| `options.defaultTtl` | Per-table default TTL in milliseconds |\n| `options.indexes` | IndexedDB secondary index fields |\n\n**Returns:** A `SchemaEntry` describing the table.\n\n```ts\nimport { table, ttl } from '@vielzeug/vault';\n\nconst users = table<{ id: number; email: string }>('id', {\n indexes: ['email'],\n defaultTtl: ttl.days(7),\n});\n```\n\n---\n\n### `ttl`\n\n```ts\nconst ttl: {\n days(n: number): number;\n hours(n: number): number;\n minutes(n: number): number;\n ms(n: number): number;\n seconds(n: number): number;\n};\n```\n\nCreates a finite, positive duration in milliseconds for writes and table defaults.\n\n**Returns:** `number`.\n\n```ts\nimport { ttl } from '@vielzeug/vault';\n\nconst cacheLifetime = ttl.minutes(5);\n```\n\n---\n\n### `isExpired()`\n\n```ts\nfunction isExpired(expiresAt: number | undefined): boolean;\n```\n\nReports whether an expiration timestamp has passed.\n\n**Returns:** `true` when `expiresAt` is defined and no later than the current time.\n\n```ts\nimport { isExpired } from '@vielzeug/vault';\n\nif (isExpired(record.expiresAt)) console.log('expired');\n```\n\n## Factories\n\nAll factory options accept `schema` and optional `validators`. The root entry does not export any factory.\n\n### `createMemory()`\n\n```ts\nfunction createMemory<S extends AnySchema>(options: BaseAdapterOptions<S>): VaultStore<S>;\n```\n\nCreates an in-memory portable store.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `validators` | Optional per-table validators with a `parse(value): T` method |\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createMemory } from '@vielzeug/vault/memory';\n\nconst store = createMemory({ schema: { users: table<{ id: number; name: string }>('id') } });\n```\n\n---\n\n### `createLocalStorage()`\n\n```ts\nfunction createLocalStorage<S extends AnySchema>(options: BaseAdapterOptions<S> & {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n}): VaultStore<S>;\n```\n\nCreates a namespaced `localStorage` store.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `validators` | Optional per-table validators |\n| `name` | Required storage namespace |\n| `onQuotaExceeded` | Handles a Web Storage quota error; returning `'ignore'` drops that write |\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\nconst store = createLocalStorage({ name: 'app', schema: { settings: table<{ id: string }>('id') } });\n```\n\n---\n\n### `createSessionStorage()`\n\n```ts\nfunction createSessionStorage<S extends AnySchema>(options: BaseAdapterOptions<S> & {\n name: string;\n onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';\n}): VaultStore<S>;\n```\n\nCreates a namespaced `sessionStorage` store. Its options and return type match `createLocalStorage()`.\n\n**Returns:** `VaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createSessionStorage } from '@vielzeug/vault/session-storage';\n\nconst store = createSessionStorage({ name: 'checkout', schema: { cart: table<{ id: string }>('id') } });\n```\n\n---\n\n### `createIndexedDB()`\n\n```ts\nfunction createIndexedDB<S extends AnySchema>(options: BaseAdapterOptions<S> & {\n migrate?: MigrationFn;\n name: string;\n version?: number;\n}): TransactionalVaultStore<S>;\n```\n\nCreates an IndexedDB store with atomic batches, lazy cursor iteration, and optional schema migrations.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables and IndexedDB secondary indexes |\n| `validators` | Optional per-table validators |\n| `name` | Required database name |\n| `version` | Positive schema version; defaults to `1` |\n| `migrate` | Synchronous upgrade callback for version changes |\n\n**Returns:** `TransactionalVaultStore<S>`.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst store = createIndexedDB({ name: 'app', schema: { users: table<{ id: number }>('id') } });\n```\n\n---\n\n### `createSQLite()`\n\n```ts\nfunction createSQLite<S extends AnySchema>(options: SQLiteVaultOptions<S>): TransactionalVaultStore<S>;\n```\n\nCreates a namespaced SQLite store with atomic batches and keyset-paginated iteration. It accepts an application-provided positional-parameter driver and never opens or imports a runtime driver.\n\n| Parameter | Description |\n| --- | --- |\n| `schema` | Tables created by `table()` |\n| `validators` | Optional per-table validators |\n| `database` | Caller-provided `SQLiteDatabase` connection |\n| `name` | Namespace within the connection |\n| `closeOnDispose` | Closes the connection during disposal; defaults to `false` |\n\n**Returns:** `TransactionalVaultStore<S>`.\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst store = createSQLite({\n database: new DatabaseSync(':memory:'),\n name: 'tests',\n schema: { users: table<{ id: number; name: string }>('id') },\n});\n```\n\nNode `DatabaseSync`, Bun `Database`, and Deno `jsr:@db/sqlite` `Database` satisfy the protocol. Values must be JSON-compatible plain objects. During a `batch()` callback, calls on every Vault store sharing that connection reject; use `tx.*` instead.\n\n## Store Capabilities\n\n### `VaultStore`\n\n```ts\ninterface VaultStore<S extends AnySchema> {\n clear<K extends keyof S & string>(table: K): Promise<void>;\n count<K extends keyof S & string>(table: K): Promise<number>;\n delete<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n deleteMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<number>;\n get<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<RecordOf<S, K> | undefined>;\n getAll<K extends keyof S & string>(table: K): Promise<RecordOf<S, K>[]>;\n getMany<K extends keyof S & string>(table: K, keys: KeyOf<S, K>[]): Promise<Array<RecordOf<S, K> | undefined>>;\n has<K extends keyof S & string>(table: K, key: KeyOf<S, K>): Promise<boolean>;\n isEmpty<K extends keyof S & string>(table: K): Promise<boolean>;\n keys<K extends keyof S & string>(table: K, filter?: (record: RecordOf<S, K>) => boolean): Promise<KeyOf<S, K>[]>;\n put<K extends keyof S & string>(table: K, value: RecordOf<S, K>, ttl?: number): Promise<void>;\n putAll<K extends keyof S & string>(table: K, values: RecordOf<S, K>[], ttl?: number): Promise<void>;\n query<K extends keyof S & string>(table: K): QueryBuilder<RecordOf<S, K>>;\n update<K extends keyof S & string>(table: K, key: KeyOf<S, K>, changes: Partial<RecordOf<S, K>>, ttl?: number): Promise<RecordOf<S, K> | undefined>;\n upsert<K extends keyof S & string>(table: K, key: KeyOf<S, K>, fn: (existing: RecordOf<S, K> | undefined) => RecordOf<S, K>, ttl?: number): Promise<RecordOf<S, K>>;\n pruneExpired(): Promise<Record<keyof S & string, number>>;\n observe<K extends keyof S & string>(table: K, listener: Observer<RecordOf<S, K>>, options?: { immediate?: boolean; signal?: AbortSignal }): Unsubscribe;\n dispose(): Promise<void>;\n readonly disposed: boolean;\n readonly disposalSignal: AbortSignal;\n [Symbol.asyncDispose](): Promise<void>;\n}\n```\n\nThe portable store API is returned by every factory. `observe()` emits the current table snapshot by default and then emits after mutations.\n\n---\n\n### `batch()` and `iterate()`\n\n```ts\ninterface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;\n}\n```\n\n`batch()` runs a scoped atomic callback. `iterate()` lazily yields table records — IndexedDB uses a cursor, SQLite uses keyset pagination. Both are provided by `createIndexedDB()` and `createSQLite()`.\n\n| Parameter | Description |\n| --- | --- |\n| `tables` | Tables the transaction may access |\n| `fn` | Async callback that uses only the supplied `tx` context |\n\n**Returns:** The callback result after commit.\n\n```ts\nawait store.batch(['users'], async (tx) => {\n await tx.put('users', { id: 1, name: 'Ada' });\n});\n\nfor await (const user of store.iterate('users')) console.log(user);\n```\n\n## Queries and Migrations\n\n### `QueryBuilder`\n\n```ts\ninterface QueryBuilder<T extends object> {\n count(): Promise<number>;\n delete(): Promise<number>;\n equals<K extends keyof T & string, V extends T[K]>(field: K, value: V): QueryBuilder<T>;\n filter(fn: (value: T, index: number, array: T[]) => boolean): QueryBuilder<T>;\n first(): Promise<T | undefined>;\n limit(n: number): QueryBuilder<T>;\n offset(n: number): QueryBuilder<T>;\n orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T>;\n toArray(): Promise<T[]>;\n}\n```\n\nBuilds a lazy table query. `count()` ignores `limit()`, `offset()`, and `orderBy()` — it always returns the full filtered-set size.\n\n```ts\nconst page = await store.query('users').equals('role', 'admin').orderBy('name').limit(20).toArray();\n```\n\n---\n\n### `defineMigration()`\n\n```ts\nfunction defineMigration(steps: MigrationStep[]): MigrationFn;\n```\n\nBuilds an idempotent IndexedDB migration callback from schema-change steps.\n\n**Returns:** An IndexedDB `MigrationFn`.\n\n```ts\nimport { defineMigration } from '@vielzeug/vault/indexeddb';\n\nconst migrate = defineMigration([{ field: 'email', table: 'users', type: 'addIndex' }]);\n```\n\n## Types\n\n```ts\ntype VaultKey = number | string;\ntype Unsubscribe = () => void;\ntype Observer<T> = (records: T[]) => void;\ntype AnySchema = Record<string, {\n defaultTtl?: number;\n indexes?: readonly string[];\n key: string;\n}>;\ntype SchemaEntry<T extends object, Key extends keyof T & string = keyof T & string> =\n T[Key] extends VaultKey ? {\n defaultTtl?: number;\n indexes?: readonly (keyof T & string)[];\n key: Key;\n } : never;\ntype RecordOf<S extends AnySchema, K extends keyof S> =\n S[K] extends SchemaEntry<infer R, infer _Key> ? R : never;\ntype KeyOf<S extends AnySchema, K extends keyof S> =\n Extract<S[K] extends SchemaEntry<infer R, infer Key> ? R[Key] : never, VaultKey>;\n```\n\n```ts\ntype BaseAdapterOptions<S extends AnySchema> = {\n schema: S;\n validators?: TableValidators<S>;\n};\n\ntype RecordValidator<T> = {\n parse(value: unknown): T;\n};\n\ntype TableValidators<S extends AnySchema> = {\n [K in keyof S]?: RecordValidator<RecordOf<S, K>>;\n};\n```\n\n```ts\ntype MigrationContext = {\n db: IDBDatabase;\n newVersion: number | null;\n oldVersion: number;\n tx: IDBTransaction;\n};\n\ntype MigrationFn = (ctx: MigrationContext) => void;\n\ntype MigrationStep =\n | { field: string; table: string; type: 'addIndex' }\n | { field: string; table: string; type: 'removeIndex' }\n | { name: string; type: 'addTable' }\n | { name: string; type: 'removeTable' };\n```\n\nImport `MigrationContext`, `MigrationFn`, and `MigrationStep` from `@vielzeug/vault/indexeddb`.\n\n```ts\ntype SQLiteParameter = null | number | string;\n\ninterface SQLiteStatement {\n all(...parameters: SQLiteParameter[]): readonly Record<string, unknown>[];\n finalize?(): void;\n get(...parameters: SQLiteParameter[]): Record<string, unknown> | undefined;\n run(...parameters: SQLiteParameter[]): unknown;\n}\n\ninterface SQLiteDatabase {\n close?(): void;\n exec(sql: string): void;\n prepare(sql: string): SQLiteStatement;\n}\n\ntype SQLiteVaultOptions<S extends AnySchema> = BaseAdapterOptions<S> & {\n closeOnDispose?: boolean;\n database: SQLiteDatabase;\n name: string;\n};\n```\n\n```ts\ninterface TransactionalVaultStore<S extends AnySchema> extends VaultStore<S> {\n batch<K extends keyof S & string, R>(\n tables: readonly K[],\n fn: (tx: TransactionContext<S, K>) => Promise<R>,\n ): Promise<R>;\n iterate<K extends keyof S & string>(table: K): AsyncIterable<RecordOf<S, K>>;\n}\n```\n\nImport `TransactionalVaultStore` from `@vielzeug/vault`.\n\n```ts\ninterface TransactionContext<S extends AnySchema, K extends keyof S & string = keyof S & string> {\n clear<T extends K>(table: T): Promise<void>;\n count<T extends K>(table: T): Promise<number>;\n delete<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\n deleteMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<number>;\n get<T extends K>(table: T, key: KeyOf<S, T>): Promise<RecordOf<S, T> | undefined>;\n getAll<T extends K>(table: T): Promise<RecordOf<S, T>[]>;\n getMany<T extends K>(table: T, keys: KeyOf<S, T>[]): Promise<Array<RecordOf<S, T> | undefined>>;\n has<T extends K>(table: T, key: KeyOf<S, T>): Promise<boolean>;\n isEmpty<T extends K>(table: T): Promise<boolean>;\n keys<T extends K>(table: T, filter?: (record: RecordOf<S, T>) => boolean): Promise<KeyOf<S, T>[]>;\n put<T extends K>(table: T, value: RecordOf<S, T>, ttl?: number): Promise<void>;\n putAll<T extends K>(table: T, values: RecordOf<S, T>[], ttl?: number): Promise<void>;\n query<T extends K>(table: T): QueryBuilder<RecordOf<S, T>>;\n update<T extends K>(table: T, key: KeyOf<S, T>, changes: Partial<RecordOf<S, T>>, ttl?: number): Promise<RecordOf<S, T> | undefined>;\n upsert<T extends K>(table: T, key: KeyOf<S, T>, fn: (existing: RecordOf<S, T> | undefined) => RecordOf<S, T>, ttl?: number): Promise<RecordOf<S, T>>;\n}\n```\n\n`TransactionContext` has the same CRUD, query, and TTL methods as `VaultStore`, narrowed to the tables declared in `batch()`. Import it from `@vielzeug/vault/indexeddb` or `@vielzeug/vault/sqlite`.\n\n```ts\n// Adapter-specific type aliases — both resolve to TransactionalVaultStore.\ntype SQLiteVaultStore<S extends AnySchema> = TransactionalVaultStore<S>;\ntype IndexedDbVaultStore<S extends AnySchema> = TransactionalVaultStore<S>;\n```\n\n`SQLiteVaultStore` is exported from `@vielzeug/vault/sqlite`. `IndexedDbVaultStore` is exported from `@vielzeug/vault/indexeddb`.\n\n## Errors\n\n| Error | Trigger |\n| --- | --- |\n| `VaultError` | Any Vault-originated validation, serialization, storage, or query error |\n| `VaultDisposedError` | An operation after the store or observer hub is disposed |\n| `VaultScopeError` | A `batch()` callback accesses a table outside its declared scope |\n| `VaultQuotaError` | A LocalStorage or SessionStorage write exceeds the browser quota |\n| `VaultMigrationError` | An IndexedDB migration callback throws |\n\nEvery listed error extends `VaultError`.\n",
6
+ "usage": "---\ntitle: Vault — Usage Guide\ndescription: Persist typed browser or SQLite data, observe table snapshots, and use atomic transactions.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a portable store with one schema and write a typed row.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createLocalStorage } from '@vielzeug/vault/local-storage';\n\ninterface Preference {\n id: string;\n theme: 'dark' | 'light';\n}\n\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n\nawait store.put('preferences', { id: 'theme', theme: 'dark' });\nconsole.log(await store.get('preferences', 'theme'));\n```\n\n## Create a Portable Store\n\nMemory, LocalStorage, and SessionStorage return `VaultStore`. They share portable string/number keys, CRUD methods, queries, TTL, and `observe()`. Vault keeps record values and expiry metadata separate; the physical storage layout is adapter-specific.\n\nThe root entry is adapter-free. Import `createMemory` from `@vielzeug/vault/memory`, `createLocalStorage` from `@vielzeug/vault/local-storage`, or `createSessionStorage` from `@vielzeug/vault/session-storage`. Import each adapter from its focused subpath so unused backends stay out of the bundle.\n\nUse a new storage name when upgrading from Vault 1. Old key and envelope formats are not read by Vault 2.\n\n```ts\nconst store = createLocalStorage({\n name: 'app-v2',\n schema: { preferences: table<Preference>('id') },\n});\n```\n\n## Read and Change Records\n\nUse `update()` for an existing row and `upsert()` when the row may not exist.\n\n```ts\nconst updated = await store.update('preferences', 'theme', { theme: 'light' });\n\nawait store.upsert('preferences', 'locale', (current) => ({\n id: 'locale',\n theme: current?.theme ?? 'dark',\n}));\n\nconsole.log(updated);\n```\n\n`update()` returns `undefined` for a missing key. `upsert()` always writes the record returned by its callback.\n\n## Query Records\n\nBuild a query from a table, then finish it with a terminal method. `count()` ignores pagination, which makes it suitable for page controls.\n\n```ts\nconst query = store.query('preferences').filter((p) => p.id.startsWith('theme'));\nconst preferences = await query.orderBy('id').limit(10).toArray();\nconst total = await query.count();\n\nconsole.log({ preferences, total });\n```\n\nQueries scan the table in memory. Use `equals()` for exact field matches and `filter()` for custom predicates. For large tables, prefer `iterate()` on IndexedDB or SQLite instead of materializing every record.\n\n## Use TTL and Pruning\n\nUse `ttl.*` helpers for expiring rows. Call `pruneExpired()` to reclaim storage from stale rows that accumulate without reads.\n\n```ts\nimport { ttl } from '@vielzeug/vault';\n\nawait store.put('preferences', { id: 'temporary', theme: 'dark' }, ttl.hours(1));\n\n// Reclaim expired rows on a schedule owned by the application.\nconst pruneInterval = setInterval(() => store.pruneExpired(), ttl.hours(6));\nstore.disposalSignal.addEventListener('abort', () => clearInterval(pruneInterval));\n```\n\n## Observe a Table\n\nUse `observe()` for current and future snapshots. Tie subscription lifetime to an `AbortSignal` when a component or request owns it.\n\n```ts\nconst controller = new AbortController();\n\nstore.observe('preferences', (preferences) => {\n console.log(preferences);\n}, { signal: controller.signal });\n\ncontroller.abort();\n```\n\n## Use IndexedDB for Browser Transactions\n\nChoose IndexedDB when browser storage needs multiple writes to commit together or cursor iteration.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb';\n\nconst db = createIndexedDB({\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait db.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nOnly await `tx.*` operations inside a batch callback. Do not await timers, fetches, or other external asynchronous work; IndexedDB can commit an inactive transaction.\n\n## Use SQLite Outside the Browser\n\nImport SQLite from the opt-in subpath so the browser root stays free of runtime drivers. Vault never opens a connection or configures its SQLite process behavior for you.\n\n```ts\nimport { DatabaseSync } from 'node:sqlite';\n\nimport { table } from '@vielzeug/vault';\nimport { createSQLite } from '@vielzeug/vault/sqlite';\n\nconst database = new DatabaseSync('app.db', { timeout: 5_000 });\nconst store = createSQLite({\n database,\n name: 'app-v2',\n schema: { events: table<{ id: number; type: string }>('id') },\n});\n\nawait store.batch(['events'], async (tx) => {\n await tx.put('events', { id: 1, type: 'opened' });\n await tx.put('events', { id: 2, type: 'saved' });\n});\n```\n\nNode's `node:sqlite` API is experimental. Bun's `bun:sqlite` `Database` satisfies the same positional `exec()` and `prepare()` contract; configure WAL from your application when the deployment needs it. Deno does not include SQLite, but `jsr:@db/sqlite`'s `Database` satisfies the same contract when its FFI, filesystem, and environment permissions are granted.\n\nSQLite stores serialize all access through the injected connection. `batch()` starts `BEGIN IMMEDIATE` and rolls back callback failures. While its callback runs, calls on any store sharing that connection reject rather than waiting behind the transaction; use `tx.*` instead. The underlying drivers are synchronous, so move large scans and writes to a worker or isolate when event-loop latency matters.\n\n## Store SQLite Values and Observe Changes\n\nSQLite accepts JSON-compatible plain-object records only. Circular values, `bigint`, dates, class instances, functions, and non-finite numbers are rejected before writing. Number and string primary keys remain distinct.\n\n`observe()` sees mutations written through Vault stores sharing the same injected connection after a commit. It cannot detect direct SQL changes, writes from another process, or writes through another connection. The connection belongs to the caller by default; use `closeOnDispose: true` only when the store owns it.\n\n## Handle IndexedDB Schema Migrations\n\nDeclare IndexedDB indexes in the schema. Use `migrate` only for IndexedDB version upgrades and mirror Vault’s fixed `value.<field>` index path.\n\n```ts\nimport { table } from '@vielzeug/vault';\nimport { createIndexedDB, type MigrationFn } from '@vielzeug/vault/indexeddb';\n\nconst schema = { users: table<{ id: number; name: string }>('id', { indexes: ['name'] }) };\nconst migrate: MigrationFn = ({ db, oldVersion, tx }) => {\n if (oldVersion < 2 && db.objectStoreNames.contains('users')) {\n tx.objectStore('users').createIndex('name', 'value.name');\n }\n};\n\ncreateIndexedDB({ name: 'app-v2', migrate, schema, version: 2 });\n```\n\n## Framework Integration\n\n::: code-group\n\n```ts [React]\nimport { useEffect, useState } from 'react';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const [rows, setRows] = useState<RecordOf<S, K>[]>([]);\n\n useEffect(() => store.observe(table, setRows), [store, table]);\n return rows;\n}\n```\n\n```ts [Vue 3]\nimport { onUnmounted, shallowRef } from 'vue';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function useTable<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n const rows = shallowRef<RecordOf<S, K>[]>([]);\n const stop = store.observe(table, (next) => (rows.value = next));\n\n onUnmounted(stop);\n return rows;\n}\n```\n\n```ts [Svelte]\nimport { readable } from 'svelte/store';\n\nimport type { AnySchema, RecordOf, VaultStore } from '@vielzeug/vault';\n\nexport function tableStore<S extends AnySchema, K extends keyof S & string>(store: VaultStore<S>, table: K) {\n return readable<RecordOf<S, K>[]>([], (set) => store.observe(table, set));\n}\n```\n\n:::\n\n## Working with Other Vielzeug Libraries\n\nUse Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple signals as application state and persist selected changes through Vault writes.\n\n## Best Practices\n\n- Define one schema per storage namespace.\n- Use string or finite-number primary keys only.\n- Choose a new namespace for Vault 1 storage unless you migrate it yourself.\n- Use `observe()` for table snapshots.\n- Use IndexedDB or SQLite for atomic work.\n- Keep external asynchronous work outside `batch()` callbacks.\n- Use `ttl.*` instead of raw durations.\n- Keep SQLite scans and writes off latency-sensitive event loops.\n- Dispose stores when their owner ends.\n",
7
7
  "examples": "---\ntitle: Vault — Examples\ndescription: Portable storage, observation, transactions, iteration, and SQLite.\n---\n\n- [CRUD](./examples/crud.md)\n- [TTL](./examples/ttl.md)\n- [Querying](./examples/querying.md)\n- [Reactive observation](./examples/reactive.md)\n- [IndexedDB iteration](./examples/iterate.md)\n- [IndexedDB batch transactions](./examples/batch.md)\n- [SQLite transactions and iteration](./examples/sqlite.md)\n- [Plugin validation](./examples/plugins.md)\n"
8
8
  },
9
9
  "examples": [
@@ -19,8 +19,8 @@
19
19
  },
20
20
  {
21
21
  "id": "cache-first",
22
- "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst db = createLocalStorage({ name: 'cache-demo', schema: { cache: table('id') } })\n\nasync function getOrComputeConfig() {\n return db.getOrDefault('cache', 'config', () => ({\n id: 'config',\n data: 'computed value',\n fetchedAt: Date.now(),\n }), ttl.minutes(5))\n}\n\nconst first = await getOrComputeConfig()\nconst second = await getOrComputeConfig()\nconsole.log('Same cached record:', first.fetchedAt === second.fetchedAt)",
23
- "name": "Cache-First with getOrDefault"
22
+ "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst db = createLocalStorage({ name: 'cache-demo', schema: { cache: table('id') } })\n\nasync function getOrComputeConfig() {\n const existing = await db.get('cache', 'config')\n if (existing) return existing\n\n const record = {\n id: 'config',\n data: 'computed value',\n fetchedAt: Date.now(),\n }\n await db.put('cache', record, ttl.minutes(5))\n return record\n}\n\nconst first = await getOrComputeConfig()\nconst second = await getOrComputeConfig()\nconsole.log('Same cached record:', first.fetchedAt === second.fetchedAt)",
23
+ "name": "Cache-First with get + put"
24
24
  },
25
25
  {
26
26
  "id": "crud-operations",
@@ -29,17 +29,17 @@
29
29
  },
30
30
  {
31
31
  "id": "indexed-db",
32
- "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb'\n\nconst schema = {\n logs: table('id'),\n}\n\n// createIndexedDB returns IndexedDbVaultStore with transactions and cursor iteration\nconst db = createIndexedDB({\n name: 'app-logs',\n schema,\n version: 1,\n})\n\nawait db.putAll('logs', [\n { id: 1, level: 'info', message: 'App started', ts: Date.now() - 3000 },\n { id: 2, level: 'warn', message: 'Slow query detected', ts: Date.now() - 2000 },\n { id: 3, level: 'error', message: 'Request failed', ts: Date.now() - 1000 },\n { id: 4, level: 'info', message: 'Request succeeded', ts: Date.now() },\n], ttl.hours(1))\n\n// batch() is atomic on IndexedDB — all writes commit or none do\nawait db.batch(['logs'], async (tx) => {\n await tx.put('logs', { id: 5, level: 'info', message: 'Batch committed', ts: Date.now() })\n await tx.deleteMany('logs', [1, 2]) // remove old entries in the same transaction\n})\n\n// iterate() — cursor-based streaming, only on IndexedDbVaultStore\n// the full table is never loaded into memory at once\nconst messages = []\nfor await (const entry of db.iterate('logs')) {\n messages.push(entry.message)\n}\nconsole.log('Streamed via iterate():', messages)\n\nconst errors = await db.query('logs').equals('level', 'error').toArray()\nconsole.log('Errors:', errors.map((e) => e.message))\nconsole.log('Total logs:', await db.query('logs').count())\n\nconst info = await db.debug()\nfor (const t of info.tables) {\n console.log(t.name + ':', t.recordCount, 'live,', t.expiredCount, 'expired')\n}\n\nawait db.dispose()",
32
+ "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createIndexedDB } from '@vielzeug/vault/indexeddb'\n\nconst schema = {\n logs: table('id'),\n}\n\n// createIndexedDB returns TransactionalVaultStore with transactions and cursor iteration\nconst db = createIndexedDB({\n name: 'app-logs',\n schema,\n version: 1,\n})\n\nawait db.putAll('logs', [\n { id: 1, level: 'info', message: 'App started', ts: Date.now() - 3000 },\n { id: 2, level: 'warn', message: 'Slow query detected', ts: Date.now() - 2000 },\n { id: 3, level: 'error', message: 'Request failed', ts: Date.now() - 1000 },\n { id: 4, level: 'info', message: 'Request succeeded', ts: Date.now() },\n], ttl.hours(1))\n\n// batch() is atomic on IndexedDB — all writes commit or none do\nawait db.batch(['logs'], async (tx) => {\n await tx.put('logs', { id: 5, level: 'info', message: 'Batch committed', ts: Date.now() })\n await tx.deleteMany('logs', [1, 2]) // remove old entries in the same transaction\n})\n\n// iterate() — cursor-based streaming, only on TransactionalVaultStore\n// the full table is never loaded into memory at once\nconst messages = []\nfor await (const entry of db.iterate('logs')) {\n messages.push(entry.message)\n}\nconsole.log('Streamed via iterate():', messages)\n\nconst errors = await db.query('logs').equals('level', 'error').toArray()\nconsole.log('Errors:', errors.map((e) => e.message))\nconsole.log('Total logs:', await db.query('logs').count())\n\n// pruneExpired() reclaims storage from TTL-expired records that haven't been read\nconst pruned = await db.pruneExpired()\nconsole.log('Pruned:', pruned)\n\nawait db.dispose()",
33
33
  "name": "IndexedDB — Atomic Batch & iterate()"
34
34
  },
35
35
  {
36
36
  "id": "prune-schedule",
37
- "code": "import { scheduleExpiredPrune, table, ttl } from '@vielzeug/vault'\nimport { createMemory } from '@vielzeug/vault/memory'\n\n// scheduleExpiredPrune runs pruneExpired() on an interval.\n// Pass disposalSignal to auto-cancel when the store is torn down.\n\nconst schema = { sessions: table('token') }\nconst db = createMemory({ schema })\n\nconst stop = scheduleExpiredPrune(db, {\n interval: ttl.minutes(15),\n signal: db.disposalSignal,\n onError: (err) => console.error('[vault] prune failed:', err),\n})\n\n// Write a session that expires in 1 ms\nawait db.put('sessions', { token: 'abc', user: 1 }, ttl.ms(1))\nawait db.put('sessions', { token: 'def', user: 2 }) // no TTL — permanent\n\nconsole.log('before prune:', await db.count('sessions')) // 2 (lazy eviction: both exist physically)\n\n// Manual prune to demonstrate the API\nawait new Promise((resolve) => setTimeout(resolve, 5))\nconst pruned = await db.pruneExpired()\nconsole.log('pruned:', pruned.sessions) // 1 (the expired session)\nconsole.log('after prune:', await db.count('sessions')) // 1\n\n// stop() before dispose, or rely on disposalSignal auto-cancel\nstop()\nawait db.dispose()",
38
- "name": "TTL — scheduleExpiredPrune with disposalSignal"
37
+ "code": "import { table, ttl } from '@vielzeug/vault'\nimport { createMemory } from '@vielzeug/vault/memory'\n\n// pruneExpired() sweeps all tables and removes expired records.\n// Schedule it with setInterval and cancel on disposalSignal.\n\nconst schema = { sessions: table('token') }\nconst db = createMemory({ schema })\n\nconst pruneInterval = setInterval(() => db.pruneExpired(), ttl.minutes(15))\ndb.disposalSignal.addEventListener('abort', () => clearInterval(pruneInterval))\n\n// Write a session that expires in 1 ms\nawait db.put('sessions', { token: 'abc', user: 1 }, ttl.ms(1))\nawait db.put('sessions', { token: 'def', user: 2 }) // no TTL — permanent\n\nconsole.log('before prune:', await db.count('sessions')) // 2 (lazy eviction: both exist physically)\n\n// Manual prune to demonstrate the API\nawait new Promise((resolve) => setTimeout(resolve, 5))\nconst pruned = await db.pruneExpired()\nconsole.log('pruned:', pruned.sessions) // 1 (the expired session)\nconsole.log('after prune:', await db.count('sessions')) // 1\n\nawait db.dispose()",
38
+ "name": "TTL — pruneExpired with disposalSignal"
39
39
  },
40
40
  {
41
41
  "id": "query-builder",
42
- "code": "import { table } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst schema = {\n products: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'shop', schema })\n\nawait db.putAll('products', [\n { id: 1, name: 'Laptop', price: 999, category: 'electronics', inStock: true },\n { id: 2, name: 'Mouse', price: 29, category: 'electronics', inStock: true },\n { id: 3, name: 'Desk', price: 299, category: 'furniture', inStock: false },\n { id: 4, name: 'Chair', price: 199, category: 'furniture', inStock: true },\n { id: 5, name: 'Monitor', price: 399, category: 'electronics', inStock: true },\n])\n\nconst pageSize = 2\nconst pageIndex = 0\n\n// Build a base query — reuse it for both the page slice and the total count\nconst q = db\n .query('products')\n .equals('category', 'electronics')\n .filter((p) => p.inStock)\n .orderBy('price', 'asc')\n\n// count() ignores limit/offset/orderBy — returns the full filtered set size\nconst page = await q.limit(pageSize).offset(pageIndex * pageSize).toArray()\nconst total = await q.count()\n\nconsole.log('Page:', page.map((p) => p.name))\nconsole.log('Total matching:', total)\nconsole.log('Page 1 of', Math.ceil(total / pageSize))\n\n// startsWith with case-insensitive flag\nconst mice = await db.query('products').startsWith('name', 'm', { ignoreCase: true }).toArray()\nconsole.log('Starts with m:', mice.map((p) => p.name))\n\n// predicate delete\nconst removed = await db.query('products').filter((p) => !p.inStock).delete()\nconsole.log('Removed out-of-stock:', removed)\n\n// first()\nconst cheapest = await db.query('products').orderBy('price', 'asc').first()\nconsole.log('Cheapest:', cheapest?.name, cheapest?.price)",
42
+ "code": "import { table } from '@vielzeug/vault'\nimport { createLocalStorage } from '@vielzeug/vault/local-storage'\n\nconst schema = {\n products: table('id'),\n}\n\nconst db = createLocalStorage({ name: 'shop', schema })\n\nawait db.putAll('products', [\n { id: 1, name: 'Laptop', price: 999, category: 'electronics', inStock: true },\n { id: 2, name: 'Mouse', price: 29, category: 'electronics', inStock: true },\n { id: 3, name: 'Desk', price: 299, category: 'furniture', inStock: false },\n { id: 4, name: 'Chair', price: 199, category: 'furniture', inStock: true },\n { id: 5, name: 'Monitor', price: 399, category: 'electronics', inStock: true },\n])\n\nconst pageSize = 2\nconst pageIndex = 0\n\n// Build a base query — reuse it for both the page slice and the total count\nconst q = db\n .query('products')\n .equals('category', 'electronics')\n .filter((p) => p.inStock)\n .orderBy('price', 'asc')\n\n// count() ignores limit/offset/orderBy — returns the full filtered set size\nconst page = await q.limit(pageSize).offset(pageIndex * pageSize).toArray()\nconst total = await q.count()\n\nconsole.log('Page:', page.map((p) => p.name))\nconsole.log('Total matching:', total)\nconsole.log('Page 1 of', Math.ceil(total / pageSize))\n\n// prefix match via filter()\nconst mice = await db\n .query('products')\n .filter((p) => p.name.toLowerCase().startsWith('m'))\n .toArray()\nconsole.log('Starts with m:', mice.map((p) => p.name))\n\n// predicate delete\nconst removed = await db.query('products').filter((p) => !p.inStock).delete()\nconsole.log('Removed out-of-stock:', removed)\n\n// first()\nconst cheapest = await db.query('products').orderBy('price', 'asc').first()\nconsole.log('Cheapest:', cheapest?.name, cheapest?.price)",
43
43
  "name": "Query Builder — Filters, Pagination, count"
44
44
  },
45
45
  {
@@ -59,27 +59,21 @@
59
59
  "VaultMigrationError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
60
60
  "VaultQuotaError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
61
61
  "VaultScopeError": "export { VaultDisposedError, VaultError, VaultMigrationError, VaultQuotaError, VaultScopeError } from './errors';",
62
- "scheduleExpiredPrune": "export { scheduleExpiredPrune } from './prune';",
63
62
  "QueryBuilder": "export type { QueryBuilder } from './query';",
64
63
  "isExpired": "export { isExpired, ttl } from './ttl';",
65
64
  "ttl": "export { isExpired, ttl } from './ttl';",
66
- "AnySchema": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
67
- "BaseAdapterOptions": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
68
- "DebugInfo": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
69
- "DebugStats": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
70
- "IterableVaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
71
- "KeyOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
72
- "MetricsEvent": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
73
- "Observer": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
74
- "RecordOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
75
- "RecordValidator": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
76
- "SchemaEntry": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
77
- "TableValidators": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
78
- "TransactionalVaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
79
- "Unsubscribe": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
80
- "VaultKey": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
81
- "VaultLogger": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
82
- "VaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n DebugInfo,\n DebugStats,\n IterableVaultStore,\n KeyOf,\n MetricsEvent,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultLogger,\n VaultStore,\n} from './types';",
65
+ "AnySchema": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
66
+ "BaseAdapterOptions": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
67
+ "KeyOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
68
+ "Observer": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
69
+ "RecordOf": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
70
+ "RecordValidator": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
71
+ "SchemaEntry": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
72
+ "TableValidators": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
73
+ "TransactionalVaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
74
+ "Unsubscribe": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
75
+ "VaultKey": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
76
+ "VaultStore": "export type {\n AnySchema,\n BaseAdapterOptions,\n KeyOf,\n Observer,\n RecordOf,\n RecordValidator,\n SchemaEntry,\n TableValidators,\n TransactionalVaultStore,\n Unsubscribe,\n VaultKey,\n VaultStore,\n} from './types';",
83
77
  "table": "export { table } from './types';"
84
78
  }
85
79
  }