import { type JSONValue } from '../core/json.ts'; import { type DeleteRangeOptions } from './delete-range.ts'; import { type ScanOptions, type Storage } from './interface.ts'; /** * Encode/decode pair that bridges a typed domain value to and from `Uint8Array`. * * Implement this interface to plug a custom serialization format into * {@link withCodec}. Two built-in factories — {@link jsonCodec} and * {@link msgpackCodec} — cover the most common cases. * * @example * ```ts * import { type StorageCodec, withCodec, MemoryStorage } from '@lostgradient/weft'; * * const encoder = new TextEncoder(); * const decoder = new TextDecoder(); * * const csvCodec: StorageCodec = { * encode: (row) => encoder.encode(row.join(',')), * decode: (bytes) => decoder.decode(bytes).split(','), * }; * * await using raw = new MemoryStorage(); * const store = withCodec(raw, csvCodec); * await store.put('row:1', ['alice', '30', 'eng']); * const row = await store.get('row:1'); * console.log(row?.join('|')); * ``` */ export interface StorageCodec { encode(value: Value): Uint8Array; decode(bytes: Uint8Array): Value; } /** * Validator or narrowing function passed as an optional argument to * {@link jsonCodec} and {@link msgpackCodec}. * * Receives the raw decoded value (`unknown`) and must return the strongly-typed * `Value` — either by assertion after runtime validation or by throwing when the * shape is unexpected. Omitting it leaves the codec untyped (`JSONValue` / * `MessagePackValue`). */ export type StorageValueParser = (value: unknown) => Value; export type MessagePackPrimitive = bigint | boolean | null | number | string | undefined; /** * Recursive union of every value that MessagePack can encode and decode. * * A superset of {@link JSONValue} that additionally supports `Date`, `Map`, * `Set`, `Uint8Array`, `RegExp`, `Error`, `ArrayBuffer`, and `bigint`. Its * array branch is `ReadonlyArray` (matching `JSONValue`), so every `JSONValue` * — including `readonly` arrays and `as const` tuples — is assignable here and * accepted by `msgpackCodec`. The encoder only * reads its input, so a readonly bound is sound. Prefer this codec when your * domain objects contain binary data or richly-typed primitives that JSON * cannot represent without custom serialisation. */ export type MessagePackValue = ArrayBuffer | Date | Error | Map | MessagePackPrimitive | ReadonlyArray | RegExp | Set | Uint8Array | { [key: string]: MessagePackValue; }; /** * Typed version of `BatchOperation` — a put or delete applied as part of an * atomic batch in a {@link TypedStorage} instance. * * Build an array of these and pass it to `TypedStorage.batch()` to apply * multiple mutations in a single round-trip without encoding each value * individually at the call site. * * @example * ```ts * import { MemoryStorage, withCodec, jsonCodec, type TypedBatchOperation } from '@lostgradient/weft'; * * await using raw = new MemoryStorage(); * const store = withCodec(raw, jsonCodec()); * * const ops: TypedBatchOperation<{ count: number }>[] = [ * { type: 'put', key: 'a', value: { count: 1 } }, * { type: 'put', key: 'b', value: { count: 2 } }, * { type: 'delete', key: 'old' }, * ]; * await store.batch(ops); * ``` */ export type TypedBatchOperation = { type: 'put'; key: string; value: Value; } | { type: 'delete'; key: string; }; /** * Typed compare-and-swap precondition used by * {@link ConditionalTypedStorage.conditionalBatch}. * * @example * ```ts * import { type TypedConditionalBatchCondition } from '@lostgradient/weft/storage'; * * type SessionMetadata = { lastUsedAt: string }; * const condition: TypedConditionalBatchCondition = { * key: 'session:1', * expectedValue: { lastUsedAt: '2026-06-01T00:00:00.000Z' }, * }; * console.log(condition.key); // 'session:1' * ``` */ export type TypedConditionalBatchCondition = { /** Key whose current decoded value must match `expectedValue`. */ key: string; /** Required current value, or `null` to require the key to be absent. */ expectedValue: Value | null; }; /** * Options for {@link withCodec}. * * @example * ```ts * import { type CodecStorageOptions } from '@lostgradient/weft/storage'; * * const options: CodecStorageOptions = { * disposeUnderlyingStorage: false, * }; * console.log(options.disposeUnderlyingStorage); // false * ``` */ export type CodecStorageOptions = { /** * Whether disposing the typed wrapper also disposes the wrapped storage. * Defaults to `true` to preserve the owning-wrapper behavior. Set `false` * when several typed views share the same storage instance. */ disposeUnderlyingStorage?: boolean; }; /** * Disposable typed key-value store interface over a raw {@link Storage}. * * Mirrors the `Storage` interface but operates on `Value` instead of * `Uint8Array` — encoding and decoding is handled transparently by the * underlying codec. Obtain a `TypedStorage` via {@link withCodec}, * {@link jsonCodec}, or {@link msgpackCodec} rather than implementing it * directly. Note: `TypedStorage` intentionally does not surface * `Storage.query` or `Storage.scoped` — drop down to the underlying raw * storage to use those operations. * * @example * ```ts * import { MemoryStorage, withCodec, jsonCodec, type TypedStorage } from '@lostgradient/weft'; * * type User = { name: string; age: number }; * * await using raw = new MemoryStorage(); * const users: TypedStorage = withCodec( * raw, * jsonCodec((v) => v as User), * ); * * await users.put('user:1', { name: 'Alice', age: 30 }); * const alice = await users.get('user:1'); * console.log(alice?.name); // 'Alice' * ``` */ export interface TypedStorage extends Disposable { get(key: string): Promise; put(key: string, value: Value): Promise; delete(key: string): Promise; scan(prefix: string, options?: ScanOptions): AsyncIterable<[string, Value]>; batch(operations: TypedBatchOperation[]): Promise; has(key: string): Promise; deletePrefix(prefix: string): Promise; deleteRange?(prefix: string, options: DeleteRangeOptions): Promise; keys(prefix: string, options?: ScanOptions): AsyncIterable; count(prefix: string): Promise; } /** * Typed storage returned by {@link withCodec}. It extends the base * {@link TypedStorage} shape with compare-and-swap support without requiring * every external `TypedStorage` implementation to define the method. */ export interface ConditionalTypedStorage extends TypedStorage { conditionalBatch(conditions: TypedConditionalBatchCondition[], operations: TypedBatchOperation[]): Promise; } /** * Wraps a raw {@link Storage} with a {@link StorageCodec} to produce a * {@link TypedStorage} that encodes and decodes values automatically. * * Use the built-in {@link jsonCodec} or {@link msgpackCodec} factories as the * `codec` argument, or supply a custom implementation. The returned store * disposes the underlying storage when its own `[Symbol.dispose]` is called * unless `disposeUnderlyingStorage: false` is provided. * * @example * ```ts * import { MemoryStorage, withCodec, jsonCodec } from '@lostgradient/weft'; * * type Config = { retries: number; timeout: number }; * * await using raw = new MemoryStorage(); * const configStore = withCodec(raw, jsonCodec((v) => v as Config)); * * await configStore.put('cfg:default', { retries: 3, timeout: 5000 }); * const cfg = await configStore.get('cfg:default'); * console.log(cfg?.retries); // 3 * ``` */ export declare function withCodec(storage: Storage, codec: StorageCodec, options?: CodecStorageOptions): ConditionalTypedStorage; /** * Creates a {@link StorageCodec} that serialises values as UTF-8 JSON. * * Call without arguments to get a `StorageCodec`. Pass an optional * {@link StorageValueParser} to narrow the output to a concrete type — useful * when you have a Zod schema or manual shape check. * * @example * ```ts * import { MemoryStorage, withCodec, jsonCodec } from '@lostgradient/weft'; * * type Point = { x: number; y: number }; * const isPoint = (v: unknown): v is Point => * typeof v === 'object' && v !== null && 'x' in v && 'y' in v; * * await using raw = new MemoryStorage(); * const points = withCodec(raw, jsonCodec((v) => { * if (!isPoint(v)) throw new TypeError('not a Point'); * return v; * })); * * await points.put('p:1', { x: 10, y: 20 }); * const p = await points.get('p:1'); * console.log(p?.x); // 10 * ``` */ export declare function jsonCodec(): StorageCodec; export declare function jsonCodec(parse: StorageValueParser): StorageCodec; /** * Creates a {@link StorageCodec} that serialises values with MessagePack. * * Prefer this over {@link jsonCodec} when your domain objects contain binary * data (`Uint8Array`, `ArrayBuffer`), `Date`, `Map`, or `Set` — types that * JSON cannot round-trip without custom replacers. Pass an optional * {@link StorageValueParser} to narrow the decoded type. * * @example * ```ts * import { MemoryStorage, withCodec, msgpackCodec } from '@lostgradient/weft'; * * type Event = { ts: Date; payload: Uint8Array }; * * await using raw = new MemoryStorage(); * const events = withCodec(raw, msgpackCodec((v) => v as Event)); * * const evt: Event = { ts: new Date(), payload: new Uint8Array([1, 2, 3]) }; * await events.put('evt:1', evt); * const loaded = await events.get('evt:1'); * console.log(loaded?.ts instanceof Date); // true * ``` */ export declare function msgpackCodec(): StorageCodec; export declare function msgpackCodec(parse: StorageValueParser): StorageCodec;