@yaebal/session

per-chat session state with a pluggable storage adapter.

install

terminal
pnpm add @yaebal/session

usage

session() is a plugin — pass it to .install() on a bot or composer. it loads ctx.session from storage before your handlers run and writes it back after next() resolves. the context type is augmented automatically.

bot.ts
import { Bot } from "@yaebal/core";
import { session } from "@yaebal/session";

interface MySession {
  count: number;
  lastCommand: string | null;
}

const bot = new Bot(process.env.BOT_TOKEN!)
  .install(session<MySession>({
    initial: () => ({ count: 0, lastCommand: null }),
  }));

// ctx.session is now fully typed as MySession
bot.on("message:text", async (ctx) => {
  ctx.session.count++;
  ctx.session.lastCommand = ctx.text;
  await ctx.reply(`message #${ctx.session.count}`);
});

bot.start();

custom storage

the default MemoryStorage is lost on restart. swap it for any object that implements StorageAdapter<T>:

redis-storage.ts
import { session, type StorageAdapter } from "@yaebal/session";

// example: a minimal Redis adapter
class RedisStorage<T> implements StorageAdapter<T> {
  constructor(private redis: Redis) {}

  async get(key: string): Promise<T | undefined> {
    const raw = await this.redis.get(key);
    return raw ? JSON.parse(raw) : undefined;
  }

  async set(key: string, value: T): Promise<void> {
    await this.redis.set(key, JSON.stringify(value));
  }

  async delete(key: string): Promise<void> {
    await this.redis.del(key);
  }
}

bot.install(session({
  initial: () => ({ count: 0 }),
  storage: new RedisStorage(redis),
}));

per-user sessions

override getKey to change the partition. defaults to ctx.chat.id.

per-user.ts
bot.install(session({
  initial: () => ({ preferences: {} }),
  // partition by user instead of chat
  getKey: (ctx) => ctx.from?.id?.toString(),
}));

api

exportsignaturedescription
session(options: SessionOptions<S>) => Plugin<Context, { session: S }>creates the session plugin
MemoryStorageclass MemoryStorage<T> implements StorageAdapter<T>default in-memory store; lost on restart
StorageAdapterinterfaceimplement this to persist sessions externally
SessionOptionsinterfaceoptions passed to session()

SessionOptions<S>

fieldtyperequireddescription
initial() => Syesfactory called when no stored session exists for the key
storageStorageAdapter<S>nodefaults to new MemoryStorage<S>()
getKey(ctx: Context) => string | undefinednodefaults to ctx.chat?.id?.toString()

StorageAdapter<T>

methodsignature
get(key: string) => T | undefined | Promise<T | undefined>
set(key: string, value: T) => unknown | Promise<unknown>
delete(key: string) => unknown | Promise<unknown>
writes happen unconditionally after next(). if a handler throws, next() never resolves, so the write is skipped and storage is left untouched — the session is not half-saved on error.

updates without a chat (e.g. poll updates) receive a throwaway session from initial() that is never persisted. getKey returning undefined triggers this path.