Sessions
- Added opt-in session middleware:
createSession<T>(options)(aliasedsession)
attaches a persistent, keyed bag reached withctx.getSession<T>(). Thestore
is required - there is no implicit default, so the durability choice is always
explicit. The returned middleware also carries.get(ctx)/.find(ctx), typed
once at construction. - Added the
SessionStorecontract: a value-agnostic string KV -
read(key),write(key, value, { ttlSeconds }),delete(key), plus optional
touch(key, ttlSeconds),init()andclose(). Encoding happens once in the
middleware's codec, never inside a store, so an in-memory store round-trips
exactly like a durable one. - Added the stores:
MemorySessionStorage(core, edge-safe, optional TTL),
FileSessionStorage(/node, one file per key, atomic temp-file +rename),
and, behind the new./bunsubpath,SqliteSessionStorage,
SqlSessionStorageandRedisSessionStorage. - The persisted value is a versioned envelope,
{ v, data, ext, createdAt, updatedAt }.extis a namespace map that layers built on sessions claim a
slot in; the timestamps are first-seen / last-active, and the two SQL stores
mirror them ascreated_at/updated_atcolumns. - The flush is skipped when the encoded envelope is unchanged, so an untouched
chat costs one read and no write;touch()refreshes a TTL in that case.
ctx.getSession().delete()evicts a key. - Updates for one key are serialized in-process by a per-key lock, so concurrent
webhook invocations cannot interleave read-modify-write. Across processes it is
last-writer-wins.
Reply and callback tracking
- Added
expectReply(ctx, messageId, marker?, { ttlSeconds? }),
matchReply<M>(ctx)andforgetReply(ctx, messageId)- "awaiting a reply to
this message" recorded as persisted data (matched on
reply_to_message.message_id), never a live continuation, so it survives a
restart and works on serverless. - Added the callback-query peers
expectCallback(ctx, messageId, marker?, { ttlSeconds? }),
matchCallback<M>(ctx, { once? })andforgetCallback(ctx, messageId), keyed on
callback_query.message.message_id. Matching a press does not consume the marker
by default (a keyboard usually stays live for several presses);{ once: true }
makes a button fire at most once - it consumes only that message's marker, and
consumption happens before the handler runs. Plaincallback_dataremains the
idiomatic router - these are for a marker over 64 bytes, one the client must not
read, or a one-shot press. - Replies and presses are kept in separate tables, so a quoted reply to a
message that also carries an inline keyboard cannot consume the button's marker. - Expectations never expire on their own (a prompt answered tomorrow is normal);
ttlSecondsbounds one, and expired entries are pruned the next time the layer
touches the session, so a bot that sets a TTL cannot grow its envelope without
limit. There is no cap and no default. - Added
taggedReplies<Tag>(ctx)-expect/match/forget/expectPress/
matchPress/forgetPressfor plain string tags, typing both ends with one union. - Note the default session key is per chat, so in a group any member's reply or
press matches a marker; carry the asker's id in the marker and check it when
that matters.
Bot lifecycle
- Added
bot.init()andbot.close(). A middleware carryinginit/closeis
picked up byuse()and byon/command/hears; setup runs once at
startup (awaited bystartPolling()andhandleUpdate()) and teardown runs in
reverse.run()(/node) closes on shutdown. init()tracks completion per plugin, so a failure retries from the plugin that
failed instead of re-running the ones that succeeded;close()closes only what
initialized, attempts every teardown even if one throws, and reports failures
afterwards (anAggregateErrorwhen several failed).
Full diff: v2.1.0...v2.2.0-rc0