89 lines
3.2 KiB
JavaScript
89 lines
3.2 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.sequentialize = void 0;
|
|
/**
|
|
* Using a runner for grammY allows your bot to run middleware concurrently.
|
|
* This has the benefit that multiple messages can be processed concurrently,
|
|
* hence making your bot drastically more scalable, but it comes at the cost
|
|
* that race conditions may occur because some messages need to be processed in
|
|
* order.
|
|
*
|
|
* The solution to this problem is by making sure that some updates wait for
|
|
* others to be done processing before running their middleware. This can be
|
|
* achieved by middleware.
|
|
*
|
|
* This function creates that middleware for you. You can pass in a constraint
|
|
* function that determines what updates could clash, and you will be provided
|
|
* by middleware that will ensure that clashes will not occur. A constraint is
|
|
* simply a string that is derived from an update.
|
|
*
|
|
* As an example, you can use this constraint function to make sure that
|
|
* messages inside the same chat are never processed concurrently:
|
|
*
|
|
* ```ts
|
|
* // Correctly order updates with the same chat identifier
|
|
* const constraint = (ctx: Context) => String(ctx.chat.id)
|
|
*
|
|
* bot.use(sequentialize(constraint))
|
|
* ```
|
|
*
|
|
* It is possible to return an array of strings if multiple constraints should
|
|
* hold, such as "process things inside the same chat in sequence, but also from
|
|
* the same user across chats":
|
|
* ```ts
|
|
* const constraints = (ctx: Context) => [String(ctx.chat.id), String(ctx.from.id)]
|
|
*
|
|
* bot.use(sequentialize(constraints))
|
|
* ```
|
|
*
|
|
* Sequentializing updates is especially important when using session middleware
|
|
* in order to prevent write-after-read hazards. In this case, you should
|
|
* provide the same function to determine constraints as you use to resolve the
|
|
* session key.
|
|
*
|
|
* @param constraint Function that determines the constraints of an update
|
|
* @returns Sequentializing middleware to be installed on the bot
|
|
*/
|
|
function sequentialize(constraint) {
|
|
const map = new Map();
|
|
return async (ctx, next) => {
|
|
const con = constraint(ctx);
|
|
const cs = (Array.isArray(con) ? con : [con]).filter((cs) => !!cs);
|
|
const clots = cs.map((c) => {
|
|
let v = map.get(c);
|
|
if (v === undefined) {
|
|
v = { chain: Promise.resolve(), len: 0 };
|
|
map.set(c, v);
|
|
}
|
|
return v;
|
|
});
|
|
const allClots = Promise.all(clots.map((p) => p.chain));
|
|
async function run() {
|
|
try {
|
|
await allClots;
|
|
}
|
|
catch {
|
|
// One of the previous middleware rejected. It is also `await`ed
|
|
// there, so we simply ignore the error here
|
|
}
|
|
try {
|
|
await next();
|
|
}
|
|
finally {
|
|
cs.forEach((c) => {
|
|
const cl = map.get(c);
|
|
if (cl !== undefined && --cl.len === 0)
|
|
map.delete(c);
|
|
});
|
|
}
|
|
}
|
|
const task = run();
|
|
clots.forEach((pr) => {
|
|
pr.len++;
|
|
pr.chain = task;
|
|
});
|
|
await task; // rethrows error
|
|
};
|
|
}
|
|
exports.sequentialize = sequentialize;
|