"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;
