/** * Typed storage abstraction for `bot/*` plugins. * * Replaces the per-plugin `botSubKey + storage.get/set` boilerplate * with three small primitives that automatically: * * - **Namespace by bot id.** Every key gets `bot-:` prefixed via * `ctx.bot.info.id`, so multiple bots sharing one Redis stay * isolated by construction (see `bot/CLAUDE.md` § Multi-bot * isolation). No plugin has to remember to wrap. * * - **Validate on read** (optional). Pass any `{ parse(data: * unknown): T }` validator (zod / valibot / arktype / yours) and a * mis-shaped record from storage throws a clear `SourcedError` * instead of NaN-ing downstream. Omit the validator and you get * the same untyped read as before. * * - **Stay typed end-to-end.** `botRecord(...)` returns * a `BotRecord` and `.get()` is `Promise`. No casts at the call site. * * ## Three primitives * * - `botRecord(storage, prefix, validator?)` — get/set/delete a * single typed value keyed by an id (`pay:charge:`, * `ac:user:`, …). Most plugin state. * * - `botIndex(storage, prefix, opts?)` — a capped, prepend-friendly * `string[]` stored under a single key. Use for "recent N * charges", "pending access requests", etc. * * - `botSentinel(storage, prefix)` — a presence flag (`"1"` / absent) * for idempotency. `claim(ctx, id)` returns `true` only on first * call; subsequent calls return `false` without setting. Note: * read-then-write is NOT atomic under concurrent load; for * production we'd want a storage backend with `setNX` semantics. * * ## Why not couple to zod? * * The validator parameter is typed as `{ parse(data: unknown): T }` — * a structural shape that zod schemas, valibot, ArkType, and hand-rolled * type guards all satisfy. The library has zero zod imports; consumers * bring whichever validator they prefer. */ import type { Storage } from "@gramio/storage"; import type { BotIdCtx } from "./ctx.js"; /** * Anything with a synchronous `parse(unknown): T`. zod schemas satisfy * this natively (`.parse` throws on invalid input). Custom guards work * the same way: `{ parse: (d) => { if (!ok(d)) throw new Error(...); return d as T } }`. */ export type RecordValidator = { parse: (data: unknown) => T; }; export type BotRecord = { /** Storage key for `id` under this record's prefix. */ keyFor: (ctx: BotIdCtx, id: string) => string; /** `undefined` on miss; throws `SourcedError` on validator failure. */ get: (ctx: BotIdCtx, id: string) => Promise; /** Overwrites unconditionally. Use `botSentinel` if you need set-if-absent. */ set: (ctx: BotIdCtx, id: string, value: T) => Promise; /** Hard delete. No-op if absent. */ delete: (ctx: BotIdCtx, id: string) => Promise; /** `true` if a record exists at `id` (no read, no validation). */ has: (ctx: BotIdCtx, id: string) => Promise; }; /** * Typed record store, auto-namespaced by bot id. * * @example per-charge record with zod validation * * const charges = botRecord( * storage, * 'pay:charge', * ChargeRecordSchema, // any zod.ZodType * ) * await charges.set(ctx, chargeId, charge) * const c = await charges.get(ctx, chargeId) * * @example without a validator * * const charges = botRecord(storage, 'pay:charge') * const c = await charges.get(ctx, chargeId) // typed but unchecked */ export declare const botRecord: (storage: Storage, prefix: string, validator?: RecordValidator) => BotRecord; export type BotIndexOptions = { /** * Maximum number of ids retained. New `prepend` calls past this * limit drop the OLDEST id. Omit for an unbounded index (a * footgun for high-volume bots — set a cap). */ capacity?: number; }; export type BotIndex = { keyFor: (ctx: BotIdCtx) => string; /** Newest-first list of ids, capped to `capacity` if configured. */ list: (ctx: BotIdCtx) => Promise; /** Insert `id` at the front. Idempotent: re-prepending an existing * id moves it to the front. Returns the new length post-cap. */ prepend: (ctx: BotIdCtx, id: string) => Promise; /** Remove `id`. No-op if absent. Returns whether anything was removed. */ remove: (ctx: BotIdCtx, id: string) => Promise; /** Wipe the whole index. */ clear: (ctx: BotIdCtx) => Promise; }; /** * Newest-first capped list of ids stored under a single key. Designed * for "user's last N charges", "pending access requests", "recent * payouts" — small, ordered, bounded. * * `prepend(id)` is the only mutator. Re-prepending an existing id moves * it to the front (de-duplication). At-cap inserts drop the tail. */ export declare const botIndex: (storage: Storage, prefix: string, opts?: BotIndexOptions) => BotIndex; export type BotSentinel = { keyFor: (ctx: BotIdCtx, id: string) => string; /** * Attempt to claim `id`. Returns `true` on first claim (and * persists), `false` if already claimed (no write). * * NOT atomic under concurrent load — the read and write happen as * two ops. For at-most-once semantics under contention, swap the * storage backend for one with `setNX` (e.g. `@gramio/storage-redis` * with a `setIfNotExists` helper) and override this method. For * single-process polling bots (the v1 target), the race window is * bounded by Telegram's retry delay and acceptable. */ claim: (ctx: BotIdCtx, id: string) => Promise; /** `true` if `id` has been claimed. No write. */ check: (ctx: BotIdCtx, id: string) => Promise; /** Release a claim. Returns whether anything was released. */ release: (ctx: BotIdCtx, id: string) => Promise; }; /** * Presence-flag store for idempotency. Each id is either claimed * (storage has the key) or not (storage doesn't). The stored value is * an opaque sentinel string — readers only care about presence. * * @example idempotent successful_payment fulfillment * * const idem = botSentinel(storage, 'pay:idem') * if (!(await idem.claim(ctx, chargeId))) return // duplicate, no-op * await fulfill(charge) */ export declare const botSentinel: (storage: Storage, prefix: string) => BotSentinel; /** * Wrap any `BotRecord` with trace-level logging for every read/write. * Useful when debugging a specific plugin's storage flow without * polluting every other plugin's logs. Defaults off. * * const charges = withTracing(botRecord(...), 'pay:charge') */ export declare const withTracing: (record: BotRecord, label: string) => BotRecord; //# sourceMappingURL=storage.d.ts.map