GramJS: Call getChannelDifference for opened chats (#6887)
This commit is contained in:
parent
c89d8089a1
commit
889bca140d
@ -192,7 +192,8 @@ export async function fetchChats({
|
|||||||
const chat = buildApiChatFromDialog(dialog, peerEntity);
|
const chat = buildApiChatFromDialog(dialog, peerEntity);
|
||||||
lastMessageByChatId[chat.id] = dialog.topMessage;
|
lastMessageByChatId[chat.id] = dialog.topMessage;
|
||||||
|
|
||||||
if (dialog.pts) {
|
const isChannel = getEntityTypeById(chat.id) === 'channel';
|
||||||
|
if (dialog.pts && isChannel) {
|
||||||
updateChannelState(chat.id, dialog.pts);
|
updateChannelState(chat.id, dialog.pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -525,6 +526,11 @@ export async function requestChatUpdate({
|
|||||||
|
|
||||||
const chatUpdate = buildApiChatFromDialog(dialog, peerEntity);
|
const chatUpdate = buildApiChatFromDialog(dialog, peerEntity);
|
||||||
|
|
||||||
|
const isChannel = getEntityTypeById(chat.id) === 'channel';
|
||||||
|
if (dialog.pts && isChannel) {
|
||||||
|
updateChannelState(chat.id, dialog.pts);
|
||||||
|
}
|
||||||
|
|
||||||
const readState = buildThreadReadState(dialog);
|
const readState = buildThreadReadState(dialog);
|
||||||
const threadInfo = buildApiThreadInfoFromDialog(chat.id, dialog);
|
const threadInfo = buildApiThreadInfoFromDialog(chat.id, dialog);
|
||||||
sendApiUpdate({
|
sendApiUpdate({
|
||||||
|
|||||||
@ -51,8 +51,9 @@ import {
|
|||||||
getDifference,
|
getDifference,
|
||||||
init as initUpdatesManager,
|
init as initUpdatesManager,
|
||||||
processUpdate,
|
processUpdate,
|
||||||
|
requestChannelDifference as requestChannelDifferenceFromUpdates,
|
||||||
reset as resetUpdatesManager,
|
reset as resetUpdatesManager,
|
||||||
scheduleGetChannelDifference,
|
setOpenedChannelIds as setOpenedChannelIdsInUpdates,
|
||||||
updateChannelState,
|
updateChannelState,
|
||||||
} from '../updates/updateManager';
|
} from '../updates/updateManager';
|
||||||
import {
|
import {
|
||||||
@ -660,5 +661,9 @@ export function setShouldDebugExportedSenders(value: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function requestChannelDifference(channelId: string) {
|
export function requestChannelDifference(channelId: string) {
|
||||||
scheduleGetChannelDifference(channelId);
|
requestChannelDifferenceFromUpdates(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOpenedChannelIds(channelIds: string[]) {
|
||||||
|
setOpenedChannelIdsInUpdates(channelIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
destroy, disconnect, downloadMedia, fetchCurrentUser, repairFileReference, abortChatRequests, abortRequestGroup,
|
destroy, disconnect, downloadMedia, fetchCurrentUser, repairFileReference, abortChatRequests, abortRequestGroup,
|
||||||
setForceHttpTransport, setShouldDebugExportedSenders, setAllowHttpTransport, requestChannelDifference,
|
setForceHttpTransport, setShouldDebugExportedSenders, setAllowHttpTransport, requestChannelDifference,
|
||||||
|
setOpenedChannelIds,
|
||||||
} from './client';
|
} from './client';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -162,6 +162,7 @@ export async function fetchMessages({
|
|||||||
const RequestClass = threadId === MAIN_THREAD_ID
|
const RequestClass = threadId === MAIN_THREAD_ID
|
||||||
? GramJs.messages.GetHistory : isSavedDialog
|
? GramJs.messages.GetHistory : isSavedDialog
|
||||||
? GramJs.messages.GetSavedHistory : GramJs.messages.GetReplies;
|
? GramJs.messages.GetSavedHistory : GramJs.messages.GetReplies;
|
||||||
|
const isChannel = getEntityTypeById(chat.id) === 'channel';
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -203,6 +204,10 @@ export async function fetchMessages({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isChannel && 'pts' in result) {
|
||||||
|
updateChannelState(chat.id, result.pts);
|
||||||
|
}
|
||||||
|
|
||||||
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
||||||
@ -259,7 +264,7 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('pts' in result) {
|
if (isChannel && 'pts' in result) {
|
||||||
updateChannelState(chat.id, result.pts);
|
updateChannelState(chat.id, result.pts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Api as GramJs, type Update } from '../../../lib/gramjs';
|
import { Api as GramJs, type Update } from '../../../lib/gramjs';
|
||||||
|
import { RPCError } from '../../../lib/gramjs/errors';
|
||||||
import { UpdateConnectionState, UpdateServerTimeOffset } from '../../../lib/gramjs/network';
|
import { UpdateConnectionState, UpdateServerTimeOffset } from '../../../lib/gramjs/network';
|
||||||
import type { Entity } from '../../../lib/gramjs/types';
|
import type { Entity } from '../../../lib/gramjs/types';
|
||||||
|
|
||||||
@ -24,16 +25,37 @@ export type State = {
|
|||||||
};
|
};
|
||||||
type SeqUpdate = (GramJs.Updates | GramJs.UpdatesCombined) & { _isFromDifference?: true };
|
type SeqUpdate = (GramJs.Updates | GramJs.UpdatesCombined) & { _isFromDifference?: true };
|
||||||
type PtsUpdate = ((GramJs.TypeUpdate & { pts: number }) | UpdatePts) & { _isFromDifference?: true };
|
type PtsUpdate = ((GramJs.TypeUpdate & { pts: number }) | UpdatePts) & { _isFromDifference?: true };
|
||||||
|
type ChannelDifferenceReason = 'gapRecovery' | 'shortpoll';
|
||||||
|
type ChannelScheduler = {
|
||||||
|
timeout?: ReturnType<typeof setTimeout>;
|
||||||
|
deadline?: number;
|
||||||
|
reason?: ChannelDifferenceReason;
|
||||||
|
isInFlight: boolean;
|
||||||
|
shortpollTimeoutMs?: number;
|
||||||
|
isShortpollEligible?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const COMMON_BOX_QUEUE_ID = '0';
|
const COMMON_BOX_QUEUE_ID = '0';
|
||||||
const CHANNEL_DIFFERENCE_LIMIT = 1000;
|
|
||||||
|
const SHORTPOLL_CHANNEL_DIFFERENCE_LIMIT = 100;
|
||||||
|
const CATCH_UP_CHANNEL_DIFFERENCE_LIMIT = 1000;
|
||||||
|
|
||||||
|
const SHORTPOLL_DEFAULT_TIMEOUT_MS = 1000;
|
||||||
|
const INITIAL_SHORTPOLL_TIMEOUT_MS = 10000;
|
||||||
|
const CHANNEL_DIFFERENCE_RETRY_TIMEOUT_MS = 5000;
|
||||||
const UPDATE_WAIT_TIMEOUT = 500;
|
const UPDATE_WAIT_TIMEOUT = 500;
|
||||||
|
|
||||||
|
const TERMINAL_CHANNEL_DIFFERENCE_ERRORS = new Set([
|
||||||
|
'CHANNEL_INVALID',
|
||||||
|
'CHANNEL_PRIVATE',
|
||||||
|
]);
|
||||||
|
|
||||||
let invoke: typeof invokeRequest;
|
let invoke: typeof invokeRequest;
|
||||||
let isInited = false;
|
let isInited = false;
|
||||||
|
|
||||||
let seqTimeout: ReturnType<typeof setTimeout> | undefined;
|
let seqTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
const PTS_TIMEOUTS = new Map<string, ReturnType<typeof setTimeout>>();
|
const CHANNEL_SCHEDULERS = new Map<string, ChannelScheduler>();
|
||||||
|
const OPENED_CHANNEL_IDS = new Set<string>();
|
||||||
|
|
||||||
const SEQ_QUEUE = new SortedQueue<SeqUpdate>(seqComparator);
|
const SEQ_QUEUE = new SortedQueue<SeqUpdate>(seqComparator);
|
||||||
const PTS_QUEUE = new Map<string, SortedQueue<PtsUpdate>>();
|
const PTS_QUEUE = new Map<string, SortedQueue<PtsUpdate>>();
|
||||||
@ -85,7 +107,7 @@ export function processUpdate(update: Update, isFromDifference?: boolean, should
|
|||||||
|
|
||||||
if ('pts' in update) {
|
if ('pts' in update) {
|
||||||
if (update instanceof GramJs.UpdateChannelTooLong) {
|
if (update instanceof GramJs.UpdateChannelTooLong) {
|
||||||
getChannelDifference(getUpdateChannelId(update));
|
scheduleChannelDifference(getUpdateChannelId(update), 'gapRecovery', 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isFromDifference) {
|
if (isFromDifference) {
|
||||||
@ -215,8 +237,8 @@ function popPtsQueue(channelId: string) {
|
|||||||
if (update._isFromDifference && pts >= localPts + ptsCount) {
|
if (update._isFromDifference && pts >= localPts + ptsCount) {
|
||||||
applyUpdate(update);
|
applyUpdate(update);
|
||||||
} else if (pts === localPts + ptsCount) {
|
} else if (pts === localPts + ptsCount) {
|
||||||
clearTimeout(PTS_TIMEOUTS.get(channelId));
|
clearScheduledChannelDifference(channelId, 'gapRecovery');
|
||||||
PTS_TIMEOUTS.delete(channelId);
|
scheduleShortpollFromNow(channelId);
|
||||||
|
|
||||||
applyUpdate(update);
|
applyUpdate(update);
|
||||||
} else if (pts > localPts + ptsCount) {
|
} else if (pts > localPts + ptsCount) {
|
||||||
@ -233,13 +255,114 @@ function popPtsQueue(channelId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function scheduleGetChannelDifference(channelId: string) {
|
export function scheduleGetChannelDifference(channelId: string) {
|
||||||
if (PTS_TIMEOUTS.has(channelId)) return;
|
scheduleChannelDifference(channelId, 'gapRecovery', UPDATE_WAIT_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(async () => {
|
export function requestChannelDifference(channelId: string) {
|
||||||
await getChannelDifference(channelId);
|
scheduleChannelDifference(channelId, 'gapRecovery', 0);
|
||||||
PTS_TIMEOUTS.delete(channelId);
|
}
|
||||||
}, UPDATE_WAIT_TIMEOUT);
|
|
||||||
PTS_TIMEOUTS.set(channelId, timeout);
|
export function setOpenedChannelIds(channelIds: string[]) {
|
||||||
|
const nextOpenedChannelIds = new Set(channelIds);
|
||||||
|
|
||||||
|
OPENED_CHANNEL_IDS.forEach((channelId) => {
|
||||||
|
if (nextOpenedChannelIds.has(channelId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrCreateChannelScheduler(channelId).isShortpollEligible = false;
|
||||||
|
clearScheduledChannelDifference(channelId, 'shortpoll');
|
||||||
|
});
|
||||||
|
|
||||||
|
channelIds.forEach((channelId) => {
|
||||||
|
const scheduler = getOrCreateChannelScheduler(channelId);
|
||||||
|
const wasOpened = OPENED_CHANNEL_IDS.has(channelId);
|
||||||
|
|
||||||
|
scheduler.isShortpollEligible = true;
|
||||||
|
|
||||||
|
if (!wasOpened) {
|
||||||
|
if (scheduler.shortpollTimeoutMs !== undefined) {
|
||||||
|
restartShortpollFromNow(channelId);
|
||||||
|
} else {
|
||||||
|
scheduleChannelDifference(channelId, 'shortpoll', INITIAL_SHORTPOLL_TIMEOUT_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
OPENED_CHANNEL_IDS.clear();
|
||||||
|
channelIds.forEach((channelId) => {
|
||||||
|
OPENED_CHANNEL_IDS.add(channelId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateChannelScheduler(channelId: string) {
|
||||||
|
const current = CHANNEL_SCHEDULERS.get(channelId);
|
||||||
|
if (current) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduler: ChannelScheduler = {
|
||||||
|
isInFlight: false,
|
||||||
|
};
|
||||||
|
CHANNEL_SCHEDULERS.set(channelId, scheduler);
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleChannelDifference(channelId: string, reason: ChannelDifferenceReason, timeoutMs: number) {
|
||||||
|
const scheduler = getOrCreateChannelScheduler(channelId);
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
if (scheduler.deadline !== undefined && scheduler.deadline <= deadline) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScheduledChannelDifference(channelId);
|
||||||
|
|
||||||
|
scheduler.reason = reason;
|
||||||
|
scheduler.deadline = deadline;
|
||||||
|
scheduler.timeout = setTimeout(() => {
|
||||||
|
scheduler.timeout = undefined;
|
||||||
|
scheduler.deadline = undefined;
|
||||||
|
if (scheduler.isInFlight) {
|
||||||
|
scheduleChannelDifference(channelId, reason, UPDATE_WAIT_TIMEOUT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void runChannelDifference(channelId, reason);
|
||||||
|
}, timeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearScheduledChannelDifference(channelId: string, reason?: ChannelDifferenceReason) {
|
||||||
|
const scheduler = CHANNEL_SCHEDULERS.get(channelId);
|
||||||
|
if (!scheduler?.timeout || (reason && scheduler.reason !== reason)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(scheduler.timeout);
|
||||||
|
scheduler.timeout = undefined;
|
||||||
|
scheduler.deadline = undefined;
|
||||||
|
scheduler.reason = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleShortpollFromNow(channelId: string) {
|
||||||
|
const scheduler = CHANNEL_SCHEDULERS.get(channelId);
|
||||||
|
if (!scheduler?.isShortpollEligible || scheduler.shortpollTimeoutMs === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleChannelDifference(channelId, 'shortpoll', scheduler.shortpollTimeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartShortpollFromNow(channelId: string) {
|
||||||
|
const scheduler = CHANNEL_SCHEDULERS.get(channelId);
|
||||||
|
if (!scheduler?.isShortpollEligible || scheduler.shortpollTimeoutMs === undefined || scheduler.isInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduler.reason === 'shortpoll') {
|
||||||
|
clearScheduledChannelDifference(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleChannelDifference(channelId, 'shortpoll', scheduler.shortpollTimeoutMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleGetDifference() {
|
function scheduleGetDifference() {
|
||||||
@ -316,9 +439,30 @@ export async function getDifference() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getChannelDifference(channelId: string) {
|
async function runChannelDifference(channelId: string, reason: ChannelDifferenceReason) {
|
||||||
|
const scheduler = getOrCreateChannelScheduler(channelId);
|
||||||
|
if (scheduler.isInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler.isInFlight = true;
|
||||||
|
scheduler.reason = reason;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await requestChannelDifferenceInternal(channelId, reason);
|
||||||
|
} finally {
|
||||||
|
scheduler.isInFlight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestChannelDifferenceInternal(channelId: string, reason: ChannelDifferenceReason): Promise<void> {
|
||||||
const channel = localDb.chats[channelId];
|
const channel = localDb.chats[channelId];
|
||||||
if (!channel || !(channel instanceof GramJs.Channel) || !channel.accessHash || !localDb.channelPtsById[channelId]) {
|
if (
|
||||||
|
!channel
|
||||||
|
|| !(channel instanceof GramJs.Channel)
|
||||||
|
|| !channel.accessHash
|
||||||
|
|| localDb.channelPtsById[channelId] === undefined
|
||||||
|
) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error('[UpdateManager] Channel for difference not found', channelId, channel);
|
console.error('[UpdateManager] Channel for difference not found', channelId, channel);
|
||||||
@ -326,18 +470,25 @@ async function getChannelDifference(channelId: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await invoke(new GramJs.updates.GetChannelDifference({
|
const limit = reason === 'shortpoll' ? SHORTPOLL_CHANNEL_DIFFERENCE_LIMIT : CATCH_UP_CHANNEL_DIFFERENCE_LIMIT;
|
||||||
channel: buildInputChannel(channelId, channel.accessHash.toString()),
|
let response: GramJs.updates.TypeChannelDifference;
|
||||||
pts: localDb.channelPtsById[channelId],
|
|
||||||
filter: new GramJs.ChannelMessagesFilterEmpty(),
|
|
||||||
limit: CHANNEL_DIFFERENCE_LIMIT,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!response) {
|
try {
|
||||||
if (DEBUG) {
|
const result = await invoke(new GramJs.updates.GetChannelDifference({
|
||||||
// eslint-disable-next-line no-console
|
channel: buildInputChannel(channelId, channel.accessHash.toString()),
|
||||||
console.warn('[UpdatesManager] Failed to get ChannelDifference', channelId, channel);
|
pts: localDb.channelPtsById[channelId],
|
||||||
|
filter: new GramJs.ChannelMessagesFilterEmpty(),
|
||||||
|
limit,
|
||||||
|
}), {
|
||||||
|
shouldThrow: true,
|
||||||
|
});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response = result;
|
||||||
|
} catch (err) {
|
||||||
|
handleChannelDifferenceError(channelId, reason, err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -347,8 +498,13 @@ async function getChannelDifference(channelId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
localDb.channelPtsById[channelId] = response.pts;
|
localDb.channelPtsById[channelId] = response.pts;
|
||||||
|
updateChannelShortpollTimeout(channelId, response);
|
||||||
|
|
||||||
if (response instanceof GramJs.updates.ChannelDifferenceEmpty) {
|
if (response instanceof GramJs.updates.ChannelDifferenceEmpty) {
|
||||||
|
if (response.final) {
|
||||||
|
scheduleShortpollIfEligible(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
popPtsQueue(channelId); // Continue processing updates in queue
|
popPtsQueue(channelId); // Continue processing updates in queue
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -356,8 +512,45 @@ async function getChannelDifference(channelId: string) {
|
|||||||
processDifference(response, channelId);
|
processDifference(response, channelId);
|
||||||
|
|
||||||
if (!response.final) {
|
if (!response.final) {
|
||||||
getChannelDifference(channelId);
|
await requestChannelDifferenceInternal(channelId, 'gapRecovery');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scheduleShortpollIfEligible(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChannelShortpollTimeout(channelId: string, response: GramJs.updates.TypeChannelDifference) {
|
||||||
|
const scheduler = getOrCreateChannelScheduler(channelId);
|
||||||
|
scheduler.shortpollTimeoutMs = ('timeout' in response && response.timeout)
|
||||||
|
? response.timeout * 1000
|
||||||
|
: SHORTPOLL_DEFAULT_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleShortpollIfEligible(channelId: string) {
|
||||||
|
const scheduler = getOrCreateChannelScheduler(channelId);
|
||||||
|
if (!scheduler.isShortpollEligible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleShortpollFromNow(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleChannelDifferenceError(channelId: string, reason: ChannelDifferenceReason, err: unknown) {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn('[UpdatesManager] Failed to get ChannelDifference', channelId, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduler = getOrCreateChannelScheduler(channelId);
|
||||||
|
const errorMessage = err instanceof RPCError ? err.errorMessage : undefined;
|
||||||
|
|
||||||
|
if (errorMessage && TERMINAL_CHANNEL_DIFFERENCE_ERRORS.has(errorMessage)) {
|
||||||
|
scheduler.isShortpollEligible = false;
|
||||||
|
clearScheduledChannelDifference(channelId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleChannelDifference(channelId, reason, CHANNEL_DIFFERENCE_RETRY_TIMEOUT_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceSync() {
|
function forceSync() {
|
||||||
@ -377,10 +570,15 @@ export function reset() {
|
|||||||
clearTimeout(seqTimeout);
|
clearTimeout(seqTimeout);
|
||||||
seqTimeout = undefined;
|
seqTimeout = undefined;
|
||||||
|
|
||||||
PTS_TIMEOUTS.forEach((timeout) => {
|
CHANNEL_SCHEDULERS.forEach(({ timeout }) => {
|
||||||
|
if (!timeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
});
|
});
|
||||||
PTS_TIMEOUTS.clear();
|
CHANNEL_SCHEDULERS.clear();
|
||||||
|
OPENED_CHANNEL_IDS.clear();
|
||||||
|
|
||||||
localDb.commonBoxState = {};
|
localDb.commonBoxState = {};
|
||||||
|
|
||||||
|
|||||||
@ -14,17 +14,20 @@ import type { ActionReturnType, GlobalState } from '../../types';
|
|||||||
|
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { getShippingError, shouldClosePaymentModal } from '../../../util/getReadableErrorText';
|
import { getShippingError, shouldClosePaymentModal } from '../../../util/getReadableErrorText';
|
||||||
import { unique } from '../../../util/iteratees';
|
|
||||||
import { getAccountsInfo, getAccountSlotUrl } from '../../../util/multiaccount';
|
import { getAccountsInfo, getAccountSlotUrl } from '../../../util/multiaccount';
|
||||||
import { oldSetLanguage } from '../../../util/oldLangProvider';
|
import { oldSetLanguage } from '../../../util/oldLangProvider';
|
||||||
import { clearWebTokenAuth } from '../../../util/routing';
|
import { clearWebTokenAuth } from '../../../util/routing';
|
||||||
import { setServerTimeOffset } from '../../../util/serverTime';
|
import { setServerTimeOffset } from '../../../util/serverTime';
|
||||||
import { updateSessionUserId } from '../../../util/sessions';
|
import { updateSessionUserId } from '../../../util/sessions';
|
||||||
import { forceWebsync } from '../../../util/websync';
|
import { forceWebsync } from '../../../util/websync';
|
||||||
import { isChatChannel, isChatSuperGroup } from '../../helpers';
|
|
||||||
import {
|
import {
|
||||||
addActionHandler, getActions, getGlobal, setGlobal,
|
addActionHandler, getActions, getGlobal, setGlobal,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
|
import {
|
||||||
|
getOpenedShortpollChannelIds,
|
||||||
|
resetOpenedChannelShortpollState,
|
||||||
|
syncOpenedShortpollChannelIds,
|
||||||
|
} from '../../openedChannelShortpoll';
|
||||||
import { updateUser, updateUserFullInfo } from '../../reducers';
|
import { updateUser, updateUserFullInfo } from '../../reducers';
|
||||||
import { updateAuth } from '../../reducers/auth';
|
import { updateAuth } from '../../reducers/auth';
|
||||||
import { updateTabState } from '../../reducers/tabs';
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
@ -85,6 +88,8 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'requestSync':
|
case 'requestSync':
|
||||||
|
resetOpenedChannelShortpollState();
|
||||||
|
syncOpenedShortpollChannelIds(global);
|
||||||
actions.sync();
|
actions.sync();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -260,15 +265,10 @@ function onUpdateConnectionState<T extends GlobalState>(
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
if (global.isSynced) {
|
if (global.isSynced) {
|
||||||
const channelStackIds = Object.values(global.byTabId)
|
const channelStackIds = getOpenedShortpollChannelIds(global);
|
||||||
.flatMap((tab) => tab.messageLists)
|
|
||||||
.map((messageList) => messageList.chatId)
|
if (connectionState === 'connectionStateReady' && tabState.isMasterTab && channelStackIds.length) {
|
||||||
.filter((chatId) => {
|
channelStackIds.forEach((chatId) => {
|
||||||
const chat = global.chats.byId[chatId];
|
|
||||||
return chat && (isChatChannel(chat) || isChatSuperGroup(chat));
|
|
||||||
});
|
|
||||||
if (connectionState === 'connectionStateReady' && channelStackIds.length) {
|
|
||||||
unique(channelStackIds).forEach((chatId) => {
|
|
||||||
actions.requestChannelDifference({ chatId });
|
actions.requestChannelDifference({ chatId });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { addCallback } from '../lib/teact/teactn';
|
|||||||
import type { GlobalState } from './types';
|
import type { GlobalState } from './types';
|
||||||
|
|
||||||
import { getServerTime } from '../util/serverTime';
|
import { getServerTime } from '../util/serverTime';
|
||||||
|
import { resetOpenedChannelShortpollState, syncOpenedShortpollChannelIds } from './openedChannelShortpoll';
|
||||||
import { removePeerStory } from './reducers';
|
import { removePeerStory } from './reducers';
|
||||||
import { selectTabState } from './selectors';
|
import { selectTabState } from './selectors';
|
||||||
import { getGlobal, setGlobal } from '.';
|
import { getGlobal, setGlobal } from '.';
|
||||||
@ -22,18 +23,30 @@ addCallback((global: GlobalState) => {
|
|||||||
if (isCurrentMaster === isPreviousMaster) return;
|
if (isCurrentMaster === isPreviousMaster) return;
|
||||||
|
|
||||||
if (isCurrentMaster && !isPreviousMaster) {
|
if (isCurrentMaster && !isPreviousMaster) {
|
||||||
startIntervals();
|
startIntervals(global);
|
||||||
} else {
|
} else {
|
||||||
stopIntervals();
|
stopIntervals();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function startIntervals() {
|
addCallback((global: GlobalState) => {
|
||||||
|
if (!selectTabState(global)?.isMasterTab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncOpenedShortpollChannelIds(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
function startIntervals(global: GlobalState) {
|
||||||
if (intervals.length) return;
|
if (intervals.length) return;
|
||||||
|
|
||||||
|
resetOpenedChannelShortpollState();
|
||||||
intervals.push(window.setInterval(checkStoryExpiration, STORY_EXPIRATION_INTERVAL));
|
intervals.push(window.setInterval(checkStoryExpiration, STORY_EXPIRATION_INTERVAL));
|
||||||
|
syncOpenedShortpollChannelIds(global);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopIntervals() {
|
function stopIntervals() {
|
||||||
|
resetOpenedChannelShortpollState();
|
||||||
intervals.forEach((interval) => clearInterval(interval));
|
intervals.forEach((interval) => clearInterval(interval));
|
||||||
intervals = [];
|
intervals = [];
|
||||||
}
|
}
|
||||||
|
|||||||
114
src/global/openedChannelShortpoll.ts
Normal file
114
src/global/openedChannelShortpoll.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import type { ThreadId } from '../types';
|
||||||
|
import type { GlobalState } from './types';
|
||||||
|
|
||||||
|
import { callApi } from '../api/gramjs';
|
||||||
|
import { isChatChannel, isChatSuperGroup } from './helpers';
|
||||||
|
import { selectCurrentMessageList, selectTabState } from './selectors';
|
||||||
|
|
||||||
|
const MAX_OPENED_CHANNELS = 10;
|
||||||
|
|
||||||
|
type OpenedChannelEntrySource = 'visible' | 'preview';
|
||||||
|
|
||||||
|
type OpenedChannelEntry = {
|
||||||
|
key: string;
|
||||||
|
channelId: string;
|
||||||
|
source: OpenedChannelEntrySource;
|
||||||
|
};
|
||||||
|
|
||||||
|
let openedEntryMru: string[] = [];
|
||||||
|
let lastReportedChannelIds: string[] = [];
|
||||||
|
|
||||||
|
export function resetOpenedChannelShortpollState() {
|
||||||
|
openedEntryMru = [];
|
||||||
|
lastReportedChannelIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOpenedShortpollChannelIds(global: GlobalState) {
|
||||||
|
const openedEntries = buildOpenedChannelEntries(global);
|
||||||
|
const entriesByKey = new Map(openedEntries.map((entry) => [entry.key, entry]));
|
||||||
|
const openedKeys = new Set(entriesByKey.keys());
|
||||||
|
|
||||||
|
openedEntryMru = openedEntryMru.filter((key) => openedKeys.has(key));
|
||||||
|
|
||||||
|
openedEntries
|
||||||
|
.map((entry) => entry.key)
|
||||||
|
.reverse()
|
||||||
|
.forEach((key) => {
|
||||||
|
if (openedEntryMru.indexOf(key) === -1) {
|
||||||
|
openedEntryMru.unshift(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const channelIds: string[] = [];
|
||||||
|
const channelIdsSet = new Set<string>();
|
||||||
|
|
||||||
|
openedEntryMru.forEach((key) => {
|
||||||
|
if (channelIds.length >= MAX_OPENED_CHANNELS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = entriesByKey.get(key);
|
||||||
|
if (!entry || channelIdsSet.has(entry.channelId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channelIds.push(entry.channelId);
|
||||||
|
channelIdsSet.add(entry.channelId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return channelIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncOpenedShortpollChannelIds(global: GlobalState) {
|
||||||
|
if (!selectTabState(global).isMasterTab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelIds = getOpenedShortpollChannelIds(global);
|
||||||
|
if (areArraysEqual(channelIds, lastReportedChannelIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastReportedChannelIds = channelIds;
|
||||||
|
void callApi('setOpenedChannelIds', channelIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOpenedChannelEntries(global: GlobalState) {
|
||||||
|
return Object.values(global.byTabId).flatMap(({ id: tabId, quickPreview }) => {
|
||||||
|
const entries: OpenedChannelEntry[] = [];
|
||||||
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
||||||
|
|
||||||
|
if (currentMessageList && shouldShortpollChannel(global, currentMessageList.chatId)) {
|
||||||
|
entries.push({
|
||||||
|
key: buildOpenedChannelEntryKey(tabId, currentMessageList.chatId, currentMessageList.threadId, 'visible'),
|
||||||
|
channelId: currentMessageList.chatId,
|
||||||
|
source: 'visible',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quickPreview && shouldShortpollChannel(global, quickPreview.chatId)) {
|
||||||
|
entries.push({
|
||||||
|
key: buildOpenedChannelEntryKey(tabId, quickPreview.chatId, quickPreview.threadId, 'preview'),
|
||||||
|
channelId: quickPreview.chatId,
|
||||||
|
source: 'preview',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldShortpollChannel(global: GlobalState, chatId: string) {
|
||||||
|
const chat = global.chats.byId[chatId];
|
||||||
|
return Boolean(chat && (isChatChannel(chat) || isChatSuperGroup(chat)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOpenedChannelEntryKey(
|
||||||
|
tabId: number, chatId: string, threadId: ThreadId | undefined, source: OpenedChannelEntrySource,
|
||||||
|
) {
|
||||||
|
return `${tabId}:${source}:${chatId}:${threadId || 0}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areArraysEqual(first: string[], second: string[]) {
|
||||||
|
return first.length === second.length && first.every((value, index) => value === second[index]);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user