Skip to content

Instantly share code, notes, and snippets.

@slavafomin
Last active November 26, 2021 19:56
Show Gist options
  • Select an option

  • Save slavafomin/97fd4a7844ade540d27925b04493298c to your computer and use it in GitHub Desktop.

Select an option

Save slavafomin/97fd4a7844ade540d27925b04493298c to your computer and use it in GitHub Desktop.
import { Transformer } from 'grammy/out/core/client';
import { Cache } from './cache';
export function createCachingTransformer(options?: {
cacheTtl?: number;
}): Transformer {
const { cacheTtl = 5 * 1000 } = (options || {});
type Response = Promise<any>;
const cache = new Cache<Response>({
defaultTtl: cacheTtl,
});
// Removing expired responses from the cache
setInterval(() => cache.evictExpired(), cacheTtl);
// Transformer function
return async (prev, method, payload) => {
if (
method === 'answerCallbackQuery' &&
'callback_query_id' in payload
) {
const queryId = payload.callback_query_id;
const cacheKey = `${method}/${queryId}`;
// Ignoring duplicate requests
if (cache.has(cacheKey)) {
console.debug('Repeating redundant request', { method, queryId });
return cache.get(cacheKey);
}
console.debug('Allowing request', { method, queryId });
const response = prev(method, payload);
console.debug('Adding request to cache', { method, queryId });
// Saving pending response to resolve
// duplicate requests later on
cache.add({ key: cacheKey, value: response });
// Returning response back to the pipeline
return response;
}
// Letting other requests to be processed as is
return prev(method, payload);
};
}
interface CacheOptions {
defaultTtl?: number;
}
type CacheKey = string;
interface CacheItem<ValueType> {
value: ValueType;
freshTill: number;
}
export class Cache<ValueType> {
private cache = new Map<
CacheKey,
CacheItem<ValueType>
>();
constructor(private readonly options: CacheOptions) {
}
public add(options: {
key: CacheKey;
value: ValueType;
ttl?: number;
}) {
const { key, value } = options;
const { defaultTtl } = this.options;
this.cache.set(key, {
value,
freshTill: Date.now() + (options.ttl || defaultTtl)
});
}
public has(key: CacheKey) {
return this.cache.has(key);
}
public get(key: CacheKey) {
return this.cache.get(key)?.value;
}
public evictExpired() {
const now = Date.now();
this.cache.forEach((item, key) => {
if (item.freshTill < now) {
this.cache.delete(key);
console.debug(`Item evicted: ${key}`);
}
});
}
}
const response1 = await context.answerCallbackQuery({
text: 'Hello!',
});
const response2 = await context.answerCallbackQuery();
if (response1 !== response2) {
throw new Error(`Response must be cached!`);
}
@KnorpelSenf

Copy link
Copy Markdown

Including automatic callback query answering suggested by @wojpawlik, this would look like the following:

bot.on('callback_query', async (ctx, next) => {
  let answered = false
  ctx.api.config.use((prev, method, payload, signal) => {
    if (method === 'answerCallbackQuery' &&
          'callback_query_id' in payload &&
          payload.callback_query_id === ctx.callbackQuery.id) {
      if (answered) return { ok: true, result: true }
      else answered = true
    }
    return prev(method, payload, signal)
  })
  await next()
  if (!answered) await ctx.answerCallbackQuery()
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment