167 lines
7.3 KiB
JavaScript
167 lines
7.3 KiB
JavaScript
"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;
|