@puregram/media-cacher
heavy files are slow to upload — sendPhoto on a 5 MB image takes real seconds, every single time. @puregram/media-cacher fixes that transparently: the first send uploads as usual and stores the resulting file_id. every later send to the same chat silently swaps the path or url for the cached MediaSource.fileId(...) before the request leaves your bot — telegram never sees the file twice
when to use
whenever you send the same MediaSource.path(...) or MediaSource.url(...) files repeatedly — static assets, fixed thumbnails, sticker packs, announcement images. buffers, streams, and base64 are typically one-off and not cached by default
install
yarn add @puregram/media-cachernpm i -S @puregram/media-cacherpnpm add @puregram/media-cacher@puregram/media-cacher re-exports MemoryStorage and KVStorage from @puregram/storage — install @puregram/storage separately only if you need LruMemoryStorage / TtlStorage or want to share one storage instance across multiple plugins
quick start
import { Telegram, MediaSource } from 'puregram'
import { mediaCacher } from '@puregram/media-cacher'
const telegram = Telegram.fromToken(process.env.TOKEN!)
.extend(mediaCacher())
telegram.onMessage((message) => {
return telegram.api.sendPhoto({
chat_id: message.chat.id,
photo: MediaSource.path('./cat.png')
})
})
await telegram.startPolling()the first send to each chat uploads ./cat.png and caches the file_id. every subsequent send to that chat is instantaneous
core api
mediaCacher(options?)
installs a tg.extend(...) plugin that hooks onBeforeRequest (before send) and onResponseIntercept (after response, to persist the fresh file_id). attaches tg.mediaCacher for manual inspection
what gets cached
| method | media field |
|---|---|
sendPhoto | photo |
sendVideo | video |
sendAnimation | animation |
sendVideoNote | video_note |
sendAudio | audio |
sendDocument | document |
sendSticker | sticker |
only MediaSource.path(...) and MediaSource.url(...) inputs trigger caching. all other methods pass through untouched
tg.mediaCacher — manual inspection
// look up the cached file_id for (storageKey, sourceValue)
const fileId = await telegram.mediaCacher.get('100', './cat.png')
if (fileId !== undefined) {
console.log('already cached:', fileId)
}
// drop a single entry — next send re-uploads
await telegram.mediaCacher.invalidate('100', './cat.png')
// direct access to the underlying KVStorage<string>
await telegram.mediaCacher.storage.set('whatever:key', 'AgADAQA…')storageKey is what getStorageKey(ctx) returned (default: String(ctx.params.chat_id)). sourceValue is the raw string the MediaSource carried — './cat.png' for path, 'https://…' for url
custom storage
by default the plugin uses an in-memory MemoryStorage — fine for development but lost on restart. swap in any KVStorage<string> for persistence:
import { mediaCacher } from '@puregram/media-cacher'
telegram.extend(mediaCacher({
storage: new RedisStorage({ /* … */ })
}))for a bounded in-memory cache (evicts the least-recently-used entry when the cap is hit):
import { LruMemoryStorage } from '@puregram/storage'
telegram.extend(mediaCacher({
storage: new LruMemoryStorage<string>({ max: 5_000 })
}))see storage plugin for the full KVStorage<V> contract and backends
getStorageKey
by default entries are keyed by chat_id, so the same ./cat.png cached for chat A doesn't leak into chat B:
mediaCacher({
// share one global cache across every chat — fine for fully public assets
getStorageKey: () => 'global'
})throws TypeError at request time if no chat_id can be found and you haven't supplied a custom getStorageKey
content-hash key strategy
flip to keyStrategy: 'hash' to key by sha-256(bytes) instead of the raw path/url string. distinct sources that resolve to identical content share one cached file_id:
telegram.extend(mediaCacher({ keyStrategy: 'hash' }))| source | how the hash is computed |
|---|---|
MediaSource.path(...) | fs.readFile, sha-256 of the bytes |
MediaSource.url(...) | fetch(url), sha-256 of the response body |
MediaSource.buffer(...) | sha-256 of the buffer directly |
MediaSource.arrayBuffer(...) | sha-256 of the arraybuffer directly |
MediaSource.stream(...) / MediaSource.file(...) | not supported — use 'sourceValue' |
WARNING
with keyStrategy: 'hash' and a url source, the cache-miss path fetches the url twice: once for the digest, once for the actual upload. for high-frequency senders on url sources, the default 'sourceValue' strategy avoids the extra fetch
for manual inspection with hash mode, pass the hash as the sourceValue argument:
import { hashMediaInput } from '@puregram/media-cacher'
import { MediaSource } from 'puregram'
const hash = await hashMediaInput(MediaSource.path('./cat.png'))
const fileId = await telegram.mediaCacher.get(String(chatId), hash)auto-evict + retry on stale file_id
telegram occasionally rotates file_ids and returns one of these 400 errors:
Bad Request: wrong file identifier/HTTP URL specifiedBad Request: wrong file_idBad Request: file is temporarily unavailable
when the plugin detects one on a request that used a cached id, it:
- evicts the stale entry from storage
- re-issues the request once with the original
MediaSource.path(...)/MediaSource.url(...) - persists the fresh
file_idfrom the retry - returns the retry's result to the caller — as if the first send had worked
if the retry itself fails the original error propagates — no second retry, no infinite loop
concurrent stale ids — if multiple in-flight sends are using the same stale file_id, only one re-upload happens. the first failure becomes the leader; everyone else waits for the fresh id and then dispatches with it
customize which descriptions trigger eviction:
mediaCacher({
staleFileIdPatterns: [
'wrong file_id',
'file is temporarily unavailable',
'file no longer exists' // custom
]
})options
mediaCacher(options?):
| option | type | default | description |
|---|---|---|---|
storage | KVStorage<string> | fresh MemoryStorage<string> | backing store |
getStorageKey | (ctx: RequestContext) => string | String(ctx.params.chat_id) | derive the cache scope key from an outgoing request |
keyStrategy | 'sourceValue' | 'hash' | 'sourceValue' | second half of the cache key — raw path/url or sha-256 of bytes |
staleFileIdPatterns | readonly string[] | three built-in patterns | substrings that trigger auto-evict + retry on a 400 response |
exported surface
import { mediaCacher } from '@puregram/media-cacher'
import type {
AllowedMediaMethod, // 'sendPhoto' | 'sendVideo' | …
KeyStrategy, // 'sourceValue' | 'hash'
KVStorage, // re-exported from @puregram/storage
MediaCacherExtension, // shape of tg.mediaCacher
MediaCacherOptions
} from '@puregram/media-cacher'
import {
ALLOWED_MEDIA_TYPES, // [MediaSourceType.Path, MediaSourceType.Url]
hashBytes, // sha-256 of arbitrary bytes as hex string
hashMediaInput, // sha-256 of the bytes a MediaInput resolves to
MEDIA_METHOD_TO_KEY_MAP, // { sendPhoto: 'photo', sendVideo: 'video', … }
MemoryStorage // re-exported from @puregram/storage
} from '@puregram/media-cacher'see also
- storage plugin —
KVStorage<V>contract, redis/sqlite backends,LruMemoryStorage - plugins & .extend — how
.extend(plugin)works - messages & media —
MediaSource.*factory methods - /api/methods — raw
sendPhoto/sendVideo/ etc signatures