From 5a3b101c9f1ff9b6862aa843905d17acd2811f61 Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Tue, 19 Sep 2023 01:33:47 +0300 Subject: [PATCH] chore: avoid using {}, use Maps instead --- packages/client/src/client.ts | 16 +- .../src/methods/dialogs/_init-conversation.ts | 6 +- .../src/methods/messages/parse-entities.ts | 6 +- .../client/src/methods/messages/send-text.ts | 4 +- .../src/methods/parse-modes/_initialize.ts | 4 +- .../src/methods/parse-modes/parse-modes.ts | 21 ++- packages/client/src/methods/updates.ts | 156 ++++++++-------- .../client/src/methods/users/resolve-peer.ts | 4 +- packages/client/src/types/conversation.ts | 66 +++---- packages/client/src/types/misc/sticker-set.ts | 11 +- .../client/src/types/peers/peers-index.ts | 12 +- packages/core/src/network/mtproto-session.ts | 4 +- packages/core/src/network/network-manager.ts | 60 +++--- packages/core/src/storage/abstract.ts | 2 +- packages/core/src/storage/json.ts | 67 +++++-- packages/core/src/storage/memory.ts | 160 ++++++++-------- packages/core/src/utils/index.ts | 2 +- packages/core/src/utils/logger.ts | 15 +- packages/core/src/utils/long-utils.ts | 174 +++--------------- packages/core/src/utils/lru-map.ts | 46 ++--- packages/core/src/utils/lru-set.ts | 65 +++++++ packages/core/src/utils/lru-string-set.ts | 111 ----------- packages/core/tests/buffer-utils.spec.ts | 58 ------ packages/core/tests/lru-map.spec.ts | 39 ---- packages/core/tests/lru-set.spec.ts | 95 ++++++++++ packages/core/tests/lru-string-set.spec.ts | 60 ------ packages/sqlite/index.ts | 6 +- 27 files changed, 541 insertions(+), 729 deletions(-) create mode 100644 packages/core/src/utils/lru-set.ts delete mode 100644 packages/core/src/utils/lru-string-set.ts create mode 100644 packages/core/tests/lru-set.spec.ts delete mode 100644 packages/core/tests/lru-string-set.spec.ts diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index e5bb71da..f9d97709 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -4019,9 +4019,9 @@ export class TelegramClient extends BaseTelegramClient { protected _userId: number | null protected _isBot: boolean protected _selfUsername: string | null - protected _pendingConversations: Record + protected _pendingConversations: Map protected _hasConversations: boolean - protected _parseModes: Record + protected _parseModes: Map 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 - protected _cptsMod: Record + protected _cpts: Map + protected _cptsMod: Map 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 diff --git a/packages/client/src/methods/dialogs/_init-conversation.ts b/packages/client/src/methods/dialogs/_init-conversation.ts index 88fcb058..60b69c4d 100644 --- a/packages/client/src/methods/dialogs/_init-conversation.ts +++ b/packages/client/src/methods/dialogs/_init-conversation.ts @@ -6,13 +6,13 @@ import { Conversation, Message } from '../../types' // @extension interface ConversationsState { - _pendingConversations: Record + _pendingConversations: Map _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 }) diff --git a/packages/client/src/methods/messages/parse-entities.ts b/packages/client/src/methods/messages/parse-entities.ts index 81a73214..3d8fca67 100644 --- a/packages/client/src/methods/messages/parse-entities.ts +++ b/packages/client/src/methods/messages/parse-entities.ts @@ -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 diff --git a/packages/client/src/methods/messages/send-text.ts b/packages/client/src/methods/messages/send-text.ts index aaf19f10..1ecfa787 100644 --- a/packages/client/src/methods/messages/send-text.ts +++ b/packages/client/src/methods/messages/send-text.ts @@ -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( diff --git a/packages/client/src/methods/parse-modes/_initialize.ts b/packages/client/src/methods/parse-modes/_initialize.ts index e8cbdc50..2e2f3e83 100644 --- a/packages/client/src/methods/parse-modes/_initialize.ts +++ b/packages/client/src/methods/parse-modes/_initialize.ts @@ -3,13 +3,13 @@ import { IMessageEntityParser } from '../../types' // @extension interface ParseModesExtension { - _parseModes: Record + _parseModes: Map _defaultParseMode: string | null } // @initialize function _initializeParseModes(this: TelegramClient) { - this._parseModes = {} + this._parseModes = new Map() this._defaultParseMode = null } diff --git a/packages/client/src/methods/parse-modes/parse-modes.ts b/packages/client/src/methods/parse-modes/parse-modes.ts index fb27a7c2..df67c4e9 100644 --- a/packages/client/src/methods/parse-modes/parse-modes.ts +++ b/packages/client/src/methods/parse-modes/parse-modes.ts @@ -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.`) } diff --git a/packages/client/src/methods/updates.ts b/packages/client/src/methods/updates.ts index f72a823e..b4efd0ea 100644 --- a/packages/client/src/methods/updates.ts +++ b/packages/client/src/methods/updates.ts @@ -78,8 +78,8 @@ interface UpdatesState { // usually set in start() method based on `catchUp` param _catchUpChannels?: boolean - _cpts: Record - _cptsMod: Record + _cpts: Map + _cptsMod: Map _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 { - for (const key in peers.users) { - const user = peers.users[key] as Exclude + for (const [key, user_] of peers.users) { + const user = user_ as Exclude 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 + for (const [key, chat_] of peers.chats) { + const chat = chat_ as Extract - 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 { - let _pts: number | null | undefined = this._cpts[channelId] + let _pts: number | null | undefined = this._cpts.get(channelId) if (!_pts && this._catchUpChannels) { _pts = await this.storage.getChannelPts(channelId) @@ -929,36 +929,39 @@ async function _fetchChannelDifference( if (diff.final) break } - this._cpts[channelId] = pts - this._cptsMod[channelId] = pts + this._cpts.set(channelId, pts) + this._cptsMod.set(channelId, pts) } function _fetchChannelDifferenceLater( this: TelegramClient, - requestedDiff: Record>, + requestedDiff: Map>, channelId: number, fallbackPts?: number, force = false, ): void { - if (!(channelId in requestedDiff)) { - requestedDiff[channelId] = _fetchChannelDifference - .call(this, channelId, fallbackPts, force) - .catch((err) => { - this._updsLog.warn( - 'error fetching difference for %d: %s', - channelId, - err, - ) - }) - .then(() => { - delete requestedDiff[channelId] - }) + if (!requestedDiff.has(channelId)) { + requestedDiff.set( + channelId, + _fetchChannelDifference + .call(this, channelId, fallbackPts, force) + .catch((err) => { + this._updsLog.warn( + 'error fetching difference for %d: %s', + channelId, + err, + ) + }) + .then(() => { + requestedDiff.delete(channelId) + }), + ) } } async function _fetchDifference( this: TelegramClient, - requestedDiff: Record>, + requestedDiff: Map>, ): Promise { for (;;) { const diff = await this.call({ @@ -1072,24 +1075,30 @@ async function _fetchDifference( function _fetchDifferenceLater( this: TelegramClient, - requestedDiff: Record>, + requestedDiff: Map>, ): void { - if (!(0 in requestedDiff)) { - requestedDiff[0] = _fetchDifference - .call(this, requestedDiff) - .catch((err) => { - this._updsLog.warn('error fetching common difference: %s', err) - }) - .then(() => { - delete requestedDiff[0] - }) + if (!requestedDiff.has(0)) { + requestedDiff.set( + 0, + _fetchDifference + .call(this, requestedDiff) + .catch((err) => { + this._updsLog.warn( + 'error fetching common difference: %s', + err, + ) + }) + .then(() => { + requestedDiff.delete(0) + }), + ) } } async function _onUpdate( this: TelegramClient, pending: PendingUpdate, - requestedDiff: Record>, + requestedDiff: Map>, postponed = false, unordered = false, ): Promise { @@ -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 { this._pendingUnorderedUpdates.length, ) - const requestedDiff: Record> = {} + const requestedDiff = new Map>() // first process pending containers while (this._pendingUpdateContainers.length) { @@ -1497,8 +1506,8 @@ export async function _updatesLoop(this: TelegramClient): Promise { 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 { ) 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 { 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 { } // 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 { // 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(), ) } diff --git a/packages/client/src/methods/users/resolve-peer.ts b/packages/client/src/methods/users/resolve-peer.ts index f1db2822..c1d340f3 100644 --- a/packages/client/src/methods/users/resolve-peer.ts +++ b/packages/client/src/methods/users/resolve-peer.ts @@ -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 } diff --git a/packages/client/src/types/conversation.ts b/packages/client/src/types/conversation.ts index 886853c9..e5faf11d 100644 --- a/packages/client/src/types/conversation.ts +++ b/packages/client/src/types/conversation.ts @@ -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() private _lock = new AsyncLock() - private _pendingEditMessage: Record> = {} + private _pendingEditMessage: Map> = new Map() private _recentEdits = new Deque(10) - private _pendingRead: Record> = {} + private _pendingRead: Map> = new Map() constructor(readonly client: TelegramClient, readonly chat: InputPeerLike) { this._onNewMessage = this._onNewMessage.bind(this) @@ -112,10 +109,10 @@ export class Conversation { this.client.on('edit_message', this._onEditMessage) this.client.on('history_read', this._onHistoryRead) - if (!(this._chatId in this.client['_pendingConversations'])) { - this.client['_pendingConversations'][this._chatId] = [] + if (this.client['_pendingConversations'].has(this._chatId)) { + this.client['_pendingConversations'].set(this._chatId, []) } - this.client['_pendingConversations'][this._chatId].push(this) + this.client['_pendingConversations'].get(this._chatId)!.push(this) this.client['_hasConversations'] = true } @@ -129,25 +126,26 @@ export class Conversation { this.client.off('edit_message', this._onEditMessage) this.client.off('history_read', this._onHistoryRead) - const pending = this.client['_pendingConversations'] + const pending = this.client['_pendingConversations'].get(this._chatId) + const pendingIdx = pending?.indexOf(this) ?? -1 - const idx = pending[this._chatId].indexOf(this) - - if (idx > -1) { + if (pendingIdx > -1) { // just in case - pending[this._chatId].splice(idx, 1) + pending!.splice(pendingIdx, 1) } - if (!pending[this._chatId].length) { - delete pending[this._chatId] + if (pending && !pending.length) { + this.client['_pendingConversations'].delete(this._chatId) } - this.client['_hasConversations'] = Object.keys(pending).length > 0 + this.client['_hasConversations'] = Boolean( + this.client['_pendingConversations'].size, + ) // reset pending status this._queuedNewMessage.clear() this._pendingNewMessages.clear() - this._pendingEditMessage = {} + this._pendingEditMessage.clear() this._recentEdits.clear() - this._pendingRead = {} + this._pendingRead.clear() this._started = false } @@ -424,15 +422,15 @@ export class Conversation { if (timeout) { timer = setTimeout(() => { promise.reject(new MtTimeoutError(timeout)) - delete this._pendingEditMessage[msgId] + this._pendingEditMessage.delete(msgId) }, timeout) } - this._pendingEditMessage[msgId] = { + this._pendingEditMessage.set(msgId, { promise, check: filter, timeout: timer, - } + }) this._processRecentEdits() @@ -476,14 +474,14 @@ export class Conversation { if (timeout !== null) { timer = setTimeout(() => { promise.reject(new MtTimeoutError(timeout)) - delete this._pendingRead[msgId] + this._pendingRead.delete(msgId) }, timeout) } - this._pendingRead[msgId] = { + this._pendingRead.set(msgId, { promise, timeout: timer, - } + }) return promise } @@ -521,10 +519,12 @@ export class Conversation { private _onEditMessage(msg: Message, fromRecent = false) { if (msg.chat.id !== this._chatId) return - const it = this._pendingEditMessage[msg.id] + const it = this._pendingEditMessage.get(msg.id) - if (!it && !fromRecent) { - this._recentEdits.pushBack(msg) + if (!it) { + if (!fromRecent) { + this._recentEdits.pushBack(msg) + } return } @@ -533,7 +533,7 @@ export class Conversation { if (!it.check || (await it.check(msg))) { if (it.timeout) clearTimeout(it.timeout) it.promise.resolve(msg) - delete this._pendingEditMessage[msg.id] + this._pendingEditMessage.delete(msg.id) } })().catch((e) => { this.client['_emitError'](e) @@ -545,12 +545,12 @@ export class Conversation { const lastRead = upd.maxReadId - for (const msgId in this._pendingRead) { - if (parseInt(msgId) <= lastRead) { - const it = this._pendingRead[msgId] + for (const msgId of this._pendingRead.keys()) { + if (msgId <= lastRead) { + const it = this._pendingRead.get(msgId)! if (it.timeout) clearTimeout(it.timeout) it.promise.resolve() - delete this._pendingRead[msgId] + this._pendingRead.delete(msgId) } } } diff --git a/packages/client/src/types/misc/sticker-set.ts b/packages/client/src/types/misc/sticker-set.ts index 0ac7f28a..518421dd 100644 --- a/packages/client/src/types/misc/sticker-set.ts +++ b/packages/client/src/types/misc/sticker-set.ts @@ -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> = {} + const index = new LongMap>() 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 } }) }) diff --git a/packages/client/src/types/peers/peers-index.ts b/packages/client/src/types/peers/peers-index.ts index 8fc3c059..1a2c445e 100644 --- a/packages/client/src/types/peers/peers-index.ts +++ b/packages/client/src/types/peers/peers-index.ts @@ -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 = {} - readonly chats: Record = {} + readonly users: Map = new Map() + readonly chats: Map = 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).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) diff --git a/packages/core/src/network/mtproto-session.ts b/packages/core/src/network/mtproto-session.ts index 1508b97f..b8e46c4a 100644 --- a/packages/core/src/network/mtproto-session.ts +++ b/packages/core/src/network/mtproto-session.ts @@ -101,8 +101,8 @@ export class MtprotoSession { /// state /// // recent msg ids - recentOutgoingMsgIds = new LruSet(1000, false, true) - recentIncomingMsgIds = new LruSet(1000, false, true) + recentOutgoingMsgIds = new LruSet(1000, true) + recentIncomingMsgIds = new LruSet(1000, true) // queues queuedRpc = new Deque() diff --git a/packages/core/src/network/network-manager.ts b/packages/core/src/network/network-manager.ts index cb197937..bf5e3ee1 100644 --- a/packages/core/src/network/network-manager.ts +++ b/packages/core/src/network/network-manager.ts @@ -420,7 +420,7 @@ export class NetworkManager { readonly _reconnectionStrategy: ReconnectionStrategy readonly _connectionCount: ConnectionCountDelegate - protected readonly _dcConnections: Record = {} + protected readonly _dcConnections = new Map() protected _primaryDc?: DcConnectionManager private _keepAliveInterval?: NodeJS.Timeout @@ -545,18 +545,18 @@ export class NetworkManager { return dc.loadKeys().then(() => dc.main.ensureConnected()) } - private _dcCreationPromise: Record> = {} + private _dcCreationPromise = new Map>() async _getOtherDc(dcId: number): Promise { - 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() - 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 = {} + private _floodWaitedRequests = new Map() async call( 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() diff --git a/packages/core/src/storage/abstract.ts b/packages/core/src/storage/abstract.ts index 0c363317..a1de7c18 100644 --- a/packages/core/src/storage/abstract.ts +++ b/packages/core/src/storage/abstract.ts @@ -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): MaybeAsync + setManyChannelPts(values: Map): MaybeAsync /** * Get cached peer information by their marked ID. diff --git a/packages/core/src/storage/json.ts b/packages/core/src/storage/json.ts index 8a0a94a4..ade7f90c 100644 --- a/packages/core/src/storage/json.ts +++ b/packages/core/src/storage/json.ts @@ -12,19 +12,32 @@ export class JsonMemoryStorage extends MemoryStorage { protected _loadJson(json: string): void { this._setStateFrom( JSON.parse(json, (key, value) => { - if (key === 'authKeys') { - const ret: Record = {} + switch (key) { + case 'authKeys': + case 'authKeysTemp': { + const ret: Record = {} - ;(value as string).split('|').forEach((pair: string) => { - const [dcId, b64] = pair.split(',') - ret[dcId] = Buffer.from(b64, 'base64') - }) + ;(value as string) + .split('|') + .forEach((pair: string) => { + const [dcId, b64] = pair.split(',') + ret[dcId] = Buffer.from(b64, 'base64') + }) - return ret - } - - if (key === 'accessHash') { - return longFromFastString(value as string) + return ret + } + case 'authKeysTempExpiry': + case 'entities': + case 'phoneIndex': + case 'usernameIndex': + case 'pts': + case 'fsm': + case 'rl': + return new Map( + Object.entries(value as Record), + ) + case 'accessHash': + return longFromFastString(value as string) } return value @@ -34,16 +47,30 @@ export class JsonMemoryStorage extends MemoryStorage { protected _saveJson(): string { return JSON.stringify(this._state, (key, value) => { - if (key === 'authKeys') { - const value_ = value as Record + switch (key) { + case 'authKeys': + case 'authKeysTemp': { + const value_ = value as Map - return Object.entries(value_) - .filter((it): it is [string, Buffer] => it[1] !== null) - .map(([dcId, key]) => dcId + ',' + key.toString('base64')) - .join('|') - } - if (key === 'accessHash') { - return longToFastString(value as tl.Long) + return [...value_.entries()] + .filter((it): it is [string, Buffer] => it[1] !== null) + .map( + ([dcId, key]) => dcId + ',' + key.toString('base64'), + ) + .join('|') + } + case 'authKeysTempExpiry': + case 'entities': + case 'phoneIndex': + case 'usernameIndex': + case 'pts': + case 'fsm': + case 'rl': + return Object.fromEntries([ + ...(value as Map).entries(), + ]) + case 'accessHash': + return longToFastString(value as tl.Long) } return value diff --git a/packages/core/src/storage/memory.ts b/packages/core/src/storage/memory.ts index c73ce464..9691cf5b 100644 --- a/packages/core/src/storage/memory.ts +++ b/packages/core/src/storage/memory.ts @@ -14,24 +14,24 @@ export interface MemorySessionState { $version: typeof CURRENT_VERSION defaultDcs: ITelegramStorage.DcOptions | null - authKeys: Record - authKeysTemp: Record - authKeysTempExpiry: Record + authKeys: Map + authKeysTemp: Map + authKeysTempExpiry: Map // marked peer id -> entity info - entities: Record + entities: Map // phone number -> peer id - phoneIndex: Record + phoneIndex: Map // username -> peer id - usernameIndex: Record + usernameIndex: Map // common pts, date, seq, qts gpts: [number, number, number, number] | null // channel pts - pts: Record + pts: Map // state for fsm - fsm: Record< + fsm: Map< string, { // value @@ -42,7 +42,7 @@ export interface MemorySessionState { > // state for rate limiter - rl: Record< + rl: Map< string, { // reset @@ -111,16 +111,16 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage { this._state = { $version: CURRENT_VERSION, defaultDcs: null, - authKeys: {}, - authKeysTemp: {}, - authKeysTempExpiry: {}, - entities: {}, - phoneIndex: {}, - usernameIndex: {}, + authKeys: new Map(), + authKeysTemp: new Map(), + authKeysTempExpiry: new Map(), + entities: new Map(), + phoneIndex: new Map(), + usernameIndex: new Map(), gpts: null, - pts: {}, - fsm: {}, - rl: {}, + pts: new Map(), + fsm: new Map(), + rl: new Map(), self: null, } } @@ -138,19 +138,20 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage { let populate = false if (!obj.phoneIndex) { - obj.phoneIndex = {} + obj.phoneIndex = new Map() populate = true } if (!obj.usernameIndex) { - obj.usernameIndex = {} + obj.usernameIndex = new Map() populate = true } if (populate) { Object.values(obj.entities).forEach( (ent: ITelegramStorage.PeerInfo) => { - if (ent.phone) obj.phoneIndex[ent.phone] = ent.id - if (ent.username) obj.usernameIndex[ent.username] = ent.id + if (ent.phone) obj.phoneIndex.set(ent.phone, ent.id) + + if (ent.username) { obj.usernameIndex.set(ent.username, ent.id) } }, ) } @@ -168,19 +169,17 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage { const fsm = state.fsm const rl = state.rl - Object.keys(fsm).forEach((key) => { - const exp = fsm[key].e - - if (exp && exp < now) { - delete fsm[key] + for (const [key, item] of fsm) { + if (item.e && item.e < now) { + fsm.delete(key) } - }) + } - Object.keys(rl).forEach((key) => { - if (rl[key].res < now) { - delete rl[key] + for (const [key, item] of rl) { + if (item.res < now) { + rl.delete(key) } - }) + } } getDefaultDcs(): ITelegramStorage.DcOptions | null { @@ -198,36 +197,47 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage { expiresAt: number, ): void { const k = `${dcId}:${index}` - this._state.authKeysTemp[k] = key - this._state.authKeysTempExpiry[k] = expiresAt + + if (key) { + this._state.authKeysTemp.set(k, key) + this._state.authKeysTempExpiry.set(k, expiresAt) + } else { + this._state.authKeysTemp.delete(k) + this._state.authKeysTempExpiry.delete(k) + } } setAuthKeyFor(dcId: number, key: Buffer | null): void { - this._state.authKeys[dcId] = key + if (key) { + this._state.authKeys.set(dcId, key) + } else { + this._state.authKeys.delete(dcId) + } } getAuthKeyFor(dcId: number, tempIndex?: number): Buffer | null { if (tempIndex !== undefined) { const k = `${dcId}:${tempIndex}` - if (Date.now() > (this._state.authKeysTempExpiry[k] ?? 0)) { + if (Date.now() > (this._state.authKeysTempExpiry.get(k) ?? 0)) { return null } - return this._state.authKeysTemp[k] + return this._state.authKeysTemp.get(k) ?? null } - return this._state.authKeys[dcId] ?? null + return this._state.authKeys.get(dcId) ?? null } dropAuthKeysFor(dcId: number): void { - this._state.authKeys[dcId] = null - Object.keys(this._state.authKeysTemp).forEach((key) => { + this._state.authKeys.delete(dcId) + + for (const key of this._state.authKeysTemp.keys()) { if (key.startsWith(`${dcId}:`)) { - delete this._state.authKeysTemp[key] - delete this._state.authKeysTempExpiry[key] + this._state.authKeysTemp.delete(key) + this._state.authKeysTempExpiry.delete(key) } - }) + } } updatePeers(peers: PeerInfoWithUpdated[]): MaybeAsync { @@ -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): void { - for (const id in values) { - this._state.pts[id] = values[id] + setManyChannelPts(values: Map): 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) } } diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 53402a47..6a476813 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -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' diff --git a/packages/core/src/utils/logger.ts b/packages/core/src/utils/logger.ts index b9e0f95c..a9e79295 100644 --- a/packages/core/src/utils/logger.ts +++ b/packages/core/src/utils/logger.ts @@ -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)] } + return JSON.stringify(val, (k, v) => { if ( typeof v === 'object' && diff --git a/packages/core/src/utils/long-utils.ts b/packages/core/src/utils/long-utils.ts index f6322876..e61fc911 100644 --- a/packages/core/src/utils/long-utils.ts +++ b/packages/core/src/utils/long-utils.ts @@ -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 { - private _map?: Map - private _obj?: any + private _map = new Map() - 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 - readonly values: () => IterableIterator - 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 { - for (const v of this._map!.keys()) { + *keys(unsigned?: boolean): IterableIterator { + for (const v of this._map.keys()) { yield longFromFastString(v, unsigned) } } - private _valuesForMap(): IterableIterator { - return this._map!.values() + values(): IterableIterator { + 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 { - for (const v of Object.keys(this._obj)) { - yield longFromFastString(v, unsigned) - } - } - - private *_valuesForObj(): IterableIterator { - 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 { * Uses fast string representation internally */ export class LongSet { - private _set?: Set - 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() 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() } } diff --git a/packages/core/src/utils/lru-map.ts b/packages/core/src/utils/lru-map.ts index 849cc2d4..f7631f9f 100644 --- a/packages/core/src/utils/lru-map.ts +++ b/packages/core/src/utils/lru-map.ts @@ -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 { } /** - * Simple class implementing LRU-like behaviour for a map, - * falling back to objects when `Map` is not available. + * Simple class implementing LRU-like behaviour for a Map * * Can be used to handle local cache of *something* * @@ -27,37 +26,16 @@ export class LruMap { private _first?: TwoWayLinkedList private _last?: TwoWayLinkedList + private _map: Map> + 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 | undefined - private readonly _del: (key: K) => void - private _markUsed(item: TwoWayLinkedList): void { if (item === this._first) { return // already the most recently used @@ -84,7 +62,7 @@ export class LruMap { } 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 { } has(key: K): boolean { - return this._has(key) + return this._map.has(key) } private _remove(item: TwoWayLinkedList): void { @@ -108,12 +86,12 @@ export class LruMap { // 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 { 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 { } delete(key: K): void { - const item = this._get(key) + const item = this._map.get(key) if (item) this._remove(item) } } diff --git a/packages/core/src/utils/lru-set.ts b/packages/core/src/utils/lru-set.ts new file mode 100644 index 00000000..50d6d779 --- /dev/null +++ b/packages/core/src/utils/lru-set.ts @@ -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 { + v: T + n?: OneWayLinkedList +} + +/** + * 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 { + private _capacity: number + private _first?: OneWayLinkedList + private _last?: OneWayLinkedList + + private _set: Set | 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) + } +} diff --git a/packages/core/src/utils/lru-string-set.ts b/packages/core/src/utils/lru-string-set.ts deleted file mode 100644 index d6feff3e..00000000 --- a/packages/core/src/utils/lru-string-set.ts +++ /dev/null @@ -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 { - v: T - n?: OneWayLinkedList -} - -/** - * 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 { - private _capacity: number - private _first?: OneWayLinkedList - private _last?: OneWayLinkedList - - private _set?: Set | 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! - } -} diff --git a/packages/core/tests/buffer-utils.spec.ts b/packages/core/tests/buffer-utils.spec.ts index 210d6e5d..5ef51241 100644 --- a/packages/core/tests/buffer-utils.spec.ts +++ b/packages/core/tests/buffer-utils.spec.ts @@ -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 -// }) -// }) diff --git a/packages/core/tests/lru-map.spec.ts b/packages/core/tests/lru-map.spec.ts index f0aa9716..9eff8d54 100644 --- a/packages/core/tests/lru-map.spec.ts +++ b/packages/core/tests/lru-map.spec.ts @@ -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(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) - }) }) diff --git a/packages/core/tests/lru-set.spec.ts b/packages/core/tests/lru-set.spec.ts new file mode 100644 index 00000000..1de980ca --- /dev/null +++ b/packages/core/tests/lru-set.spec.ts @@ -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 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 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 + }) + }) +}) diff --git a/packages/core/tests/lru-string-set.spec.ts b/packages/core/tests/lru-string-set.spec.ts deleted file mode 100644 index fa8989ef..00000000 --- a/packages/core/tests/lru-string-set.spec.ts +++ /dev/null @@ -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 - }) -}) diff --git a/packages/sqlite/index.ts b/packages/sqlite/index.ts index 0bac8fa7..9a5af938 100644 --- a/packages/sqlite/index.ts +++ b/packages/sqlite/index.ts @@ -611,10 +611,10 @@ export class SqliteStorage implements ITelegramStorage, IStateStorage { return row ? (row as { pts: number }).pts : null } - setManyChannelPts(values: Record): void { - Object.entries(values).forEach(([cid, pts]) => { + setManyChannelPts(values: Map): void { + for (const [cid, pts] of values) { this._pending.push([this._statements.setPts, [cid, pts]]) - }) + } } updatePeers(peers: ITelegramStorage.PeerInfo[]): void {