Skip to content

Instantly share code, notes, and snippets.

@slavafomin
Last active November 26, 2021 19:56
Show Gist options
  • Save slavafomin/97fd4a7844ade540d27925b04493298c to your computer and use it in GitHub Desktop.
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

The renaming is not possible because ctx.api is an instance of exactly the same class as bot.api. It is important that it stays that way. We do not want to have two separate classes that both do the same thing as Api, just with different names.

Thanks for the correction, I updated the comment.

@KnorpelSenf
Copy link

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