UNPKG

telegraf

Version:

Modern Telegram Bot Framework

167 lines (166 loc) 7.47 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isSessionContext = exports.MemorySessionStore = exports.session = void 0; const debug_1 = __importDefault(require("debug")); const debug = (0, debug_1.default)('telegraf:session'); /** * Returns middleware that adds `ctx.session` for storing arbitrary state per session key. * * The default `getSessionKey` is `${ctx.from.id}:${ctx.chat.id}`. * If either `ctx.from` or `ctx.chat` is `undefined`, default session key and thus `ctx.session` are also `undefined`. * * > ⚠️ Session data is kept only in memory by default, which means that all data will be lost when the process is terminated. * > * > If you want to persist data across process restarts, or share it among multiple instances, you should use * [@telegraf/session](https://www.npmjs.com/package/@telegraf/session), or pass custom `storage`. * * @see {@link https://github.com/feathers-studio/telegraf-docs/blob/b694bcc36b4f71fb1cd650a345c2009ab4d2a2a5/guide/session.md Telegraf Docs | Session} * @see {@link https://github.com/feathers-studio/telegraf-docs/blob/master/examples/session-bot.ts Example} */ function session(options) { var _a, _b, _c; const prop = (_a = options === null || options === void 0 ? void 0 : options.property) !== null && _a !== void 0 ? _a : 'session'; const getSessionKey = (_b = options === null || options === void 0 ? void 0 : options.getSessionKey) !== null && _b !== void 0 ? _b : defaultGetSessionKey; const store = (_c = options === null || options === void 0 ? void 0 : options.store) !== null && _c !== void 0 ? _c : new MemorySessionStore(); // caches value from store in-memory while simultaneous updates share it // when counter reaches 0, the cached ref will be freed from memory const cache = new Map(); // temporarily stores concurrent requests const concurrents = new Map(); // this function must be handled with care // read full description on the original PR: https://github.com/telegraf/telegraf/pull/1713 // make sure to update the tests in test/session.js if you make any changes or fix bugs here return async (ctx, next) => { var _a; const updId = ctx.update.update_id; let released = false; function releaseChecks() { if (released && process.env.EXPERIMENTAL_SESSION_CHECKS) throw new Error("Session was accessed or assigned to after the middleware chain exhausted. This is a bug in your code. You're probably accessing session asynchronously and missing awaits."); } // because this is async, requests may still race here, but it will get autocorrected at (1) // v5 getSessionKey should probably be synchronous to avoid that const key = await getSessionKey(ctx); if (!key) { // Leaving this here could be useful to check for `prop in ctx` in future middleware ctx[prop] = undefined; return await next(); } let cached = cache.get(key); if (cached) { debug(`(${updId}) found cached session, reusing from cache`); ++cached.counter; } else { debug(`(${updId}) did not find cached session`); // if another concurrent request has already sent a store request, fetch that instead let promise = concurrents.get(key); if (promise) debug(`(${updId}) found a concurrent request, reusing promise`); else { debug(`(${updId}) fetching from upstream store`); promise = store.get(key); } // synchronously store promise so concurrent requests can share response concurrents.set(key, promise); const upstream = await promise; // all concurrent awaits will have promise in their closure, safe to remove now concurrents.delete(key); debug(`(${updId}) updating cache`); // another request may have beaten us to the punch const c = cache.get(key); if (c) { // another request did beat us to the punch c.counter++; // (1) preserve cached reference; in-memory reference is always newer than from store cached = c; } else { // we're the first, so we must cache the reference cached = { ref: upstream !== null && upstream !== void 0 ? upstream : (_a = options === null || options === void 0 ? void 0 : options.defaultSession) === null || _a === void 0 ? void 0 : _a.call(options, ctx), counter: 1 }; cache.set(key, cached); } } // TS already knows cached is always defined by this point, but does not guard cached. // It will, however, guard `c` here. const c = cached; let touched = false; Object.defineProperty(ctx, prop, { get() { releaseChecks(); touched = true; return c.ref; }, set(value) { releaseChecks(); touched = true; c.ref = value; }, }); try { await next(); released = true; } finally { if (--c.counter === 0) { // decrement to avoid memory leak debug(`(${updId}) refcounter reached 0, removing cached`); cache.delete(key); } debug(`(${updId}) middlewares completed, checking session`); // only update store if ctx.session was touched if (touched) if (c.ref == null) { debug(`(${updId}) ctx.${prop} missing, removing from store`); await store.delete(key); } else { debug(`(${updId}) ctx.${prop} found, updating store`); await store.set(key, c.ref); } } }; } exports.session = session; function defaultGetSessionKey(ctx) { var _a, _b; const fromId = (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id; const chatId = (_b = ctx.chat) === null || _b === void 0 ? void 0 : _b.id; if (fromId == null || chatId == null) return undefined; return `${fromId}:${chatId}`; } /** @deprecated Use `Map` */ class MemorySessionStore { constructor(ttl = Infinity) { this.ttl = ttl; this.store = new Map(); } get(name) { const entry = this.store.get(name); if (entry == null) { return undefined; } else if (entry.expires < Date.now()) { this.delete(name); return undefined; } return entry.session; } set(name, value) { const now = Date.now(); this.store.set(name, { session: value, expires: now + this.ttl }); } delete(name) { this.store.delete(name); } } exports.MemorySessionStore = MemorySessionStore; /** @deprecated session can use custom properties now. Directly use `'session' in ctx` instead */ function isSessionContext(ctx) { return 'session' in ctx; } exports.isSessionContext = isSessionContext;