@mtcute/core 0.29.7 → 0.30.0
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/highlevel/client.d.cts +4 -6
- package/highlevel/client.d.ts +4 -6
- package/highlevel/methods/chats/join-chat.cjs +38 -16
- package/highlevel/methods/chats/join-chat.d.cts +20 -6
- package/highlevel/methods/chats/join-chat.d.ts +20 -6
- package/highlevel/methods/chats/join-chat.js +38 -16
- package/highlevel/methods/chats/save-draft.d.cts +4 -1
- package/highlevel/methods/chats/save-draft.d.ts +4 -1
- package/highlevel/methods.d.cts +2 -0
- package/highlevel/methods.d.ts +2 -0
- package/highlevel/storage/service/peers.cjs +1 -0
- package/highlevel/storage/service/peers.js +1 -0
- package/highlevel/types/files/utils.d.cts +1 -1
- package/highlevel/types/files/utils.d.ts +1 -1
- package/highlevel/types/messages/index.d.cts +1 -0
- package/highlevel/types/messages/index.d.ts +1 -0
- package/highlevel/types/messages/message.cjs +5 -0
- package/highlevel/types/messages/message.d.cts +3 -0
- package/highlevel/types/messages/message.d.ts +3 -0
- package/highlevel/types/messages/message.js +5 -0
- package/highlevel/types/messages/rich-message.cjs +50 -0
- package/highlevel/types/messages/rich-message.d.cts +18 -0
- package/highlevel/types/messages/rich-message.d.ts +18 -0
- package/highlevel/types/messages/rich-message.js +45 -0
- package/highlevel/types/peers/full-chat.cjs +6 -0
- package/highlevel/types/peers/full-chat.d.cts +2 -0
- package/highlevel/types/peers/full-chat.d.ts +2 -0
- package/highlevel/types/peers/full-chat.js +6 -0
- package/highlevel/types/peers/user.cjs +4 -0
- package/highlevel/types/peers/user.d.cts +2 -0
- package/highlevel/types/peers/user.d.ts +2 -0
- package/highlevel/types/peers/user.js +4 -0
- package/highlevel/types/updates/user-typing-update.cjs +2 -0
- package/highlevel/types/updates/user-typing-update.js +2 -0
- package/highlevel/updates/manager.cjs +191 -0
- package/highlevel/updates/manager.js +191 -0
- package/highlevel/utils/inspectable.cjs +1 -0
- package/highlevel/utils/inspectable.d.cts +1 -0
- package/highlevel/utils/inspectable.d.ts +1 -0
- package/highlevel/utils/inspectable.js +1 -0
- package/index.cjs +2 -0
- package/index.js +2 -0
- package/network/flood-control.cjs +149 -0
- package/network/flood-control.d.cts +79 -0
- package/network/flood-control.d.ts +79 -0
- package/network/flood-control.js +144 -0
- package/network/flood-control.test.d.cts +1 -0
- package/network/flood-control.test.d.ts +1 -0
- package/network/mtproto-session.cjs +1 -21
- package/network/mtproto-session.d.cts +1 -5
- package/network/mtproto-session.d.ts +1 -5
- package/network/mtproto-session.js +2 -22
- package/network/multi-session-connection.cjs +26 -16
- package/network/multi-session-connection.d.cts +1 -1
- package/network/multi-session-connection.d.ts +1 -1
- package/network/multi-session-connection.js +26 -16
- package/network/multi-session-connection.test.d.cts +1 -0
- package/network/multi-session-connection.test.d.ts +1 -0
- package/network/network-manager.cjs +3 -2
- package/network/network-manager.d.cts +12 -0
- package/network/network-manager.d.ts +12 -0
- package/network/network-manager.js +3 -2
- package/network/persistent-connection.cjs +25 -1
- package/network/persistent-connection.d.cts +5 -0
- package/network/persistent-connection.d.ts +5 -0
- package/network/persistent-connection.js +25 -1
- package/network/session-connection.cjs +5 -10
- package/network/session-connection.d.cts +0 -2
- package/network/session-connection.d.ts +0 -2
- package/network/session-connection.js +5 -10
- package/package.json +1 -1
- package/tl/api-schema.json +1 -1
- package/tl/binary/reader.cjs +176 -26
- package/tl/binary/reader.js +176 -26
- package/tl/binary/writer.cjs +439 -20
- package/tl/binary/writer.js +439 -20
- package/tl/compat/reader.cjs +23 -0
- package/tl/compat/reader.js +23 -0
- package/tl/index.d.cts +414 -14
- package/tl/index.d.ts +414 -14
- package/tl/inner-tl.cjs +20 -8
- package/tl/inner-tl.js +20 -8
- package/utils/binary/compat.cjs +79 -0
- package/utils/binary/compat.js +79 -0
- package/utils/index.d.cts +1 -0
- package/utils/index.d.ts +1 -0
- package/utils/long-utils.cjs +10 -0
- package/utils/long-utils.js +10 -0
- package/utils.cjs +7 -0
- package/utils.js +8 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
if (typeof globalThis !== "undefined" && !globalThis._MTCUTE_CJS_DEPRECATION_WARNED) {
|
|
2
|
+
globalThis._MTCUTE_CJS_DEPRECATION_WARNED = true;
|
|
3
|
+
console.warn("[@mtcute/core] CommonJS bundles are deprecated. They will be removed completely in one of the upcoming releases. No support is provided for CommonJS users. Please consider switching to ESM, it's " + (/* @__PURE__ */ new Date()).getFullYear() + " already.");
|
|
4
|
+
console.warn("[@mtcute/core] Learn more about switching to ESM: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c");
|
|
5
|
+
}
|
|
6
|
+
"use strict";
|
|
7
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
8
|
+
const utils = require("@fuman/utils");
|
|
9
|
+
class FloodControl {
|
|
10
|
+
_rules;
|
|
11
|
+
_maxWindow;
|
|
12
|
+
_events = [];
|
|
13
|
+
constructor(rules) {
|
|
14
|
+
if (rules.length === 0) throw new Error("FloodControl needs at least one rule");
|
|
15
|
+
this._rules = rules;
|
|
16
|
+
this._maxWindow = rules.reduce((m, r) => Math.max(m, r.windowMs), 0);
|
|
17
|
+
}
|
|
18
|
+
/** Earliest absolute perf-now ms at which a new event is permitted. */
|
|
19
|
+
getWakeupAt(now) {
|
|
20
|
+
let wakeup = 0;
|
|
21
|
+
for (const { count, windowMs } of this._rules) {
|
|
22
|
+
const cutoff = now - windowMs;
|
|
23
|
+
let inWindow = 0;
|
|
24
|
+
let oldestInWindowIdx = -1;
|
|
25
|
+
for (let i = this._events.length - 1; i >= 0; i--) {
|
|
26
|
+
if (this._events[i] <= cutoff) break;
|
|
27
|
+
inWindow++;
|
|
28
|
+
oldestInWindowIdx = i;
|
|
29
|
+
}
|
|
30
|
+
if (inWindow >= count) {
|
|
31
|
+
const t = this._events[oldestInWindowIdx] + windowMs;
|
|
32
|
+
if (t > wakeup) wakeup = t;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return wakeup;
|
|
36
|
+
}
|
|
37
|
+
addEvent(now) {
|
|
38
|
+
this._events.push(now);
|
|
39
|
+
this._prune(now);
|
|
40
|
+
}
|
|
41
|
+
_prune(now) {
|
|
42
|
+
const cutoff = now - this._maxWindow;
|
|
43
|
+
let i = 0;
|
|
44
|
+
while (i < this._events.length && this._events[i] < cutoff) i++;
|
|
45
|
+
if (i > 0) this._events.splice(0, i);
|
|
46
|
+
}
|
|
47
|
+
reset() {
|
|
48
|
+
this._events = [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const DEFAULT_SANITY_LIMITS = [
|
|
52
|
+
{ count: 5, windowMs: 1e4 }
|
|
53
|
+
];
|
|
54
|
+
const DEFAULT_FLOOD_LIMITS = [
|
|
55
|
+
{ count: 1, windowMs: 1e3 },
|
|
56
|
+
{ count: 4, windowMs: 2e3 },
|
|
57
|
+
{ count: 8, windowMs: 3e3 }
|
|
58
|
+
];
|
|
59
|
+
const DEFAULT_MTPROTO_ERROR_LIMITS = [
|
|
60
|
+
{ count: 1, windowMs: 1e3 },
|
|
61
|
+
{ count: 4, windowMs: 2e3 },
|
|
62
|
+
{ count: 8, windowMs: 3e3 }
|
|
63
|
+
];
|
|
64
|
+
class ConnectionFloodController {
|
|
65
|
+
sanity;
|
|
66
|
+
flood;
|
|
67
|
+
mtprotoError;
|
|
68
|
+
_log;
|
|
69
|
+
constructor(limits = {}) {
|
|
70
|
+
this.sanity = new FloodControl(limits.sanity ?? DEFAULT_SANITY_LIMITS);
|
|
71
|
+
this.flood = new FloodControl(limits.flood ?? DEFAULT_FLOOD_LIMITS);
|
|
72
|
+
this.mtprotoError = new FloodControl(limits.mtprotoError ?? DEFAULT_MTPROTO_ERROR_LIMITS);
|
|
73
|
+
}
|
|
74
|
+
setLogger(log) {
|
|
75
|
+
this._log = log;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Notify a network-up transition.
|
|
79
|
+
*
|
|
80
|
+
* TDLib clears sanity + online-regime flood + backoff on
|
|
81
|
+
* `ConnectionCreator::on_network(true)`. We don't model the online regime,
|
|
82
|
+
* and our transport layer owns its own reconnect backoff, so only the
|
|
83
|
+
* sanity limiter is reset here.
|
|
84
|
+
*/
|
|
85
|
+
notifyNetworkUp() {
|
|
86
|
+
this._log?.debug("network up, resetting sanity limiter");
|
|
87
|
+
this.sanity.reset();
|
|
88
|
+
}
|
|
89
|
+
/** Record an MTProto-level error (transport 429; per TDLib, other codes don't tick this). */
|
|
90
|
+
addMtprotoError(now = performance.now()) {
|
|
91
|
+
this._log?.debug("recording mtproto error");
|
|
92
|
+
this.mtprotoError.addEvent(now);
|
|
93
|
+
}
|
|
94
|
+
/** Earliest perf-now ms at which a new connection attempt is permitted. */
|
|
95
|
+
getWakeupAt(now) {
|
|
96
|
+
return Math.max(
|
|
97
|
+
this.sanity.getWakeupAt(now),
|
|
98
|
+
this.flood.getWakeupAt(now),
|
|
99
|
+
this.mtprotoError.getWakeupAt(now)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Block until a connection attempt is permitted under all applicable
|
|
104
|
+
* limiters, then record the attempt. Concurrent waiters serialize fairly
|
|
105
|
+
* because the first to wake records its event before the next checks state.
|
|
106
|
+
*
|
|
107
|
+
* If `signal` aborts, the pending timer is cleared and the promise rejects
|
|
108
|
+
* with the signal's reason.
|
|
109
|
+
*/
|
|
110
|
+
async wait(signal) {
|
|
111
|
+
let waited = false;
|
|
112
|
+
while (true) {
|
|
113
|
+
signal?.throwIfAborted();
|
|
114
|
+
const now = performance.now();
|
|
115
|
+
const wakeup = this.getWakeupAt(now);
|
|
116
|
+
if (wakeup <= now) {
|
|
117
|
+
this.sanity.addEvent(now);
|
|
118
|
+
this.flood.addEvent(now);
|
|
119
|
+
if (waited) this._log?.debug("admitted connect after waiting");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const delay = wakeup - now;
|
|
123
|
+
this._log?.debug("connect throttled (waiting %dms)", Math.round(delay));
|
|
124
|
+
waited = true;
|
|
125
|
+
await new Promise((resolve, reject) => {
|
|
126
|
+
const handle = utils.timers.setTimeout(() => {
|
|
127
|
+
signal?.removeEventListener("abort", onAbort);
|
|
128
|
+
resolve();
|
|
129
|
+
}, delay);
|
|
130
|
+
const onAbort = () => {
|
|
131
|
+
utils.timers.clearTimeout(handle);
|
|
132
|
+
reject(signal.reason);
|
|
133
|
+
};
|
|
134
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Reset all internal state. Mainly for tests. */
|
|
139
|
+
reset() {
|
|
140
|
+
this.sanity.reset();
|
|
141
|
+
this.flood.reset();
|
|
142
|
+
this.mtprotoError.reset();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.ConnectionFloodController = ConnectionFloodController;
|
|
146
|
+
exports.DEFAULT_FLOOD_LIMITS = DEFAULT_FLOOD_LIMITS;
|
|
147
|
+
exports.DEFAULT_MTPROTO_ERROR_LIMITS = DEFAULT_MTPROTO_ERROR_LIMITS;
|
|
148
|
+
exports.DEFAULT_SANITY_LIMITS = DEFAULT_SANITY_LIMITS;
|
|
149
|
+
exports.FloodControl = FloodControl;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
export interface FloodLimit {
|
|
3
|
+
/** maximum events allowed within `windowMs` */
|
|
4
|
+
count: number;
|
|
5
|
+
/** window size in ms */
|
|
6
|
+
windowMs: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sliding-window flood control. Port of TDLib's `FloodControlStrict`
|
|
10
|
+
* (tdutils/td/utils/FloodControlStrict.h).
|
|
11
|
+
*/
|
|
12
|
+
export declare class FloodControl {
|
|
13
|
+
private readonly _rules;
|
|
14
|
+
private readonly _maxWindow;
|
|
15
|
+
private _events;
|
|
16
|
+
constructor(rules: readonly FloodLimit[]);
|
|
17
|
+
/** Earliest absolute perf-now ms at which a new event is permitted. */
|
|
18
|
+
getWakeupAt(now: number): number;
|
|
19
|
+
addEvent(now: number): void;
|
|
20
|
+
private _prune;
|
|
21
|
+
reset(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_SANITY_LIMITS: readonly FloodLimit[];
|
|
24
|
+
export declare const DEFAULT_FLOOD_LIMITS: readonly FloodLimit[];
|
|
25
|
+
export declare const DEFAULT_MTPROTO_ERROR_LIMITS: readonly FloodLimit[];
|
|
26
|
+
export interface ConnectionFloodLimits {
|
|
27
|
+
sanity?: readonly FloodLimit[];
|
|
28
|
+
flood?: readonly FloodLimit[];
|
|
29
|
+
mtprotoError?: readonly FloodLimit[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Per-connection-slot flood control bundle modeled after TDLib's
|
|
33
|
+
* `ConnectionCreator` client state (`ConnectionCreator.cpp`) — TDLib keys
|
|
34
|
+
* its flood control by pool slot (the `SessionProxy#i` hash), so each
|
|
35
|
+
* socket's connect history is throttled independently.
|
|
36
|
+
*
|
|
37
|
+
* Three limiters compose:
|
|
38
|
+
* - `sanity` — absolute ceiling, always applied
|
|
39
|
+
* - `flood` — burst limit on connection attempts, always applied
|
|
40
|
+
* - `mtprotoError` — ticks only on MTProto-level errors, always applied
|
|
41
|
+
*
|
|
42
|
+
* TDLib additionally has a stricter "online" regime keyed to app foreground
|
|
43
|
+
* state — we have no such signal, so only the permissive regime is modeled.
|
|
44
|
+
*
|
|
45
|
+
* `wait()` blocks until all limiters allow a fresh connection and records
|
|
46
|
+
* the event atomically (sanity + flood).
|
|
47
|
+
*/
|
|
48
|
+
export declare class ConnectionFloodController {
|
|
49
|
+
readonly sanity: FloodControl;
|
|
50
|
+
readonly flood: FloodControl;
|
|
51
|
+
readonly mtprotoError: FloodControl;
|
|
52
|
+
private _log?;
|
|
53
|
+
constructor(limits?: ConnectionFloodLimits);
|
|
54
|
+
setLogger(log: Logger): void;
|
|
55
|
+
/**
|
|
56
|
+
* Notify a network-up transition.
|
|
57
|
+
*
|
|
58
|
+
* TDLib clears sanity + online-regime flood + backoff on
|
|
59
|
+
* `ConnectionCreator::on_network(true)`. We don't model the online regime,
|
|
60
|
+
* and our transport layer owns its own reconnect backoff, so only the
|
|
61
|
+
* sanity limiter is reset here.
|
|
62
|
+
*/
|
|
63
|
+
notifyNetworkUp(): void;
|
|
64
|
+
/** Record an MTProto-level error (transport 429; per TDLib, other codes don't tick this). */
|
|
65
|
+
addMtprotoError(now?: number): void;
|
|
66
|
+
/** Earliest perf-now ms at which a new connection attempt is permitted. */
|
|
67
|
+
getWakeupAt(now: number): number;
|
|
68
|
+
/**
|
|
69
|
+
* Block until a connection attempt is permitted under all applicable
|
|
70
|
+
* limiters, then record the attempt. Concurrent waiters serialize fairly
|
|
71
|
+
* because the first to wake records its event before the next checks state.
|
|
72
|
+
*
|
|
73
|
+
* If `signal` aborts, the pending timer is cleared and the promise rejects
|
|
74
|
+
* with the signal's reason.
|
|
75
|
+
*/
|
|
76
|
+
wait(signal?: AbortSignal): Promise<void>;
|
|
77
|
+
/** Reset all internal state. Mainly for tests. */
|
|
78
|
+
reset(): void;
|
|
79
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
export interface FloodLimit {
|
|
3
|
+
/** maximum events allowed within `windowMs` */
|
|
4
|
+
count: number;
|
|
5
|
+
/** window size in ms */
|
|
6
|
+
windowMs: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Sliding-window flood control. Port of TDLib's `FloodControlStrict`
|
|
10
|
+
* (tdutils/td/utils/FloodControlStrict.h).
|
|
11
|
+
*/
|
|
12
|
+
export declare class FloodControl {
|
|
13
|
+
private readonly _rules;
|
|
14
|
+
private readonly _maxWindow;
|
|
15
|
+
private _events;
|
|
16
|
+
constructor(rules: readonly FloodLimit[]);
|
|
17
|
+
/** Earliest absolute perf-now ms at which a new event is permitted. */
|
|
18
|
+
getWakeupAt(now: number): number;
|
|
19
|
+
addEvent(now: number): void;
|
|
20
|
+
private _prune;
|
|
21
|
+
reset(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare const DEFAULT_SANITY_LIMITS: readonly FloodLimit[];
|
|
24
|
+
export declare const DEFAULT_FLOOD_LIMITS: readonly FloodLimit[];
|
|
25
|
+
export declare const DEFAULT_MTPROTO_ERROR_LIMITS: readonly FloodLimit[];
|
|
26
|
+
export interface ConnectionFloodLimits {
|
|
27
|
+
sanity?: readonly FloodLimit[];
|
|
28
|
+
flood?: readonly FloodLimit[];
|
|
29
|
+
mtprotoError?: readonly FloodLimit[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Per-connection-slot flood control bundle modeled after TDLib's
|
|
33
|
+
* `ConnectionCreator` client state (`ConnectionCreator.cpp`) — TDLib keys
|
|
34
|
+
* its flood control by pool slot (the `SessionProxy#i` hash), so each
|
|
35
|
+
* socket's connect history is throttled independently.
|
|
36
|
+
*
|
|
37
|
+
* Three limiters compose:
|
|
38
|
+
* - `sanity` — absolute ceiling, always applied
|
|
39
|
+
* - `flood` — burst limit on connection attempts, always applied
|
|
40
|
+
* - `mtprotoError` — ticks only on MTProto-level errors, always applied
|
|
41
|
+
*
|
|
42
|
+
* TDLib additionally has a stricter "online" regime keyed to app foreground
|
|
43
|
+
* state — we have no such signal, so only the permissive regime is modeled.
|
|
44
|
+
*
|
|
45
|
+
* `wait()` blocks until all limiters allow a fresh connection and records
|
|
46
|
+
* the event atomically (sanity + flood).
|
|
47
|
+
*/
|
|
48
|
+
export declare class ConnectionFloodController {
|
|
49
|
+
readonly sanity: FloodControl;
|
|
50
|
+
readonly flood: FloodControl;
|
|
51
|
+
readonly mtprotoError: FloodControl;
|
|
52
|
+
private _log?;
|
|
53
|
+
constructor(limits?: ConnectionFloodLimits);
|
|
54
|
+
setLogger(log: Logger): void;
|
|
55
|
+
/**
|
|
56
|
+
* Notify a network-up transition.
|
|
57
|
+
*
|
|
58
|
+
* TDLib clears sanity + online-regime flood + backoff on
|
|
59
|
+
* `ConnectionCreator::on_network(true)`. We don't model the online regime,
|
|
60
|
+
* and our transport layer owns its own reconnect backoff, so only the
|
|
61
|
+
* sanity limiter is reset here.
|
|
62
|
+
*/
|
|
63
|
+
notifyNetworkUp(): void;
|
|
64
|
+
/** Record an MTProto-level error (transport 429; per TDLib, other codes don't tick this). */
|
|
65
|
+
addMtprotoError(now?: number): void;
|
|
66
|
+
/** Earliest perf-now ms at which a new connection attempt is permitted. */
|
|
67
|
+
getWakeupAt(now: number): number;
|
|
68
|
+
/**
|
|
69
|
+
* Block until a connection attempt is permitted under all applicable
|
|
70
|
+
* limiters, then record the attempt. Concurrent waiters serialize fairly
|
|
71
|
+
* because the first to wake records its event before the next checks state.
|
|
72
|
+
*
|
|
73
|
+
* If `signal` aborts, the pending timer is cleared and the promise rejects
|
|
74
|
+
* with the signal's reason.
|
|
75
|
+
*/
|
|
76
|
+
wait(signal?: AbortSignal): Promise<void>;
|
|
77
|
+
/** Reset all internal state. Mainly for tests. */
|
|
78
|
+
reset(): void;
|
|
79
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { timers } from "@fuman/utils";
|
|
2
|
+
class FloodControl {
|
|
3
|
+
_rules;
|
|
4
|
+
_maxWindow;
|
|
5
|
+
_events = [];
|
|
6
|
+
constructor(rules) {
|
|
7
|
+
if (rules.length === 0) throw new Error("FloodControl needs at least one rule");
|
|
8
|
+
this._rules = rules;
|
|
9
|
+
this._maxWindow = rules.reduce((m, r) => Math.max(m, r.windowMs), 0);
|
|
10
|
+
}
|
|
11
|
+
/** Earliest absolute perf-now ms at which a new event is permitted. */
|
|
12
|
+
getWakeupAt(now) {
|
|
13
|
+
let wakeup = 0;
|
|
14
|
+
for (const { count, windowMs } of this._rules) {
|
|
15
|
+
const cutoff = now - windowMs;
|
|
16
|
+
let inWindow = 0;
|
|
17
|
+
let oldestInWindowIdx = -1;
|
|
18
|
+
for (let i = this._events.length - 1; i >= 0; i--) {
|
|
19
|
+
if (this._events[i] <= cutoff) break;
|
|
20
|
+
inWindow++;
|
|
21
|
+
oldestInWindowIdx = i;
|
|
22
|
+
}
|
|
23
|
+
if (inWindow >= count) {
|
|
24
|
+
const t = this._events[oldestInWindowIdx] + windowMs;
|
|
25
|
+
if (t > wakeup) wakeup = t;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return wakeup;
|
|
29
|
+
}
|
|
30
|
+
addEvent(now) {
|
|
31
|
+
this._events.push(now);
|
|
32
|
+
this._prune(now);
|
|
33
|
+
}
|
|
34
|
+
_prune(now) {
|
|
35
|
+
const cutoff = now - this._maxWindow;
|
|
36
|
+
let i = 0;
|
|
37
|
+
while (i < this._events.length && this._events[i] < cutoff) i++;
|
|
38
|
+
if (i > 0) this._events.splice(0, i);
|
|
39
|
+
}
|
|
40
|
+
reset() {
|
|
41
|
+
this._events = [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const DEFAULT_SANITY_LIMITS = [
|
|
45
|
+
{ count: 5, windowMs: 1e4 }
|
|
46
|
+
];
|
|
47
|
+
const DEFAULT_FLOOD_LIMITS = [
|
|
48
|
+
{ count: 1, windowMs: 1e3 },
|
|
49
|
+
{ count: 4, windowMs: 2e3 },
|
|
50
|
+
{ count: 8, windowMs: 3e3 }
|
|
51
|
+
];
|
|
52
|
+
const DEFAULT_MTPROTO_ERROR_LIMITS = [
|
|
53
|
+
{ count: 1, windowMs: 1e3 },
|
|
54
|
+
{ count: 4, windowMs: 2e3 },
|
|
55
|
+
{ count: 8, windowMs: 3e3 }
|
|
56
|
+
];
|
|
57
|
+
class ConnectionFloodController {
|
|
58
|
+
sanity;
|
|
59
|
+
flood;
|
|
60
|
+
mtprotoError;
|
|
61
|
+
_log;
|
|
62
|
+
constructor(limits = {}) {
|
|
63
|
+
this.sanity = new FloodControl(limits.sanity ?? DEFAULT_SANITY_LIMITS);
|
|
64
|
+
this.flood = new FloodControl(limits.flood ?? DEFAULT_FLOOD_LIMITS);
|
|
65
|
+
this.mtprotoError = new FloodControl(limits.mtprotoError ?? DEFAULT_MTPROTO_ERROR_LIMITS);
|
|
66
|
+
}
|
|
67
|
+
setLogger(log) {
|
|
68
|
+
this._log = log;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Notify a network-up transition.
|
|
72
|
+
*
|
|
73
|
+
* TDLib clears sanity + online-regime flood + backoff on
|
|
74
|
+
* `ConnectionCreator::on_network(true)`. We don't model the online regime,
|
|
75
|
+
* and our transport layer owns its own reconnect backoff, so only the
|
|
76
|
+
* sanity limiter is reset here.
|
|
77
|
+
*/
|
|
78
|
+
notifyNetworkUp() {
|
|
79
|
+
this._log?.debug("network up, resetting sanity limiter");
|
|
80
|
+
this.sanity.reset();
|
|
81
|
+
}
|
|
82
|
+
/** Record an MTProto-level error (transport 429; per TDLib, other codes don't tick this). */
|
|
83
|
+
addMtprotoError(now = performance.now()) {
|
|
84
|
+
this._log?.debug("recording mtproto error");
|
|
85
|
+
this.mtprotoError.addEvent(now);
|
|
86
|
+
}
|
|
87
|
+
/** Earliest perf-now ms at which a new connection attempt is permitted. */
|
|
88
|
+
getWakeupAt(now) {
|
|
89
|
+
return Math.max(
|
|
90
|
+
this.sanity.getWakeupAt(now),
|
|
91
|
+
this.flood.getWakeupAt(now),
|
|
92
|
+
this.mtprotoError.getWakeupAt(now)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Block until a connection attempt is permitted under all applicable
|
|
97
|
+
* limiters, then record the attempt. Concurrent waiters serialize fairly
|
|
98
|
+
* because the first to wake records its event before the next checks state.
|
|
99
|
+
*
|
|
100
|
+
* If `signal` aborts, the pending timer is cleared and the promise rejects
|
|
101
|
+
* with the signal's reason.
|
|
102
|
+
*/
|
|
103
|
+
async wait(signal) {
|
|
104
|
+
let waited = false;
|
|
105
|
+
while (true) {
|
|
106
|
+
signal?.throwIfAborted();
|
|
107
|
+
const now = performance.now();
|
|
108
|
+
const wakeup = this.getWakeupAt(now);
|
|
109
|
+
if (wakeup <= now) {
|
|
110
|
+
this.sanity.addEvent(now);
|
|
111
|
+
this.flood.addEvent(now);
|
|
112
|
+
if (waited) this._log?.debug("admitted connect after waiting");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const delay = wakeup - now;
|
|
116
|
+
this._log?.debug("connect throttled (waiting %dms)", Math.round(delay));
|
|
117
|
+
waited = true;
|
|
118
|
+
await new Promise((resolve, reject) => {
|
|
119
|
+
const handle = timers.setTimeout(() => {
|
|
120
|
+
signal?.removeEventListener("abort", onAbort);
|
|
121
|
+
resolve();
|
|
122
|
+
}, delay);
|
|
123
|
+
const onAbort = () => {
|
|
124
|
+
timers.clearTimeout(handle);
|
|
125
|
+
reject(signal.reason);
|
|
126
|
+
};
|
|
127
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Reset all internal state. Mainly for tests. */
|
|
132
|
+
reset() {
|
|
133
|
+
this.sanity.reset();
|
|
134
|
+
this.flood.reset();
|
|
135
|
+
this.mtprotoError.reset();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
ConnectionFloodController,
|
|
140
|
+
DEFAULT_FLOOD_LIMITS,
|
|
141
|
+
DEFAULT_MTPROTO_ERROR_LIMITS,
|
|
142
|
+
DEFAULT_SANITY_LIMITS,
|
|
143
|
+
FloodControl
|
|
144
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -36,6 +36,7 @@ class MtprotoSession {
|
|
|
36
36
|
// recent msg ids
|
|
37
37
|
recentOutgoingMsgIds = new utils.LruSet(1e3, longUtils.LongSet);
|
|
38
38
|
recentIncomingMsgIds = new utils.LruSet(1e3, longUtils.LongSet);
|
|
39
|
+
// <deno-tsignore>
|
|
39
40
|
recentStateRequests = new utils.LruMap(1e3, longUtils.LongMap);
|
|
40
41
|
// queues
|
|
41
42
|
queuedRpc = new utils.Deque();
|
|
@@ -60,9 +61,6 @@ class MtprotoSession {
|
|
|
60
61
|
lastSessionCreatedUid = Long.ZERO;
|
|
61
62
|
initConnectionCalled = false;
|
|
62
63
|
authorizationPending = false;
|
|
63
|
-
next429Timeout = 1e3;
|
|
64
|
-
current429Timeout;
|
|
65
|
-
next429ResetTimeout;
|
|
66
64
|
get hasPendingMessages() {
|
|
67
65
|
return Boolean(
|
|
68
66
|
this.queuedRpc.length || this.queuedAcks.length || this.queuedStateReq.length || this.queuedResendReq.length || this.queuedCancelReq.length
|
|
@@ -75,7 +73,6 @@ class MtprotoSession {
|
|
|
75
73
|
if (withAuthKey) {
|
|
76
74
|
this.resetAuthKey();
|
|
77
75
|
}
|
|
78
|
-
utils.timers.clearTimeout(this.current429Timeout);
|
|
79
76
|
this.resetState(withAuthKey);
|
|
80
77
|
}
|
|
81
78
|
resetAuthKey() {
|
|
@@ -197,23 +194,6 @@ class MtprotoSession {
|
|
|
197
194
|
else writer.object(content);
|
|
198
195
|
return messageId;
|
|
199
196
|
}
|
|
200
|
-
onTransportFlood(callback) {
|
|
201
|
-
if (this.current429Timeout) return;
|
|
202
|
-
this.resetLastPing(true);
|
|
203
|
-
const timeout = this.next429Timeout;
|
|
204
|
-
this.next429Timeout = Math.min(this.next429Timeout * 2, 32e3);
|
|
205
|
-
utils.timers.clearTimeout(this.current429Timeout);
|
|
206
|
-
utils.timers.clearTimeout(this.next429ResetTimeout);
|
|
207
|
-
this.current429Timeout = utils.timers.setTimeout(() => {
|
|
208
|
-
this.current429Timeout = void 0;
|
|
209
|
-
callback();
|
|
210
|
-
}, timeout);
|
|
211
|
-
this.next429ResetTimeout = utils.timers.setTimeout(() => {
|
|
212
|
-
this.next429ResetTimeout = void 0;
|
|
213
|
-
this.next429Timeout = 1e3;
|
|
214
|
-
}, 6e4);
|
|
215
|
-
this.log.debug("transport flood, waiting for %d ms before proceeding", timeout);
|
|
216
|
-
}
|
|
217
197
|
resetLastPing(withTime = false) {
|
|
218
198
|
if (withTime) this.lastPingTime = 0;
|
|
219
199
|
if (!this.lastPingMsgId.isZero()) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Deferred, Deque, LruMap, LruSet
|
|
1
|
+
import { Deferred, timers, Deque, LruMap, LruSet } from '@fuman/utils';
|
|
2
2
|
import { TlBinaryWriter, TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime';
|
|
3
3
|
import { mtp, tl } from '../tl/index.js';
|
|
4
4
|
import { ICryptoProvider, Logger, LongMap, SortedArray } from '../utils/index.js';
|
|
@@ -94,9 +94,6 @@ export declare class MtprotoSession {
|
|
|
94
94
|
lastSessionCreatedUid: Long;
|
|
95
95
|
initConnectionCalled: boolean;
|
|
96
96
|
authorizationPending: boolean;
|
|
97
|
-
next429Timeout: number;
|
|
98
|
-
current429Timeout?: timers.Timer;
|
|
99
|
-
next429ResetTimeout?: timers.Timer;
|
|
100
97
|
constructor(_crypto: ICryptoProvider, log: Logger, _readerMap: TlReaderMap, _writerMap: TlWriterMap, _salts: ServerSaltManager);
|
|
101
98
|
get hasPendingMessages(): boolean;
|
|
102
99
|
/**
|
|
@@ -120,7 +117,6 @@ export declare class MtprotoSession {
|
|
|
120
117
|
/** Decrypt a single MTProto message using session's keys */
|
|
121
118
|
decryptMessage(data: Uint8Array, callback: Parameters<AuthKey['decryptMessage']>[2]): void;
|
|
122
119
|
writeMessage(writer: TlBinaryWriter, content: tl.TlObject | mtp.TlObject | Uint8Array, isContentRelated?: boolean): Long;
|
|
123
|
-
onTransportFlood(callback: () => void): void;
|
|
124
120
|
resetLastPing(withTime?: boolean): void;
|
|
125
121
|
addToChain(chainId: string | number, msgId: Long): Long | undefined;
|
|
126
122
|
removeFromChain(chainId: string | number, msgId: Long): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Deferred, Deque, LruMap, LruSet
|
|
1
|
+
import { Deferred, timers, Deque, LruMap, LruSet } from '@fuman/utils';
|
|
2
2
|
import { TlBinaryWriter, TlReaderMap, TlWriterMap } from '@mtcute/tl-runtime';
|
|
3
3
|
import { mtp, tl } from '../tl/index.js';
|
|
4
4
|
import { ICryptoProvider, Logger, LongMap, SortedArray } from '../utils/index.js';
|
|
@@ -94,9 +94,6 @@ export declare class MtprotoSession {
|
|
|
94
94
|
lastSessionCreatedUid: Long;
|
|
95
95
|
initConnectionCalled: boolean;
|
|
96
96
|
authorizationPending: boolean;
|
|
97
|
-
next429Timeout: number;
|
|
98
|
-
current429Timeout?: timers.Timer;
|
|
99
|
-
next429ResetTimeout?: timers.Timer;
|
|
100
97
|
constructor(_crypto: ICryptoProvider, log: Logger, _readerMap: TlReaderMap, _writerMap: TlWriterMap, _salts: ServerSaltManager);
|
|
101
98
|
get hasPendingMessages(): boolean;
|
|
102
99
|
/**
|
|
@@ -120,7 +117,6 @@ export declare class MtprotoSession {
|
|
|
120
117
|
/** Decrypt a single MTProto message using session's keys */
|
|
121
118
|
decryptMessage(data: Uint8Array, callback: Parameters<AuthKey['decryptMessage']>[2]): void;
|
|
122
119
|
writeMessage(writer: TlBinaryWriter, content: tl.TlObject | mtp.TlObject | Uint8Array, isContentRelated?: boolean): Long;
|
|
123
|
-
onTransportFlood(callback: () => void): void;
|
|
124
120
|
resetLastPing(withTime?: boolean): void;
|
|
125
121
|
addToChain(chainId: string | number, msgId: Long): Long | undefined;
|
|
126
122
|
removeFromChain(chainId: string | number, msgId: Long): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LruSet, LruMap, Deque
|
|
1
|
+
import { LruSet, LruMap, Deque } from "@fuman/utils";
|
|
2
2
|
import { TlSerializationCounter } from "@mtcute/tl-runtime";
|
|
3
3
|
import Long from "long";
|
|
4
4
|
import { randomLong, LongSet, LongMap, compareLongs } from "../utils/long-utils.js";
|
|
@@ -29,6 +29,7 @@ class MtprotoSession {
|
|
|
29
29
|
// recent msg ids
|
|
30
30
|
recentOutgoingMsgIds = new LruSet(1e3, LongSet);
|
|
31
31
|
recentIncomingMsgIds = new LruSet(1e3, LongSet);
|
|
32
|
+
// <deno-tsignore>
|
|
32
33
|
recentStateRequests = new LruMap(1e3, LongMap);
|
|
33
34
|
// queues
|
|
34
35
|
queuedRpc = new Deque();
|
|
@@ -53,9 +54,6 @@ class MtprotoSession {
|
|
|
53
54
|
lastSessionCreatedUid = Long.ZERO;
|
|
54
55
|
initConnectionCalled = false;
|
|
55
56
|
authorizationPending = false;
|
|
56
|
-
next429Timeout = 1e3;
|
|
57
|
-
current429Timeout;
|
|
58
|
-
next429ResetTimeout;
|
|
59
57
|
get hasPendingMessages() {
|
|
60
58
|
return Boolean(
|
|
61
59
|
this.queuedRpc.length || this.queuedAcks.length || this.queuedStateReq.length || this.queuedResendReq.length || this.queuedCancelReq.length
|
|
@@ -68,7 +66,6 @@ class MtprotoSession {
|
|
|
68
66
|
if (withAuthKey) {
|
|
69
67
|
this.resetAuthKey();
|
|
70
68
|
}
|
|
71
|
-
timers.clearTimeout(this.current429Timeout);
|
|
72
69
|
this.resetState(withAuthKey);
|
|
73
70
|
}
|
|
74
71
|
resetAuthKey() {
|
|
@@ -190,23 +187,6 @@ class MtprotoSession {
|
|
|
190
187
|
else writer.object(content);
|
|
191
188
|
return messageId;
|
|
192
189
|
}
|
|
193
|
-
onTransportFlood(callback) {
|
|
194
|
-
if (this.current429Timeout) return;
|
|
195
|
-
this.resetLastPing(true);
|
|
196
|
-
const timeout = this.next429Timeout;
|
|
197
|
-
this.next429Timeout = Math.min(this.next429Timeout * 2, 32e3);
|
|
198
|
-
timers.clearTimeout(this.current429Timeout);
|
|
199
|
-
timers.clearTimeout(this.next429ResetTimeout);
|
|
200
|
-
this.current429Timeout = timers.setTimeout(() => {
|
|
201
|
-
this.current429Timeout = void 0;
|
|
202
|
-
callback();
|
|
203
|
-
}, timeout);
|
|
204
|
-
this.next429ResetTimeout = timers.setTimeout(() => {
|
|
205
|
-
this.next429ResetTimeout = void 0;
|
|
206
|
-
this.next429Timeout = 1e3;
|
|
207
|
-
}, 6e4);
|
|
208
|
-
this.log.debug("transport flood, waiting for %d ms before proceeding", timeout);
|
|
209
|
-
}
|
|
210
190
|
resetLastPing(withTime = false) {
|
|
211
191
|
if (withTime) this.lastPingTime = 0;
|
|
212
192
|
if (!this.lastPingMsgId.isZero()) {
|
|
@@ -16,6 +16,24 @@ class MultiSessionConnection {
|
|
|
16
16
|
this._enforcePfs = _count > 1 && params.isMainConnection;
|
|
17
17
|
this._updateConnections();
|
|
18
18
|
}
|
|
19
|
+
static _pickConnection(loads, ready) {
|
|
20
|
+
let min = Infinity;
|
|
21
|
+
let minIdx = 0;
|
|
22
|
+
let foundReady = false;
|
|
23
|
+
for (let i = 0; i < loads.length; i++) {
|
|
24
|
+
const isReady = ready[i];
|
|
25
|
+
if (foundReady && !isReady) continue;
|
|
26
|
+
if (isReady && !foundReady) {
|
|
27
|
+
foundReady = true;
|
|
28
|
+
min = Infinity;
|
|
29
|
+
}
|
|
30
|
+
if (loads[i] < min) {
|
|
31
|
+
min = loads[i];
|
|
32
|
+
minIdx = i;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return minIdx;
|
|
36
|
+
}
|
|
19
37
|
_log;
|
|
20
38
|
_enforcePfs = false;
|
|
21
39
|
// NB: dont forget to update .reset()
|
|
@@ -103,10 +121,6 @@ class MultiSessionConnection {
|
|
|
103
121
|
conn.onUsable.add(() => this.onUsable.emit(i));
|
|
104
122
|
conn.onWait.add(() => this.onWait.emit(i));
|
|
105
123
|
conn.onRequestAuth.add(() => this.onRequestAuth.emit(i));
|
|
106
|
-
conn.onFloodDone.add(() => {
|
|
107
|
-
this._log.debug("received flood-done from connection %d", i);
|
|
108
|
-
this._connections.forEach((it) => it.flushWhenIdle());
|
|
109
|
-
});
|
|
110
124
|
this._connections.push(conn);
|
|
111
125
|
if (this._authKey !== null) conn._session._authKey.setup(this._authKey);
|
|
112
126
|
if (connect && !enforcePfsChanged) conn.connect();
|
|
@@ -127,19 +141,15 @@ class MultiSessionConnection {
|
|
|
127
141
|
this.onRequestAuth.clear();
|
|
128
142
|
this._destroyed = true;
|
|
129
143
|
}
|
|
130
|
-
_nextConnection = 0;
|
|
131
144
|
sendRpc(request, timeout, abortSignal, chainId) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return this._connections[minIdx].sendRpc(request, timeout, abortSignal, chainId);
|
|
145
|
+
const loads = this._connections.map((c) => c._session.queuedRpc.length + c._session.pendingMessages.size);
|
|
146
|
+
const ready = this._connections.map((c) => c.isConnected);
|
|
147
|
+
return this._connections[MultiSessionConnection._pickConnection(loads, ready)].sendRpc(
|
|
148
|
+
request,
|
|
149
|
+
timeout,
|
|
150
|
+
abortSignal,
|
|
151
|
+
chainId
|
|
152
|
+
);
|
|
143
153
|
}
|
|
144
154
|
connect() {
|
|
145
155
|
for (const conn of this._connections) {
|