chore: avoid using {}, use Maps instead
This commit is contained in:
parent
80d4c59c69
commit
5a3b101c9f
27 changed files with 541 additions and 729 deletions
|
@ -4019,9 +4019,9 @@ export class TelegramClient extends BaseTelegramClient {
|
||||||
protected _userId: number | null
|
protected _userId: number | null
|
||||||
protected _isBot: boolean
|
protected _isBot: boolean
|
||||||
protected _selfUsername: string | null
|
protected _selfUsername: string | null
|
||||||
protected _pendingConversations: Record<number, Conversation[]>
|
protected _pendingConversations: Map<number, Conversation[]>
|
||||||
protected _hasConversations: boolean
|
protected _hasConversations: boolean
|
||||||
protected _parseModes: Record<string, IMessageEntityParser>
|
protected _parseModes: Map<string, IMessageEntityParser>
|
||||||
protected _defaultParseMode: string | null
|
protected _defaultParseMode: string | null
|
||||||
protected _updatesLoopActive: boolean
|
protected _updatesLoopActive: boolean
|
||||||
protected _updatesLoopCv: ConditionVariable
|
protected _updatesLoopCv: ConditionVariable
|
||||||
|
@ -4044,8 +4044,8 @@ export class TelegramClient extends BaseTelegramClient {
|
||||||
protected _oldSeq?: number
|
protected _oldSeq?: number
|
||||||
protected _selfChanged: boolean
|
protected _selfChanged: boolean
|
||||||
protected _catchUpChannels?: boolean
|
protected _catchUpChannels?: boolean
|
||||||
protected _cpts: Record<number, number>
|
protected _cpts: Map<number, number>
|
||||||
protected _cptsMod: Record<number, number>
|
protected _cptsMod: Map<number, number>
|
||||||
protected _updsLog: Logger
|
protected _updsLog: Logger
|
||||||
constructor(opts: BaseTelegramClientOptions) {
|
constructor(opts: BaseTelegramClientOptions) {
|
||||||
super(opts)
|
super(opts)
|
||||||
|
@ -4053,9 +4053,9 @@ export class TelegramClient extends BaseTelegramClient {
|
||||||
this._isBot = false
|
this._isBot = false
|
||||||
this._selfUsername = null
|
this._selfUsername = null
|
||||||
this.log.prefix = '[USER N/A] '
|
this.log.prefix = '[USER N/A] '
|
||||||
this._pendingConversations = {}
|
this._pendingConversations = new Map()
|
||||||
this._hasConversations = false
|
this._hasConversations = false
|
||||||
this._parseModes = {}
|
this._parseModes = new Map()
|
||||||
this._defaultParseMode = null
|
this._defaultParseMode = null
|
||||||
this._updatesLoopActive = false
|
this._updatesLoopActive = false
|
||||||
this._updatesLoopCv = new ConditionVariable()
|
this._updatesLoopCv = new ConditionVariable()
|
||||||
|
@ -4083,10 +4083,10 @@ export class TelegramClient extends BaseTelegramClient {
|
||||||
|
|
||||||
// channel PTS are not loaded immediately, and instead are cached here
|
// channel PTS are not loaded immediately, and instead are cached here
|
||||||
// after the first time they were retrieved from the storage.
|
// after the first time they were retrieved from the storage.
|
||||||
this._cpts = {}
|
this._cpts = new Map()
|
||||||
// modified channel pts, to avoid unnecessary
|
// modified channel pts, to avoid unnecessary
|
||||||
// DB calls for not modified cpts
|
// DB calls for not modified cpts
|
||||||
this._cptsMod = {}
|
this._cptsMod = new Map()
|
||||||
|
|
||||||
this._selfChanged = false
|
this._selfChanged = false
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,13 @@ import { Conversation, Message } from '../../types'
|
||||||
|
|
||||||
// @extension
|
// @extension
|
||||||
interface ConversationsState {
|
interface ConversationsState {
|
||||||
_pendingConversations: Record<number, Conversation[]>
|
_pendingConversations: Map<number, Conversation[]>
|
||||||
_hasConversations: boolean
|
_hasConversations: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// @initialize
|
// @initialize
|
||||||
function _initializeConversation(this: TelegramClient) {
|
function _initializeConversation(this: TelegramClient) {
|
||||||
this._pendingConversations = {}
|
this._pendingConversations = new Map()
|
||||||
this._hasConversations = false
|
this._hasConversations = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ export function _pushConversationMessage(
|
||||||
const chatId = getMarkedPeerId(msg.raw.peerId)
|
const chatId = getMarkedPeerId(msg.raw.peerId)
|
||||||
const msgId = msg.raw.id
|
const msgId = msg.raw.id
|
||||||
|
|
||||||
this._pendingConversations[chatId].forEach((conv) => {
|
this._pendingConversations.get(chatId)?.forEach((conv) => {
|
||||||
conv['_lastMessage'] = msgId
|
conv['_lastMessage'] = msgId
|
||||||
if (incoming) conv['_lastReceivedMessage'] = msgId
|
if (incoming) conv['_lastReceivedMessage'] = msgId
|
||||||
})
|
})
|
||||||
|
|
|
@ -30,11 +30,13 @@ export async function _parseEntities(
|
||||||
// either explicitly disabled or no available parser
|
// either explicitly disabled or no available parser
|
||||||
if (!mode) return [text, []]
|
if (!mode) return [text, []]
|
||||||
|
|
||||||
if (!(mode in this._parseModes)) {
|
const modeImpl = this._parseModes.get(mode)
|
||||||
|
|
||||||
|
if (!modeImpl) {
|
||||||
throw new MtClientError(`Parse mode ${mode} is not registered.`)
|
throw new MtClientError(`Parse mode ${mode} is not registered.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
[text, entities] = this._parseModes[mode].parse(text)
|
[text, entities] = modeImpl.parse(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace mentionName entities with input ones
|
// replace mentionName entities with input ones
|
||||||
|
|
|
@ -230,13 +230,13 @@ export async function sendText(
|
||||||
|
|
||||||
switch (cached._) {
|
switch (cached._) {
|
||||||
case 'user':
|
case 'user':
|
||||||
peers.users[cached.id] = cached
|
peers.users.set(cached.id, cached)
|
||||||
break
|
break
|
||||||
case 'chat':
|
case 'chat':
|
||||||
case 'chatForbidden':
|
case 'chatForbidden':
|
||||||
case 'channel':
|
case 'channel':
|
||||||
case 'channelForbidden':
|
case 'channelForbidden':
|
||||||
peers.chats[cached.id] = cached
|
peers.chats.set(cached.id, cached)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw new MtTypeAssertionError(
|
throw new MtTypeAssertionError(
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { IMessageEntityParser } from '../../types'
|
||||||
|
|
||||||
// @extension
|
// @extension
|
||||||
interface ParseModesExtension {
|
interface ParseModesExtension {
|
||||||
_parseModes: Record<string, IMessageEntityParser>
|
_parseModes: Map<string, IMessageEntityParser>
|
||||||
_defaultParseMode: string | null
|
_defaultParseMode: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// @initialize
|
// @initialize
|
||||||
function _initializeParseModes(this: TelegramClient) {
|
function _initializeParseModes(this: TelegramClient) {
|
||||||
this._parseModes = {}
|
this._parseModes = new Map()
|
||||||
this._defaultParseMode = null
|
this._defaultParseMode = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,12 @@ export function registerParseMode(
|
||||||
): void {
|
): void {
|
||||||
const name = parseMode.name
|
const name = parseMode.name
|
||||||
|
|
||||||
if (name in this._parseModes) {
|
if (this._parseModes.has(name)) {
|
||||||
throw new MtClientError(
|
throw new MtClientError(
|
||||||
`Parse mode ${name} is already registered. Unregister it first!`,
|
`Parse mode ${name} is already registered. Unregister it first!`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
this._parseModes[name] = parseMode
|
this._parseModes.set(name, parseMode)
|
||||||
|
|
||||||
if (!this._defaultParseMode) {
|
if (!this._defaultParseMode) {
|
||||||
this._defaultParseMode = name
|
this._defaultParseMode = name
|
||||||
|
@ -38,10 +38,11 @@ export function registerParseMode(
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function unregisterParseMode(this: TelegramClient, name: string): void {
|
export function unregisterParseMode(this: TelegramClient, name: string): void {
|
||||||
delete this._parseModes[name]
|
this._parseModes.delete(name)
|
||||||
|
|
||||||
if (this._defaultParseMode === name) {
|
if (this._defaultParseMode === name) {
|
||||||
this._defaultParseMode = Object.keys(this._defaultParseMode)[0] ?? null
|
const [first] = this._parseModes.keys()
|
||||||
|
this._defaultParseMode = first ?? null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,16 +59,20 @@ export function getParseMode(
|
||||||
name?: string | null,
|
name?: string | null,
|
||||||
): IMessageEntityParser {
|
): IMessageEntityParser {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
if (!this._defaultParseMode) { throw new MtClientError('There is no default parse mode') }
|
if (!this._defaultParseMode) {
|
||||||
|
throw new MtClientError('There is no default parse mode')
|
||||||
|
}
|
||||||
|
|
||||||
name = this._defaultParseMode
|
name = this._defaultParseMode
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(name in this._parseModes)) {
|
const mode = this._parseModes.get(name)
|
||||||
|
|
||||||
|
if (!mode) {
|
||||||
throw new MtClientError(`Parse mode ${name} is not registered.`)
|
throw new MtClientError(`Parse mode ${name} is not registered.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._parseModes[name]
|
return mode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,7 +83,7 @@ export function getParseMode(
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export function setDefaultParseMode(this: TelegramClient, name: string): void {
|
export function setDefaultParseMode(this: TelegramClient, name: string): void {
|
||||||
if (!(name in this._parseModes)) {
|
if (!this._parseModes.has(name)) {
|
||||||
throw new MtClientError(`Parse mode ${name} is not registered.`)
|
throw new MtClientError(`Parse mode ${name} is not registered.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,8 +78,8 @@ interface UpdatesState {
|
||||||
// usually set in start() method based on `catchUp` param
|
// usually set in start() method based on `catchUp` param
|
||||||
_catchUpChannels?: boolean
|
_catchUpChannels?: boolean
|
||||||
|
|
||||||
_cpts: Record<number, number>
|
_cpts: Map<number, number>
|
||||||
_cptsMod: Record<number, number>
|
_cptsMod: Map<number, number>
|
||||||
|
|
||||||
_updsLog: Logger
|
_updsLog: Logger
|
||||||
}
|
}
|
||||||
|
@ -112,10 +112,10 @@ function _initializeUpdates(this: TelegramClient) {
|
||||||
|
|
||||||
// channel PTS are not loaded immediately, and instead are cached here
|
// channel PTS are not loaded immediately, and instead are cached here
|
||||||
// after the first time they were retrieved from the storage.
|
// after the first time they were retrieved from the storage.
|
||||||
this._cpts = {}
|
this._cpts = new Map()
|
||||||
// modified channel pts, to avoid unnecessary
|
// modified channel pts, to avoid unnecessary
|
||||||
// DB calls for not modified cpts
|
// DB calls for not modified cpts
|
||||||
this._cptsMod = {}
|
this._cptsMod = new Map()
|
||||||
|
|
||||||
this._selfChanged = false
|
this._selfChanged = false
|
||||||
|
|
||||||
|
@ -358,7 +358,7 @@ export async function _saveStorage(
|
||||||
this._oldSeq = this._seq
|
this._oldSeq = this._seq
|
||||||
|
|
||||||
await this.storage.setManyChannelPts(this._cptsMod)
|
await this.storage.setManyChannelPts(this._cptsMod)
|
||||||
this._cptsMod = {}
|
this._cptsMod.clear()
|
||||||
}
|
}
|
||||||
if (this._userId !== null && this._selfChanged) {
|
if (this._userId !== null && this._selfChanged) {
|
||||||
await this.storage.setSelf({
|
await this.storage.setSelf({
|
||||||
|
@ -472,33 +472,33 @@ async function _replaceMinPeers(
|
||||||
this: TelegramClient,
|
this: TelegramClient,
|
||||||
peers: PeersIndex,
|
peers: PeersIndex,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
for (const key in peers.users) {
|
for (const [key, user_] of peers.users) {
|
||||||
const user = peers.users[key] as Exclude<tl.TypeUser, tl.RawUserEmpty>
|
const user = user_ as Exclude<tl.TypeUser, tl.RawUserEmpty>
|
||||||
|
|
||||||
if (user.min) {
|
if (user.min) {
|
||||||
const cached = await this.storage.getFullPeerById(user.id)
|
const cached = await this.storage.getFullPeerById(user.id)
|
||||||
if (!cached) return false
|
if (!cached) return false
|
||||||
peers.users[key] = cached as tl.TypeUser
|
peers.users.set(key, cached as tl.TypeUser)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in peers.chats) {
|
for (const [key, chat_] of peers.chats) {
|
||||||
const c = peers.chats[key] as Extract<tl.TypeChat, { min?: boolean }>
|
const chat = chat_ as Extract<tl.TypeChat, { min?: boolean }>
|
||||||
|
|
||||||
if (c.min) {
|
if (chat.min) {
|
||||||
let id: number
|
let id: number
|
||||||
|
|
||||||
switch (c._) {
|
switch (chat._) {
|
||||||
case 'channel':
|
case 'channel':
|
||||||
id = toggleChannelIdMark(c.id)
|
id = toggleChannelIdMark(chat.id)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
id = -c.id
|
id = -chat.id
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = await this.storage.getFullPeerById(id)
|
const cached = await this.storage.getFullPeerById(id)
|
||||||
if (!cached) return false
|
if (!cached) return false
|
||||||
peers.chats[key] = cached as tl.TypeChat
|
peers.chats.set(key, cached as tl.TypeChat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -527,9 +527,9 @@ async function _fetchPeersForShort(
|
||||||
if (!cached) return false
|
if (!cached) return false
|
||||||
|
|
||||||
if (marked > 0) {
|
if (marked > 0) {
|
||||||
peers.users[bare] = cached as tl.TypeUser
|
peers.users.set(bare, cached as tl.TypeUser)
|
||||||
} else {
|
} else {
|
||||||
peers.chats[bare] = cached as tl.TypeChat
|
peers.chats.set(bare, cached as tl.TypeChat)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -787,7 +787,7 @@ async function _fetchChannelDifference(
|
||||||
fallbackPts?: number,
|
fallbackPts?: number,
|
||||||
force = false,
|
force = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let _pts: number | null | undefined = this._cpts[channelId]
|
let _pts: number | null | undefined = this._cpts.get(channelId)
|
||||||
|
|
||||||
if (!_pts && this._catchUpChannels) {
|
if (!_pts && this._catchUpChannels) {
|
||||||
_pts = await this.storage.getChannelPts(channelId)
|
_pts = await this.storage.getChannelPts(channelId)
|
||||||
|
@ -929,36 +929,39 @@ async function _fetchChannelDifference(
|
||||||
if (diff.final) break
|
if (diff.final) break
|
||||||
}
|
}
|
||||||
|
|
||||||
this._cpts[channelId] = pts
|
this._cpts.set(channelId, pts)
|
||||||
this._cptsMod[channelId] = pts
|
this._cptsMod.set(channelId, pts)
|
||||||
}
|
}
|
||||||
|
|
||||||
function _fetchChannelDifferenceLater(
|
function _fetchChannelDifferenceLater(
|
||||||
this: TelegramClient,
|
this: TelegramClient,
|
||||||
requestedDiff: Record<number, Promise<void>>,
|
requestedDiff: Map<number, Promise<void>>,
|
||||||
channelId: number,
|
channelId: number,
|
||||||
fallbackPts?: number,
|
fallbackPts?: number,
|
||||||
force = false,
|
force = false,
|
||||||
): void {
|
): void {
|
||||||
if (!(channelId in requestedDiff)) {
|
if (!requestedDiff.has(channelId)) {
|
||||||
requestedDiff[channelId] = _fetchChannelDifference
|
requestedDiff.set(
|
||||||
.call(this, channelId, fallbackPts, force)
|
channelId,
|
||||||
.catch((err) => {
|
_fetchChannelDifference
|
||||||
this._updsLog.warn(
|
.call(this, channelId, fallbackPts, force)
|
||||||
'error fetching difference for %d: %s',
|
.catch((err) => {
|
||||||
channelId,
|
this._updsLog.warn(
|
||||||
err,
|
'error fetching difference for %d: %s',
|
||||||
)
|
channelId,
|
||||||
})
|
err,
|
||||||
.then(() => {
|
)
|
||||||
delete requestedDiff[channelId]
|
})
|
||||||
})
|
.then(() => {
|
||||||
|
requestedDiff.delete(channelId)
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _fetchDifference(
|
async function _fetchDifference(
|
||||||
this: TelegramClient,
|
this: TelegramClient,
|
||||||
requestedDiff: Record<number, Promise<void>>,
|
requestedDiff: Map<number, Promise<void>>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const diff = await this.call({
|
const diff = await this.call({
|
||||||
|
@ -1072,24 +1075,30 @@ async function _fetchDifference(
|
||||||
|
|
||||||
function _fetchDifferenceLater(
|
function _fetchDifferenceLater(
|
||||||
this: TelegramClient,
|
this: TelegramClient,
|
||||||
requestedDiff: Record<number, Promise<void>>,
|
requestedDiff: Map<number, Promise<void>>,
|
||||||
): void {
|
): void {
|
||||||
if (!(0 in requestedDiff)) {
|
if (!requestedDiff.has(0)) {
|
||||||
requestedDiff[0] = _fetchDifference
|
requestedDiff.set(
|
||||||
.call(this, requestedDiff)
|
0,
|
||||||
.catch((err) => {
|
_fetchDifference
|
||||||
this._updsLog.warn('error fetching common difference: %s', err)
|
.call(this, requestedDiff)
|
||||||
})
|
.catch((err) => {
|
||||||
.then(() => {
|
this._updsLog.warn(
|
||||||
delete requestedDiff[0]
|
'error fetching common difference: %s',
|
||||||
})
|
err,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
requestedDiff.delete(0)
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _onUpdate(
|
async function _onUpdate(
|
||||||
this: TelegramClient,
|
this: TelegramClient,
|
||||||
pending: PendingUpdate,
|
pending: PendingUpdate,
|
||||||
requestedDiff: Record<number, Promise<void>>,
|
requestedDiff: Map<number, Promise<void>>,
|
||||||
postponed = false,
|
postponed = false,
|
||||||
unordered = false,
|
unordered = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -1153,7 +1162,7 @@ async function _onUpdate(
|
||||||
|
|
||||||
if (pending.pts) {
|
if (pending.pts) {
|
||||||
const localPts = pending.channelId ?
|
const localPts = pending.channelId ?
|
||||||
this._cpts[pending.channelId] :
|
this._cpts.get(pending.channelId) :
|
||||||
this._pts
|
this._pts
|
||||||
|
|
||||||
if (localPts && pending.ptsBefore !== localPts) {
|
if (localPts && pending.ptsBefore !== localPts) {
|
||||||
|
@ -1179,8 +1188,8 @@ async function _onUpdate(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (pending.channelId) {
|
if (pending.channelId) {
|
||||||
this._cpts[pending.channelId] = pending.pts!
|
this._cpts.set(pending.channelId, pending.pts)
|
||||||
this._cptsMod[pending.channelId] = pending.pts!
|
this._cptsMod.set(pending.channelId, pending.pts)
|
||||||
} else {
|
} else {
|
||||||
this._pts = pending.pts
|
this._pts = pending.pts
|
||||||
}
|
}
|
||||||
|
@ -1274,7 +1283,7 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
|
||||||
this._pendingUnorderedUpdates.length,
|
this._pendingUnorderedUpdates.length,
|
||||||
)
|
)
|
||||||
|
|
||||||
const requestedDiff: Record<number, Promise<void>> = {}
|
const requestedDiff = new Map<number, Promise<void>>()
|
||||||
|
|
||||||
// first process pending containers
|
// first process pending containers
|
||||||
while (this._pendingUpdateContainers.length) {
|
while (this._pendingUpdateContainers.length) {
|
||||||
|
@ -1497,8 +1506,8 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
|
||||||
let localPts: number | null = null
|
let localPts: number | null = null
|
||||||
|
|
||||||
if (!pending.channelId) localPts = this._pts!
|
if (!pending.channelId) localPts = this._pts!
|
||||||
else if (pending.channelId in this._cpts) {
|
else if (this._cpts.has(pending.channelId)) {
|
||||||
localPts = this._cpts[pending.channelId]
|
localPts = this._cpts.get(pending.channelId)!
|
||||||
} else if (this._catchUpChannels) {
|
} else if (this._catchUpChannels) {
|
||||||
// only load stored channel pts in case
|
// only load stored channel pts in case
|
||||||
// the user has enabled catching up.
|
// the user has enabled catching up.
|
||||||
|
@ -1512,7 +1521,8 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (saved) {
|
if (saved) {
|
||||||
this._cpts[pending.channelId] = localPts = saved
|
this._cpts.set(pending.channelId, saved)
|
||||||
|
localPts = saved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1581,8 +1591,8 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
|
||||||
let localPts
|
let localPts
|
||||||
|
|
||||||
if (!pending.channelId) localPts = this._pts!
|
if (!pending.channelId) localPts = this._pts!
|
||||||
else if (pending.channelId in this._cpts) {
|
else if (this._cpts.has(pending.channelId)) {
|
||||||
localPts = this._cpts[pending.channelId]
|
localPts = this._cpts.get(pending.channelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// channel pts from storage will be available because we loaded it earlier
|
// channel pts from storage will be available because we loaded it earlier
|
||||||
|
@ -1750,26 +1760,23 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for all pending diffs to load
|
// wait for all pending diffs to load
|
||||||
let pendingDiffs = Object.values(requestedDiff)
|
while (requestedDiff.size) {
|
||||||
|
|
||||||
while (pendingDiffs.length) {
|
|
||||||
log.debug(
|
log.debug(
|
||||||
'waiting for %d pending diffs before processing unordered: %j',
|
'waiting for %d pending diffs before processing unordered: %J',
|
||||||
pendingDiffs.length,
|
requestedDiff.size,
|
||||||
Object.keys(requestedDiff),
|
requestedDiff.keys(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// is this necessary?
|
// is this necessary?
|
||||||
// this.primaryConnection._flushSendQueue()
|
// this.primaryConnection._flushSendQueue()
|
||||||
|
|
||||||
await Promise.all(pendingDiffs)
|
await Promise.all([...requestedDiff.values()])
|
||||||
|
|
||||||
// diff results may as well contain new diffs to be requested
|
// diff results may as well contain new diffs to be requested
|
||||||
pendingDiffs = Object.values(requestedDiff)
|
|
||||||
log.debug(
|
log.debug(
|
||||||
'pending diffs awaited, new diffs requested: %d (%j)',
|
'pending diffs awaited, new diffs requested: %d (%J)',
|
||||||
pendingDiffs.length,
|
requestedDiff.size,
|
||||||
Object.keys(requestedDiff),
|
requestedDiff.keys(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1783,26 +1790,23 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
|
||||||
// onUpdate may also call getDiff in some cases, so we also need to check
|
// onUpdate may also call getDiff in some cases, so we also need to check
|
||||||
// diff may also contain new updates, which will be processed in the next tick,
|
// diff may also contain new updates, which will be processed in the next tick,
|
||||||
// but we don't want to postpone diff fetching
|
// but we don't want to postpone diff fetching
|
||||||
pendingDiffs = Object.values(requestedDiff)
|
while (requestedDiff.size) {
|
||||||
|
|
||||||
while (pendingDiffs.length) {
|
|
||||||
log.debug(
|
log.debug(
|
||||||
'waiting for %d pending diffs after processing unordered: %j',
|
'waiting for %d pending diffs after processing unordered: %J',
|
||||||
pendingDiffs.length,
|
requestedDiff.size,
|
||||||
Object.keys(requestedDiff),
|
requestedDiff.keys(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// is this necessary?
|
// is this necessary?
|
||||||
// this.primaryConnection._flushSendQueue()
|
// this.primaryConnection._flushSendQueue()
|
||||||
|
|
||||||
await Promise.all(pendingDiffs)
|
await Promise.all([...requestedDiff.values()])
|
||||||
|
|
||||||
// diff results may as well contain new diffs to be requested
|
// diff results may as well contain new diffs to be requested
|
||||||
pendingDiffs = Object.values(requestedDiff)
|
|
||||||
log.debug(
|
log.debug(
|
||||||
'pending diffs awaited, new diffs requested: %d (%j)',
|
'pending diffs awaited, new diffs requested: %d (%j)',
|
||||||
pendingDiffs.length,
|
requestedDiff.size,
|
||||||
Object.keys(requestedDiff),
|
requestedDiff.keys(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,9 @@ export async function resolvePeer(
|
||||||
} else {
|
} else {
|
||||||
// username
|
// username
|
||||||
if (!force) {
|
if (!force) {
|
||||||
const fromStorage = await this.storage.getPeerByUsername(peerId)
|
const fromStorage = await this.storage.getPeerByUsername(
|
||||||
|
peerId.toLowerCase(),
|
||||||
|
)
|
||||||
if (fromStorage) return fromStorage
|
if (fromStorage) return fromStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
/* eslint-disable dot-notation */
|
/* eslint-disable dot-notation */
|
||||||
import { AsyncLock, Deque, getMarkedPeerId, MaybeAsync } from '@mtcute/core'
|
import { AsyncLock, Deque, getMarkedPeerId, MaybeAsync } from '@mtcute/core'
|
||||||
import {
|
import { ControllablePromise, createControllablePromise } from '@mtcute/core'
|
||||||
ControllablePromise,
|
|
||||||
createControllablePromise,
|
|
||||||
} from '@mtcute/core/src/utils/controllable-promise'
|
|
||||||
import { tl } from '@mtcute/tl'
|
import { tl } from '@mtcute/tl'
|
||||||
|
|
||||||
import { TelegramClient } from '../client'
|
import { TelegramClient } from '../client'
|
||||||
|
@ -43,10 +40,10 @@ export class Conversation {
|
||||||
private _pendingNewMessages = new Deque<Message>()
|
private _pendingNewMessages = new Deque<Message>()
|
||||||
private _lock = new AsyncLock()
|
private _lock = new AsyncLock()
|
||||||
|
|
||||||
private _pendingEditMessage: Record<number, QueuedHandler<Message>> = {}
|
private _pendingEditMessage: Map<number, QueuedHandler<Message>> = new Map()
|
||||||
private _recentEdits = new Deque<Message>(10)
|
private _recentEdits = new Deque<Message>(10)
|
||||||
|
|
||||||
private _pendingRead: Record<number, QueuedHandler<void>> = {}
|
private _pendingRead: Map<number, QueuedHandler<void>> = new Map()
|
||||||
|
|
||||||
constructor(readonly client: TelegramClient, readonly chat: InputPeerLike) {
|
constructor(readonly client: TelegramClient, readonly chat: InputPeerLike) {
|
||||||
this._onNewMessage = this._onNewMessage.bind(this)
|
this._onNewMessage = this._onNewMessage.bind(this)
|
||||||
|
@ -112,10 +109,10 @@ export class Conversation {
|
||||||
this.client.on('edit_message', this._onEditMessage)
|
this.client.on('edit_message', this._onEditMessage)
|
||||||
this.client.on('history_read', this._onHistoryRead)
|
this.client.on('history_read', this._onHistoryRead)
|
||||||
|
|
||||||
if (!(this._chatId in this.client['_pendingConversations'])) {
|
if (this.client['_pendingConversations'].has(this._chatId)) {
|
||||||
this.client['_pendingConversations'][this._chatId] = []
|
this.client['_pendingConversations'].set(this._chatId, [])
|
||||||
}
|
}
|
||||||
this.client['_pendingConversations'][this._chatId].push(this)
|
this.client['_pendingConversations'].get(this._chatId)!.push(this)
|
||||||
this.client['_hasConversations'] = true
|
this.client['_hasConversations'] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,25 +126,26 @@ export class Conversation {
|
||||||
this.client.off('edit_message', this._onEditMessage)
|
this.client.off('edit_message', this._onEditMessage)
|
||||||
this.client.off('history_read', this._onHistoryRead)
|
this.client.off('history_read', this._onHistoryRead)
|
||||||
|
|
||||||
const pending = this.client['_pendingConversations']
|
const pending = this.client['_pendingConversations'].get(this._chatId)
|
||||||
|
const pendingIdx = pending?.indexOf(this) ?? -1
|
||||||
|
|
||||||
const idx = pending[this._chatId].indexOf(this)
|
if (pendingIdx > -1) {
|
||||||
|
|
||||||
if (idx > -1) {
|
|
||||||
// just in case
|
// just in case
|
||||||
pending[this._chatId].splice(idx, 1)
|
pending!.splice(pendingIdx, 1)
|
||||||
}
|
}
|
||||||
if (!pending[this._chatId].length) {
|
if (pending && !pending.length) {
|
||||||
delete pending[this._chatId]
|
this.client['_pendingConversations'].delete(this._chatId)
|
||||||
}
|
}
|
||||||
this.client['_hasConversations'] = Object.keys(pending).length > 0
|
this.client['_hasConversations'] = Boolean(
|
||||||
|
this.client['_pendingConversations'].size,
|
||||||
|
)
|
||||||
|
|
||||||
// reset pending status
|
// reset pending status
|
||||||
this._queuedNewMessage.clear()
|
this._queuedNewMessage.clear()
|
||||||
this._pendingNewMessages.clear()
|
this._pendingNewMessages.clear()
|
||||||
this._pendingEditMessage = {}
|
this._pendingEditMessage.clear()
|
||||||
this._recentEdits.clear()
|
this._recentEdits.clear()
|
||||||
this._pendingRead = {}
|
this._pendingRead.clear()
|
||||||
|
|
||||||
this._started = false
|
this._started = false
|
||||||
}
|
}
|
||||||
|
@ -424,15 +422,15 @@ export class Conversation {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
promise.reject(new MtTimeoutError(timeout))
|
promise.reject(new MtTimeoutError(timeout))
|
||||||
delete this._pendingEditMessage[msgId]
|
this._pendingEditMessage.delete(msgId)
|
||||||
}, timeout)
|
}, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
this._pendingEditMessage[msgId] = {
|
this._pendingEditMessage.set(msgId, {
|
||||||
promise,
|
promise,
|
||||||
check: filter,
|
check: filter,
|
||||||
timeout: timer,
|
timeout: timer,
|
||||||
}
|
})
|
||||||
|
|
||||||
this._processRecentEdits()
|
this._processRecentEdits()
|
||||||
|
|
||||||
|
@ -476,14 +474,14 @@ export class Conversation {
|
||||||
if (timeout !== null) {
|
if (timeout !== null) {
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
promise.reject(new MtTimeoutError(timeout))
|
promise.reject(new MtTimeoutError(timeout))
|
||||||
delete this._pendingRead[msgId]
|
this._pendingRead.delete(msgId)
|
||||||
}, timeout)
|
}, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
this._pendingRead[msgId] = {
|
this._pendingRead.set(msgId, {
|
||||||
promise,
|
promise,
|
||||||
timeout: timer,
|
timeout: timer,
|
||||||
}
|
})
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
@ -521,10 +519,12 @@ export class Conversation {
|
||||||
private _onEditMessage(msg: Message, fromRecent = false) {
|
private _onEditMessage(msg: Message, fromRecent = false) {
|
||||||
if (msg.chat.id !== this._chatId) return
|
if (msg.chat.id !== this._chatId) return
|
||||||
|
|
||||||
const it = this._pendingEditMessage[msg.id]
|
const it = this._pendingEditMessage.get(msg.id)
|
||||||
|
|
||||||
if (!it && !fromRecent) {
|
if (!it) {
|
||||||
this._recentEdits.pushBack(msg)
|
if (!fromRecent) {
|
||||||
|
this._recentEdits.pushBack(msg)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -533,7 +533,7 @@ export class Conversation {
|
||||||
if (!it.check || (await it.check(msg))) {
|
if (!it.check || (await it.check(msg))) {
|
||||||
if (it.timeout) clearTimeout(it.timeout)
|
if (it.timeout) clearTimeout(it.timeout)
|
||||||
it.promise.resolve(msg)
|
it.promise.resolve(msg)
|
||||||
delete this._pendingEditMessage[msg.id]
|
this._pendingEditMessage.delete(msg.id)
|
||||||
}
|
}
|
||||||
})().catch((e) => {
|
})().catch((e) => {
|
||||||
this.client['_emitError'](e)
|
this.client['_emitError'](e)
|
||||||
|
@ -545,12 +545,12 @@ export class Conversation {
|
||||||
|
|
||||||
const lastRead = upd.maxReadId
|
const lastRead = upd.maxReadId
|
||||||
|
|
||||||
for (const msgId in this._pendingRead) {
|
for (const msgId of this._pendingRead.keys()) {
|
||||||
if (parseInt(msgId) <= lastRead) {
|
if (msgId <= lastRead) {
|
||||||
const it = this._pendingRead[msgId]
|
const it = this._pendingRead.get(msgId)!
|
||||||
if (it.timeout) clearTimeout(it.timeout)
|
if (it.timeout) clearTimeout(it.timeout)
|
||||||
it.promise.resolve()
|
it.promise.resolve()
|
||||||
delete this._pendingRead[msgId]
|
this._pendingRead.delete(msgId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { LongMap } from '@mtcute/core'
|
||||||
import { tl } from '@mtcute/tl'
|
import { tl } from '@mtcute/tl'
|
||||||
|
|
||||||
import { TelegramClient } from '../../client'
|
import { TelegramClient } from '../../client'
|
||||||
|
@ -164,7 +165,7 @@ export class StickerSet {
|
||||||
|
|
||||||
if (!this._stickers) {
|
if (!this._stickers) {
|
||||||
this._stickers = []
|
this._stickers = []
|
||||||
const index: Record<string, tl.Mutable<StickerInfo>> = {}
|
const index = new LongMap<tl.Mutable<StickerInfo>>()
|
||||||
|
|
||||||
this.full!.documents.forEach((doc) => {
|
this.full!.documents.forEach((doc) => {
|
||||||
const sticker = parseDocument(
|
const sticker = parseDocument(
|
||||||
|
@ -186,15 +187,15 @@ export class StickerSet {
|
||||||
sticker,
|
sticker,
|
||||||
}
|
}
|
||||||
this._stickers!.push(info)
|
this._stickers!.push(info)
|
||||||
index[doc.id.toString()] = info
|
index.set(doc.id, info)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.full!.packs.forEach((pack) => {
|
this.full!.packs.forEach((pack) => {
|
||||||
pack.documents.forEach((id) => {
|
pack.documents.forEach((id) => {
|
||||||
const sid = id.toString()
|
const item = index.get(id)
|
||||||
|
|
||||||
if (sid in index) {
|
if (item) {
|
||||||
index[sid].emoji += pack.emoticon
|
item.emoji += pack.emoticon
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,8 +6,8 @@ const ERROR_MSG =
|
||||||
'Given peer is not available in this index. This is most likely an internal library error.'
|
'Given peer is not available in this index. This is most likely an internal library error.'
|
||||||
|
|
||||||
export class PeersIndex {
|
export class PeersIndex {
|
||||||
readonly users: Record<number, tl.TypeUser> = {}
|
readonly users: Map<number, tl.TypeUser> = new Map()
|
||||||
readonly chats: Record<number, tl.TypeChat> = {}
|
readonly chats: Map<number, tl.TypeChat> = new Map()
|
||||||
|
|
||||||
hasMin = false
|
hasMin = false
|
||||||
|
|
||||||
|
@ -18,14 +18,14 @@ export class PeersIndex {
|
||||||
const index = new PeersIndex()
|
const index = new PeersIndex()
|
||||||
|
|
||||||
obj.users?.forEach((user) => {
|
obj.users?.forEach((user) => {
|
||||||
index.users[user.id] = user
|
index.users.set(user.id, user)
|
||||||
|
|
||||||
if ((user as Exclude<typeof user, tl.RawUserEmpty>).min) {
|
if ((user as Exclude<typeof user, tl.RawUserEmpty>).min) {
|
||||||
index.hasMin = true
|
index.hasMin = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
obj.chats?.forEach((chat) => {
|
obj.chats?.forEach((chat) => {
|
||||||
index.chats[chat.id] = chat
|
index.chats.set(chat.id, chat)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(
|
(
|
||||||
|
@ -46,7 +46,7 @@ export class PeersIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
user(id: number): tl.TypeUser {
|
user(id: number): tl.TypeUser {
|
||||||
const r = this.users[id]
|
const r = this.users.get(id)
|
||||||
|
|
||||||
if (!r) {
|
if (!r) {
|
||||||
throw new MtArgumentError(ERROR_MSG)
|
throw new MtArgumentError(ERROR_MSG)
|
||||||
|
@ -56,7 +56,7 @@ export class PeersIndex {
|
||||||
}
|
}
|
||||||
|
|
||||||
chat(id: number): tl.TypeChat {
|
chat(id: number): tl.TypeChat {
|
||||||
const r = this.chats[id]
|
const r = this.chats.get(id)
|
||||||
|
|
||||||
if (!r) {
|
if (!r) {
|
||||||
throw new MtArgumentError(ERROR_MSG)
|
throw new MtArgumentError(ERROR_MSG)
|
||||||
|
|
|
@ -101,8 +101,8 @@ export class MtprotoSession {
|
||||||
|
|
||||||
/// state ///
|
/// state ///
|
||||||
// recent msg ids
|
// recent msg ids
|
||||||
recentOutgoingMsgIds = new LruSet<Long>(1000, false, true)
|
recentOutgoingMsgIds = new LruSet<Long>(1000, true)
|
||||||
recentIncomingMsgIds = new LruSet<Long>(1000, false, true)
|
recentIncomingMsgIds = new LruSet<Long>(1000, true)
|
||||||
|
|
||||||
// queues
|
// queues
|
||||||
queuedRpc = new Deque<PendingRpc>()
|
queuedRpc = new Deque<PendingRpc>()
|
||||||
|
|
|
@ -420,7 +420,7 @@ export class NetworkManager {
|
||||||
readonly _reconnectionStrategy: ReconnectionStrategy<PersistentConnectionParams>
|
readonly _reconnectionStrategy: ReconnectionStrategy<PersistentConnectionParams>
|
||||||
readonly _connectionCount: ConnectionCountDelegate
|
readonly _connectionCount: ConnectionCountDelegate
|
||||||
|
|
||||||
protected readonly _dcConnections: Record<number, DcConnectionManager> = {}
|
protected readonly _dcConnections = new Map<number, DcConnectionManager>()
|
||||||
protected _primaryDc?: DcConnectionManager
|
protected _primaryDc?: DcConnectionManager
|
||||||
|
|
||||||
private _keepAliveInterval?: NodeJS.Timeout
|
private _keepAliveInterval?: NodeJS.Timeout
|
||||||
|
@ -545,18 +545,18 @@ export class NetworkManager {
|
||||||
return dc.loadKeys().then(() => dc.main.ensureConnected())
|
return dc.loadKeys().then(() => dc.main.ensureConnected())
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dcCreationPromise: Record<number, Promise<void>> = {}
|
private _dcCreationPromise = new Map<number, Promise<void>>()
|
||||||
async _getOtherDc(dcId: number): Promise<DcConnectionManager> {
|
async _getOtherDc(dcId: number): Promise<DcConnectionManager> {
|
||||||
if (!this._dcConnections[dcId]) {
|
if (!this._dcConnections.has(dcId)) {
|
||||||
if (dcId in this._dcCreationPromise) {
|
if (this._dcCreationPromise.has(dcId)) {
|
||||||
this._log.debug('waiting for DC %d to be created', dcId)
|
this._log.debug('waiting for DC %d to be created', dcId)
|
||||||
await this._dcCreationPromise[dcId]
|
await this._dcCreationPromise.get(dcId)
|
||||||
|
|
||||||
return this._dcConnections[dcId]
|
return this._dcConnections.get(dcId)!
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = createControllablePromise<void>()
|
const promise = createControllablePromise<void>()
|
||||||
this._dcCreationPromise[dcId] = promise
|
this._dcCreationPromise.set(dcId, promise)
|
||||||
|
|
||||||
this._log.debug('creating new DC %d', dcId)
|
this._log.debug('creating new DC %d', dcId)
|
||||||
|
|
||||||
|
@ -569,14 +569,14 @@ export class NetworkManager {
|
||||||
dc.main.requestAuth()
|
dc.main.requestAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
this._dcConnections[dcId] = dc
|
this._dcConnections.set(dcId, dc)
|
||||||
promise.resolve()
|
promise.resolve()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
promise.reject(e)
|
promise.reject(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._dcConnections[dcId]
|
return this._dcConnections.get(dcId)!
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -589,13 +589,13 @@ export class NetworkManager {
|
||||||
throw new Error('Default DCs must be the same')
|
throw new Error('Default DCs must be the same')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._dcConnections[defaultDcs.main.id]) {
|
if (this._dcConnections.has(defaultDcs.main.id)) {
|
||||||
// shouldn't happen
|
// shouldn't happen
|
||||||
throw new Error('DC manager already exists')
|
throw new Error('DC manager already exists')
|
||||||
}
|
}
|
||||||
|
|
||||||
const dc = new DcConnectionManager(this, defaultDcs.main.id, defaultDcs)
|
const dc = new DcConnectionManager(this, defaultDcs.main.id, defaultDcs)
|
||||||
this._dcConnections[defaultDcs.main.id] = dc
|
this._dcConnections.set(defaultDcs.main.id, dc)
|
||||||
await this._switchPrimaryDc(dc)
|
await this._switchPrimaryDc(dc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -648,9 +648,10 @@ export class NetworkManager {
|
||||||
setIsPremium(isPremium: boolean): void {
|
setIsPremium(isPremium: boolean): void {
|
||||||
this._log.debug('setting isPremium to %s', isPremium)
|
this._log.debug('setting isPremium to %s', isPremium)
|
||||||
this.params.isPremium = isPremium
|
this.params.isPremium = isPremium
|
||||||
Object.values(this._dcConnections).forEach((dc) => {
|
|
||||||
|
for (const dc of this._dcConnections.values()) {
|
||||||
dc.setIsPremium(isPremium)
|
dc.setIsPremium(isPremium)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// future-proofing. should probably remove once the implementation is stable
|
// future-proofing. should probably remove once the implementation is stable
|
||||||
|
@ -693,20 +694,19 @@ export class NetworkManager {
|
||||||
|
|
||||||
const options = await this._findDcOptions(newDc)
|
const options = await this._findDcOptions(newDc)
|
||||||
|
|
||||||
if (!this._dcConnections[newDc]) {
|
if (!this._dcConnections.has(newDc)) {
|
||||||
this._dcConnections[newDc] = new DcConnectionManager(
|
this._dcConnections.set(
|
||||||
this,
|
|
||||||
newDc,
|
newDc,
|
||||||
options,
|
new DcConnectionManager(this, newDc, options),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._storage.setDefaultDcs(options)
|
await this._storage.setDefaultDcs(options)
|
||||||
|
|
||||||
await this._switchPrimaryDc(this._dcConnections[newDc])
|
await this._switchPrimaryDc(this._dcConnections.get(newDc)!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _floodWaitedRequests: Record<string, number> = {}
|
private _floodWaitedRequests = new Map<string, number>()
|
||||||
async call<T extends tl.RpcMethod>(
|
async call<T extends tl.RpcMethod>(
|
||||||
message: T,
|
message: T,
|
||||||
params?: RpcCallOptions,
|
params?: RpcCallOptions,
|
||||||
|
@ -721,15 +721,15 @@ export class NetworkManager {
|
||||||
const maxRetryCount = params?.maxRetryCount ?? this.params.maxRetryCount
|
const maxRetryCount = params?.maxRetryCount ?? this.params.maxRetryCount
|
||||||
|
|
||||||
// do not send requests that are in flood wait
|
// do not send requests that are in flood wait
|
||||||
if (message._ in this._floodWaitedRequests) {
|
if (this._floodWaitedRequests.has(message._)) {
|
||||||
const delta = this._floodWaitedRequests[message._] - Date.now()
|
const delta = this._floodWaitedRequests.get(message._)! - Date.now()
|
||||||
|
|
||||||
if (delta <= 3000) {
|
if (delta <= 3000) {
|
||||||
// flood waits below 3 seconds are "ignored"
|
// flood waits below 3 seconds are "ignored"
|
||||||
delete this._floodWaitedRequests[message._]
|
this._floodWaitedRequests.delete(message._)
|
||||||
} else if (delta <= this.params.floodSleepThreshold) {
|
} else if (delta <= this.params.floodSleepThreshold) {
|
||||||
await sleep(delta)
|
await sleep(delta)
|
||||||
delete this._floodWaitedRequests[message._]
|
this._floodWaitedRequests.delete(message._)
|
||||||
} else {
|
} else {
|
||||||
const err = tl.RpcError.create(
|
const err = tl.RpcError.create(
|
||||||
tl.RpcError.FLOOD,
|
tl.RpcError.FLOOD,
|
||||||
|
@ -792,8 +792,10 @@ export class NetworkManager {
|
||||||
) {
|
) {
|
||||||
if (e.text !== 'SLOWMODE_WAIT_%d') {
|
if (e.text !== 'SLOWMODE_WAIT_%d') {
|
||||||
// SLOW_MODE_WAIT is chat-specific, not request-specific
|
// SLOW_MODE_WAIT is chat-specific, not request-specific
|
||||||
this._floodWaitedRequests[message._] =
|
this._floodWaitedRequests.set(
|
||||||
Date.now() + e.seconds * 1000
|
message._,
|
||||||
|
Date.now() + e.seconds * 1000,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// In test servers, FLOOD_WAIT_0 has been observed, and sleeping for
|
// In test servers, FLOOD_WAIT_0 has been observed, and sleeping for
|
||||||
|
@ -845,16 +847,16 @@ export class NetworkManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
changeTransport(factory: TransportFactory): void {
|
changeTransport(factory: TransportFactory): void {
|
||||||
Object.values(this._dcConnections).forEach((dc) => {
|
for (const dc of this._dcConnections.values()) {
|
||||||
dc.main.changeTransport(factory)
|
dc.main.changeTransport(factory)
|
||||||
dc.upload.changeTransport(factory)
|
dc.upload.changeTransport(factory)
|
||||||
dc.download.changeTransport(factory)
|
dc.download.changeTransport(factory)
|
||||||
dc.downloadSmall.changeTransport(factory)
|
dc.downloadSmall.changeTransport(factory)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPoolSize(kind: ConnectionKind, dcId?: number) {
|
getPoolSize(kind: ConnectionKind, dcId?: number) {
|
||||||
const dc = dcId ? this._dcConnections[dcId] : this._primaryDc
|
const dc = dcId ? this._dcConnections.get(dcId) : this._primaryDc
|
||||||
|
|
||||||
if (!dc) {
|
if (!dc) {
|
||||||
if (!this._primaryDc) {
|
if (!this._primaryDc) {
|
||||||
|
@ -880,7 +882,7 @@ export class NetworkManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
for (const dc of Object.values(this._dcConnections)) {
|
for (const dc of this._dcConnections.values()) {
|
||||||
dc.main.destroy()
|
dc.main.destroy()
|
||||||
dc.upload.destroy()
|
dc.upload.destroy()
|
||||||
dc.download.destroy()
|
dc.download.destroy()
|
||||||
|
|
|
@ -168,7 +168,7 @@ export interface ITelegramStorage {
|
||||||
* Storage is supposed to replace stored channel `pts` values
|
* Storage is supposed to replace stored channel `pts` values
|
||||||
* with given in the object (key is unmarked peer id, value is the `pts`)
|
* with given in the object (key is unmarked peer id, value is the `pts`)
|
||||||
*/
|
*/
|
||||||
setManyChannelPts(values: Record<number, number>): MaybeAsync<void>
|
setManyChannelPts(values: Map<number, number>): MaybeAsync<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get cached peer information by their marked ID.
|
* Get cached peer information by their marked ID.
|
||||||
|
|
|
@ -12,19 +12,32 @@ export class JsonMemoryStorage extends MemoryStorage {
|
||||||
protected _loadJson(json: string): void {
|
protected _loadJson(json: string): void {
|
||||||
this._setStateFrom(
|
this._setStateFrom(
|
||||||
JSON.parse(json, (key, value) => {
|
JSON.parse(json, (key, value) => {
|
||||||
if (key === 'authKeys') {
|
switch (key) {
|
||||||
const ret: Record<string, Buffer> = {}
|
case 'authKeys':
|
||||||
|
case 'authKeysTemp': {
|
||||||
|
const ret: Record<string, Buffer> = {}
|
||||||
|
|
||||||
;(value as string).split('|').forEach((pair: string) => {
|
;(value as string)
|
||||||
const [dcId, b64] = pair.split(',')
|
.split('|')
|
||||||
ret[dcId] = Buffer.from(b64, 'base64')
|
.forEach((pair: string) => {
|
||||||
})
|
const [dcId, b64] = pair.split(',')
|
||||||
|
ret[dcId] = Buffer.from(b64, 'base64')
|
||||||
|
})
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
case 'authKeysTempExpiry':
|
||||||
if (key === 'accessHash') {
|
case 'entities':
|
||||||
return longFromFastString(value as string)
|
case 'phoneIndex':
|
||||||
|
case 'usernameIndex':
|
||||||
|
case 'pts':
|
||||||
|
case 'fsm':
|
||||||
|
case 'rl':
|
||||||
|
return new Map(
|
||||||
|
Object.entries(value as Record<string, string>),
|
||||||
|
)
|
||||||
|
case 'accessHash':
|
||||||
|
return longFromFastString(value as string)
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
@ -34,16 +47,30 @@ export class JsonMemoryStorage extends MemoryStorage {
|
||||||
|
|
||||||
protected _saveJson(): string {
|
protected _saveJson(): string {
|
||||||
return JSON.stringify(this._state, (key, value) => {
|
return JSON.stringify(this._state, (key, value) => {
|
||||||
if (key === 'authKeys') {
|
switch (key) {
|
||||||
const value_ = value as Record<string, Buffer | null>
|
case 'authKeys':
|
||||||
|
case 'authKeysTemp': {
|
||||||
|
const value_ = value as Map<string, Buffer | null>
|
||||||
|
|
||||||
return Object.entries(value_)
|
return [...value_.entries()]
|
||||||
.filter((it): it is [string, Buffer] => it[1] !== null)
|
.filter((it): it is [string, Buffer] => it[1] !== null)
|
||||||
.map(([dcId, key]) => dcId + ',' + key.toString('base64'))
|
.map(
|
||||||
.join('|')
|
([dcId, key]) => dcId + ',' + key.toString('base64'),
|
||||||
}
|
)
|
||||||
if (key === 'accessHash') {
|
.join('|')
|
||||||
return longToFastString(value as tl.Long)
|
}
|
||||||
|
case 'authKeysTempExpiry':
|
||||||
|
case 'entities':
|
||||||
|
case 'phoneIndex':
|
||||||
|
case 'usernameIndex':
|
||||||
|
case 'pts':
|
||||||
|
case 'fsm':
|
||||||
|
case 'rl':
|
||||||
|
return Object.fromEntries([
|
||||||
|
...(value as Map<string, string>).entries(),
|
||||||
|
])
|
||||||
|
case 'accessHash':
|
||||||
|
return longToFastString(value as tl.Long)
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
|
@ -14,24 +14,24 @@ export interface MemorySessionState {
|
||||||
$version: typeof CURRENT_VERSION
|
$version: typeof CURRENT_VERSION
|
||||||
|
|
||||||
defaultDcs: ITelegramStorage.DcOptions | null
|
defaultDcs: ITelegramStorage.DcOptions | null
|
||||||
authKeys: Record<number, Buffer | null>
|
authKeys: Map<number, Buffer>
|
||||||
authKeysTemp: Record<string, Buffer | null>
|
authKeysTemp: Map<string, Buffer>
|
||||||
authKeysTempExpiry: Record<string, number>
|
authKeysTempExpiry: Map<string, number>
|
||||||
|
|
||||||
// marked peer id -> entity info
|
// marked peer id -> entity info
|
||||||
entities: Record<number, PeerInfoWithUpdated>
|
entities: Map<number, PeerInfoWithUpdated>
|
||||||
// phone number -> peer id
|
// phone number -> peer id
|
||||||
phoneIndex: Record<string, number>
|
phoneIndex: Map<string, number>
|
||||||
// username -> peer id
|
// username -> peer id
|
||||||
usernameIndex: Record<string, number>
|
usernameIndex: Map<string, number>
|
||||||
|
|
||||||
// common pts, date, seq, qts
|
// common pts, date, seq, qts
|
||||||
gpts: [number, number, number, number] | null
|
gpts: [number, number, number, number] | null
|
||||||
// channel pts
|
// channel pts
|
||||||
pts: Record<number, number>
|
pts: Map<number, number>
|
||||||
|
|
||||||
// state for fsm
|
// state for fsm
|
||||||
fsm: Record<
|
fsm: Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
// value
|
// value
|
||||||
|
@ -42,7 +42,7 @@ export interface MemorySessionState {
|
||||||
>
|
>
|
||||||
|
|
||||||
// state for rate limiter
|
// state for rate limiter
|
||||||
rl: Record<
|
rl: Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
// reset
|
// reset
|
||||||
|
@ -111,16 +111,16 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
this._state = {
|
this._state = {
|
||||||
$version: CURRENT_VERSION,
|
$version: CURRENT_VERSION,
|
||||||
defaultDcs: null,
|
defaultDcs: null,
|
||||||
authKeys: {},
|
authKeys: new Map(),
|
||||||
authKeysTemp: {},
|
authKeysTemp: new Map(),
|
||||||
authKeysTempExpiry: {},
|
authKeysTempExpiry: new Map(),
|
||||||
entities: {},
|
entities: new Map(),
|
||||||
phoneIndex: {},
|
phoneIndex: new Map(),
|
||||||
usernameIndex: {},
|
usernameIndex: new Map(),
|
||||||
gpts: null,
|
gpts: null,
|
||||||
pts: {},
|
pts: new Map(),
|
||||||
fsm: {},
|
fsm: new Map(),
|
||||||
rl: {},
|
rl: new Map(),
|
||||||
self: null,
|
self: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,19 +138,20 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
let populate = false
|
let populate = false
|
||||||
|
|
||||||
if (!obj.phoneIndex) {
|
if (!obj.phoneIndex) {
|
||||||
obj.phoneIndex = {}
|
obj.phoneIndex = new Map()
|
||||||
populate = true
|
populate = true
|
||||||
}
|
}
|
||||||
if (!obj.usernameIndex) {
|
if (!obj.usernameIndex) {
|
||||||
obj.usernameIndex = {}
|
obj.usernameIndex = new Map()
|
||||||
populate = true
|
populate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (populate) {
|
if (populate) {
|
||||||
Object.values(obj.entities).forEach(
|
Object.values(obj.entities).forEach(
|
||||||
(ent: ITelegramStorage.PeerInfo) => {
|
(ent: ITelegramStorage.PeerInfo) => {
|
||||||
if (ent.phone) obj.phoneIndex[ent.phone] = ent.id
|
if (ent.phone) obj.phoneIndex.set(ent.phone, ent.id)
|
||||||
if (ent.username) obj.usernameIndex[ent.username] = ent.id
|
|
||||||
|
if (ent.username) { obj.usernameIndex.set(ent.username, ent.id) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -168,19 +169,17 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
const fsm = state.fsm
|
const fsm = state.fsm
|
||||||
const rl = state.rl
|
const rl = state.rl
|
||||||
|
|
||||||
Object.keys(fsm).forEach((key) => {
|
for (const [key, item] of fsm) {
|
||||||
const exp = fsm[key].e
|
if (item.e && item.e < now) {
|
||||||
|
fsm.delete(key)
|
||||||
if (exp && exp < now) {
|
|
||||||
delete fsm[key]
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
Object.keys(rl).forEach((key) => {
|
for (const [key, item] of rl) {
|
||||||
if (rl[key].res < now) {
|
if (item.res < now) {
|
||||||
delete rl[key]
|
rl.delete(key)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultDcs(): ITelegramStorage.DcOptions | null {
|
getDefaultDcs(): ITelegramStorage.DcOptions | null {
|
||||||
|
@ -198,36 +197,47 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
expiresAt: number,
|
expiresAt: number,
|
||||||
): void {
|
): void {
|
||||||
const k = `${dcId}:${index}`
|
const k = `${dcId}:${index}`
|
||||||
this._state.authKeysTemp[k] = key
|
|
||||||
this._state.authKeysTempExpiry[k] = expiresAt
|
if (key) {
|
||||||
|
this._state.authKeysTemp.set(k, key)
|
||||||
|
this._state.authKeysTempExpiry.set(k, expiresAt)
|
||||||
|
} else {
|
||||||
|
this._state.authKeysTemp.delete(k)
|
||||||
|
this._state.authKeysTempExpiry.delete(k)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setAuthKeyFor(dcId: number, key: Buffer | null): void {
|
setAuthKeyFor(dcId: number, key: Buffer | null): void {
|
||||||
this._state.authKeys[dcId] = key
|
if (key) {
|
||||||
|
this._state.authKeys.set(dcId, key)
|
||||||
|
} else {
|
||||||
|
this._state.authKeys.delete(dcId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthKeyFor(dcId: number, tempIndex?: number): Buffer | null {
|
getAuthKeyFor(dcId: number, tempIndex?: number): Buffer | null {
|
||||||
if (tempIndex !== undefined) {
|
if (tempIndex !== undefined) {
|
||||||
const k = `${dcId}:${tempIndex}`
|
const k = `${dcId}:${tempIndex}`
|
||||||
|
|
||||||
if (Date.now() > (this._state.authKeysTempExpiry[k] ?? 0)) {
|
if (Date.now() > (this._state.authKeysTempExpiry.get(k) ?? 0)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._state.authKeysTemp[k]
|
return this._state.authKeysTemp.get(k) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._state.authKeys[dcId] ?? null
|
return this._state.authKeys.get(dcId) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
dropAuthKeysFor(dcId: number): void {
|
dropAuthKeysFor(dcId: number): void {
|
||||||
this._state.authKeys[dcId] = null
|
this._state.authKeys.delete(dcId)
|
||||||
Object.keys(this._state.authKeysTemp).forEach((key) => {
|
|
||||||
|
for (const key of this._state.authKeysTemp.keys()) {
|
||||||
if (key.startsWith(`${dcId}:`)) {
|
if (key.startsWith(`${dcId}:`)) {
|
||||||
delete this._state.authKeysTemp[key]
|
this._state.authKeysTemp.delete(key)
|
||||||
delete this._state.authKeysTempExpiry[key]
|
this._state.authKeysTempExpiry.delete(key)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePeers(peers: PeerInfoWithUpdated[]): MaybeAsync<void> {
|
updatePeers(peers: PeerInfoWithUpdated[]): MaybeAsync<void> {
|
||||||
|
@ -235,26 +245,25 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
this._cachedFull.set(peer.id, peer.full)
|
this._cachedFull.set(peer.id, peer.full)
|
||||||
|
|
||||||
peer.updated = Date.now()
|
peer.updated = Date.now()
|
||||||
const old = this._state.entities[peer.id]
|
const old = this._state.entities.get(peer.id)
|
||||||
|
|
||||||
if (old) {
|
if (old) {
|
||||||
// min peer
|
|
||||||
// if (peer.fromMessage) continue
|
|
||||||
|
|
||||||
// delete old index entries if needed
|
// delete old index entries if needed
|
||||||
if (old.username && old.username !== peer.username) {
|
if (old.username && peer.username !== old.username) {
|
||||||
delete this._state.usernameIndex[old.username]
|
this._state.usernameIndex.delete(old.username)
|
||||||
}
|
}
|
||||||
if (old.phone && old.phone !== peer.phone) {
|
if (old.phone && old.phone !== peer.phone) {
|
||||||
delete this._state.phoneIndex[old.phone]
|
this._state.phoneIndex.delete(old.phone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (peer.username) {
|
if (peer.username) {
|
||||||
this._state.usernameIndex[peer.username.toLowerCase()] = peer.id
|
this._state.usernameIndex.set(peer.username, peer.id)
|
||||||
}
|
}
|
||||||
if (peer.phone) this._state.phoneIndex[peer.phone] = peer.id
|
|
||||||
this._state.entities[peer.id] = peer
|
if (peer.phone) this._state.phoneIndex.set(peer.phone, peer.id)
|
||||||
|
|
||||||
|
this._state.entities.set(peer.id, peer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,22 +299,23 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
if (this._cachedInputPeers.has(peerId)) {
|
if (this._cachedInputPeers.has(peerId)) {
|
||||||
return this._cachedInputPeers.get(peerId)!
|
return this._cachedInputPeers.get(peerId)!
|
||||||
}
|
}
|
||||||
const peer = this._getInputPeer(this._state.entities[peerId])
|
const peer = this._getInputPeer(this._state.entities.get(peerId))
|
||||||
if (peer) this._cachedInputPeers.set(peerId, peer)
|
if (peer) this._cachedInputPeers.set(peerId, peer)
|
||||||
|
|
||||||
return peer
|
return peer
|
||||||
}
|
}
|
||||||
|
|
||||||
getPeerByPhone(phone: string): tl.TypeInputPeer | null {
|
getPeerByPhone(phone: string): tl.TypeInputPeer | null {
|
||||||
return this._getInputPeer(
|
const peerId = this._state.phoneIndex.get(phone)
|
||||||
this._state.entities[this._state.phoneIndex[phone]],
|
if (!peerId) return null
|
||||||
)
|
|
||||||
|
return this._getInputPeer(this._state.entities.get(peerId))
|
||||||
}
|
}
|
||||||
|
|
||||||
getPeerByUsername(username: string): tl.TypeInputPeer | null {
|
getPeerByUsername(username: string): tl.TypeInputPeer | null {
|
||||||
const id = this._state.usernameIndex[username.toLowerCase()]
|
const id = this._state.usernameIndex.get(username.toLowerCase())
|
||||||
if (!id) return null
|
if (!id) return null
|
||||||
const peer = this._state.entities[id]
|
const peer = this._state.entities.get(id)
|
||||||
if (!peer) return null
|
if (!peer) return null
|
||||||
|
|
||||||
if (Date.now() - peer.updated > USERNAME_TTL) return null
|
if (Date.now() - peer.updated > USERNAME_TTL) return null
|
||||||
|
@ -321,14 +331,14 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
this._state.self = self
|
this._state.self = self
|
||||||
}
|
}
|
||||||
|
|
||||||
setManyChannelPts(values: Record<number, number>): void {
|
setManyChannelPts(values: Map<number, number>): void {
|
||||||
for (const id in values) {
|
for (const [id, pts] of values) {
|
||||||
this._state.pts[id] = values[id]
|
this._state.pts.set(id, pts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelPts(entityId: number): number | null {
|
getChannelPts(entityId: number): number | null {
|
||||||
return this._state.pts[entityId] ?? null
|
return this._state.pts.get(entityId) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
getUpdatesState(): MaybeAsync<[number, number, number, number] | null> {
|
getUpdatesState(): MaybeAsync<[number, number, number, number] | null> {
|
||||||
|
@ -362,12 +372,12 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
// IStateStorage implementation
|
// IStateStorage implementation
|
||||||
|
|
||||||
getState(key: string): unknown {
|
getState(key: string): unknown {
|
||||||
const val = this._state.fsm[key]
|
const val = this._state.fsm.get(key)
|
||||||
if (!val) return null
|
if (!val) return null
|
||||||
|
|
||||||
if (val.e && val.e < Date.now()) {
|
if (val.e && val.e < Date.now()) {
|
||||||
// expired
|
// expired
|
||||||
delete this._state.fsm[key]
|
this._state.fsm.delete(key)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -376,14 +386,14 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(key: string, state: unknown, ttl?: number): void {
|
setState(key: string, state: unknown, ttl?: number): void {
|
||||||
this._state.fsm[key] = {
|
this._state.fsm.set(key, {
|
||||||
v: state,
|
v: state,
|
||||||
e: ttl ? Date.now() + ttl * 1000 : undefined,
|
e: ttl ? Date.now() + ttl * 1000 : undefined,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteState(key: string): void {
|
deleteState(key: string): void {
|
||||||
delete this._state.fsm[key]
|
this._state.fsm.delete(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentScene(key: string): string | null {
|
getCurrentScene(key: string): string | null {
|
||||||
|
@ -395,26 +405,26 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteCurrentScene(key: string): void {
|
deleteCurrentScene(key: string): void {
|
||||||
delete this._state.fsm[`$current_scene_${key}`]
|
this._state.fsm.delete(`$current_scene_${key}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
getRateLimit(key: string, limit: number, window: number): [number, number] {
|
getRateLimit(key: string, limit: number, window: number): [number, number] {
|
||||||
// leaky bucket
|
// leaky bucket
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
if (!(key in this._state.rl)) {
|
const item = this._state.rl.get(key)
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
const state = {
|
const state = {
|
||||||
res: now + window * 1000,
|
res: now + window * 1000,
|
||||||
rem: limit,
|
rem: limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
this._state.rl[key] = state
|
this._state.rl.set(key, state)
|
||||||
|
|
||||||
return [state.rem, state.res]
|
return [state.rem, state.res]
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = this._state.rl[key]
|
|
||||||
|
|
||||||
if (item.res < now) {
|
if (item.res < now) {
|
||||||
// expired
|
// expired
|
||||||
|
|
||||||
|
@ -423,7 +433,7 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
rem: limit,
|
rem: limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
this._state.rl[key] = state
|
this._state.rl.set(key, state)
|
||||||
|
|
||||||
return [state.rem, state.res]
|
return [state.rem, state.res]
|
||||||
}
|
}
|
||||||
|
@ -434,6 +444,6 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
resetRateLimit(key: string): void {
|
resetRateLimit(key: string): void {
|
||||||
delete this._state.rl[key]
|
this._state.rl.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export * from './linked-list'
|
||||||
export * from './logger'
|
export * from './logger'
|
||||||
export * from './long-utils'
|
export * from './long-utils'
|
||||||
export * from './lru-map'
|
export * from './lru-map'
|
||||||
export * from './lru-string-set'
|
export * from './lru-set'
|
||||||
export * from './misc-utils'
|
export * from './misc-utils'
|
||||||
export * from './peer-utils'
|
export * from './peer-utils'
|
||||||
export * from './sorted-array'
|
export * from './sorted-array'
|
||||||
|
|
|
@ -65,12 +65,19 @@ export class Logger {
|
||||||
fmt.includes('%h') ||
|
fmt.includes('%h') ||
|
||||||
fmt.includes('%b') ||
|
fmt.includes('%b') ||
|
||||||
fmt.includes('%j') ||
|
fmt.includes('%j') ||
|
||||||
|
fmt.includes('%J') ||
|
||||||
fmt.includes('%l')
|
fmt.includes('%l')
|
||||||
) {
|
) {
|
||||||
let idx = 0
|
let idx = 0
|
||||||
fmt = fmt.replace(FORMATTER_RE, (m) => {
|
fmt = fmt.replace(FORMATTER_RE, (m) => {
|
||||||
if (m === '%h' || m === '%b' || m === '%j' || m === '%l') {
|
if (
|
||||||
const val = args[idx]
|
m === '%h' ||
|
||||||
|
m === '%b' ||
|
||||||
|
m === '%j' ||
|
||||||
|
m === '%J' ||
|
||||||
|
m === '%l'
|
||||||
|
) {
|
||||||
|
let val = args[idx]
|
||||||
|
|
||||||
args.splice(idx, 1)
|
args.splice(idx, 1)
|
||||||
|
|
||||||
|
@ -82,7 +89,9 @@ export class Logger {
|
||||||
}
|
}
|
||||||
if (m === '%b') return String(Boolean(val))
|
if (m === '%b') return String(Boolean(val))
|
||||||
|
|
||||||
if (m === '%j') {
|
if (m === '%j' || m === '%J') {
|
||||||
|
if (m === '%J') { val = [...(val as IterableIterator<unknown>)] }
|
||||||
|
|
||||||
return JSON.stringify(val, (k, v) => {
|
return JSON.stringify(val, (k, v) => {
|
||||||
if (
|
if (
|
||||||
typeof v === 'object' &&
|
typeof v === 'object' &&
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return */
|
|
||||||
// ^^ because of performance reasons
|
|
||||||
import Long from 'long'
|
import Long from 'long'
|
||||||
|
|
||||||
import { getRandomInt } from './misc-utils'
|
import { getRandomInt } from './misc-utils'
|
||||||
|
@ -98,108 +95,40 @@ export function longFromFastString(val: string, unsigned = false): Long {
|
||||||
* Uses fast string representation internally.
|
* Uses fast string representation internally.
|
||||||
*/
|
*/
|
||||||
export class LongMap<V> {
|
export class LongMap<V> {
|
||||||
private _map?: Map<string, V>
|
private _map = new Map<string, V>()
|
||||||
private _obj?: any
|
|
||||||
|
|
||||||
constructor(useObject = false) {
|
set(key: Long, value: V): void {
|
||||||
if (typeof Map === 'undefined' || useObject) {
|
this._map.set(longToFastString(key), value)
|
||||||
this._obj = Object.create(null)
|
|
||||||
this.set = this._setForObj.bind(this)
|
|
||||||
this.has = this._hasForObj.bind(this)
|
|
||||||
this.get = this._getForObj.bind(this)
|
|
||||||
this.delete = this._deleteForObj.bind(this)
|
|
||||||
this.keys = this._keysForObj.bind(this)
|
|
||||||
this.values = this._valuesForObj.bind(this)
|
|
||||||
this.clear = this._clearForObj.bind(this)
|
|
||||||
this.size = this._sizeForObj.bind(this)
|
|
||||||
} else {
|
|
||||||
this._map = new Map()
|
|
||||||
this.set = this._setForMap.bind(this)
|
|
||||||
this.has = this._hasForMap.bind(this)
|
|
||||||
this.get = this._getForMap.bind(this)
|
|
||||||
this.delete = this._deleteForMap.bind(this)
|
|
||||||
this.keys = this._keysForMap.bind(this)
|
|
||||||
this.values = this._valuesForMap.bind(this)
|
|
||||||
this.clear = this._clearForMap.bind(this)
|
|
||||||
this.size = this._sizeForMap.bind(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly set: (key: Long, value: V) => void
|
has(key: Long): boolean {
|
||||||
readonly has: (key: Long) => boolean
|
return this._map.has(longToFastString(key))
|
||||||
readonly get: (key: Long) => V | undefined
|
|
||||||
readonly delete: (key: Long) => void
|
|
||||||
readonly keys: (unsigned?: boolean) => IterableIterator<Long>
|
|
||||||
readonly values: () => IterableIterator<V>
|
|
||||||
readonly clear: () => void
|
|
||||||
readonly size: () => number
|
|
||||||
|
|
||||||
private _setForMap(key: Long, value: V): void {
|
|
||||||
this._map!.set(longToFastString(key), value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _hasForMap(key: Long): boolean {
|
get(key: Long): V | undefined {
|
||||||
return this._map!.has(longToFastString(key))
|
return this._map.get(longToFastString(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getForMap(key: Long): V | undefined {
|
delete(key: Long): void {
|
||||||
return this._map!.get(longToFastString(key))
|
this._map.delete(longToFastString(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteForMap(key: Long): void {
|
*keys(unsigned?: boolean): IterableIterator<Long> {
|
||||||
this._map!.delete(longToFastString(key))
|
for (const v of this._map.keys()) {
|
||||||
}
|
|
||||||
|
|
||||||
private *_keysForMap(unsigned?: boolean): IterableIterator<Long> {
|
|
||||||
for (const v of this._map!.keys()) {
|
|
||||||
yield longFromFastString(v, unsigned)
|
yield longFromFastString(v, unsigned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valuesForMap(): IterableIterator<V> {
|
values(): IterableIterator<V> {
|
||||||
return this._map!.values()
|
return this._map.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearForMap(): void {
|
clear(): void {
|
||||||
this._map!.clear()
|
this._map.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private _sizeForMap(): number {
|
size(): number {
|
||||||
return this._map!.size
|
return this._map.size
|
||||||
}
|
|
||||||
|
|
||||||
private _setForObj(key: Long, value: V): void {
|
|
||||||
this._obj[longToFastString(key)] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private _hasForObj(key: Long): boolean {
|
|
||||||
return longToFastString(key) in this._obj
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getForObj(key: Long): V | undefined {
|
|
||||||
return this._obj[longToFastString(key)]
|
|
||||||
}
|
|
||||||
|
|
||||||
private _deleteForObj(key: Long): void {
|
|
||||||
delete this._obj[longToFastString(key)]
|
|
||||||
}
|
|
||||||
|
|
||||||
private *_keysForObj(unsigned?: boolean): IterableIterator<Long> {
|
|
||||||
for (const v of Object.keys(this._obj)) {
|
|
||||||
yield longFromFastString(v, unsigned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private *_valuesForObj(): IterableIterator<V> {
|
|
||||||
yield* Object.values(this._obj) as any
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearForObj(): void {
|
|
||||||
this._obj = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _sizeForObj(): number {
|
|
||||||
return Object.keys(this._obj).length
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,74 +138,25 @@ export class LongMap<V> {
|
||||||
* Uses fast string representation internally
|
* Uses fast string representation internally
|
||||||
*/
|
*/
|
||||||
export class LongSet {
|
export class LongSet {
|
||||||
private _set?: Set<string>
|
private _set = new Set<string>()
|
||||||
private _obj?: any
|
|
||||||
private _objSize?: number
|
|
||||||
|
|
||||||
constructor(useObject = false) {
|
|
||||||
if (typeof Set === 'undefined' || useObject) {
|
|
||||||
this._obj = Object.create(null)
|
|
||||||
this._objSize = 0
|
|
||||||
this.add = this._addForObj.bind(this)
|
|
||||||
this.delete = this._deleteForObj.bind(this)
|
|
||||||
this.has = this._hasForObj.bind(this)
|
|
||||||
this.clear = this._clearForObj.bind(this)
|
|
||||||
} else {
|
|
||||||
this._set = new Set()
|
|
||||||
this.add = this._addForSet.bind(this)
|
|
||||||
this.delete = this._deleteForSet.bind(this)
|
|
||||||
this.has = this._hasForSet.bind(this)
|
|
||||||
this.clear = this._clearForSet.bind(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly add: (val: Long) => void
|
|
||||||
readonly delete: (val: Long) => void
|
|
||||||
readonly has: (val: Long) => boolean
|
|
||||||
readonly clear: () => void
|
|
||||||
|
|
||||||
get size(): number {
|
get size(): number {
|
||||||
return this._objSize ?? this._set!.size
|
return this._set.size
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addForSet(val: Long) {
|
add(val: Long) {
|
||||||
this._set!.add(longToFastString(val))
|
this._set.add(longToFastString(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
private _deleteForSet(val: Long) {
|
delete(val: Long) {
|
||||||
this._set!.delete(longToFastString(val))
|
this._set.delete(longToFastString(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
private _hasForSet(val: Long) {
|
has(val: Long) {
|
||||||
return this._set!.has(longToFastString(val))
|
return this._set.has(longToFastString(val))
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearForSet() {
|
clear() {
|
||||||
this._set!.clear()
|
this._set.clear()
|
||||||
}
|
|
||||||
|
|
||||||
private _addForObj(val: Long) {
|
|
||||||
const k = longToFastString(val)
|
|
||||||
if (k in this._obj) return
|
|
||||||
|
|
||||||
this._obj[k] = true
|
|
||||||
this._objSize! += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private _deleteForObj(val: Long) {
|
|
||||||
const k = longToFastString(val)
|
|
||||||
if (!(k in this._obj)) return
|
|
||||||
|
|
||||||
delete this._obj[k]
|
|
||||||
this._objSize! -= 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private _hasForObj(val: Long) {
|
|
||||||
return longToFastString(val) in this._obj
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearForObj() {
|
|
||||||
this._obj = {}
|
|
||||||
this._objSize = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
// ^^ because of performance reasons
|
// ^^ because of performance reasons
|
||||||
import { LongMap } from './long-utils'
|
import { LongMap } from './long-utils'
|
||||||
|
|
||||||
|
@ -15,8 +15,7 @@ interface TwoWayLinkedList<K, T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple class implementing LRU-like behaviour for a map,
|
* Simple class implementing LRU-like behaviour for a Map
|
||||||
* falling back to objects when `Map` is not available.
|
|
||||||
*
|
*
|
||||||
* Can be used to handle local cache of *something*
|
* Can be used to handle local cache of *something*
|
||||||
*
|
*
|
||||||
|
@ -27,37 +26,16 @@ export class LruMap<K extends string | number, V> {
|
||||||
private _first?: TwoWayLinkedList<K, V>
|
private _first?: TwoWayLinkedList<K, V>
|
||||||
private _last?: TwoWayLinkedList<K, V>
|
private _last?: TwoWayLinkedList<K, V>
|
||||||
|
|
||||||
|
private _map: Map<K, TwoWayLinkedList<K, V>>
|
||||||
|
|
||||||
private _size = 0
|
private _size = 0
|
||||||
|
|
||||||
constructor(capacity: number, useObject = false, forLong = false) {
|
constructor(capacity: number, forLong = false) {
|
||||||
this._capacity = capacity
|
this._capacity = capacity
|
||||||
|
|
||||||
if (forLong) {
|
this._map = forLong ? (new LongMap() as any) : new Map()
|
||||||
const map = new LongMap(useObject)
|
|
||||||
this._set = map.set.bind(map) as any
|
|
||||||
this._has = map.has.bind(map) as any
|
|
||||||
this._get = map.get.bind(map) as any
|
|
||||||
this._del = map.delete.bind(map) as any
|
|
||||||
} else if (typeof Map === 'undefined' || useObject) {
|
|
||||||
const obj = Object.create(null)
|
|
||||||
this._set = (k, v) => (obj[k] = v)
|
|
||||||
this._has = (k) => k in obj
|
|
||||||
this._get = (k) => obj[k]
|
|
||||||
this._del = (k) => delete obj[k]
|
|
||||||
} else {
|
|
||||||
const map = new Map()
|
|
||||||
this._set = map.set.bind(map)
|
|
||||||
this._has = map.has.bind(map)
|
|
||||||
this._get = map.get.bind(map)
|
|
||||||
this._del = map.delete.bind(map)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _set: (key: K, value: V) => void
|
|
||||||
private readonly _has: (key: K) => boolean
|
|
||||||
private readonly _get: (key: K) => TwoWayLinkedList<K, V> | undefined
|
|
||||||
private readonly _del: (key: K) => void
|
|
||||||
|
|
||||||
private _markUsed(item: TwoWayLinkedList<K, V>): void {
|
private _markUsed(item: TwoWayLinkedList<K, V>): void {
|
||||||
if (item === this._first) {
|
if (item === this._first) {
|
||||||
return // already the most recently used
|
return // already the most recently used
|
||||||
|
@ -84,7 +62,7 @@ export class LruMap<K extends string | number, V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key: K): V | undefined {
|
get(key: K): V | undefined {
|
||||||
const item = this._get(key)
|
const item = this._map.get(key)
|
||||||
if (!item) return undefined
|
if (!item) return undefined
|
||||||
|
|
||||||
this._markUsed(item)
|
this._markUsed(item)
|
||||||
|
@ -93,7 +71,7 @@ export class LruMap<K extends string | number, V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
has(key: K): boolean {
|
has(key: K): boolean {
|
||||||
return this._has(key)
|
return this._map.has(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
private _remove(item: TwoWayLinkedList<K, V>): void {
|
private _remove(item: TwoWayLinkedList<K, V>): void {
|
||||||
|
@ -108,12 +86,12 @@ export class LruMap<K extends string | number, V> {
|
||||||
|
|
||||||
// remove strong refs to and from the item
|
// remove strong refs to and from the item
|
||||||
item.p = item.n = undefined
|
item.p = item.n = undefined
|
||||||
this._del(item.k)
|
this._map.delete(item.k)
|
||||||
this._size -= 1
|
this._size -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: K, value: V): void {
|
set(key: K, value: V): void {
|
||||||
let item = this._get(key)
|
let item = this._map.get(key)
|
||||||
|
|
||||||
if (item) {
|
if (item) {
|
||||||
// already in cache, update
|
// already in cache, update
|
||||||
|
@ -130,7 +108,7 @@ export class LruMap<K extends string | number, V> {
|
||||||
n: undefined,
|
n: undefined,
|
||||||
p: undefined,
|
p: undefined,
|
||||||
}
|
}
|
||||||
this._set(key, item as any)
|
this._map.set(key, item as any)
|
||||||
|
|
||||||
if (this._first) {
|
if (this._first) {
|
||||||
this._first.p = item
|
this._first.p = item
|
||||||
|
@ -154,7 +132,7 @@ export class LruMap<K extends string | number, V> {
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(key: K): void {
|
delete(key: K): void {
|
||||||
const item = this._get(key)
|
const item = this._map.get(key)
|
||||||
if (item) this._remove(item)
|
if (item) this._remove(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
65
packages/core/src/utils/lru-set.ts
Normal file
65
packages/core/src/utils/lru-set.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
// ^^ because of performance reasons
|
||||||
|
import Long from 'long'
|
||||||
|
|
||||||
|
import { LongSet } from './long-utils'
|
||||||
|
|
||||||
|
interface OneWayLinkedList<T> {
|
||||||
|
v: T
|
||||||
|
n?: OneWayLinkedList<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple class implementing LRU-like behaviour for a Set.
|
||||||
|
*
|
||||||
|
* Note: this is not exactly LRU, but rather "least recently added"
|
||||||
|
* and doesn't mark items as recently added if they are already in the set.
|
||||||
|
* This is enough for our use case, so we don't bother with more complex implementation.
|
||||||
|
*
|
||||||
|
* Used to store recently received message IDs in {@link SessionConnection}
|
||||||
|
*
|
||||||
|
* Uses one-way linked list internally to keep track of insertion order
|
||||||
|
*/
|
||||||
|
export class LruSet<T extends string | number | Long> {
|
||||||
|
private _capacity: number
|
||||||
|
private _first?: OneWayLinkedList<T>
|
||||||
|
private _last?: OneWayLinkedList<T>
|
||||||
|
|
||||||
|
private _set: Set<T> | LongSet
|
||||||
|
|
||||||
|
constructor(capacity: number, forLong = false) {
|
||||||
|
this._capacity = capacity
|
||||||
|
|
||||||
|
this._set = forLong ? new LongSet() : new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._first = this._last = undefined
|
||||||
|
this._set.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
add(val: T) {
|
||||||
|
if (this._set.has(val as any)) return
|
||||||
|
|
||||||
|
if (!this._first) this._first = { v: val }
|
||||||
|
|
||||||
|
if (!this._last) this._last = this._first
|
||||||
|
else {
|
||||||
|
this._last.n = { v: val }
|
||||||
|
this._last = this._last.n
|
||||||
|
}
|
||||||
|
|
||||||
|
this._set.add(val as any)
|
||||||
|
|
||||||
|
if (this._set.size > this._capacity && this._first) {
|
||||||
|
// remove least recently used
|
||||||
|
this._set.delete(this._first.v as any)
|
||||||
|
this._first = this._first.n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has(val: T) {
|
||||||
|
return this._set.has(val as any)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,111 +0,0 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
|
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
||||||
// ^^ because of performance reasons
|
|
||||||
import Long from 'long'
|
|
||||||
|
|
||||||
import { LongSet } from './long-utils'
|
|
||||||
|
|
||||||
interface OneWayLinkedList<T> {
|
|
||||||
v: T
|
|
||||||
n?: OneWayLinkedList<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple class implementing LRU-like behaviour for a set,
|
|
||||||
* falling back to objects when `Set` is not available.
|
|
||||||
*
|
|
||||||
* Used to store recently received message IDs in {@link SessionConnection}
|
|
||||||
*
|
|
||||||
* Uses one-way linked list internally to keep track of insertion order
|
|
||||||
*/
|
|
||||||
export class LruSet<T extends string | number | Long> {
|
|
||||||
private _capacity: number
|
|
||||||
private _first?: OneWayLinkedList<T>
|
|
||||||
private _last?: OneWayLinkedList<T>
|
|
||||||
|
|
||||||
private _set?: Set<T> | LongSet
|
|
||||||
private _obj?: object
|
|
||||||
private _objSize?: number
|
|
||||||
|
|
||||||
constructor(capacity: number, useObject = false, forLong = false) {
|
|
||||||
this._capacity = capacity
|
|
||||||
|
|
||||||
if (!forLong && (typeof Set === 'undefined' || useObject)) {
|
|
||||||
this._obj = Object.create(null)
|
|
||||||
this._objSize = 0
|
|
||||||
this.add = this._addForObj.bind(this)
|
|
||||||
this.has = this._hasForObj.bind(this)
|
|
||||||
this.clear = this._clearForObj.bind(this)
|
|
||||||
} else {
|
|
||||||
this._set = forLong ? new LongSet(useObject) : new Set()
|
|
||||||
this.add = this._addForSet.bind(this)
|
|
||||||
this.has = this._hasForSet.bind(this)
|
|
||||||
this.clear = this._clearForSet.bind(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly add: (val: T) => void
|
|
||||||
readonly has: (val: T) => boolean
|
|
||||||
readonly clear: () => void
|
|
||||||
|
|
||||||
private _clearForSet() {
|
|
||||||
this._first = this._last = undefined
|
|
||||||
this._set!.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearForObj() {
|
|
||||||
this._first = this._last = undefined
|
|
||||||
this._obj = {}
|
|
||||||
this._objSize = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addForSet(val: T) {
|
|
||||||
if (this._set!.has(val as any)) return
|
|
||||||
|
|
||||||
if (!this._first) this._first = { v: val }
|
|
||||||
|
|
||||||
if (!this._last) this._last = this._first
|
|
||||||
else {
|
|
||||||
this._last.n = { v: val }
|
|
||||||
this._last = this._last.n
|
|
||||||
}
|
|
||||||
|
|
||||||
this._set!.add(val as any)
|
|
||||||
|
|
||||||
if (this._set!.size > this._capacity && this._first) {
|
|
||||||
// remove least recently used
|
|
||||||
this._set!.delete(this._first.v as any)
|
|
||||||
this._first = this._first.n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _hasForSet(val: T) {
|
|
||||||
return this._set!.has(val as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addForObj(val: T) {
|
|
||||||
if ((val as any) in this._obj!) return
|
|
||||||
|
|
||||||
if (!this._first) this._first = { v: val }
|
|
||||||
|
|
||||||
if (!this._last) this._last = this._first
|
|
||||||
else {
|
|
||||||
this._last.n = { v: val }
|
|
||||||
this._last = this._last.n
|
|
||||||
}
|
|
||||||
|
|
||||||
(this._obj as any)[val] = true
|
|
||||||
|
|
||||||
if (this._objSize === this._capacity) {
|
|
||||||
// remove least recently used
|
|
||||||
delete (this._obj as any)[this._first.v]
|
|
||||||
this._first = this._first.n
|
|
||||||
} else {
|
|
||||||
this._objSize! += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _hasForObj(val: T) {
|
|
||||||
return (val as any) in this._obj!
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -134,61 +134,3 @@ describe('encodeUrlSafeBase64', () => {
|
||||||
).eq('qu7d8aGTeuF6-g')
|
).eq('qu7d8aGTeuF6-g')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// describe('isProbablyPlainText', () => {
|
|
||||||
// it('should return true for buffers only containing printable ascii', () => {
|
|
||||||
// expect(
|
|
||||||
// isProbablyPlainText(Buffer.from('hello this is some ascii text'))
|
|
||||||
// ).to.be.true
|
|
||||||
// expect(
|
|
||||||
// isProbablyPlainText(
|
|
||||||
// Buffer.from(
|
|
||||||
// 'hello this is some ascii text\nwith unix new lines'
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// ).to.be.true
|
|
||||||
// expect(
|
|
||||||
// isProbablyPlainText(
|
|
||||||
// Buffer.from(
|
|
||||||
// 'hello this is some ascii text\r\nwith windows new lines'
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// ).to.be.true
|
|
||||||
// expect(
|
|
||||||
// isProbablyPlainText(
|
|
||||||
// Buffer.from(
|
|
||||||
// 'hello this is some ascii text\n\twith unix new lines and tabs'
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// ).to.be.true
|
|
||||||
// expect(
|
|
||||||
// isProbablyPlainText(
|
|
||||||
// Buffer.from(
|
|
||||||
// 'hello this is some ascii text\r\n\twith windows new lines and tabs'
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// ).to.be.true
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// it('should return false for buffers containing some binary data', () => {
|
|
||||||
// expect(isProbablyPlainText(Buffer.from('hello this is cedilla: ç'))).to
|
|
||||||
// .be.false
|
|
||||||
// expect(
|
|
||||||
// isProbablyPlainText(
|
|
||||||
// Buffer.from('hello this is some ascii text with emojis 🌸')
|
|
||||||
// )
|
|
||||||
// ).to.be.false
|
|
||||||
//
|
|
||||||
// // random strings of 16 bytes
|
|
||||||
// expect(
|
|
||||||
// isProbablyPlainText(
|
|
||||||
// Buffer.from('717f80f08eb9d88c3931712c0e2be32f', 'hex')
|
|
||||||
// )
|
|
||||||
// ).to.be.false
|
|
||||||
// expect(
|
|
||||||
// isProbablyPlainText(
|
|
||||||
// Buffer.from('20e8e218e54254c813b261432b0330d7', 'hex')
|
|
||||||
// )
|
|
||||||
// ).to.be.false
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
|
|
|
@ -42,43 +42,4 @@ describe('LruMap', () => {
|
||||||
expect(lru.get('third')).eq(undefined)
|
expect(lru.get('third')).eq(undefined)
|
||||||
expect(lru.get('fourth')).eq(4)
|
expect(lru.get('fourth')).eq(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Object backend', () => {
|
|
||||||
const lru = new LruMap<string, number>(2, true)
|
|
||||||
|
|
||||||
lru.set('first', 1)
|
|
||||||
expect(lru.has('first')).true
|
|
||||||
expect(lru.has('second')).false
|
|
||||||
expect(lru.get('first')).eq(1)
|
|
||||||
|
|
||||||
lru.set('first', 42)
|
|
||||||
expect(lru.has('first')).true
|
|
||||||
expect(lru.has('second')).false
|
|
||||||
expect(lru.get('first')).eq(42)
|
|
||||||
|
|
||||||
lru.set('second', 2)
|
|
||||||
expect(lru.has('first')).true
|
|
||||||
expect(lru.has('second')).true
|
|
||||||
expect(lru.get('first')).eq(42)
|
|
||||||
expect(lru.get('second')).eq(2)
|
|
||||||
|
|
||||||
lru.set('third', 3)
|
|
||||||
expect(lru.has('first')).false
|
|
||||||
expect(lru.has('second')).true
|
|
||||||
expect(lru.has('third')).true
|
|
||||||
expect(lru.get('first')).eq(undefined)
|
|
||||||
expect(lru.get('second')).eq(2)
|
|
||||||
expect(lru.get('third')).eq(3)
|
|
||||||
|
|
||||||
lru.get('second') // update lru so that last = third
|
|
||||||
lru.set('fourth', 4)
|
|
||||||
expect(lru.has('first')).false
|
|
||||||
expect(lru.has('second')).true
|
|
||||||
expect(lru.has('third')).false
|
|
||||||
expect(lru.has('fourth')).true
|
|
||||||
expect(lru.get('first')).eq(undefined)
|
|
||||||
expect(lru.get('second')).eq(2)
|
|
||||||
expect(lru.get('third')).eq(undefined)
|
|
||||||
expect(lru.get('fourth')).eq(4)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
95
packages/core/tests/lru-set.spec.ts
Normal file
95
packages/core/tests/lru-set.spec.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import Long from 'long'
|
||||||
|
import { describe, it } from 'mocha'
|
||||||
|
|
||||||
|
import { LruSet } from '../src'
|
||||||
|
|
||||||
|
describe('LruSet', () => {
|
||||||
|
describe('for strings', () => {
|
||||||
|
it('when 1 item is added, it is in the set', () => {
|
||||||
|
const set = new LruSet(2)
|
||||||
|
|
||||||
|
set.add('first')
|
||||||
|
expect(set.has('first')).true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when =capacity items are added, they are all in the set', () => {
|
||||||
|
const set = new LruSet(2)
|
||||||
|
|
||||||
|
set.add('first')
|
||||||
|
set.add('second')
|
||||||
|
|
||||||
|
expect(set.has('first')).true
|
||||||
|
expect(set.has('second')).true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when >capacity items are added, only the last <capacity> are in the set', () => {
|
||||||
|
const set = new LruSet(2)
|
||||||
|
|
||||||
|
set.add('first')
|
||||||
|
set.add('second')
|
||||||
|
set.add('third')
|
||||||
|
|
||||||
|
expect(set.has('first')).false
|
||||||
|
expect(set.has('second')).true
|
||||||
|
expect(set.has('third')).true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when the same added is while not eliminated, it is ignored', () => {
|
||||||
|
const set = new LruSet(2)
|
||||||
|
|
||||||
|
set.add('first')
|
||||||
|
set.add('second')
|
||||||
|
set.add('first')
|
||||||
|
set.add('third')
|
||||||
|
|
||||||
|
expect(set.has('first')).false
|
||||||
|
expect(set.has('second')).true
|
||||||
|
expect(set.has('third')).true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('for Longs', () => {
|
||||||
|
it('when 1 item is added, it is in the set', () => {
|
||||||
|
const set = new LruSet(2, true)
|
||||||
|
|
||||||
|
set.add(Long.fromNumber(1))
|
||||||
|
expect(set.has(Long.fromNumber(1))).true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when =capacity items are added, they are all in the set', () => {
|
||||||
|
const set = new LruSet(2, true)
|
||||||
|
|
||||||
|
set.add(Long.fromNumber(1))
|
||||||
|
set.add(Long.fromNumber(2))
|
||||||
|
|
||||||
|
expect(set.has(Long.fromNumber(1))).true
|
||||||
|
expect(set.has(Long.fromNumber(2))).true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when >capacity items are added, only the last <capacity> are in the set', () => {
|
||||||
|
const set = new LruSet(2, true)
|
||||||
|
|
||||||
|
set.add(Long.fromNumber(1))
|
||||||
|
set.add(Long.fromNumber(2))
|
||||||
|
set.add(Long.fromNumber(3))
|
||||||
|
|
||||||
|
expect(set.has(Long.fromNumber(1))).false
|
||||||
|
expect(set.has(Long.fromNumber(2))).true
|
||||||
|
expect(set.has(Long.fromNumber(3))).true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('when the same added is while not eliminated, it is ignored', () => {
|
||||||
|
const set = new LruSet(2, true)
|
||||||
|
|
||||||
|
set.add(Long.fromNumber(1))
|
||||||
|
set.add(Long.fromNumber(2))
|
||||||
|
set.add(Long.fromNumber(1))
|
||||||
|
set.add(Long.fromNumber(3))
|
||||||
|
|
||||||
|
expect(set.has(Long.fromNumber(1))).false
|
||||||
|
expect(set.has(Long.fromNumber(2))).true
|
||||||
|
expect(set.has(Long.fromNumber(3))).true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,60 +0,0 @@
|
||||||
import { expect } from 'chai'
|
|
||||||
import { describe, it } from 'mocha'
|
|
||||||
|
|
||||||
import { LruSet } from '../src'
|
|
||||||
|
|
||||||
describe('LruStringSet', () => {
|
|
||||||
it('Set backend', () => {
|
|
||||||
const set = new LruSet(2)
|
|
||||||
|
|
||||||
set.add('first')
|
|
||||||
expect(set.has('first')).true
|
|
||||||
|
|
||||||
set.add('second')
|
|
||||||
expect(set.has('first')).true
|
|
||||||
expect(set.has('second')).true
|
|
||||||
|
|
||||||
set.add('third')
|
|
||||||
expect(set.has('first')).false
|
|
||||||
expect(set.has('second')).true
|
|
||||||
expect(set.has('third')).true
|
|
||||||
|
|
||||||
set.add('third')
|
|
||||||
expect(set.has('first')).false
|
|
||||||
expect(set.has('second')).true
|
|
||||||
expect(set.has('third')).true
|
|
||||||
|
|
||||||
set.add('fourth')
|
|
||||||
expect(set.has('first')).false
|
|
||||||
expect(set.has('second')).false
|
|
||||||
expect(set.has('third')).true
|
|
||||||
expect(set.has('fourth')).true
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Object backend', () => {
|
|
||||||
const set = new LruSet(2, true)
|
|
||||||
|
|
||||||
set.add('first')
|
|
||||||
expect(set.has('first')).true
|
|
||||||
|
|
||||||
set.add('second')
|
|
||||||
expect(set.has('first')).true
|
|
||||||
expect(set.has('second')).true
|
|
||||||
|
|
||||||
set.add('third')
|
|
||||||
expect(set.has('first')).false
|
|
||||||
expect(set.has('second')).true
|
|
||||||
expect(set.has('third')).true
|
|
||||||
|
|
||||||
set.add('third')
|
|
||||||
expect(set.has('first')).false
|
|
||||||
expect(set.has('second')).true
|
|
||||||
expect(set.has('third')).true
|
|
||||||
|
|
||||||
set.add('fourth')
|
|
||||||
expect(set.has('first')).false
|
|
||||||
expect(set.has('second')).false
|
|
||||||
expect(set.has('third')).true
|
|
||||||
expect(set.has('fourth')).true
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -611,10 +611,10 @@ export class SqliteStorage implements ITelegramStorage, IStateStorage {
|
||||||
return row ? (row as { pts: number }).pts : null
|
return row ? (row as { pts: number }).pts : null
|
||||||
}
|
}
|
||||||
|
|
||||||
setManyChannelPts(values: Record<number, number>): void {
|
setManyChannelPts(values: Map<number, number>): void {
|
||||||
Object.entries(values).forEach(([cid, pts]) => {
|
for (const [cid, pts] of values) {
|
||||||
this._pending.push([this._statements.setPts, [cid, pts]])
|
this._pending.push([this._statements.setPts, [cid, pts]])
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePeers(peers: ITelegramStorage.PeerInfo[]): void {
|
updatePeers(peers: ITelegramStorage.PeerInfo[]): void {
|
||||||
|
|
Loading…
Reference in a new issue