UNPKG

7.05 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6exports.isSessionContext = exports.MemorySessionStore = exports.session = void 0;
7const debug_1 = __importDefault(require("debug"));
8const debug = (0, debug_1.default)('telegraf:session');
9/**
10 * Returns middleware that adds `ctx.session` for storing arbitrary state per session key.
11 *
12 * The default `getSessionKey` is `${ctx.from.id}:${ctx.chat.id}`.
13 * If either `ctx.from` or `ctx.chat` is `undefined`, default session key and thus `ctx.session` are also `undefined`.
14 *
15 * > ⚠️ Session data is kept only in memory by default, which means that all data will be lost when the process is terminated.
16 * >
17 * > If you want to persist data across process restarts, or share it among multiple instances, you should use
18 * [@telegraf/session](https://www.npmjs.com/package/@telegraf/session), or pass custom `storage`.
19 *
20 * @see {@link https://github.com/feathers-studio/telegraf-docs/blob/b694bcc36b4f71fb1cd650a345c2009ab4d2a2a5/guide/session.md Telegraf Docs | Session}
21 * @see {@link https://github.com/feathers-studio/telegraf-docs/blob/master/examples/session-bot.ts Example}
22 */
23function session(options) {
24 var _a, _b, _c;
25 const prop = (_a = options === null || options === void 0 ? void 0 : options.property) !== null && _a !== void 0 ? _a : 'session';
26 const getSessionKey = (_b = options === null || options === void 0 ? void 0 : options.getSessionKey) !== null && _b !== void 0 ? _b : defaultGetSessionKey;
27 const store = (_c = options === null || options === void 0 ? void 0 : options.store) !== null && _c !== void 0 ? _c : new MemorySessionStore();
28 // caches value from store in-memory while simultaneous updates share it
29 // when counter reaches 0, the cached ref will be freed from memory
30 const cache = new Map();
31 // temporarily stores concurrent requests
32 const concurrents = new Map();
33 // this function must be handled with care
34 // read full description on the original PR: https://github.com/telegraf/telegraf/pull/1713
35 // make sure to update the tests in test/session.js if you make any changes or fix bugs here
36 return async (ctx, next) => {
37 var _a;
38 const updId = ctx.update.update_id;
39 // because this is async, requests may still race here, but it will get autocorrected at (1)
40 // v5 getSessionKey should probably be synchronous to avoid that
41 const key = await getSessionKey(ctx);
42 if (!key) {
43 // Leaving this here could be useful to check for `prop in ctx` in future middleware
44 ctx[prop] = undefined;
45 return await next();
46 }
47 let cached = cache.get(key);
48 if (cached) {
49 debug(`(${updId}) found cached session, reusing from cache`);
50 ++cached.counter;
51 }
52 else {
53 debug(`(${updId}) did not find cached session`);
54 // if another concurrent request has already sent a store request, fetch that instead
55 let promise = concurrents.get(key);
56 if (promise)
57 debug(`(${updId}) found a concurrent request, reusing promise`);
58 else {
59 debug(`(${updId}) fetching from upstream store`);
60 promise = store.get(key);
61 }
62 // synchronously store promise so concurrent requests can share response
63 concurrents.set(key, promise);
64 const upstream = await promise;
65 // all concurrent awaits will have promise in their closure, safe to remove now
66 concurrents.delete(key);
67 debug(`(${updId}) updating cache`);
68 // another request may have beaten us to the punch
69 const c = cache.get(key);
70 if (c) {
71 // another request did beat us to the punch
72 c.counter++;
73 // (1) preserve cached reference; in-memory reference is always newer than from store
74 cached = c;
75 }
76 else {
77 // we're the first, so we must cache the reference
78 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 };
79 cache.set(key, cached);
80 }
81 }
82 // TS already knows cached is always defined by this point, but does not guard cached.
83 // It will, however, guard `c` here.
84 const c = cached;
85 let touched = false;
86 Object.defineProperty(ctx, prop, {
87 get() {
88 touched = true;
89 return c.ref;
90 },
91 set(value) {
92 touched = true;
93 c.ref = value;
94 },
95 });
96 try {
97 await next();
98 }
99 finally {
100 if (--c.counter === 0) {
101 // decrement to avoid memory leak
102 debug(`(${updId}) refcounter reached 0, removing cached`);
103 cache.delete(key);
104 }
105 debug(`(${updId}) middlewares completed, checking session`);
106 // only update store if ctx.session was touched
107 if (touched)
108 if (ctx[prop] == null) {
109 debug(`(${updId}) ctx.${prop} missing, removing from store`);
110 await store.delete(key);
111 }
112 else {
113 debug(`(${updId}) ctx.${prop} found, updating store`);
114 await store.set(key, ctx[prop]);
115 }
116 }
117 };
118}
119exports.session = session;
120async function defaultGetSessionKey(ctx) {
121 var _a, _b;
122 const fromId = (_a = ctx.from) === null || _a === void 0 ? void 0 : _a.id;
123 const chatId = (_b = ctx.chat) === null || _b === void 0 ? void 0 : _b.id;
124 if (fromId == null || chatId == null) {
125 return undefined;
126 }
127 return `${fromId}:${chatId}`;
128}
129/** @deprecated Use `Map` */
130class MemorySessionStore {
131 constructor(ttl = Infinity) {
132 this.ttl = ttl;
133 this.store = new Map();
134 }
135 get(name) {
136 const entry = this.store.get(name);
137 if (entry == null) {
138 return undefined;
139 }
140 else if (entry.expires < Date.now()) {
141 this.delete(name);
142 return undefined;
143 }
144 return entry.session;
145 }
146 set(name, value) {
147 const now = Date.now();
148 this.store.set(name, { session: value, expires: now + this.ttl });
149 }
150 delete(name) {
151 this.store.delete(name);
152 }
153}
154exports.MemorySessionStore = MemorySessionStore;
155/** @deprecated session can use custom properties now. Directly use `'session' in ctx` instead */
156function isSessionContext(ctx) {
157 return 'session' in ctx;
158}
159exports.isSessionContext = isSessionContext;