@xbibzlibrary/telebibz 3.1.6 → 3.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.id.md +1 -0
- package/README.md +1 -0
- package/index.d.ts +2 -0
- package/lib/runner.js +64 -33
- package/lib/session.js +27 -8
- package/lib/telebibz.js +31 -4
- package/lib/update-processor.js +56 -0
- package/package.json +1 -1
- package/test/all.test.js +65 -3
- package/test/audit.test.js +53 -0
package/README.id.md
CHANGED
|
@@ -202,6 +202,7 @@ jadi `BotError` dan mengalir ke `opts.onError` atau reporter manusiawi.
|
|
|
202
202
|
```js
|
|
203
203
|
const bot = new TeleBibz('TOKEN', {
|
|
204
204
|
allowedUpdates: [...], // batasi tipe update (default: tipe umum + Business)
|
|
205
|
+
maxConcurrentUpdates: 256, // batas update aktif (default: 256)
|
|
205
206
|
onError: (err, ctx) {}, // handle error sendiri (default: reporter manusiawi)
|
|
206
207
|
silent: false, // tanpa banner boot
|
|
207
208
|
dropPending: false, // buang update lama saat start
|
package/README.md
CHANGED
|
@@ -201,6 +201,7 @@ and flows to `opts.onError` or the humanized reporter.
|
|
|
201
201
|
```js
|
|
202
202
|
const bot = new TeleBibz('TOKEN', {
|
|
203
203
|
allowedUpdates: [...], // limit update types (default: common + Business types)
|
|
204
|
+
maxConcurrentUpdates: 256, // active update limit (default: 256)
|
|
204
205
|
onError: (err, ctx) {}, // custom error handler (default: humanized reporter)
|
|
205
206
|
silent: false, // hide boot banner
|
|
206
207
|
dropPending: false, // discard old updates on start
|
package/index.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface SessionOptions {
|
|
|
68
68
|
}
|
|
69
69
|
export interface TeleBibzOptions {
|
|
70
70
|
allowedUpdates?: string[];
|
|
71
|
+
/** Maximum active updates per bot process. Default: 256. Updates sharing a session key are serialized. */
|
|
72
|
+
maxConcurrentUpdates?: number;
|
|
71
73
|
onError?: (err: unknown, ctx?: Context) => unknown;
|
|
72
74
|
silent?: boolean;
|
|
73
75
|
dropPending?: boolean;
|
package/lib/runner.js
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
|
-
// lib/runner.js — long polling tahan banting
|
|
1
|
+
// lib/runner.js — long polling tahan banting dengan dispatch konkuren.
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Loop getUpdates
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - error jaringan (fetch gagal) → retry 1 dtk
|
|
11
|
-
* - 409 (instance lain masih polling) → log callback onConflict + retry 5 dtk
|
|
12
|
-
* - error API lain → oper ke onFatal (default: berhenti bersih)
|
|
7
|
+
* Loop getUpdates dengan backpressure. Update dipanggil tanpa menunggu satu sama
|
|
8
|
+
* lain; handleUpdate mengatur batas worker dan urutan per session key. Promise
|
|
9
|
+
* yang masih berjalan tetap dilacak agar stop() dapat melakukan graceful drain.
|
|
13
10
|
*/
|
|
14
|
-
async function pollLoop({
|
|
11
|
+
async function pollLoop({
|
|
12
|
+
api,
|
|
13
|
+
handleUpdate,
|
|
14
|
+
allowedUpdates,
|
|
15
|
+
dropPending,
|
|
16
|
+
onConflict,
|
|
17
|
+
onFatal,
|
|
18
|
+
shouldStop,
|
|
19
|
+
conflictDelay = 5000,
|
|
20
|
+
maxInFlightUpdates = 800,
|
|
21
|
+
}) {
|
|
15
22
|
let offset = 0;
|
|
23
|
+
const inFlight = new Set();
|
|
24
|
+
const inFlightLimit = Number.isSafeInteger(maxInFlightUpdates) && maxInFlightUpdates > 0
|
|
25
|
+
? maxInFlightUpdates
|
|
26
|
+
: 800;
|
|
27
|
+
|
|
28
|
+
const dispatch = (update) => {
|
|
29
|
+
let task;
|
|
30
|
+
task = Promise.resolve()
|
|
31
|
+
.then(() => handleUpdate(update))
|
|
32
|
+
.catch(() => { /* error sudah ditangani bot; jangan hentikan poller */ })
|
|
33
|
+
.then(() => inFlight.delete(task));
|
|
34
|
+
inFlight.add(task);
|
|
35
|
+
};
|
|
36
|
+
|
|
16
37
|
if (dropPending) {
|
|
17
38
|
try {
|
|
18
39
|
const u = await api.callApi('getUpdates', { offset: -1, timeout: 0, allowed_updates: allowedUpdates });
|
|
@@ -20,34 +41,44 @@ async function pollLoop({ api, handleUpdate, allowedUpdates, dropPending, onConf
|
|
|
20
41
|
} catch { /* abaikan — loop utama yang menangani */ }
|
|
21
42
|
}
|
|
22
43
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
try {
|
|
45
|
+
while (!shouldStop()) {
|
|
46
|
+
let updates;
|
|
47
|
+
try {
|
|
48
|
+
updates = await api.callApi('getUpdates', { offset, timeout: 25, allowed_updates: allowedUpdates });
|
|
49
|
+
if (!Array.isArray(updates)) updates = [];
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const code = err && (err.error_code || err.code);
|
|
52
|
+
if (shouldStop()) break;
|
|
53
|
+
if (code === 409) { if (onConflict) await onConflict(err); await sleep(conflictDelay); continue; }
|
|
54
|
+
if (code === 429) {
|
|
55
|
+
const retryAfter = Number(err && err.parameters && err.parameters.retry_after);
|
|
56
|
+
await sleep(Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter * 1000 : 1000);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (Number.isInteger(code) && code >= 500 && code < 600) { await sleep(1000); continue; }
|
|
60
|
+
const networkCode = err && ['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'EPIPE', 'ENETUNREACH', 'ECONNREFUSED'].includes(err.code);
|
|
61
|
+
if (!err || err.network || networkCode || !err.error_code) {
|
|
62
|
+
await sleep(1000); continue; // jaringan putus/time-out: retry, bukan fatal
|
|
63
|
+
}
|
|
64
|
+
if (onFatal) await onFatal(err);
|
|
65
|
+
break;
|
|
36
66
|
}
|
|
37
|
-
if (Number.isInteger(code) && code >= 500 && code < 600) { await sleep(1000); continue; }
|
|
38
|
-
const networkCode = err && ['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'EPIPE', 'ENETUNREACH', 'ECONNREFUSED'].includes(err.code);
|
|
39
|
-
if (!err || err.network || networkCode || !err.error_code) {
|
|
40
|
-
await sleep(1000); continue; // jaringan putus/time-out: retry, bukan fatal
|
|
41
|
-
}
|
|
42
|
-
if (onFatal) await onFatal(err);
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
67
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
68
|
+
for (const update of updates) {
|
|
69
|
+
if (shouldStop()) break;
|
|
70
|
+
while (inFlight.size >= inFlightLimit) {
|
|
71
|
+
await Promise.race(inFlight);
|
|
72
|
+
if (shouldStop()) break;
|
|
73
|
+
}
|
|
74
|
+
if (shouldStop()) break;
|
|
75
|
+
offset = update.update_id + 1;
|
|
76
|
+
dispatch(update);
|
|
77
|
+
}
|
|
50
78
|
}
|
|
79
|
+
} finally {
|
|
80
|
+
// Stop/fatal error menghentikan intake, bukan memutus handler yang sedang berjalan.
|
|
81
|
+
await Promise.all([...inFlight]);
|
|
51
82
|
}
|
|
52
83
|
}
|
|
53
84
|
|
package/lib/session.js
CHANGED
|
@@ -6,22 +6,41 @@
|
|
|
6
6
|
* - default getKey: from.id + chat.id (khas), fallback chat.id.
|
|
7
7
|
* - default storage: Map di memori. Boleh kirim storage custom { read(key), write(key,val), delete(key) }.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const SESSION_KEY_CACHE = Symbol('telebibz.sessionKeyCache');
|
|
10
|
+
|
|
11
|
+
function getSessionKey(ctx, opts = {}) {
|
|
12
|
+
const cached = ctx[SESSION_KEY_CACHE];
|
|
13
|
+
if (cached) {
|
|
14
|
+
if (cached.failed) throw cached.error;
|
|
15
|
+
return cached.key;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const getKey = opts.getKey || ((current) => {
|
|
19
|
+
const from = current.from && current.from.id;
|
|
20
|
+
const chat = current.chat && current.chat.id;
|
|
15
21
|
if (from !== undefined && chat !== undefined) return `${from}:${chat}`;
|
|
16
22
|
return chat !== undefined ? String(chat) : undefined;
|
|
17
23
|
});
|
|
24
|
+
try {
|
|
25
|
+
const key = getKey(ctx);
|
|
26
|
+
Object.defineProperty(ctx, SESSION_KEY_CACHE, { value: { key, failed: false } });
|
|
27
|
+
return key;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
Object.defineProperty(ctx, SESSION_KEY_CACHE, { value: { error, failed: true } });
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function session(opts = {}) {
|
|
35
|
+
const initial = opts.initial || (() => ({}));
|
|
36
|
+
const store = opts.storage || new Map();
|
|
18
37
|
|
|
19
38
|
const read = (k) => (store.read ? store.read(k) : store.get(k));
|
|
20
39
|
const write = (k, v) => (store.write ? store.write(k, v) : store.set(k, v));
|
|
21
40
|
const del = (k) => (store.delete ? store.delete(k) : store.delete(k));
|
|
22
41
|
|
|
23
42
|
return async (ctx, next) => {
|
|
24
|
-
const key =
|
|
43
|
+
const key = getSessionKey(ctx, opts);
|
|
25
44
|
if (key === undefined) { ctx.session = {}; return next(); }
|
|
26
45
|
let s = await read(key);
|
|
27
46
|
if (s === undefined || s === null) s = initial();
|
|
@@ -41,4 +60,4 @@ function session(opts = {}) {
|
|
|
41
60
|
};
|
|
42
61
|
}
|
|
43
62
|
|
|
44
|
-
module.exports = { session };
|
|
63
|
+
module.exports = { session, getSessionKey };
|
package/lib/telebibz.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
const { makeApi } = require('./api');
|
|
5
5
|
const { Context } = require('./context');
|
|
6
6
|
const { Composer } = require('./composer');
|
|
7
|
-
const { session } = require('./session');
|
|
7
|
+
const { session, getSessionKey } = require('./session');
|
|
8
8
|
const { matchInlineQuery } = require('./inline-query');
|
|
9
9
|
const { Menu, MenuContainer } = require('./menus');
|
|
10
10
|
const { pollLoop } = require('./runner');
|
|
11
|
+
const { UpdateProcessor } = require('./update-processor');
|
|
11
12
|
const wizard = require('./wizard');
|
|
12
13
|
const log = require('./logger');
|
|
13
14
|
const { humanize } = require('./errors');
|
|
@@ -22,11 +23,13 @@ const DEFAULT_UPDATES = [
|
|
|
22
23
|
'purchased_paid_media', 'poll', 'poll_answer', 'my_chat_member', 'chat_member',
|
|
23
24
|
'chat_join_request', 'chat_boost', 'removed_chat_boost', 'stopped_message_generation',
|
|
24
25
|
];
|
|
26
|
+
const DEFAULT_MAX_CONCURRENT_UPDATES = 256;
|
|
25
27
|
|
|
26
28
|
class TeleBibz {
|
|
27
29
|
/**
|
|
28
30
|
* new TeleBibz('123:ABC', {
|
|
29
|
-
* allowedUpdates: [...],
|
|
31
|
+
* allowedUpdates: [...], maxConcurrentUpdates: 256,
|
|
32
|
+
* onError(err, ctx), silent, dropPending,
|
|
30
33
|
* transport: (method, payload) => Promise<result>, // ← suntik untuk test
|
|
31
34
|
* session: {...opsi session}, apiRoot: 'https://api.telegram.org'
|
|
32
35
|
* })
|
|
@@ -35,8 +38,14 @@ class TeleBibz {
|
|
|
35
38
|
if (!token || !/^\d+:[\w-]+$/.test(token)) {
|
|
36
39
|
throw new Error('TeleBibz: token tidak valid. Ambil dari @BotFather → /newbot.');
|
|
37
40
|
}
|
|
41
|
+
const maxConcurrentUpdates = opts.maxConcurrentUpdates ?? DEFAULT_MAX_CONCURRENT_UPDATES;
|
|
42
|
+
if (!Number.isSafeInteger(maxConcurrentUpdates) || maxConcurrentUpdates < 1) {
|
|
43
|
+
throw new RangeError('TeleBibz: maxConcurrentUpdates harus bilangan bulat positif.');
|
|
44
|
+
}
|
|
38
45
|
this.opts = opts;
|
|
39
46
|
this.token = token;
|
|
47
|
+
this.maxConcurrentUpdates = maxConcurrentUpdates;
|
|
48
|
+
this._updateProcessor = new UpdateProcessor(maxConcurrentUpdates);
|
|
40
49
|
this.api = makeApi(token, opts.transport, opts);
|
|
41
50
|
this.botInfo = undefined;
|
|
42
51
|
|
|
@@ -109,8 +118,25 @@ class TeleBibz {
|
|
|
109
118
|
/** Tangani SATU update mentah (webhook/serverless/test). */
|
|
110
119
|
async handleUpdate(update) {
|
|
111
120
|
const ctx = new Context(update, this.api, this.botInfo);
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
await this._updateProcessor.process(this._updateKey(ctx), async () => {
|
|
122
|
+
try { await this._root.run(ctx); }
|
|
123
|
+
catch (err) { await this._report({ error: err, ctx }); }
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Updates dengan session key sama berjalan berurutan; key berbeda berjalan paralel. */
|
|
128
|
+
_updateKey(ctx) {
|
|
129
|
+
let key;
|
|
130
|
+
try { key = getSessionKey(ctx, this.opts.session || {}); }
|
|
131
|
+
catch { /* session middleware reports the cached getKey error */ }
|
|
132
|
+
if (key !== undefined) return `session:${String(key)}`;
|
|
133
|
+
|
|
134
|
+
// Updates without a session key (for example inline queries) still keep a
|
|
135
|
+
// single sender's stateful handlers ordered when sender information exists.
|
|
136
|
+
const fromId = ctx.from && ctx.from.id;
|
|
137
|
+
if (fromId !== undefined) return `user:${String(fromId)}`;
|
|
138
|
+
const chatId = ctx.chat && ctx.chat.id;
|
|
139
|
+
return chatId === undefined ? undefined : `chat:${String(chatId)}`;
|
|
114
140
|
}
|
|
115
141
|
|
|
116
142
|
/** Nyalakan long polling (dengan retry 409 & backoff jaringan). */
|
|
@@ -130,6 +156,7 @@ class TeleBibz {
|
|
|
130
156
|
api: this.api,
|
|
131
157
|
handleUpdate: (u) => this.handleUpdate(u),
|
|
132
158
|
allowedUpdates: this.opts.allowedUpdates || DEFAULT_UPDATES,
|
|
159
|
+
maxInFlightUpdates: Math.max(100, this.maxConcurrentUpdates * 4),
|
|
133
160
|
dropPending: launchOpts.dropPending ?? this.opts.dropPending ?? false,
|
|
134
161
|
shouldStop: () => this._stopping,
|
|
135
162
|
conflictDelay: launchOpts.conflictDelay ?? 5000,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Execute independent updates concurrently while keeping a session key ordered.
|
|
5
|
+
* A Promise returned by process() resolves only after that update is complete.
|
|
6
|
+
*/
|
|
7
|
+
class UpdateProcessor {
|
|
8
|
+
constructor(maxConcurrentUpdates = 256) {
|
|
9
|
+
this.maxConcurrentUpdates = maxConcurrentUpdates;
|
|
10
|
+
this.active = 0;
|
|
11
|
+
this.pending = [];
|
|
12
|
+
this.activeKeys = new Set();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
process(key, handler) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
this.pending.push({ key: normalizeKey(key), handler, resolve, reject });
|
|
18
|
+
this._drain();
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_drain() {
|
|
23
|
+
while (this.active < this.maxConcurrentUpdates) {
|
|
24
|
+
// Preserve FIFO ordering for the same session, but skip a busy key so
|
|
25
|
+
// updates from other users can continue through the queue.
|
|
26
|
+
const index = this.pending.findIndex((task) => task.key === undefined || !this.activeKeys.has(task.key));
|
|
27
|
+
if (index === -1) return;
|
|
28
|
+
|
|
29
|
+
const [task] = this.pending.splice(index, 1);
|
|
30
|
+
if (task.key !== undefined) this.activeKeys.add(task.key);
|
|
31
|
+
this.active++;
|
|
32
|
+
|
|
33
|
+
Promise.resolve()
|
|
34
|
+
.then(task.handler)
|
|
35
|
+
.then(
|
|
36
|
+
(value) => task.resolve(value),
|
|
37
|
+
(error) => task.reject(error),
|
|
38
|
+
)
|
|
39
|
+
.then(() => {
|
|
40
|
+
this.active--;
|
|
41
|
+
if (task.key !== undefined) this.activeKeys.delete(task.key);
|
|
42
|
+
this._drain();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeKey(key) {
|
|
49
|
+
if (key === undefined) return undefined;
|
|
50
|
+
if (key === null) return 'null:null';
|
|
51
|
+
const type = typeof key;
|
|
52
|
+
if (!['string', 'number', 'bigint', 'boolean'].includes(type)) return undefined;
|
|
53
|
+
return `${type}:${String(key)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { UpdateProcessor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbibzlibrary/telebibz",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.8",
|
|
4
4
|
"description": "Telegram Bot API 10.3 framework: all 185 methods with typed payloads, rich messages, live photos, drafts, ephemeral messages, inline queries, sessions, webhooks, menus and wizards. Xbibz Technology ID",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
package/test/all.test.js
CHANGED
|
@@ -6,7 +6,7 @@ const { TeleBibz, btn, url, kb, webApp, copy, humanize, File, InlineKeyboard,
|
|
|
6
6
|
Menu, MenuContainer, iq, autoRetry, throttler, limiter, InputMediaBuilder } = require('..');
|
|
7
7
|
|
|
8
8
|
let N = 0, OK = 0;
|
|
9
|
-
const TOTAL =
|
|
9
|
+
const TOTAL = 33;
|
|
10
10
|
const t = (nama, fn) => {
|
|
11
11
|
const done = () => { N++; if (N === TOTAL) { console.log(`\n${OK}/${N} lulus`); process.exit(OK === N ? 0 : 1); } };
|
|
12
12
|
Promise.resolve()
|
|
@@ -16,7 +16,7 @@ const t = (nama, fn) => {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
/* ===== harness: bot dengan transport palsu (merekam panggilan API) ===== */
|
|
19
|
-
function buatBot(overrides = {}) {
|
|
19
|
+
function buatBot(overrides = {}, options = {}) {
|
|
20
20
|
const calls = [];
|
|
21
21
|
const transport = async (method, payload = {}) => {
|
|
22
22
|
calls.push({ method, payload });
|
|
@@ -25,7 +25,7 @@ function buatBot(overrides = {}) {
|
|
|
25
25
|
if (overrides[method]) return overrides[method](payload);
|
|
26
26
|
return true;
|
|
27
27
|
};
|
|
28
|
-
const bot = new TeleBibz('123456:TESTTOKEN-TESTTOKEN-TESTTOKEN-TESTOKEN', { silent: true, transport });
|
|
28
|
+
const bot = new TeleBibz('123456:TESTTOKEN-TESTTOKEN-TESTTOKEN-TESTOKEN', { ...options, silent: true, transport });
|
|
29
29
|
return { bot, calls, me: null };
|
|
30
30
|
}
|
|
31
31
|
async function boot(b) { await b.init(); }
|
|
@@ -202,6 +202,68 @@ t('launch: getUpdates 409 → retry halus; stop() resolve runPromise', async ()
|
|
|
202
202
|
assert.ok(polls >= 2, `harus retry minimal sekali, polls=${polls}`);
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
/* ===== Concurrency: 256 pengguna berbeda diproses tanpa serial menunggu ===== */
|
|
206
|
+
t('handleUpdate menjalankan 256 pengguna secara konkuren secara default', async () => {
|
|
207
|
+
const { bot } = buatBot();
|
|
208
|
+
await boot(bot);
|
|
209
|
+
const target = 256;
|
|
210
|
+
let active = 0, peak = 0, started = 0, release;
|
|
211
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
212
|
+
const fallback = setTimeout(release, 1000);
|
|
213
|
+
bot.on(':text', async () => {
|
|
214
|
+
active++;
|
|
215
|
+
started++;
|
|
216
|
+
peak = Math.max(peak, active);
|
|
217
|
+
if (started === target) release();
|
|
218
|
+
await gate;
|
|
219
|
+
active--;
|
|
220
|
+
});
|
|
221
|
+
const updates = Array.from({ length: target }, (_, i) => ({
|
|
222
|
+
update_id: 10000 + i,
|
|
223
|
+
message: { message_id: i + 1, date: 1, text: `user-${i}`, from: { id: i + 1 }, chat: { id: i + 1, type: 'private' } },
|
|
224
|
+
}));
|
|
225
|
+
await Promise.all(updates.map((update) => bot.handleUpdate(update)));
|
|
226
|
+
clearTimeout(fallback);
|
|
227
|
+
assert.equal(started, target);
|
|
228
|
+
assert.equal(peak, target, 'semua 256 update harus dapat berjalan bersamaan');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
t('maxConcurrentUpdates membatasi worker update dan memvalidasi nilai', async () => {
|
|
232
|
+
const { bot } = buatBot({}, { maxConcurrentUpdates: 3 });
|
|
233
|
+
await boot(bot);
|
|
234
|
+
let active = 0, peak = 0;
|
|
235
|
+
bot.on(':text', async () => {
|
|
236
|
+
active++;
|
|
237
|
+
peak = Math.max(peak, active);
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
239
|
+
active--;
|
|
240
|
+
});
|
|
241
|
+
const updates = Array.from({ length: 12 }, (_, i) => ({
|
|
242
|
+
update_id: 20000 + i,
|
|
243
|
+
message: { message_id: i + 1, date: 1, text: `user-${i}`, from: { id: i + 1 }, chat: { id: i + 1, type: 'private' } },
|
|
244
|
+
}));
|
|
245
|
+
await Promise.all(updates.map((update) => bot.handleUpdate(update)));
|
|
246
|
+
assert.equal(peak, 3);
|
|
247
|
+
assert.throws(() => new TeleBibz('123456:TESTTOKEN', { maxConcurrentUpdates: 0 }), /maxConcurrentUpdates/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
t('update dari session key sama tetap berurutan untuk menjaga state', async () => {
|
|
251
|
+
const { bot } = buatBot();
|
|
252
|
+
await boot(bot);
|
|
253
|
+
const order = [];
|
|
254
|
+
bot.on(':text', async (ctx) => {
|
|
255
|
+
order.push(`start:${ctx.msg.text}`);
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
257
|
+
order.push(`end:${ctx.msg.text}`);
|
|
258
|
+
});
|
|
259
|
+
const makeUpdate = (update_id, text) => ({
|
|
260
|
+
update_id,
|
|
261
|
+
message: { message_id: update_id, date: 1, text, from: { id: 700 }, chat: { id: 700, type: 'private' } },
|
|
262
|
+
});
|
|
263
|
+
await Promise.all([bot.handleUpdate(makeUpdate(1, 'first')), bot.handleUpdate(makeUpdate(2, 'second'))]);
|
|
264
|
+
assert.deepEqual(order, ['start:first', 'end:first', 'start:second', 'end:second']);
|
|
265
|
+
});
|
|
266
|
+
|
|
205
267
|
/* ===== 15. Proxy API generik: metode apapun ===== */
|
|
206
268
|
t('Proxy: api.metodeBebas(payload) → callApi mentah', async () => {
|
|
207
269
|
const { bot, calls } = buatBot(); await boot(bot);
|
package/test/audit.test.js
CHANGED
|
@@ -77,6 +77,30 @@ async function main() {
|
|
|
77
77
|
assert.equal(data.get('7:7').count, 2);
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
+
await test('session.getKey dihitung sekali dan menjaga urutan lintas update', async () => {
|
|
81
|
+
let keyCalls = 0;
|
|
82
|
+
const storage = new Map();
|
|
83
|
+
const bot = new TeleBibz('123456:TESTTOKEN', {
|
|
84
|
+
silent: true,
|
|
85
|
+
transport: async () => true,
|
|
86
|
+
session: { storage, getKey: (ctx) => { keyCalls++; return String(ctx.from.id); } },
|
|
87
|
+
});
|
|
88
|
+
const observed = [];
|
|
89
|
+
bot.on(':text', async (ctx) => {
|
|
90
|
+
observed.push(ctx.session.count || 0);
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
92
|
+
ctx.session.count = (ctx.session.count || 0) + 1;
|
|
93
|
+
});
|
|
94
|
+
const makeUpdate = (update_id, chatId) => ({
|
|
95
|
+
update_id,
|
|
96
|
+
message: { message_id: update_id, date: 1, text: 'increment', from: { id: 7 }, chat: { id: chatId, type: 'group' } },
|
|
97
|
+
});
|
|
98
|
+
await Promise.all([bot.handleUpdate(makeUpdate(1, 70)), bot.handleUpdate(makeUpdate(2, 71))]);
|
|
99
|
+
assert.equal(keyCalls, 2, 'getKey hanya perlu dipanggil sekali untuk setiap update');
|
|
100
|
+
assert.deepEqual(observed, [0, 1]);
|
|
101
|
+
assert.equal(storage.get('7').count, 2);
|
|
102
|
+
});
|
|
103
|
+
|
|
80
104
|
await test('guest_message diteruskan ke Context dan answerGuestQuery membentuk payload resmi', async () => {
|
|
81
105
|
const calls = [];
|
|
82
106
|
const bot = new TeleBibz('123456:TESTTOKEN', { silent: true, transport: async (m, p) => { calls.push({ m, p }); return true; } });
|
|
@@ -135,6 +159,35 @@ async function main() {
|
|
|
135
159
|
assert.equal(fatals, 0);
|
|
136
160
|
});
|
|
137
161
|
|
|
162
|
+
await test('poller dispatch batch secara konkuren dan menunggu handler saat stop', async () => {
|
|
163
|
+
const updates = Array.from({ length: 12 }, (_, i) => ({ update_id: i + 1 }));
|
|
164
|
+
let polls = 0, stopped = false, active = 0, peak = 0, entered = 0, release;
|
|
165
|
+
const gate = new Promise((resolve) => { release = resolve; });
|
|
166
|
+
const fallback = setTimeout(release, 1000);
|
|
167
|
+
const api = { callApi: async () => {
|
|
168
|
+
polls++;
|
|
169
|
+
if (polls === 1) return updates;
|
|
170
|
+
stopped = true;
|
|
171
|
+
return [];
|
|
172
|
+
} };
|
|
173
|
+
await pollLoop({
|
|
174
|
+
api,
|
|
175
|
+
handleUpdate: async () => {
|
|
176
|
+
active++;
|
|
177
|
+
entered++;
|
|
178
|
+
peak = Math.max(peak, active);
|
|
179
|
+
if (entered === updates.length) release();
|
|
180
|
+
await gate;
|
|
181
|
+
active--;
|
|
182
|
+
},
|
|
183
|
+
allowedUpdates: [], dropPending: false, shouldStop: () => stopped, maxInFlightUpdates: updates.length,
|
|
184
|
+
});
|
|
185
|
+
clearTimeout(fallback);
|
|
186
|
+
assert.equal(entered, updates.length);
|
|
187
|
+
assert.equal(peak, updates.length, 'poller tidak boleh menunggu satu update sebelum dispatch berikutnya');
|
|
188
|
+
assert.equal(active, 0, 'pollLoop menyelesaikan graceful drain sebelum return');
|
|
189
|
+
});
|
|
190
|
+
|
|
138
191
|
await test('throttler tetap menjalankan antrean setelah satu request gagal', async () => {
|
|
139
192
|
const { throttler } = require('../lib/ratelimit');
|
|
140
193
|
const calls = [];
|