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 _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

View file

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

View file

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

View file

@ -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(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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.' '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)

View file

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

View file

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

View file

@ -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.

View file

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

View file

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

View file

@ -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'

View file

@ -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' &&

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

View file

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

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') ).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('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)
})
}) })

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 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 {