chore: avoid using {}, use Maps instead

This commit is contained in:
alina 🌸 2023-09-19 01:33:47 +03:00
parent 80d4c59c69
commit 5a3b101c9f
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
27 changed files with 541 additions and 729 deletions

View file

@ -4019,9 +4019,9 @@ export class TelegramClient extends BaseTelegramClient {
protected _userId: number | null
protected _isBot: boolean
protected _selfUsername: string | null
protected _pendingConversations: Record<number, Conversation[]>
protected _pendingConversations: Map<number, Conversation[]>
protected _hasConversations: boolean
protected _parseModes: Record<string, IMessageEntityParser>
protected _parseModes: Map<string, IMessageEntityParser>
protected _defaultParseMode: string | null
protected _updatesLoopActive: boolean
protected _updatesLoopCv: ConditionVariable
@ -4044,8 +4044,8 @@ export class TelegramClient extends BaseTelegramClient {
protected _oldSeq?: number
protected _selfChanged: boolean
protected _catchUpChannels?: boolean
protected _cpts: Record<number, number>
protected _cptsMod: Record<number, number>
protected _cpts: Map<number, number>
protected _cptsMod: Map<number, number>
protected _updsLog: Logger
constructor(opts: BaseTelegramClientOptions) {
super(opts)
@ -4053,9 +4053,9 @@ export class TelegramClient extends BaseTelegramClient {
this._isBot = false
this._selfUsername = null
this.log.prefix = '[USER N/A] '
this._pendingConversations = {}
this._pendingConversations = new Map()
this._hasConversations = false
this._parseModes = {}
this._parseModes = new Map()
this._defaultParseMode = null
this._updatesLoopActive = false
this._updatesLoopCv = new ConditionVariable()
@ -4083,10 +4083,10 @@ export class TelegramClient extends BaseTelegramClient {
// channel PTS are not loaded immediately, and instead are cached here
// after the first time they were retrieved from the storage.
this._cpts = {}
this._cpts = new Map()
// modified channel pts, to avoid unnecessary
// DB calls for not modified cpts
this._cptsMod = {}
this._cptsMod = new Map()
this._selfChanged = false

View file

@ -6,13 +6,13 @@ import { Conversation, Message } from '../../types'
// @extension
interface ConversationsState {
_pendingConversations: Record<number, Conversation[]>
_pendingConversations: Map<number, Conversation[]>
_hasConversations: boolean
}
// @initialize
function _initializeConversation(this: TelegramClient) {
this._pendingConversations = {}
this._pendingConversations = new Map()
this._hasConversations = false
}
@ -28,7 +28,7 @@ export function _pushConversationMessage(
const chatId = getMarkedPeerId(msg.raw.peerId)
const msgId = msg.raw.id
this._pendingConversations[chatId].forEach((conv) => {
this._pendingConversations.get(chatId)?.forEach((conv) => {
conv['_lastMessage'] = msgId
if (incoming) conv['_lastReceivedMessage'] = msgId
})

View file

@ -30,11 +30,13 @@ export async function _parseEntities(
// either explicitly disabled or no available parser
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.`)
}
[text, entities] = this._parseModes[mode].parse(text)
[text, entities] = modeImpl.parse(text)
}
// replace mentionName entities with input ones

View file

@ -230,13 +230,13 @@ export async function sendText(
switch (cached._) {
case 'user':
peers.users[cached.id] = cached
peers.users.set(cached.id, cached)
break
case 'chat':
case 'chatForbidden':
case 'channel':
case 'channelForbidden':
peers.chats[cached.id] = cached
peers.chats.set(cached.id, cached)
break
default:
throw new MtTypeAssertionError(

View file

@ -3,13 +3,13 @@ import { IMessageEntityParser } from '../../types'
// @extension
interface ParseModesExtension {
_parseModes: Record<string, IMessageEntityParser>
_parseModes: Map<string, IMessageEntityParser>
_defaultParseMode: string | null
}
// @initialize
function _initializeParseModes(this: TelegramClient) {
this._parseModes = {}
this._parseModes = new Map()
this._defaultParseMode = null
}

View file

@ -16,12 +16,12 @@ export function registerParseMode(
): void {
const name = parseMode.name
if (name in this._parseModes) {
if (this._parseModes.has(name)) {
throw new MtClientError(
`Parse mode ${name} is already registered. Unregister it first!`,
)
}
this._parseModes[name] = parseMode
this._parseModes.set(name, parseMode)
if (!this._defaultParseMode) {
this._defaultParseMode = name
@ -38,10 +38,11 @@ export function registerParseMode(
* @internal
*/
export function unregisterParseMode(this: TelegramClient, name: string): void {
delete this._parseModes[name]
this._parseModes.delete(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,
): IMessageEntityParser {
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
}
if (!(name in this._parseModes)) {
const mode = this._parseModes.get(name)
if (!mode) {
throw new MtClientError(`Parse mode ${name} is not registered.`)
}
return this._parseModes[name]
return mode
}
/**
@ -78,7 +83,7 @@ export function getParseMode(
* @internal
*/
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.`)
}

View file

@ -78,8 +78,8 @@ interface UpdatesState {
// usually set in start() method based on `catchUp` param
_catchUpChannels?: boolean
_cpts: Record<number, number>
_cptsMod: Record<number, number>
_cpts: Map<number, number>
_cptsMod: Map<number, number>
_updsLog: Logger
}
@ -112,10 +112,10 @@ function _initializeUpdates(this: TelegramClient) {
// channel PTS are not loaded immediately, and instead are cached here
// after the first time they were retrieved from the storage.
this._cpts = {}
this._cpts = new Map()
// modified channel pts, to avoid unnecessary
// DB calls for not modified cpts
this._cptsMod = {}
this._cptsMod = new Map()
this._selfChanged = false
@ -358,7 +358,7 @@ export async function _saveStorage(
this._oldSeq = this._seq
await this.storage.setManyChannelPts(this._cptsMod)
this._cptsMod = {}
this._cptsMod.clear()
}
if (this._userId !== null && this._selfChanged) {
await this.storage.setSelf({
@ -472,33 +472,33 @@ async function _replaceMinPeers(
this: TelegramClient,
peers: PeersIndex,
): Promise<boolean> {
for (const key in peers.users) {
const user = peers.users[key] as Exclude<tl.TypeUser, tl.RawUserEmpty>
for (const [key, user_] of peers.users) {
const user = user_ as Exclude<tl.TypeUser, tl.RawUserEmpty>
if (user.min) {
const cached = await this.storage.getFullPeerById(user.id)
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) {
const c = peers.chats[key] as Extract<tl.TypeChat, { min?: boolean }>
for (const [key, chat_] of peers.chats) {
const chat = chat_ as Extract<tl.TypeChat, { min?: boolean }>
if (c.min) {
if (chat.min) {
let id: number
switch (c._) {
switch (chat._) {
case 'channel':
id = toggleChannelIdMark(c.id)
id = toggleChannelIdMark(chat.id)
break
default:
id = -c.id
id = -chat.id
}
const cached = await this.storage.getFullPeerById(id)
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 (marked > 0) {
peers.users[bare] = cached as tl.TypeUser
peers.users.set(bare, cached as tl.TypeUser)
} else {
peers.chats[bare] = cached as tl.TypeChat
peers.chats.set(bare, cached as tl.TypeChat)
}
return true
@ -787,7 +787,7 @@ async function _fetchChannelDifference(
fallbackPts?: number,
force = false,
): Promise<void> {
let _pts: number | null | undefined = this._cpts[channelId]
let _pts: number | null | undefined = this._cpts.get(channelId)
if (!_pts && this._catchUpChannels) {
_pts = await this.storage.getChannelPts(channelId)
@ -929,36 +929,39 @@ async function _fetchChannelDifference(
if (diff.final) break
}
this._cpts[channelId] = pts
this._cptsMod[channelId] = pts
this._cpts.set(channelId, pts)
this._cptsMod.set(channelId, pts)
}
function _fetchChannelDifferenceLater(
this: TelegramClient,
requestedDiff: Record<number, Promise<void>>,
requestedDiff: Map<number, Promise<void>>,
channelId: number,
fallbackPts?: number,
force = false,
): void {
if (!(channelId in requestedDiff)) {
requestedDiff[channelId] = _fetchChannelDifference
.call(this, channelId, fallbackPts, force)
.catch((err) => {
this._updsLog.warn(
'error fetching difference for %d: %s',
channelId,
err,
)
})
.then(() => {
delete requestedDiff[channelId]
})
if (!requestedDiff.has(channelId)) {
requestedDiff.set(
channelId,
_fetchChannelDifference
.call(this, channelId, fallbackPts, force)
.catch((err) => {
this._updsLog.warn(
'error fetching difference for %d: %s',
channelId,
err,
)
})
.then(() => {
requestedDiff.delete(channelId)
}),
)
}
}
async function _fetchDifference(
this: TelegramClient,
requestedDiff: Record<number, Promise<void>>,
requestedDiff: Map<number, Promise<void>>,
): Promise<void> {
for (;;) {
const diff = await this.call({
@ -1072,24 +1075,30 @@ async function _fetchDifference(
function _fetchDifferenceLater(
this: TelegramClient,
requestedDiff: Record<number, Promise<void>>,
requestedDiff: Map<number, Promise<void>>,
): void {
if (!(0 in requestedDiff)) {
requestedDiff[0] = _fetchDifference
.call(this, requestedDiff)
.catch((err) => {
this._updsLog.warn('error fetching common difference: %s', err)
})
.then(() => {
delete requestedDiff[0]
})
if (!requestedDiff.has(0)) {
requestedDiff.set(
0,
_fetchDifference
.call(this, requestedDiff)
.catch((err) => {
this._updsLog.warn(
'error fetching common difference: %s',
err,
)
})
.then(() => {
requestedDiff.delete(0)
}),
)
}
}
async function _onUpdate(
this: TelegramClient,
pending: PendingUpdate,
requestedDiff: Record<number, Promise<void>>,
requestedDiff: Map<number, Promise<void>>,
postponed = false,
unordered = false,
): Promise<void> {
@ -1153,7 +1162,7 @@ async function _onUpdate(
if (pending.pts) {
const localPts = pending.channelId ?
this._cpts[pending.channelId] :
this._cpts.get(pending.channelId) :
this._pts
if (localPts && pending.ptsBefore !== localPts) {
@ -1179,8 +1188,8 @@ async function _onUpdate(
)
if (pending.channelId) {
this._cpts[pending.channelId] = pending.pts!
this._cptsMod[pending.channelId] = pending.pts!
this._cpts.set(pending.channelId, pending.pts)
this._cptsMod.set(pending.channelId, pending.pts)
} else {
this._pts = pending.pts
}
@ -1274,7 +1283,7 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
this._pendingUnorderedUpdates.length,
)
const requestedDiff: Record<number, Promise<void>> = {}
const requestedDiff = new Map<number, Promise<void>>()
// first process pending containers
while (this._pendingUpdateContainers.length) {
@ -1497,8 +1506,8 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
let localPts: number | null = null
if (!pending.channelId) localPts = this._pts!
else if (pending.channelId in this._cpts) {
localPts = this._cpts[pending.channelId]
else if (this._cpts.has(pending.channelId)) {
localPts = this._cpts.get(pending.channelId)!
} else if (this._catchUpChannels) {
// only load stored channel pts in case
// the user has enabled catching up.
@ -1512,7 +1521,8 @@ export async function _updatesLoop(this: TelegramClient): Promise<void> {
)
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
if (!pending.channelId) localPts = this._pts!
else if (pending.channelId in this._cpts) {
localPts = this._cpts[pending.channelId]
else if (this._cpts.has(pending.channelId)) {
localPts = this._cpts.get(pending.channelId)
}
// 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
let pendingDiffs = Object.values(requestedDiff)
while (pendingDiffs.length) {
while (requestedDiff.size) {
log.debug(
'waiting for %d pending diffs before processing unordered: %j',
pendingDiffs.length,
Object.keys(requestedDiff),
'waiting for %d pending diffs before processing unordered: %J',
requestedDiff.size,
requestedDiff.keys(),
)
// is this necessary?
// this.primaryConnection._flushSendQueue()
await Promise.all(pendingDiffs)
await Promise.all([...requestedDiff.values()])
// diff results may as well contain new diffs to be requested
pendingDiffs = Object.values(requestedDiff)
log.debug(
'pending diffs awaited, new diffs requested: %d (%j)',
pendingDiffs.length,
Object.keys(requestedDiff),
'pending diffs awaited, new diffs requested: %d (%J)',
requestedDiff.size,
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
// diff may also contain new updates, which will be processed in the next tick,
// but we don't want to postpone diff fetching
pendingDiffs = Object.values(requestedDiff)
while (pendingDiffs.length) {
while (requestedDiff.size) {
log.debug(
'waiting for %d pending diffs after processing unordered: %j',
pendingDiffs.length,
Object.keys(requestedDiff),
'waiting for %d pending diffs after processing unordered: %J',
requestedDiff.size,
requestedDiff.keys(),
)
// is this necessary?
// this.primaryConnection._flushSendQueue()
await Promise.all(pendingDiffs)
await Promise.all([...requestedDiff.values()])
// diff results may as well contain new diffs to be requested
pendingDiffs = Object.values(requestedDiff)
log.debug(
'pending diffs awaited, new diffs requested: %d (%j)',
pendingDiffs.length,
Object.keys(requestedDiff),
requestedDiff.size,
requestedDiff.keys(),
)
}

View file

@ -78,7 +78,9 @@ export async function resolvePeer(
} else {
// username
if (!force) {
const fromStorage = await this.storage.getPeerByUsername(peerId)
const fromStorage = await this.storage.getPeerByUsername(
peerId.toLowerCase(),
)
if (fromStorage) return fromStorage
}

View file

@ -1,9 +1,6 @@
/* eslint-disable dot-notation */
import { AsyncLock, Deque, getMarkedPeerId, MaybeAsync } from '@mtcute/core'
import {
ControllablePromise,
createControllablePromise,
} from '@mtcute/core/src/utils/controllable-promise'
import { ControllablePromise, createControllablePromise } from '@mtcute/core'
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../client'
@ -43,10 +40,10 @@ export class Conversation {
private _pendingNewMessages = new Deque<Message>()
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 _pendingRead: Record<number, QueuedHandler<void>> = {}
private _pendingRead: Map<number, QueuedHandler<void>> = new Map()
constructor(readonly client: TelegramClient, readonly chat: InputPeerLike) {
this._onNewMessage = this._onNewMessage.bind(this)
@ -112,10 +109,10 @@ export class Conversation {
this.client.on('edit_message', this._onEditMessage)
this.client.on('history_read', this._onHistoryRead)
if (!(this._chatId in this.client['_pendingConversations'])) {
this.client['_pendingConversations'][this._chatId] = []
if (this.client['_pendingConversations'].has(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
}
@ -129,25 +126,26 @@ export class Conversation {
this.client.off('edit_message', this._onEditMessage)
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 (idx > -1) {
if (pendingIdx > -1) {
// just in case
pending[this._chatId].splice(idx, 1)
pending!.splice(pendingIdx, 1)
}
if (!pending[this._chatId].length) {
delete pending[this._chatId]
if (pending && !pending.length) {
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
this._queuedNewMessage.clear()
this._pendingNewMessages.clear()
this._pendingEditMessage = {}
this._pendingEditMessage.clear()
this._recentEdits.clear()
this._pendingRead = {}
this._pendingRead.clear()
this._started = false
}
@ -424,15 +422,15 @@ export class Conversation {
if (timeout) {
timer = setTimeout(() => {
promise.reject(new MtTimeoutError(timeout))
delete this._pendingEditMessage[msgId]
this._pendingEditMessage.delete(msgId)
}, timeout)
}
this._pendingEditMessage[msgId] = {
this._pendingEditMessage.set(msgId, {
promise,
check: filter,
timeout: timer,
}
})
this._processRecentEdits()
@ -476,14 +474,14 @@ export class Conversation {
if (timeout !== null) {
timer = setTimeout(() => {
promise.reject(new MtTimeoutError(timeout))
delete this._pendingRead[msgId]
this._pendingRead.delete(msgId)
}, timeout)
}
this._pendingRead[msgId] = {
this._pendingRead.set(msgId, {
promise,
timeout: timer,
}
})
return promise
}
@ -521,10 +519,12 @@ export class Conversation {
private _onEditMessage(msg: Message, fromRecent = false) {
if (msg.chat.id !== this._chatId) return
const it = this._pendingEditMessage[msg.id]
const it = this._pendingEditMessage.get(msg.id)
if (!it && !fromRecent) {
this._recentEdits.pushBack(msg)
if (!it) {
if (!fromRecent) {
this._recentEdits.pushBack(msg)
}
return
}
@ -533,7 +533,7 @@ export class Conversation {
if (!it.check || (await it.check(msg))) {
if (it.timeout) clearTimeout(it.timeout)
it.promise.resolve(msg)
delete this._pendingEditMessage[msg.id]
this._pendingEditMessage.delete(msg.id)
}
})().catch((e) => {
this.client['_emitError'](e)
@ -545,12 +545,12 @@ export class Conversation {
const lastRead = upd.maxReadId
for (const msgId in this._pendingRead) {
if (parseInt(msgId) <= lastRead) {
const it = this._pendingRead[msgId]
for (const msgId of this._pendingRead.keys()) {
if (msgId <= lastRead) {
const it = this._pendingRead.get(msgId)!
if (it.timeout) clearTimeout(it.timeout)
it.promise.resolve()
delete this._pendingRead[msgId]
this._pendingRead.delete(msgId)
}
}
}

View file

@ -1,3 +1,4 @@
import { LongMap } from '@mtcute/core'
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
@ -164,7 +165,7 @@ export class StickerSet {
if (!this._stickers) {
this._stickers = []
const index: Record<string, tl.Mutable<StickerInfo>> = {}
const index = new LongMap<tl.Mutable<StickerInfo>>()
this.full!.documents.forEach((doc) => {
const sticker = parseDocument(
@ -186,15 +187,15 @@ export class StickerSet {
sticker,
}
this._stickers!.push(info)
index[doc.id.toString()] = info
index.set(doc.id, info)
})
this.full!.packs.forEach((pack) => {
pack.documents.forEach((id) => {
const sid = id.toString()
const item = index.get(id)
if (sid in index) {
index[sid].emoji += pack.emoticon
if (item) {
item.emoji += pack.emoticon
}
})
})

View file

@ -6,8 +6,8 @@ const ERROR_MSG =
'Given peer is not available in this index. This is most likely an internal library error.'
export class PeersIndex {
readonly users: Record<number, tl.TypeUser> = {}
readonly chats: Record<number, tl.TypeChat> = {}
readonly users: Map<number, tl.TypeUser> = new Map()
readonly chats: Map<number, tl.TypeChat> = new Map()
hasMin = false
@ -18,14 +18,14 @@ export class PeersIndex {
const index = new PeersIndex()
obj.users?.forEach((user) => {
index.users[user.id] = user
index.users.set(user.id, user)
if ((user as Exclude<typeof user, tl.RawUserEmpty>).min) {
index.hasMin = true
}
})
obj.chats?.forEach((chat) => {
index.chats[chat.id] = chat
index.chats.set(chat.id, chat)
if (
(
@ -46,7 +46,7 @@ export class PeersIndex {
}
user(id: number): tl.TypeUser {
const r = this.users[id]
const r = this.users.get(id)
if (!r) {
throw new MtArgumentError(ERROR_MSG)
@ -56,7 +56,7 @@ export class PeersIndex {
}
chat(id: number): tl.TypeChat {
const r = this.chats[id]
const r = this.chats.get(id)
if (!r) {
throw new MtArgumentError(ERROR_MSG)

View file

@ -101,8 +101,8 @@ export class MtprotoSession {
/// state ///
// recent msg ids
recentOutgoingMsgIds = new LruSet<Long>(1000, false, true)
recentIncomingMsgIds = new LruSet<Long>(1000, false, true)
recentOutgoingMsgIds = new LruSet<Long>(1000, true)
recentIncomingMsgIds = new LruSet<Long>(1000, true)
// queues
queuedRpc = new Deque<PendingRpc>()

View file

@ -420,7 +420,7 @@ export class NetworkManager {
readonly _reconnectionStrategy: ReconnectionStrategy<PersistentConnectionParams>
readonly _connectionCount: ConnectionCountDelegate
protected readonly _dcConnections: Record<number, DcConnectionManager> = {}
protected readonly _dcConnections = new Map<number, DcConnectionManager>()
protected _primaryDc?: DcConnectionManager
private _keepAliveInterval?: NodeJS.Timeout
@ -545,18 +545,18 @@ export class NetworkManager {
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> {
if (!this._dcConnections[dcId]) {
if (dcId in this._dcCreationPromise) {
if (!this._dcConnections.has(dcId)) {
if (this._dcCreationPromise.has(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>()
this._dcCreationPromise[dcId] = promise
this._dcCreationPromise.set(dcId, promise)
this._log.debug('creating new DC %d', dcId)
@ -569,14 +569,14 @@ export class NetworkManager {
dc.main.requestAuth()
}
this._dcConnections[dcId] = dc
this._dcConnections.set(dcId, dc)
promise.resolve()
} catch (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')
}
if (this._dcConnections[defaultDcs.main.id]) {
if (this._dcConnections.has(defaultDcs.main.id)) {
// shouldn't happen
throw new Error('DC manager already exists')
}
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)
}
@ -648,9 +648,10 @@ export class NetworkManager {
setIsPremium(isPremium: boolean): void {
this._log.debug('setting isPremium to %s', isPremium)
this.params.isPremium = isPremium
Object.values(this._dcConnections).forEach((dc) => {
for (const dc of this._dcConnections.values()) {
dc.setIsPremium(isPremium)
})
}
}
// future-proofing. should probably remove once the implementation is stable
@ -693,20 +694,19 @@ export class NetworkManager {
const options = await this._findDcOptions(newDc)
if (!this._dcConnections[newDc]) {
this._dcConnections[newDc] = new DcConnectionManager(
this,
if (!this._dcConnections.has(newDc)) {
this._dcConnections.set(
newDc,
options,
new DcConnectionManager(this, newDc, 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>(
message: T,
params?: RpcCallOptions,
@ -721,15 +721,15 @@ export class NetworkManager {
const maxRetryCount = params?.maxRetryCount ?? this.params.maxRetryCount
// do not send requests that are in flood wait
if (message._ in this._floodWaitedRequests) {
const delta = this._floodWaitedRequests[message._] - Date.now()
if (this._floodWaitedRequests.has(message._)) {
const delta = this._floodWaitedRequests.get(message._)! - Date.now()
if (delta <= 3000) {
// flood waits below 3 seconds are "ignored"
delete this._floodWaitedRequests[message._]
this._floodWaitedRequests.delete(message._)
} else if (delta <= this.params.floodSleepThreshold) {
await sleep(delta)
delete this._floodWaitedRequests[message._]
this._floodWaitedRequests.delete(message._)
} else {
const err = tl.RpcError.create(
tl.RpcError.FLOOD,
@ -792,8 +792,10 @@ export class NetworkManager {
) {
if (e.text !== 'SLOWMODE_WAIT_%d') {
// SLOW_MODE_WAIT is chat-specific, not request-specific
this._floodWaitedRequests[message._] =
Date.now() + e.seconds * 1000
this._floodWaitedRequests.set(
message._,
Date.now() + e.seconds * 1000,
)
}
// In test servers, FLOOD_WAIT_0 has been observed, and sleeping for
@ -845,16 +847,16 @@ export class NetworkManager {
}
changeTransport(factory: TransportFactory): void {
Object.values(this._dcConnections).forEach((dc) => {
for (const dc of this._dcConnections.values()) {
dc.main.changeTransport(factory)
dc.upload.changeTransport(factory)
dc.download.changeTransport(factory)
dc.downloadSmall.changeTransport(factory)
})
}
}
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 (!this._primaryDc) {
@ -880,7 +882,7 @@ export class NetworkManager {
}
destroy(): void {
for (const dc of Object.values(this._dcConnections)) {
for (const dc of this._dcConnections.values()) {
dc.main.destroy()
dc.upload.destroy()
dc.download.destroy()

View file

@ -168,7 +168,7 @@ export interface ITelegramStorage {
* Storage is supposed to replace stored channel `pts` values
* 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.

View file

@ -12,19 +12,32 @@ export class JsonMemoryStorage extends MemoryStorage {
protected _loadJson(json: string): void {
this._setStateFrom(
JSON.parse(json, (key, value) => {
if (key === 'authKeys') {
const ret: Record<string, Buffer> = {}
switch (key) {
case 'authKeys':
case 'authKeysTemp': {
const ret: Record<string, Buffer> = {}
;(value as string).split('|').forEach((pair: string) => {
const [dcId, b64] = pair.split(',')
ret[dcId] = Buffer.from(b64, 'base64')
})
;(value as string)
.split('|')
.forEach((pair: string) => {
const [dcId, b64] = pair.split(',')
ret[dcId] = Buffer.from(b64, 'base64')
})
return ret
}
if (key === 'accessHash') {
return longFromFastString(value as string)
return ret
}
case 'authKeysTempExpiry':
case 'entities':
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
@ -34,16 +47,30 @@ export class JsonMemoryStorage extends MemoryStorage {
protected _saveJson(): string {
return JSON.stringify(this._state, (key, value) => {
if (key === 'authKeys') {
const value_ = value as Record<string, Buffer | null>
switch (key) {
case 'authKeys':
case 'authKeysTemp': {
const value_ = value as Map<string, Buffer | null>
return Object.entries(value_)
.filter((it): it is [string, Buffer] => it[1] !== null)
.map(([dcId, key]) => dcId + ',' + key.toString('base64'))
.join('|')
}
if (key === 'accessHash') {
return longToFastString(value as tl.Long)
return [...value_.entries()]
.filter((it): it is [string, Buffer] => it[1] !== null)
.map(
([dcId, key]) => dcId + ',' + key.toString('base64'),
)
.join('|')
}
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

View file

@ -14,24 +14,24 @@ export interface MemorySessionState {
$version: typeof CURRENT_VERSION
defaultDcs: ITelegramStorage.DcOptions | null
authKeys: Record<number, Buffer | null>
authKeysTemp: Record<string, Buffer | null>
authKeysTempExpiry: Record<string, number>
authKeys: Map<number, Buffer>
authKeysTemp: Map<string, Buffer>
authKeysTempExpiry: Map<string, number>
// marked peer id -> entity info
entities: Record<number, PeerInfoWithUpdated>
entities: Map<number, PeerInfoWithUpdated>
// phone number -> peer id
phoneIndex: Record<string, number>
phoneIndex: Map<string, number>
// username -> peer id
usernameIndex: Record<string, number>
usernameIndex: Map<string, number>
// common pts, date, seq, qts
gpts: [number, number, number, number] | null
// channel pts
pts: Record<number, number>
pts: Map<number, number>
// state for fsm
fsm: Record<
fsm: Map<
string,
{
// value
@ -42,7 +42,7 @@ export interface MemorySessionState {
>
// state for rate limiter
rl: Record<
rl: Map<
string,
{
// reset
@ -111,16 +111,16 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
this._state = {
$version: CURRENT_VERSION,
defaultDcs: null,
authKeys: {},
authKeysTemp: {},
authKeysTempExpiry: {},
entities: {},
phoneIndex: {},
usernameIndex: {},
authKeys: new Map(),
authKeysTemp: new Map(),
authKeysTempExpiry: new Map(),
entities: new Map(),
phoneIndex: new Map(),
usernameIndex: new Map(),
gpts: null,
pts: {},
fsm: {},
rl: {},
pts: new Map(),
fsm: new Map(),
rl: new Map(),
self: null,
}
}
@ -138,19 +138,20 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
let populate = false
if (!obj.phoneIndex) {
obj.phoneIndex = {}
obj.phoneIndex = new Map()
populate = true
}
if (!obj.usernameIndex) {
obj.usernameIndex = {}
obj.usernameIndex = new Map()
populate = true
}
if (populate) {
Object.values(obj.entities).forEach(
(ent: ITelegramStorage.PeerInfo) => {
if (ent.phone) obj.phoneIndex[ent.phone] = ent.id
if (ent.username) obj.usernameIndex[ent.username] = ent.id
if (ent.phone) obj.phoneIndex.set(ent.phone, 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 rl = state.rl
Object.keys(fsm).forEach((key) => {
const exp = fsm[key].e
if (exp && exp < now) {
delete fsm[key]
for (const [key, item] of fsm) {
if (item.e && item.e < now) {
fsm.delete(key)
}
})
}
Object.keys(rl).forEach((key) => {
if (rl[key].res < now) {
delete rl[key]
for (const [key, item] of rl) {
if (item.res < now) {
rl.delete(key)
}
})
}
}
getDefaultDcs(): ITelegramStorage.DcOptions | null {
@ -198,36 +197,47 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
expiresAt: number,
): void {
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 {
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 {
if (tempIndex !== undefined) {
const k = `${dcId}:${tempIndex}`
if (Date.now() > (this._state.authKeysTempExpiry[k] ?? 0)) {
if (Date.now() > (this._state.authKeysTempExpiry.get(k) ?? 0)) {
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 {
this._state.authKeys[dcId] = null
Object.keys(this._state.authKeysTemp).forEach((key) => {
this._state.authKeys.delete(dcId)
for (const key of this._state.authKeysTemp.keys()) {
if (key.startsWith(`${dcId}:`)) {
delete this._state.authKeysTemp[key]
delete this._state.authKeysTempExpiry[key]
this._state.authKeysTemp.delete(key)
this._state.authKeysTempExpiry.delete(key)
}
})
}
}
updatePeers(peers: PeerInfoWithUpdated[]): MaybeAsync<void> {
@ -235,26 +245,25 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
this._cachedFull.set(peer.id, peer.full)
peer.updated = Date.now()
const old = this._state.entities[peer.id]
const old = this._state.entities.get(peer.id)
if (old) {
// min peer
// if (peer.fromMessage) continue
// delete old index entries if needed
if (old.username && old.username !== peer.username) {
delete this._state.usernameIndex[old.username]
if (old.username && peer.username !== old.username) {
this._state.usernameIndex.delete(old.username)
}
if (old.phone && old.phone !== peer.phone) {
delete this._state.phoneIndex[old.phone]
this._state.phoneIndex.delete(old.phone)
}
}
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)) {
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)
return peer
}
getPeerByPhone(phone: string): tl.TypeInputPeer | null {
return this._getInputPeer(
this._state.entities[this._state.phoneIndex[phone]],
)
const peerId = this._state.phoneIndex.get(phone)
if (!peerId) return null
return this._getInputPeer(this._state.entities.get(peerId))
}
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
const peer = this._state.entities[id]
const peer = this._state.entities.get(id)
if (!peer) 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
}
setManyChannelPts(values: Record<number, number>): void {
for (const id in values) {
this._state.pts[id] = values[id]
setManyChannelPts(values: Map<number, number>): void {
for (const [id, pts] of values) {
this._state.pts.set(id, pts)
}
}
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> {
@ -362,12 +372,12 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
// IStateStorage implementation
getState(key: string): unknown {
const val = this._state.fsm[key]
const val = this._state.fsm.get(key)
if (!val) return null
if (val.e && val.e < Date.now()) {
// expired
delete this._state.fsm[key]
this._state.fsm.delete(key)
return null
}
@ -376,14 +386,14 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
}
setState(key: string, state: unknown, ttl?: number): void {
this._state.fsm[key] = {
this._state.fsm.set(key, {
v: state,
e: ttl ? Date.now() + ttl * 1000 : undefined,
}
})
}
deleteState(key: string): void {
delete this._state.fsm[key]
this._state.fsm.delete(key)
}
getCurrentScene(key: string): string | null {
@ -395,26 +405,26 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
}
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] {
// leaky bucket
const now = Date.now()
if (!(key in this._state.rl)) {
const item = this._state.rl.get(key)
if (!item) {
const state = {
res: now + window * 1000,
rem: limit,
}
this._state.rl[key] = state
this._state.rl.set(key, state)
return [state.rem, state.res]
}
const item = this._state.rl[key]
if (item.res < now) {
// expired
@ -423,7 +433,7 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
rem: limit,
}
this._state.rl[key] = state
this._state.rl.set(key, state)
return [state.rem, state.res]
}
@ -434,6 +444,6 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
}
resetRateLimit(key: string): void {
delete this._state.rl[key]
this._state.rl.delete(key)
}
}

View file

@ -13,7 +13,7 @@ export * from './linked-list'
export * from './logger'
export * from './long-utils'
export * from './lru-map'
export * from './lru-string-set'
export * from './lru-set'
export * from './misc-utils'
export * from './peer-utils'
export * from './sorted-array'

View file

@ -65,12 +65,19 @@ export class Logger {
fmt.includes('%h') ||
fmt.includes('%b') ||
fmt.includes('%j') ||
fmt.includes('%J') ||
fmt.includes('%l')
) {
let idx = 0
fmt = fmt.replace(FORMATTER_RE, (m) => {
if (m === '%h' || m === '%b' || m === '%j' || m === '%l') {
const val = args[idx]
if (
m === '%h' ||
m === '%b' ||
m === '%j' ||
m === '%J' ||
m === '%l'
) {
let val = args[idx]
args.splice(idx, 1)
@ -82,7 +89,9 @@ export class Logger {
}
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) => {
if (
typeof v === 'object' &&

View file

@ -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 { getRandomInt } from './misc-utils'
@ -98,108 +95,40 @@ export function longFromFastString(val: string, unsigned = false): Long {
* Uses fast string representation internally.
*/
export class LongMap<V> {
private _map?: Map<string, V>
private _obj?: any
private _map = new Map<string, V>()
constructor(useObject = false) {
if (typeof Map === 'undefined' || useObject) {
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)
}
set(key: Long, value: V): void {
this._map.set(longToFastString(key), value)
}
readonly set: (key: Long, value: V) => void
readonly has: (key: Long) => boolean
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)
has(key: Long): boolean {
return this._map.has(longToFastString(key))
}
private _hasForMap(key: Long): boolean {
return this._map!.has(longToFastString(key))
get(key: Long): V | undefined {
return this._map.get(longToFastString(key))
}
private _getForMap(key: Long): V | undefined {
return this._map!.get(longToFastString(key))
delete(key: Long): void {
this._map.delete(longToFastString(key))
}
private _deleteForMap(key: Long): void {
this._map!.delete(longToFastString(key))
}
private *_keysForMap(unsigned?: boolean): IterableIterator<Long> {
for (const v of this._map!.keys()) {
*keys(unsigned?: boolean): IterableIterator<Long> {
for (const v of this._map.keys()) {
yield longFromFastString(v, unsigned)
}
}
private _valuesForMap(): IterableIterator<V> {
return this._map!.values()
values(): IterableIterator<V> {
return this._map.values()
}
private _clearForMap(): void {
this._map!.clear()
clear(): void {
this._map.clear()
}
private _sizeForMap(): number {
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
size(): number {
return this._map.size
}
}
@ -209,74 +138,25 @@ export class LongMap<V> {
* Uses fast string representation internally
*/
export class LongSet {
private _set?: 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
private _set = new Set<string>()
get size(): number {
return this._objSize ?? this._set!.size
return this._set.size
}
private _addForSet(val: Long) {
this._set!.add(longToFastString(val))
add(val: Long) {
this._set.add(longToFastString(val))
}
private _deleteForSet(val: Long) {
this._set!.delete(longToFastString(val))
delete(val: Long) {
this._set.delete(longToFastString(val))
}
private _hasForSet(val: Long) {
return this._set!.has(longToFastString(val))
has(val: Long) {
return this._set.has(longToFastString(val))
}
private _clearForSet() {
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
clear() {
this._set.clear()
}
}

View file

@ -1,5 +1,5 @@
/* 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
import { LongMap } from './long-utils'
@ -15,8 +15,7 @@ interface TwoWayLinkedList<K, T> {
}
/**
* Simple class implementing LRU-like behaviour for a map,
* falling back to objects when `Map` is not available.
* Simple class implementing LRU-like behaviour for a Map
*
* 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 _last?: TwoWayLinkedList<K, V>
private _map: Map<K, TwoWayLinkedList<K, V>>
private _size = 0
constructor(capacity: number, useObject = false, forLong = false) {
constructor(capacity: number, forLong = false) {
this._capacity = capacity
if (forLong) {
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)
}
this._map = forLong ? (new LongMap() as any) : new 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 {
if (item === this._first) {
return // already the most recently used
@ -84,7 +62,7 @@ export class LruMap<K extends string | number, V> {
}
get(key: K): V | undefined {
const item = this._get(key)
const item = this._map.get(key)
if (!item) return undefined
this._markUsed(item)
@ -93,7 +71,7 @@ export class LruMap<K extends string | number, V> {
}
has(key: K): boolean {
return this._has(key)
return this._map.has(key)
}
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
item.p = item.n = undefined
this._del(item.k)
this._map.delete(item.k)
this._size -= 1
}
set(key: K, value: V): void {
let item = this._get(key)
let item = this._map.get(key)
if (item) {
// already in cache, update
@ -130,7 +108,7 @@ export class LruMap<K extends string | number, V> {
n: undefined,
p: undefined,
}
this._set(key, item as any)
this._map.set(key, item as any)
if (this._first) {
this._first.p = item
@ -154,7 +132,7 @@ export class LruMap<K extends string | number, V> {
}
delete(key: K): void {
const item = this._get(key)
const item = this._map.get(key)
if (item) this._remove(item)
}
}

View 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)
}
}

View file

@ -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!
}
}

View file

@ -134,61 +134,3 @@ describe('encodeUrlSafeBase64', () => {
).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
// })
// })

View file

@ -42,43 +42,4 @@ describe('LruMap', () => {
expect(lru.get('third')).eq(undefined)
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)
})
})

View 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
})
})
})

View file

@ -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
})
})

View file

@ -611,10 +611,10 @@ export class SqliteStorage implements ITelegramStorage, IStateStorage {
return row ? (row as { pts: number }).pts : null
}
setManyChannelPts(values: Record<number, number>): void {
Object.entries(values).forEach(([cid, pts]) => {
setManyChannelPts(values: Map<number, number>): void {
for (const [cid, pts] of values) {
this._pending.push([this._statements.setPts, [cid, pts]])
})
}
}
updatePeers(peers: ITelegramStorage.PeerInfo[]): void {