chore: avoid using {}, use Maps instead
This commit is contained in:
parent
80d4c59c69
commit
5a3b101c9f
27 changed files with 541 additions and 729 deletions
|
@ -4019,9 +4019,9 @@ export class TelegramClient extends BaseTelegramClient {
|
|||
protected _userId: number | null
|
||||
protected _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
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.`)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,19 +929,21 @@ 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
|
||||
if (!requestedDiff.has(channelId)) {
|
||||
requestedDiff.set(
|
||||
channelId,
|
||||
_fetchChannelDifference
|
||||
.call(this, channelId, fallbackPts, force)
|
||||
.catch((err) => {
|
||||
this._updsLog.warn(
|
||||
|
@ -951,14 +953,15 @@ function _fetchChannelDifferenceLater(
|
|||
)
|
||||
})
|
||||
.then(() => {
|
||||
delete requestedDiff[channelId]
|
||||
})
|
||||
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
|
||||
if (!requestedDiff.has(0)) {
|
||||
requestedDiff.set(
|
||||
0,
|
||||
_fetchDifference
|
||||
.call(this, requestedDiff)
|
||||
.catch((err) => {
|
||||
this._updsLog.warn('error fetching common difference: %s', err)
|
||||
this._updsLog.warn(
|
||||
'error fetching common difference: %s',
|
||||
err,
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
delete requestedDiff[0]
|
||||
})
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -12,18 +12,31 @@ export class JsonMemoryStorage extends MemoryStorage {
|
|||
protected _loadJson(json: string): void {
|
||||
this._setStateFrom(
|
||||
JSON.parse(json, (key, value) => {
|
||||
if (key === 'authKeys') {
|
||||
switch (key) {
|
||||
case 'authKeys':
|
||||
case 'authKeysTemp': {
|
||||
const ret: Record<string, Buffer> = {}
|
||||
|
||||
;(value as string).split('|').forEach((pair: string) => {
|
||||
;(value as string)
|
||||
.split('|')
|
||||
.forEach((pair: string) => {
|
||||
const [dcId, b64] = pair.split(',')
|
||||
ret[dcId] = Buffer.from(b64, 'base64')
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
if (key === 'accessHash') {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -34,15 +47,29 @@ 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_)
|
||||
return [...value_.entries()]
|
||||
.filter((it): it is [string, Buffer] => it[1] !== null)
|
||||
.map(([dcId, key]) => dcId + ',' + key.toString('base64'))
|
||||
.map(
|
||||
([dcId, key]) => dcId + ',' + key.toString('base64'),
|
||||
)
|
||||
.join('|')
|
||||
}
|
||||
if (key === 'accessHash') {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, item] of rl) {
|
||||
if (item.res < now) {
|
||||
rl.delete(key)
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(rl).forEach((key) => {
|
||||
if (rl[key].res < now) {
|
||||
delete rl[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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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' &&
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,36 +26,15 @@ 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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
65
packages/core/src/utils/lru-set.ts
Normal file
65
packages/core/src/utils/lru-set.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
// ^^ because of performance reasons
|
||||
import Long from 'long'
|
||||
|
||||
import { LongSet } from './long-utils'
|
||||
|
||||
interface OneWayLinkedList<T> {
|
||||
v: T
|
||||
n?: OneWayLinkedList<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple class implementing LRU-like behaviour for a Set.
|
||||
*
|
||||
* Note: this is not exactly LRU, but rather "least recently added"
|
||||
* and doesn't mark items as recently added if they are already in the set.
|
||||
* This is enough for our use case, so we don't bother with more complex implementation.
|
||||
*
|
||||
* Used to store recently received message IDs in {@link SessionConnection}
|
||||
*
|
||||
* Uses one-way linked list internally to keep track of insertion order
|
||||
*/
|
||||
export class LruSet<T extends string | number | Long> {
|
||||
private _capacity: number
|
||||
private _first?: OneWayLinkedList<T>
|
||||
private _last?: OneWayLinkedList<T>
|
||||
|
||||
private _set: Set<T> | LongSet
|
||||
|
||||
constructor(capacity: number, forLong = false) {
|
||||
this._capacity = capacity
|
||||
|
||||
this._set = forLong ? new LongSet() : new Set()
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._first = this._last = undefined
|
||||
this._set.clear()
|
||||
}
|
||||
|
||||
add(val: T) {
|
||||
if (this._set.has(val as any)) return
|
||||
|
||||
if (!this._first) this._first = { v: val }
|
||||
|
||||
if (!this._last) this._last = this._first
|
||||
else {
|
||||
this._last.n = { v: val }
|
||||
this._last = this._last.n
|
||||
}
|
||||
|
||||
this._set.add(val as any)
|
||||
|
||||
if (this._set.size > this._capacity && this._first) {
|
||||
// remove least recently used
|
||||
this._set.delete(this._first.v as any)
|
||||
this._first = this._first.n
|
||||
}
|
||||
}
|
||||
|
||||
has(val: T) {
|
||||
return this._set.has(val as any)
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
// ^^ because of performance reasons
|
||||
import Long from 'long'
|
||||
|
||||
import { LongSet } from './long-utils'
|
||||
|
||||
interface OneWayLinkedList<T> {
|
||||
v: T
|
||||
n?: OneWayLinkedList<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple class implementing LRU-like behaviour for a set,
|
||||
* falling back to objects when `Set` is not available.
|
||||
*
|
||||
* Used to store recently received message IDs in {@link SessionConnection}
|
||||
*
|
||||
* Uses one-way linked list internally to keep track of insertion order
|
||||
*/
|
||||
export class LruSet<T extends string | number | Long> {
|
||||
private _capacity: number
|
||||
private _first?: OneWayLinkedList<T>
|
||||
private _last?: OneWayLinkedList<T>
|
||||
|
||||
private _set?: Set<T> | LongSet
|
||||
private _obj?: object
|
||||
private _objSize?: number
|
||||
|
||||
constructor(capacity: number, useObject = false, forLong = false) {
|
||||
this._capacity = capacity
|
||||
|
||||
if (!forLong && (typeof Set === 'undefined' || useObject)) {
|
||||
this._obj = Object.create(null)
|
||||
this._objSize = 0
|
||||
this.add = this._addForObj.bind(this)
|
||||
this.has = this._hasForObj.bind(this)
|
||||
this.clear = this._clearForObj.bind(this)
|
||||
} else {
|
||||
this._set = forLong ? new LongSet(useObject) : new Set()
|
||||
this.add = this._addForSet.bind(this)
|
||||
this.has = this._hasForSet.bind(this)
|
||||
this.clear = this._clearForSet.bind(this)
|
||||
}
|
||||
}
|
||||
|
||||
readonly add: (val: T) => void
|
||||
readonly has: (val: T) => boolean
|
||||
readonly clear: () => void
|
||||
|
||||
private _clearForSet() {
|
||||
this._first = this._last = undefined
|
||||
this._set!.clear()
|
||||
}
|
||||
|
||||
private _clearForObj() {
|
||||
this._first = this._last = undefined
|
||||
this._obj = {}
|
||||
this._objSize = 0
|
||||
}
|
||||
|
||||
private _addForSet(val: T) {
|
||||
if (this._set!.has(val as any)) return
|
||||
|
||||
if (!this._first) this._first = { v: val }
|
||||
|
||||
if (!this._last) this._last = this._first
|
||||
else {
|
||||
this._last.n = { v: val }
|
||||
this._last = this._last.n
|
||||
}
|
||||
|
||||
this._set!.add(val as any)
|
||||
|
||||
if (this._set!.size > this._capacity && this._first) {
|
||||
// remove least recently used
|
||||
this._set!.delete(this._first.v as any)
|
||||
this._first = this._first.n
|
||||
}
|
||||
}
|
||||
|
||||
private _hasForSet(val: T) {
|
||||
return this._set!.has(val as any)
|
||||
}
|
||||
|
||||
private _addForObj(val: T) {
|
||||
if ((val as any) in this._obj!) return
|
||||
|
||||
if (!this._first) this._first = { v: val }
|
||||
|
||||
if (!this._last) this._last = this._first
|
||||
else {
|
||||
this._last.n = { v: val }
|
||||
this._last = this._last.n
|
||||
}
|
||||
|
||||
(this._obj as any)[val] = true
|
||||
|
||||
if (this._objSize === this._capacity) {
|
||||
// remove least recently used
|
||||
delete (this._obj as any)[this._first.v]
|
||||
this._first = this._first.n
|
||||
} else {
|
||||
this._objSize! += 1
|
||||
}
|
||||
}
|
||||
|
||||
private _hasForObj(val: T) {
|
||||
return (val as any) in this._obj!
|
||||
}
|
||||
}
|
|
@ -134,61 +134,3 @@ describe('encodeUrlSafeBase64', () => {
|
|||
).eq('qu7d8aGTeuF6-g')
|
||||
})
|
||||
})
|
||||
|
||||
// describe('isProbablyPlainText', () => {
|
||||
// it('should return true for buffers only containing printable ascii', () => {
|
||||
// expect(
|
||||
// isProbablyPlainText(Buffer.from('hello this is some ascii text'))
|
||||
// ).to.be.true
|
||||
// expect(
|
||||
// isProbablyPlainText(
|
||||
// Buffer.from(
|
||||
// 'hello this is some ascii text\nwith unix new lines'
|
||||
// )
|
||||
// )
|
||||
// ).to.be.true
|
||||
// expect(
|
||||
// isProbablyPlainText(
|
||||
// Buffer.from(
|
||||
// 'hello this is some ascii text\r\nwith windows new lines'
|
||||
// )
|
||||
// )
|
||||
// ).to.be.true
|
||||
// expect(
|
||||
// isProbablyPlainText(
|
||||
// Buffer.from(
|
||||
// 'hello this is some ascii text\n\twith unix new lines and tabs'
|
||||
// )
|
||||
// )
|
||||
// ).to.be.true
|
||||
// expect(
|
||||
// isProbablyPlainText(
|
||||
// Buffer.from(
|
||||
// 'hello this is some ascii text\r\n\twith windows new lines and tabs'
|
||||
// )
|
||||
// )
|
||||
// ).to.be.true
|
||||
// })
|
||||
//
|
||||
// it('should return false for buffers containing some binary data', () => {
|
||||
// expect(isProbablyPlainText(Buffer.from('hello this is cedilla: ç'))).to
|
||||
// .be.false
|
||||
// expect(
|
||||
// isProbablyPlainText(
|
||||
// Buffer.from('hello this is some ascii text with emojis 🌸')
|
||||
// )
|
||||
// ).to.be.false
|
||||
//
|
||||
// // random strings of 16 bytes
|
||||
// expect(
|
||||
// isProbablyPlainText(
|
||||
// Buffer.from('717f80f08eb9d88c3931712c0e2be32f', 'hex')
|
||||
// )
|
||||
// ).to.be.false
|
||||
// expect(
|
||||
// isProbablyPlainText(
|
||||
// Buffer.from('20e8e218e54254c813b261432b0330d7', 'hex')
|
||||
// )
|
||||
// ).to.be.false
|
||||
// })
|
||||
// })
|
||||
|
|
|
@ -42,43 +42,4 @@ describe('LruMap', () => {
|
|||
expect(lru.get('third')).eq(undefined)
|
||||
expect(lru.get('fourth')).eq(4)
|
||||
})
|
||||
|
||||
it('Object backend', () => {
|
||||
const lru = new LruMap<string, number>(2, true)
|
||||
|
||||
lru.set('first', 1)
|
||||
expect(lru.has('first')).true
|
||||
expect(lru.has('second')).false
|
||||
expect(lru.get('first')).eq(1)
|
||||
|
||||
lru.set('first', 42)
|
||||
expect(lru.has('first')).true
|
||||
expect(lru.has('second')).false
|
||||
expect(lru.get('first')).eq(42)
|
||||
|
||||
lru.set('second', 2)
|
||||
expect(lru.has('first')).true
|
||||
expect(lru.has('second')).true
|
||||
expect(lru.get('first')).eq(42)
|
||||
expect(lru.get('second')).eq(2)
|
||||
|
||||
lru.set('third', 3)
|
||||
expect(lru.has('first')).false
|
||||
expect(lru.has('second')).true
|
||||
expect(lru.has('third')).true
|
||||
expect(lru.get('first')).eq(undefined)
|
||||
expect(lru.get('second')).eq(2)
|
||||
expect(lru.get('third')).eq(3)
|
||||
|
||||
lru.get('second') // update lru so that last = third
|
||||
lru.set('fourth', 4)
|
||||
expect(lru.has('first')).false
|
||||
expect(lru.has('second')).true
|
||||
expect(lru.has('third')).false
|
||||
expect(lru.has('fourth')).true
|
||||
expect(lru.get('first')).eq(undefined)
|
||||
expect(lru.get('second')).eq(2)
|
||||
expect(lru.get('third')).eq(undefined)
|
||||
expect(lru.get('fourth')).eq(4)
|
||||
})
|
||||
})
|
||||
|
|
95
packages/core/tests/lru-set.spec.ts
Normal file
95
packages/core/tests/lru-set.spec.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { expect } from 'chai'
|
||||
import Long from 'long'
|
||||
import { describe, it } from 'mocha'
|
||||
|
||||
import { LruSet } from '../src'
|
||||
|
||||
describe('LruSet', () => {
|
||||
describe('for strings', () => {
|
||||
it('when 1 item is added, it is in the set', () => {
|
||||
const set = new LruSet(2)
|
||||
|
||||
set.add('first')
|
||||
expect(set.has('first')).true
|
||||
})
|
||||
|
||||
it('when =capacity items are added, they are all in the set', () => {
|
||||
const set = new LruSet(2)
|
||||
|
||||
set.add('first')
|
||||
set.add('second')
|
||||
|
||||
expect(set.has('first')).true
|
||||
expect(set.has('second')).true
|
||||
})
|
||||
|
||||
it('when >capacity items are added, only the last <capacity> are in the set', () => {
|
||||
const set = new LruSet(2)
|
||||
|
||||
set.add('first')
|
||||
set.add('second')
|
||||
set.add('third')
|
||||
|
||||
expect(set.has('first')).false
|
||||
expect(set.has('second')).true
|
||||
expect(set.has('third')).true
|
||||
})
|
||||
|
||||
it('when the same added is while not eliminated, it is ignored', () => {
|
||||
const set = new LruSet(2)
|
||||
|
||||
set.add('first')
|
||||
set.add('second')
|
||||
set.add('first')
|
||||
set.add('third')
|
||||
|
||||
expect(set.has('first')).false
|
||||
expect(set.has('second')).true
|
||||
expect(set.has('third')).true
|
||||
})
|
||||
})
|
||||
|
||||
describe('for Longs', () => {
|
||||
it('when 1 item is added, it is in the set', () => {
|
||||
const set = new LruSet(2, true)
|
||||
|
||||
set.add(Long.fromNumber(1))
|
||||
expect(set.has(Long.fromNumber(1))).true
|
||||
})
|
||||
|
||||
it('when =capacity items are added, they are all in the set', () => {
|
||||
const set = new LruSet(2, true)
|
||||
|
||||
set.add(Long.fromNumber(1))
|
||||
set.add(Long.fromNumber(2))
|
||||
|
||||
expect(set.has(Long.fromNumber(1))).true
|
||||
expect(set.has(Long.fromNumber(2))).true
|
||||
})
|
||||
|
||||
it('when >capacity items are added, only the last <capacity> are in the set', () => {
|
||||
const set = new LruSet(2, true)
|
||||
|
||||
set.add(Long.fromNumber(1))
|
||||
set.add(Long.fromNumber(2))
|
||||
set.add(Long.fromNumber(3))
|
||||
|
||||
expect(set.has(Long.fromNumber(1))).false
|
||||
expect(set.has(Long.fromNumber(2))).true
|
||||
expect(set.has(Long.fromNumber(3))).true
|
||||
})
|
||||
|
||||
it('when the same added is while not eliminated, it is ignored', () => {
|
||||
const set = new LruSet(2, true)
|
||||
|
||||
set.add(Long.fromNumber(1))
|
||||
set.add(Long.fromNumber(2))
|
||||
set.add(Long.fromNumber(1))
|
||||
set.add(Long.fromNumber(3))
|
||||
|
||||
expect(set.has(Long.fromNumber(1))).false
|
||||
expect(set.has(Long.fromNumber(2))).true
|
||||
expect(set.has(Long.fromNumber(3))).true
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,60 +0,0 @@
|
|||
import { expect } from 'chai'
|
||||
import { describe, it } from 'mocha'
|
||||
|
||||
import { LruSet } from '../src'
|
||||
|
||||
describe('LruStringSet', () => {
|
||||
it('Set backend', () => {
|
||||
const set = new LruSet(2)
|
||||
|
||||
set.add('first')
|
||||
expect(set.has('first')).true
|
||||
|
||||
set.add('second')
|
||||
expect(set.has('first')).true
|
||||
expect(set.has('second')).true
|
||||
|
||||
set.add('third')
|
||||
expect(set.has('first')).false
|
||||
expect(set.has('second')).true
|
||||
expect(set.has('third')).true
|
||||
|
||||
set.add('third')
|
||||
expect(set.has('first')).false
|
||||
expect(set.has('second')).true
|
||||
expect(set.has('third')).true
|
||||
|
||||
set.add('fourth')
|
||||
expect(set.has('first')).false
|
||||
expect(set.has('second')).false
|
||||
expect(set.has('third')).true
|
||||
expect(set.has('fourth')).true
|
||||
})
|
||||
|
||||
it('Object backend', () => {
|
||||
const set = new LruSet(2, true)
|
||||
|
||||
set.add('first')
|
||||
expect(set.has('first')).true
|
||||
|
||||
set.add('second')
|
||||
expect(set.has('first')).true
|
||||
expect(set.has('second')).true
|
||||
|
||||
set.add('third')
|
||||
expect(set.has('first')).false
|
||||
expect(set.has('second')).true
|
||||
expect(set.has('third')).true
|
||||
|
||||
set.add('third')
|
||||
expect(set.has('first')).false
|
||||
expect(set.has('second')).true
|
||||
expect(set.has('third')).true
|
||||
|
||||
set.add('fourth')
|
||||
expect(set.has('first')).false
|
||||
expect(set.has('second')).false
|
||||
expect(set.has('third')).true
|
||||
expect(set.has('fourth')).true
|
||||
})
|
||||
})
|
|
@ -611,10 +611,10 @@ export class SqliteStorage implements ITelegramStorage, IStateStorage {
|
|||
return row ? (row as { pts: number }).pts : null
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
Loading…
Reference in a new issue