Media: Fix big file download speed (#6421)

This commit is contained in:
zubiden 2025-11-11 14:37:51 +01:00 committed by Alexander Zinchuk
parent 833db81897
commit fd11226602
20 changed files with 461 additions and 334 deletions

View File

@ -419,7 +419,6 @@
"ClearOtherWebSessionsHelp" = "You can log in on websites that support signing in with Telegram."; "ClearOtherWebSessionsHelp" = "You can log in on websites that support signing in with Telegram.";
"AreYouSureWebSessions" = "Are you sure you want to disconnect all websites where you logged in with Telegram?"; "AreYouSureWebSessions" = "Are you sure you want to disconnect all websites where you logged in with Telegram?";
"AutodownloadSizeLimitUpTo" = "up to {limit}"; "AutodownloadSizeLimitUpTo" = "up to {limit}";
"FileSizeMB" = "{count} MB";
"AutoDownloadMaxFileSize" = "Maximum file size"; "AutoDownloadMaxFileSize" = "Maximum file size";
"AutoDownloadSettingsContacts" = "Contacts"; "AutoDownloadSettingsContacts" = "Contacts";
"AutoDownloadSettingsPrivateChats" = "Other Private Chats"; "AutoDownloadSettingsPrivateChats" = "Other Private Chats";
@ -590,7 +589,6 @@
"ContactPhone" = "Phone Number"; "ContactPhone" = "Phone Number";
"NewContact" = "New Contact"; "NewContact" = "New Contact";
"Done" = "Done"; "Done" = "Done";
"FileSizeGB" = "{count} GB";
"LimitReached" = "Limit Reached"; "LimitReached" = "Limit Reached";
"IncreaseLimit" = "Increase Limit"; "IncreaseLimit" = "Increase Limit";
"LimitFree" = "Free"; "LimitFree" = "Free";
@ -1223,8 +1221,6 @@
"WaitingForNetwork" = "Waiting for network..."; "WaitingForNetwork" = "Waiting for network...";
"ScheduleSendWhenOnline" = "Send When Online"; "ScheduleSendWhenOnline" = "Send When Online";
"VoipIncoming" = "Incoming call"; "VoipIncoming" = "Incoming call";
"FileSizeB" = "{count} B";
"FileSizeKB" = "{count} KB";
"Years_one" = "{count} year"; "Years_one" = "{count} year";
"Years_other" = "{count} years"; "Years_other" = "{count} years";
"MessageTimerShortHours" = "{count}h"; "MessageTimerShortHours" = "{count}h";
@ -2315,6 +2311,15 @@
"UserNoteHint" = "only visible to you"; "UserNoteHint" = "only visible to you";
"EditUserNoteHint" = "Notes are only visible to you."; "EditUserNoteHint" = "Notes are only visible to you.";
"AriaStoryTogglerOpen" = "Open Story List"; "AriaStoryTogglerOpen" = "Open Story List";
"FileTransferProgress" = "{currentSize} / {totalSize}";
"MediaSizeB_one" = "{size}B";
"MediaSizeB_other" = "{size}B";
"MediaSizeKB_one" = "{size}KB";
"MediaSizeKB_other" = "{size}KB";
"MediaSizeMB_one" = "{size}MB";
"MediaSizeMB_other" = "{size}MB";
"MediaSizeGB_one" = "{size}GB";
"MediaSizeGB_other" = "{size}GB";
"InviteBlockedTitle" = "Invite via Link"; "InviteBlockedTitle" = "Invite via Link";
"InviteBlockedOneMessage" = "This user restricts adding them to groups. You can send an invite link in a private message instead."; "InviteBlockedOneMessage" = "This user restricts adding them to groups. You can send an invite link in a private message instead.";
"InviteBlockedManyMessage" = "Some users restrict adding them to groups. You can send these people an invite link in a private message instead."; "InviteBlockedManyMessage" = "Some users restrict adding them to groups. You can send these people an invite link in a private message instead.";

View File

@ -0,0 +1,87 @@
import { memo, useEffect, useRef, useState, useUnmountCleanup } from '@teact';
import { formatFileSize } from '../../util/textFormat';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
type OwnProps = {
className?: string;
size: number;
progress?: number;
};
const SKIP_AFTER = 1024 * 1024 * 1024; // 1GB
const MIN_SIZE_INCREASE = 256 * 1024; // 256KB
const UPDATES_PER_SECOND = 10;
const AnimatedFileSize = ({
className,
size,
progress,
}: OwnProps) => {
const lang = useLang();
const [currentSize, setCurrentSize] = useState<number>(0);
const timerRef = useRef<number>();
const resetAnimation = useLastCallback(() => {
clearTimeout(timerRef.current);
timerRef.current = undefined;
});
const animateSize = useLastCallback(() => {
if (progress === undefined) return;
const currentTarget = size * progress;
const diff = currentTarget - currentSize;
if (diff !== 0) {
const increase = Math.max(MIN_SIZE_INCREASE, diff / UPDATES_PER_SECOND);
const newSize = Math.min(currentTarget, currentSize + increase);
setCurrentSize(newSize);
}
timerRef.current = window.setTimeout(() => {
animateSize();
}, 1000 / UPDATES_PER_SECOND);
});
useEffect(() => {
if (progress === undefined) {
resetAnimation();
setCurrentSize(0);
return;
};
const currentProgress = size * progress;
if (currentProgress > SKIP_AFTER || progress === 1) {
resetAnimation();
setCurrentSize(currentProgress);
return;
}
if (timerRef.current) return;
animateSize();
}, [progress, size]);
useUnmountCleanup(resetAnimation);
const currentSizeString = formatFileSize(lang, currentSize);
const totalSizeString = formatFileSize(lang, size);
if (progress === undefined || progress === 1) {
return totalSizeString;
}
return (
<span className={className} dir={lang.isRtl ? 'rtl' : undefined}>
{lang('FileTransferProgress', {
currentSize: currentSizeString,
totalSize: totalSizeString,
}, { withNodes: true })}
</span>
);
};
export default memo(AnimatedFileSize);

View File

@ -32,7 +32,6 @@ import { captureEvents } from '../../util/captureEvents';
import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '../../util/dates/dateFormat'; import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '../../util/dates/dateFormat';
import { decodeWaveform, interpolateArray } from '../../util/waveform'; import { decodeWaveform, interpolateArray } from '../../util/waveform';
import { LOCAL_TGS_URLS } from './helpers/animatedAssets'; import { LOCAL_TGS_URLS } from './helpers/animatedAssets';
import { getFileSizeString } from './helpers/documentInfo';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import { MAX_EMPTY_WAVEFORM_POINTS, renderWaveform } from './helpers/waveform'; import { MAX_EMPTY_WAVEFORM_POINTS, renderWaveform } from './helpers/waveform';
@ -49,6 +48,7 @@ import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated
import Button from '../ui/Button'; import Button from '../ui/Button';
import Link from '../ui/Link'; import Link from '../ui/Link';
import ProgressSpinner from '../ui/ProgressSpinner'; import ProgressSpinner from '../ui/ProgressSpinner';
import AnimatedFileSize from './AnimatedFileSize';
import AnimatedIcon from './AnimatedIcon'; import AnimatedIcon from './AnimatedIcon';
import Icon from './icons/Icon'; import Icon from './icons/Icon';
@ -540,10 +540,7 @@ function renderAudio(
</div> </div>
)} )}
{!showSeekline && showProgress && ( {!showSeekline && showProgress && (
<div className="meta" dir={isRtl ? 'rtl' : undefined}> <AnimatedFileSize className="meta" size={audio.size} progress={progress} />
{progress ? `${getFileSizeString(audio.size * progress)} / ` : undefined}
{getFileSizeString(audio.size)}
</div>
)} )}
{!showSeekline && !showProgress && ( {!showSeekline && !showProgress && (
<div className="meta" dir={isRtl ? 'rtl' : undefined}> <div className="meta" dir={isRtl ? 'rtl' : undefined}>

View File

@ -1,6 +1,6 @@
import type { ElementRef, FC } from '../../lib/teact/teact'; import type { ElementRef } from '../../lib/teact/teact';
import { import {
memo, useMemo, useRef, useState, memo, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
@ -8,7 +8,7 @@ import type { IconName } from '../../types/icons';
import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/browser/windowEnvironment'; import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatMediaDateTime, formatPastTimeShort } from '../../util/dates/dateFormat'; import { formatMediaDateTime, formatPastTimeShort } from '../../util/dates/dateFormat';
import { getColorFromExtension, getFileSizeString } from './helpers/documentInfo'; import { getColorFromExtension } from './helpers/documentInfo';
import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions'; import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
@ -21,6 +21,7 @@ import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated
import Link from '../ui/Link'; import Link from '../ui/Link';
import ProgressSpinner from '../ui/ProgressSpinner'; import ProgressSpinner from '../ui/ProgressSpinner';
import AnimatedFileSize from './AnimatedFileSize';
import Icon from './icons/Icon'; import Icon from './icons/Icon';
import './File.scss'; import './File.scss';
@ -47,7 +48,7 @@ type OwnProps = {
onDateClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void; onDateClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}; };
const File: FC<OwnProps> = ({ const File = ({
ref, ref,
id, id,
name, name,
@ -67,7 +68,7 @@ const File: FC<OwnProps> = ({
actionIcon, actionIcon,
onClick, onClick,
onDateClick, onDateClick,
}) => { }: OwnProps) => {
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
let elementRef = useRef<HTMLDivElement>(); let elementRef = useRef<HTMLDivElement>();
@ -87,11 +88,6 @@ const File: FC<OwnProps> = ({
} = useShowTransitionDeprecated(isTransferring, undefined, true); } = useShowTransitionDeprecated(isTransferring, undefined, true);
const color = getColorFromExtension(extension); const color = getColorFromExtension(extension);
const sizeString = getFileSizeString(size);
const subtitle = useMemo(() => {
if (!isTransferring || !transferProgress) return sizeString;
return `${getFileSizeString(size * transferProgress)} / ${sizeString}`;
}, [isTransferring, size, sizeString, transferProgress]);
const { width, height } = getDocumentThumbnailDimensions(smaller); const { width, height } = getDocumentThumbnailDimensions(smaller);
@ -154,9 +150,7 @@ const File: FC<OwnProps> = ({
<div className="file-info"> <div className="file-info">
<div className="file-title" dir="auto" title={name}>{renderText(name)}</div> <div className="file-title" dir="auto" title={name}>{renderText(name)}</div>
<div className="file-subtitle" dir="auto"> <div className="file-subtitle" dir="auto">
<span> <AnimatedFileSize size={size} progress={isTransferring ? transferProgress : undefined} />
{subtitle}
</span>
{sender && <span className="file-sender">{renderText(sender)}</span>} {sender && <span className="file-sender">{renderText(sender)}</span>}
{!sender && Boolean(timestamp) && ( {!sender && Boolean(timestamp) && (
<> <>

View File

@ -1,18 +1,5 @@
import type { ApiDocument } from '../../../api/types'; import type { ApiDocument } from '../../../api/types';
const ONE_GIGABYTE = 1024 * 1024 * 1024;
const ONE_MEGABYTE = 1024 * 1024;
export function getFileSizeString(bytes: number) {
if (bytes > (ONE_GIGABYTE / 2)) {
return `${(bytes / ONE_GIGABYTE).toFixed(1)} GB`;
}
if (bytes > (ONE_MEGABYTE / 2)) {
return `${(bytes / ONE_MEGABYTE).toFixed(1)} MB`;
}
return `${(bytes / (1024)).toFixed(1)} KB`;
}
export function getDocumentExtension(document: ApiDocument) { export function getDocumentExtension(document: ApiDocument) {
const { fileName, mimeType } = document; const { fileName, mimeType } = document;

View File

@ -61,8 +61,9 @@ const SettingsDataStorage: FC<OwnProps & StateProps> = ({
}); });
const renderFileSizeCallback = useCallback((value: number) => { const renderFileSizeCallback = useCallback((value: number) => {
const size = AUTODOWNLOAD_FILESIZE_MB_LIMITS[value];
return lang('AutodownloadSizeLimitUpTo', { return lang('AutodownloadSizeLimitUpTo', {
limit: lang('FileSizeMB', { count: AUTODOWNLOAD_FILESIZE_MB_LIMITS[value] }), limit: lang('MediaSizeMB', { size }, { pluralValue: size }),
}); });
}, [lang]); }, [lang]);

View File

@ -9,10 +9,12 @@ import type { IconName } from '../../../../types/icons';
import { MAX_UPLOAD_FILEPART_SIZE } from '../../../../config'; import { MAX_UPLOAD_FILEPART_SIZE } from '../../../../config';
import { selectIsCurrentUserPremium, selectIsPremiumPurchaseBlocked } from '../../../../global/selectors'; import { selectIsCurrentUserPremium, selectIsPremiumPurchaseBlocked } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import { type LangFn } from '../../../../util/localization';
import { formatFileSize } from '../../../../util/textFormat'; import { formatFileSize } from '../../../../util/textFormat';
import renderText from '../../../common/helpers/renderText'; import renderText from '../../../common/helpers/renderText';
import useFlag from '../../../../hooks/useFlag'; import useFlag from '../../../../hooks/useFlag';
import useLang from '../../../../hooks/useLang';
import useOldLang from '../../../../hooks/useOldLang'; import useOldLang from '../../../../hooks/useOldLang';
import Icon from '../../../common/icons/Icon'; import Icon from '../../../common/icons/Icon';
@ -71,16 +73,17 @@ const LIMIT_ICON: Record<ApiLimitTypeWithModal, IconName> = {
}; };
const LIMIT_VALUE_FORMATTER: Partial<Record<ApiLimitTypeWithModal, (...args: any[]) => string>> = { const LIMIT_VALUE_FORMATTER: Partial<Record<ApiLimitTypeWithModal, (...args: any[]) => string>> = {
uploadMaxFileparts: (lang: OldLangFn, value: number) => { uploadMaxFileparts: (lang: LangFn, value: number) => {
// The real size is not exactly 4gb, so we need to round it // The real size is not exactly 4gb, so we need to round it
if (value === 8000) return lang('FileSize.GB', '4'); if (value === 8000) return lang('MediaSizeGB', { size: 4 }, { pluralValue: 4 });
if (value === 4000) return lang('FileSize.GB', '2'); if (value === 4000) return lang('MediaSizeGB', { size: 2 }, { pluralValue: 2 });
return formatFileSize(lang, value * MAX_UPLOAD_FILEPART_SIZE); return formatFileSize(lang, value * MAX_UPLOAD_FILEPART_SIZE);
}, },
}; };
function getLimiterDescription({ function getLimiterDescription({
lang, lang,
oldLang,
limitType, limitType,
isPremium, isPremium,
canBuyPremium, canBuyPremium,
@ -88,7 +91,8 @@ function getLimiterDescription({
premiumValue, premiumValue,
valueFormatter, valueFormatter,
}: { }: {
lang: OldLangFn; lang: LangFn;
oldLang: OldLangFn;
limitType?: ApiLimitTypeWithModal; limitType?: ApiLimitTypeWithModal;
isPremium?: boolean; isPremium?: boolean;
canBuyPremium?: boolean; canBuyPremium?: boolean;
@ -104,13 +108,13 @@ function getLimiterDescription({
const premiumValueFormatted = valueFormatter ? valueFormatter(lang, premiumValue) : premiumValue; const premiumValueFormatted = valueFormatter ? valueFormatter(lang, premiumValue) : premiumValue;
if (isPremium) { if (isPremium) {
return lang(LIMIT_DESCRIPTION_PREMIUM[limitType], premiumValueFormatted); return oldLang(LIMIT_DESCRIPTION_PREMIUM[limitType], premiumValueFormatted);
} }
return canBuyPremium return canBuyPremium
? lang(LIMIT_DESCRIPTION[limitType], ? oldLang(LIMIT_DESCRIPTION[limitType],
limitType === 'channelsPublic' ? premiumValueFormatted : [defaultValueFormatted, premiumValueFormatted]) limitType === 'channelsPublic' ? premiumValueFormatted : [defaultValueFormatted, premiumValueFormatted])
: lang(LIMIT_DESCRIPTION_BLOCKED[limitType], defaultValueFormatted); : oldLang(LIMIT_DESCRIPTION_BLOCKED[limitType], defaultValueFormatted);
} }
export type OwnProps = { export type OwnProps = {
@ -132,7 +136,8 @@ const PremiumLimitReachedModal: FC<OwnProps & StateProps> = ({
canBuyPremium, canBuyPremium,
}) => { }) => {
const { closeLimitReachedModal, openPremiumModal } = getActions(); const { closeLimitReachedModal, openPremiumModal } = getActions();
const lang = useOldLang(); const lang = useLang();
const oldLang = useOldLang();
const [isClosing, startClosing, stopClosing] = useFlag(); const [isClosing, startClosing, stopClosing] = useFlag();
@ -149,6 +154,7 @@ const PremiumLimitReachedModal: FC<OwnProps & StateProps> = ({
const valueFormatter = limit && LIMIT_VALUE_FORMATTER[limit]; const valueFormatter = limit && LIMIT_VALUE_FORMATTER[limit];
const description = getLimiterDescription({ const description = getLimiterDescription({
lang, lang,
oldLang,
limitType: limit, limitType: limit,
isPremium, isPremium,
canBuyPremium, canBuyPremium,

View File

@ -14,7 +14,6 @@ import type { IconName } from '../../types/icons';
import { IS_IOS, IS_TOUCH_ENV } from '../../util/browser/windowEnvironment'; import { IS_IOS, IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { formatMediaDuration } from '../../util/dates/dateFormat'; import { formatMediaDuration } from '../../util/dates/dateFormat';
import { formatFileSize } from '../../util/textFormat';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal'; import useCurrentTimeSignal from '../../hooks/useCurrentTimeSignal';
@ -24,6 +23,7 @@ import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import useControlsSignal from './hooks/useControlsSignal'; import useControlsSignal from './hooks/useControlsSignal';
import AnimatedFileSize from '../common/AnimatedFileSize';
import Icon from '../common/icons/Icon'; import Icon from '../common/icons/Icon';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Menu from '../ui/Menu'; import Menu from '../ui/Menu';
@ -209,7 +209,7 @@ const VideoPlayerControls: FC<OwnProps> = ({
{renderTime(currentTime, duration)} {renderTime(currentTime, duration)}
{!isBuffered && ( {!isBuffered && (
<div className="player-file-size"> <div className="player-file-size">
{`${formatFileSize(lang, fileSize * bufferedProgress)} / ${formatFileSize(lang, fileSize)}`} <AnimatedFileSize size={fileSize} progress={bufferedProgress} />
</div> </div>
)} )}
<div className="spacer" /> <div className="spacer" />

View File

@ -458,7 +458,7 @@ export function getMediaTransferState(
progress?: number, isLoadNeeded = false, isUploading = false, progress?: number, isLoadNeeded = false, isUploading = false,
) { ) {
const isTransferring = isUploading || isLoadNeeded; const isTransferring = isUploading || isLoadNeeded;
const transferProgress = Number(progress); const transferProgress = progress || 0;
return { return {
isUploading, isTransferring, transferProgress, isUploading, isTransferring, transferProgress,

View File

@ -1,5 +1,5 @@
import { import {
useEffect, useMemo, useRef, useState, useEffect, useRef, useState,
} from '../lib/teact/teact'; } from '../lib/teact/teact';
import { ApiMediaFormat } from '../api/types'; import { ApiMediaFormat } from '../api/types';
@ -7,9 +7,9 @@ import { ApiMediaFormat } from '../api/types';
import { selectIsSynced } from '../global/selectors'; import { selectIsSynced } from '../global/selectors';
import { IS_PROGRESSIVE_SUPPORTED } from '../util/browser/windowEnvironment'; import { IS_PROGRESSIVE_SUPPORTED } from '../util/browser/windowEnvironment';
import * as mediaLoader from '../util/mediaLoader'; import * as mediaLoader from '../util/mediaLoader';
import { throttle } from '../util/schedulers';
import useSelector from './data/useSelector'; import useSelector from './data/useSelector';
import useForceUpdate from './useForceUpdate'; import useForceUpdate from './useForceUpdate';
import useThrottledCallback from './useThrottledCallback';
import useUniqueId from './useUniqueId'; import useUniqueId from './useUniqueId';
const STREAMING_PROGRESS = 0.75; const STREAMING_PROGRESS = 0.75;
@ -34,13 +34,11 @@ export default function useMediaWithLoadProgress(
const [loadProgress, setLoadProgress] = useState(mediaData && !isStreaming ? 1 : 0); const [loadProgress, setLoadProgress] = useState(mediaData && !isStreaming ? 1 : 0);
const startedAtRef = useRef<number>(); const startedAtRef = useRef<number>();
const handleProgress = useMemo(() => { const handleProgress = useThrottledCallback((progress: number) => {
return throttle((progress: number) => { if (startedAtRef.current && id && (!delay || (Date.now() - startedAtRef.current > delay))) {
if (startedAtRef.current && (!delay || (Date.now() - startedAtRef.current > delay))) { setLoadProgress(progress);
setLoadProgress(progress); }
} }, [delay, id], PROGRESS_THROTTLE, true);
}, PROGRESS_THROTTLE, true);
}, [delay]);
useEffect(() => { useEffect(() => {
if (!noLoad && mediaHash) { if (!noLoad && mediaHash) {

View File

@ -1,8 +1,8 @@
import type TelegramClient from './TelegramClient'; import type TelegramClient from './TelegramClient';
import type { SizeType } from './TelegramClient'; import type { SizeType } from './TelegramClient';
import { getDcBandwidthManager } from '../../../util/dcBandwithManager';
import Deferred from '../../../util/Deferred'; import Deferred from '../../../util/Deferred';
import { Foreman } from '../../../util/foreman';
import { FloodPremiumWaitError, FloodWaitError, RPCError } from '../errors'; import { FloodPremiumWaitError, FloodWaitError, RPCError } from '../errors';
import Api from '../tl/api'; import Api from '../tl/api';
@ -41,8 +41,6 @@ const DEFAULT_CHUNK_SIZE = 64; // kb
const ONE_MB = 1024 * 1024; const ONE_MB = 1024 * 1024;
const DISCONNECT_SLEEP = 1000; const DISCONNECT_SLEEP = 1000;
const NEW_CONNECTION_QUEUE_THRESHOLD = 5;
// when the sender requests hangs for 60 second we will reimport // when the sender requests hangs for 60 second we will reimport
const SENDER_TIMEOUT = 60 * 1000; const SENDER_TIMEOUT = 60 * 1000;
// Telegram may have server issues so we try several times // Telegram may have server issues so we try several times
@ -82,14 +80,23 @@ class FileView {
write(data: Uint8Array, offset: number) { write(data: Uint8Array, offset: number) {
if (this.type === 'opfs') { if (this.type === 'opfs') {
this.largeFileAccessHandle!.write(data, { at: offset }); this.largeFileAccessHandle!.write(data, { at: offset });
} else if (this.size) { return;
for (let i = 0; i < data.length; i++) {
if (offset + i >= this.buffer!.length) return;
this.buffer!.writeUInt8(data[i], offset + i);
}
} else {
this.buffer = Buffer.concat([this.buffer!, data]);
} }
if (this.size) {
const endOffset = offset + data.length;
if (endOffset > this.buffer!.length) { // Slow path for potential overflow
if (offset >= this.buffer!.length) return; // Ignore writes past the end
const writeLength = this.buffer!.length - offset;
this.buffer!.set(data.subarray(0, writeLength), offset);
return;
}
this.buffer!.set(data, offset);
return;
}
this.buffer = Buffer.concat([this.buffer!, data]);
} }
async getData(): Promise<Buffer<ArrayBuffer> | File> { async getData(): Promise<Buffer<ArrayBuffer> | File> {
@ -126,14 +133,6 @@ export async function downloadFile(
return undefined; return undefined;
} }
const MAX_CONCURRENT_CONNECTIONS = 3;
const MAX_CONCURRENT_CONNECTIONS_PREMIUM = 6;
const MAX_WORKERS_PER_CONNECTION = 10;
const MULTIPLE_CONNECTIONS_MIN_FILE_SIZE = 10485760; // 10MB
const foremans = Array(MAX_CONCURRENT_CONNECTIONS_PREMIUM).fill(undefined)
.map(() => new Foreman(MAX_WORKERS_PER_CONNECTION));
async function downloadFile2( async function downloadFile2(
client: TelegramClient, client: TelegramClient,
inputLocation: Api.TypeInputFileLocation, inputLocation: Api.TypeInputFileLocation,
@ -170,9 +169,6 @@ async function downloadFile2(
const partSize = partSizeKb * 1024; const partSize = partSizeKb * 1024;
const partsCount = rangeSize ? Math.ceil(rangeSize / partSize) : 1; const partsCount = rangeSize ? Math.ceil(rangeSize / partSize) : 1;
const noParallel = !end; const noParallel = !end;
const shouldUseMultipleConnections = Boolean(fileSize)
&& fileSize >= MULTIPLE_CONNECTIONS_MIN_FILE_SIZE
&& !noParallel;
let deferred: Deferred | undefined; let deferred: Deferred | undefined;
if (partSize % MIN_CHUNK_SIZE !== 0) { if (partSize % MIN_CHUNK_SIZE !== 0) {
@ -188,9 +184,7 @@ async function downloadFile2(
let hasEnded = false; let hasEnded = false;
let progress = 0; let progress = 0;
if (progressCallback) { progressCallback?.(progress);
progressCallback(progress);
}
// Limit updates to one per file // Limit updates to one per file
let isPremiumFloodWaitSent = false; let isPremiumFloodWaitSent = false;
@ -198,6 +192,8 @@ async function downloadFile2(
// Allocate memory // Allocate memory
await fileView.init(); await fileView.init();
const dcManager = getDcBandwidthManager(dcId, isPremium);
while (true) { while (true) {
let limit = partSize; let limit = partSize;
let isPrecise = false; let isPrecise = false;
@ -211,23 +207,28 @@ async function downloadFile2(
isPrecise = true; isPrecise = true;
} }
const senderIndex = getFreeForemanIndex(isPremium, shouldUseMultipleConnections);
await foremans[senderIndex].requestWorker(isPriority);
if (deferred) await deferred.promise; if (deferred) await deferred.promise;
if (noParallel) deferred = new Deferred(); if (noParallel) deferred = new Deferred();
if (hasEnded) { if (hasEnded) {
foremans[senderIndex].releaseWorker();
break; break;
} }
const logWithSenderIndex = (...args: any[]) => {
const senderIndex = await dcManager.requestWorker(Boolean(isPriority), limit);
const logWithSenderIndex = (...args: unknown[]) => {
logWithId(`[${senderIndex}/${dcId}]`, ...args); logWithId(`[${senderIndex}/${dcId}]`, ...args);
}; };
promises.push((async (offsetMemo: number) => { // Check again after waiting for capacity
if (progressCallback?.isCanceled) {
hasEnded = true;
dcManager.releaseWorker(senderIndex, limit);
deferred?.resolve();
break;
}
promises.push((async (offsetMemo: number, limitMemo: number, senderIndexMemo: number) => {
while (true) { while (true) {
let sender; let sender;
try { try {
@ -238,7 +239,7 @@ async function downloadFile2(
logWithSenderIndex(`❗️️ getSender took too long ${offsetMemo}`); logWithSenderIndex(`❗️️ getSender took too long ${offsetMemo}`);
}, 8000); }, 8000);
} }
sender = await client.getSender(dcId, senderIndex, isPremium); sender = await client.getSender(dcId, senderIndexMemo, isPremium);
isDone = true; isDone = true;
let isDone2 = false; let isDone2 = false;
@ -253,7 +254,7 @@ async function downloadFile2(
sender.send(new Api.upload.GetFile({ sender.send(new Api.upload.GetFile({
location: inputLocation, location: inputLocation,
offset: BigInt(offsetMemo), offset: BigInt(offsetMemo),
limit, limit: limitMemo,
precise: isPrecise || undefined, precise: isPrecise || undefined,
})), })),
sleep(SENDER_TIMEOUT).then(() => { sleep(SENDER_TIMEOUT).then(() => {
@ -284,11 +285,11 @@ async function downloadFile2(
progressCallback(progress); progressCallback(progress);
} }
if (!end && (result.bytes.length < limit)) { if (!end && (result.bytes.length < limitMemo)) {
hasEnded = true; hasEnded = true;
} }
foremans[senderIndex].releaseWorker(); dcManager.releaseWorker(senderIndexMemo, limitMemo);
if (deferred) deferred.resolve(); if (deferred) deferred.resolve();
fileView.write(result.bytes, offsetMemo - start); fileView.write(result.bytes, offsetMemo - start);
@ -308,7 +309,7 @@ async function downloadFile2(
} }
logWithSenderIndex(`Ended not gracefully ${offsetMemo}`); logWithSenderIndex(`Ended not gracefully ${offsetMemo}`);
foremans[senderIndex].releaseWorker(); dcManager.releaseWorker(senderIndexMemo, limitMemo);
if (deferred) deferred.resolve(); if (deferred) deferred.resolve();
hasEnded = true; hasEnded = true;
@ -316,7 +317,7 @@ async function downloadFile2(
throw err; throw err;
} }
} }
})(offset)); })(offset, limit, senderIndex));
offset += limit; offset += limit;
@ -327,27 +328,3 @@ async function downloadFile2(
await Promise.all(promises); await Promise.all(promises);
return fileView.getData(); return fileView.getData();
} }
function getFreeForemanIndex(isPremium: boolean, forceNewConnection?: boolean) {
const availableConnections = isPremium ? MAX_CONCURRENT_CONNECTIONS_PREMIUM : MAX_CONCURRENT_CONNECTIONS;
let foremanIndex = 0;
let minQueueLength = Infinity;
for (let i = 0; i < availableConnections; i++) {
const foreman = foremans[i];
// If worker is free, return it
if (!foreman.queueLength) return i;
// Potentially create a new connection if the current queue is too long
if (!forceNewConnection && foreman.queueLength <= NEW_CONNECTION_QUEUE_THRESHOLD) {
return i;
}
// If every connection is equally busy, prefer the last one in the list
if (foreman.queueLength <= minQueueLength) {
foremanIndex = i;
minQueueLength = foreman.activeWorkers;
}
}
return foremanIndex;
}

View File

@ -1,6 +1,6 @@
import type TelegramClient from './TelegramClient'; import type TelegramClient from './TelegramClient';
import { Foreman } from '../../../util/foreman'; import { getDcBandwidthManager } from '../../../util/dcBandwithManager';
import { FloodPremiumWaitError, FloodWaitError } from '../errors'; import { FloodPremiumWaitError, FloodWaitError } from '../errors';
import Api from '../tl/api'; import Api from '../tl/api';
@ -24,12 +24,6 @@ export interface UploadFileParams {
const KB_TO_BYTES = 1024; const KB_TO_BYTES = 1024;
const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024; const LARGE_FILE_THRESHOLD = 10 * 1024 * 1024;
const DISCONNECT_SLEEP = 1000; const DISCONNECT_SLEEP = 1000;
const MAX_CONCURRENT_CONNECTIONS = 3;
const MAX_CONCURRENT_CONNECTIONS_PREMIUM = 6;
const MAX_WORKERS_PER_CONNECTION = 10;
const foremans = Array(MAX_CONCURRENT_CONNECTIONS_PREMIUM).fill(undefined)
.map(() => new Foreman(MAX_WORKERS_PER_CONNECTION));
export async function uploadFile( export async function uploadFile(
client: TelegramClient, client: TelegramClient,
@ -54,11 +48,7 @@ export async function uploadFile(
const partSize = getUploadPartSize(size) * KB_TO_BYTES; const partSize = getUploadPartSize(size) * KB_TO_BYTES;
const partCount = Math.floor((size + partSize - 1) / partSize); const partCount = Math.floor((size + partSize - 1) / partSize);
// Pick the least busy foreman const dcManager = getDcBandwidthManager(client.session.dcId, isPremium);
// For some reason, fresh connections give out a higher speed for the first couple of seconds
// I have no idea why, but this may speed up the download of small files
const activeCounts = foremans.map(({ activeWorkers }) => activeWorkers);
let currentForemanIndex = activeCounts.indexOf(Math.min(...activeCounts));
let progress = 0; let progress = 0;
if (onProgress) { if (onProgress) {
@ -71,14 +61,10 @@ export async function uploadFile(
const promises: Promise<any>[] = []; const promises: Promise<any>[] = [];
for (let i = 0; i < partCount; i++) { for (let i = 0; i < partCount; i++) {
const senderIndex = currentForemanIndex % ( const senderIndex = await dcManager.requestWorker(false, partSize);
isPremium ? MAX_CONCURRENT_CONNECTIONS_PREMIUM : MAX_CONCURRENT_CONNECTIONS
);
await foremans[senderIndex].requestWorker();
if (onProgress?.isCanceled) { if (onProgress?.isCanceled) {
foremans[senderIndex].releaseWorker(); dcManager.releaseWorker(senderIndex, partSize);
break; break;
} }
@ -140,13 +126,13 @@ export async function uploadFile(
await sleep(err.seconds * 1000); await sleep(err.seconds * 1000);
continue; continue;
} }
foremans[senderIndex].releaseWorker(); dcManager.releaseWorker(senderIndex, partSize);
if (sender) client.releaseExportedSender(sender); if (sender) client.releaseExportedSender(sender);
throw err; throw err;
} }
foremans[senderIndex].releaseWorker(); dcManager.releaseWorker(senderIndex, partSize);
if (onProgress) { if (onProgress) {
if (onProgress.isCanceled) { if (onProgress.isCanceled) {
@ -160,8 +146,6 @@ export async function uploadFile(
break; break;
} }
})(i, blobSlice)); })(i, blobSlice));
currentForemanIndex++;
} }
await Promise.all(promises); await Promise.all(promises);

View File

@ -1,55 +0,0 @@
/**
* Uint32Array -> ArrayBuffer (low-endian os)
*/
export function i2abLow(buf: Uint32Array): ArrayBuffer {
const uint8 = new Uint8Array(buf.length * 4);
let i = 0;
for (let j = 0; j < buf.length; j++) {
const int = buf[j];
uint8[i++] = int >>> 24;
uint8[i++] = (int >> 16) & 0xFF;
uint8[i++] = (int >> 8) & 0xFF;
uint8[i++] = int & 0xFF;
}
return uint8.buffer;
}
/**
* Uint32Array -> ArrayBuffer (big-endian os)
*/
export function i2abBig(buf: Uint32Array<ArrayBuffer>): ArrayBuffer {
return buf.buffer;
}
/**
* ArrayBuffer -> Uint32Array (low-endian os)
*/
export function ab2iLow(ab: ArrayBuffer | Uint8Array): Uint32Array {
const uint8 = new Uint8Array(ab);
const buf = new Uint32Array(uint8.length / 4);
for (let i = 0; i < uint8.length; i += 4) {
buf[i / 4] = (
uint8[i] << 24
^ uint8[i + 1] << 16
^ uint8[i + 2] << 8
^ uint8[i + 3]
);
}
return buf;
}
/**
* ArrayBuffer -> Uint32Array (big-endian os)
*/
export function ab2iBig(ab: ArrayBuffer | Uint8Array): Uint32Array {
return new Uint32Array(ab);
}
export const isBigEndian = new Uint8Array(new Uint32Array([0x01020304]))[0] === 0x01;
export const i2ab = isBigEndian ? i2abBig : i2abLow;
export const ab2i = isBigEndian ? ab2iBig : ab2iLow;

View File

@ -1,21 +1,18 @@
import AES from '@cryptography/aes'; import AES from '@cryptography/aes';
import { ab2i, i2ab } from './converters';
import { getWords } from './words';
class Counter { class Counter {
_counter: Buffer<ArrayBuffer>; public counter: Buffer<ArrayBuffer>;
constructor(initialValue: Buffer) { constructor(initialValue: Buffer) {
this._counter = Buffer.from(initialValue); this.counter = Buffer.from(initialValue);
} }
increment() { increment() {
for (let i = 15; i >= 0; i--) { for (let i = 15; i >= 0; i--) {
if (this._counter[i] === 255) { if (this.counter[i] === 255) {
this._counter[i] = 0; this.counter[i] = 0;
} else { } else {
this._counter[i]++; this.counter[i]++;
break; break;
} }
} }
@ -25,9 +22,9 @@ class Counter {
class CTR { class CTR {
private _counter: Counter; private _counter: Counter;
private _remainingCounter?: Buffer; private _carryBlock: Buffer | undefined;
private _remainingCounterIndex: number; private _carryOffset: number;
private _aes: AES; private _aes: AES;
@ -38,37 +35,72 @@ class CTR {
this._counter = counter; this._counter = counter;
this._remainingCounter = undefined; this._carryBlock = undefined;
this._remainingCounterIndex = 16; this._carryOffset = 0;
this._aes = new AES(getWords(key)); this._aes = new AES(key);
} }
update(plainText: Buffer<ArrayBuffer>) { update(plainText: Buffer<ArrayBuffer>) {
return this.encrypt(plainText); return this.encrypt(plainText);
} }
encrypt(plainText: Buffer<ArrayBuffer>) { encrypt(plain: Buffer): Buffer {
const encrypted = Buffer.from(plainText); const aes = this._aes;
const ctr = this._counter;
for (let i = 0; i < encrypted.length; i++) { const src = plain;
if (this._remainingCounterIndex === 16) { const n = src.length;
this._remainingCounter = Buffer.from(
i2ab( const dst = Buffer.allocUnsafe(n);
this._aes.encrypt(
ab2i(this._counter._counter), let pos = 0;
) as Uint32Array<ArrayBuffer>,
), // 1) Consume any carried keystream from the previous call
); if (this._carryBlock) {
this._remainingCounterIndex = 0; const take = Math.min(16 - this._carryOffset, n);
this._counter.increment(); for (let j = 0; j < take; j++) {
dst[pos + j] = src[pos + j] ^ this._carryBlock[this._carryOffset + j];
} }
if (this._remainingCounter) { pos += take;
encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++]; this._carryOffset += take;
if (this._carryOffset === 16) {
this._carryBlock = undefined;
this._carryOffset = 0;
} }
} }
return encrypted; // Temporary keystream block for this call
const keystream = Buffer.allocUnsafe(16);
// 2) Full 16-byte blocks
while (pos + 16 <= n) {
const words = aes.encrypt(ctr.counter);
writeU32WordsBE(words, keystream);
ctr.increment();
for (let j = 0; j < 16; j++) {
dst[pos + j] = src[pos + j] ^ keystream[j];
}
pos += 16;
}
// 3) Tail (<16 bytes) — store carryover for next call
if (pos < n) {
const words = aes.encrypt(ctr.counter);
writeU32WordsBE(words, keystream);
ctr.increment();
let used = 0;
for (; pos < n; pos++, used++) {
dst[pos] = src[pos] ^ keystream[used];
}
this._carryBlock = keystream;
this._carryOffset = used;
}
return dst;
} }
} }
@ -100,7 +132,7 @@ export function randomBytes(count: number) {
class Hash { class Hash {
private data = new Uint8Array(0); private data = new Uint8Array(0);
constructor(private algorithm: 'sha1' | 'sha256') {} constructor(private algorithm: 'sha1' | 'sha256') { }
update(data: ArrayLike<number>) { update(data: ArrayLike<number>) {
// We shouldn't be needing new Uint8Array but it doesn't // We shouldn't be needing new Uint8Array but it doesn't
@ -130,3 +162,9 @@ export async function pbkdf2(password: Buffer<ArrayBuffer>, salt: Buffer<ArrayBu
export function createHash(algorithm: 'sha1' | 'sha256') { export function createHash(algorithm: 'sha1' | 'sha256') {
return new Hash(algorithm); return new Hash(algorithm);
} }
function writeU32WordsBE(words: Uint32Array, out: Buffer) {
for (let i = 0; i < words.length; i++) {
out.writeUInt32BE(words[i], i * 4);
}
}

View File

@ -1,51 +0,0 @@
/*
* Imported from https://github.com/spalt08/cryptography/blob/master/packages/aes/src/utils/words.ts
*/
export function s2i(str: string, pos: number) {
return (
str.charCodeAt(pos) << 24
^ str.charCodeAt(pos + 1) << 16
^ str.charCodeAt(pos + 2) << 8
^ str.charCodeAt(pos + 3)
);
}
/**
* Helper function for transforming string key to Uint32Array
*/
export function getWords(key: string | Uint8Array | Uint32Array) {
if (key instanceof Uint32Array) {
return key;
}
if (typeof key === 'string') {
if (key.length % 4 !== 0) for (let i = key.length % 4; i <= 4; i++) key += '\0x00';
const buf = new Uint32Array(key.length / 4);
for (let i = 0; i < key.length; i += 4) buf[i / 4] = s2i(key, i);
return buf;
}
if (key instanceof Uint8Array) {
const buf = new Uint32Array(key.length / 4);
for (let i = 0; i < key.length; i += 4) {
buf[i / 4] = (
key[i] << 24
^ key[i + 1] << 16
^ key[i + 2] << 8
^ key[i + 3]
);
}
return buf;
}
throw new Error('Unable to create 32-bit words');
}
export function xor(left: Uint32Array, right: Uint32Array, to = left) {
for (let i = 0; i < left.length; i++) to[i] = left[i] ^ right[i];
}

View File

@ -1848,9 +1848,6 @@ export interface LangPairWithVariables<V = LangVariable> {
'AutodownloadSizeLimitUpTo': { 'AutodownloadSizeLimitUpTo': {
'limit': V; 'limit': V;
}; };
'FileSizeMB': {
'count': V;
};
'WebAppAddToAttachmentText': { 'WebAppAddToAttachmentText': {
'bot': V; 'bot': V;
}; };
@ -1872,9 +1869,6 @@ export interface LangPairWithVariables<V = LangVariable> {
'AddContactSharedContactExceptionInfo': { 'AddContactSharedContactExceptionInfo': {
'user': V; 'user': V;
}; };
'FileSizeGB': {
'count': V;
};
'SubscribeToPremium': { 'SubscribeToPremium': {
'price': V; 'price': V;
}; };
@ -2029,12 +2023,6 @@ export interface LangPairWithVariables<V = LangVariable> {
'count': V; 'count': V;
'total': V; 'total': V;
}; };
'FileSizeB': {
'count': V;
};
'FileSizeKB': {
'count': V;
};
'MessageTimerShortHours': { 'MessageTimerShortHours': {
'count': V; 'count': V;
}; };
@ -2994,6 +2982,10 @@ export interface LangPairWithVariables<V = LangVariable> {
'ActionStarGiftPrepaidUpgraded': { 'ActionStarGiftPrepaidUpgraded': {
'user': V; 'user': V;
}; };
'FileTransferProgress': {
'currentSize': V;
'totalSize': V;
};
'InviteRestrictedPremiumReasonSingle': { 'InviteRestrictedPremiumReasonSingle': {
'user': V; 'user': V;
}; };
@ -3349,6 +3341,18 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
'points': V; 'points': V;
'link': V; 'link': V;
}; };
'MediaSizeB': {
'size': V;
};
'MediaSizeKB': {
'size': V;
};
'MediaSizeMB': {
'size': V;
};
'MediaSizeGB': {
'size': V;
};
'InviteRestrictedUsers': { 'InviteRestrictedUsers': {
'count': V; 'count': V;
}; };

View File

@ -0,0 +1,191 @@
import { DEBUG } from '../config';
import Deferred from './Deferred';
const MAX_CONCURRENT_CONNECTIONS = 3;
const MAX_CONCURRENT_CONNECTIONS_PREMIUM = 6;
const MAX_ACTIVE_REQUEST_SIZE = 9 * 1024 * 1024;
const MAX_ACTIVE_REQUEST_SIZE_PREMIUM = 20 * 1024 * 1024;
const FOREMAN_MAX_HEAP_SIZE = MAX_ACTIVE_REQUEST_SIZE_PREMIUM / MAX_CONCURRENT_CONNECTIONS_PREMIUM;
interface QueuedRequest {
deferred: Deferred;
requestSize: number;
}
const dcManagers: Record<number, DcBandwidthManager> = {};
export function getDcBandwidthManager(dcId: number, isPremium: boolean): DcBandwidthManager {
if (!dcManagers[dcId]) {
dcManagers[dcId] = new DcBandwidthManager();
}
const dcManager = dcManagers[dcId];
dcManager.updateIsPremium(isPremium);
return dcManager;
}
if (DEBUG) {
(globalThis as any).getDcManagers = () => dcManagers;
}
class Foreman {
private queuedRequests: QueuedRequest[] = [];
private priorityQueuedRequests: QueuedRequest[] = [];
activeRequestHeapSize = 0;
queuedRequestHeapSize = 0;
constructor(private maxRequestHeapSize: number) { }
requestWorker(requestSize: number, isPriority?: boolean) {
if (this.activeRequestHeapSize + requestSize > this.maxRequestHeapSize) {
const deferred = new Deferred();
const queuedRequest = { deferred, requestSize };
if (isPriority) {
this.priorityQueuedRequests.push(queuedRequest);
} else {
this.queuedRequests.push(queuedRequest);
}
this.queuedRequestHeapSize += requestSize;
return deferred.promise;
}
this.activeRequestHeapSize += requestSize;
return Promise.resolve();
}
releaseWorker(requestSize: number) {
this.activeRequestHeapSize -= requestSize;
// Try to process queued requests
while (this.queueLength > 0) {
const queuedRequest = this.priorityQueuedRequests[0] || this.queuedRequests[0];
if (!queuedRequest) break;
// Check if we can process the next queued request
if (this.activeRequestHeapSize + queuedRequest.requestSize <= this.maxRequestHeapSize) {
const request = (this.priorityQueuedRequests.shift() || this.queuedRequests.shift())!;
this.queuedRequestHeapSize -= request.requestSize;
this.activeRequestHeapSize += request.requestSize;
request.deferred.resolve();
} else {
break;
}
}
}
canAccept(requestSize: number) {
return this.activeRequestHeapSize + requestSize <= this.maxRequestHeapSize;
}
get queueLength() {
return this.queuedRequests.length + this.priorityQueuedRequests.length;
}
}
class DcBandwidthManager {
private foremans: Foreman[] = [];
private maxConnections: number = MAX_CONCURRENT_CONNECTIONS;
private maxActiveSize: number = MAX_ACTIVE_REQUEST_SIZE;
private queuedRequests: QueuedRequest[] = [];
private priorityQueuedRequests: QueuedRequest[] = [];
activeRequestSize = 0;
constructor() {
const maxForemans = Math.max(MAX_CONCURRENT_CONNECTIONS, MAX_CONCURRENT_CONNECTIONS_PREMIUM);
this.foremans = Array(maxForemans)
.fill(undefined)
.map(() => new Foreman(FOREMAN_MAX_HEAP_SIZE));
}
updateIsPremium(isPremium: boolean) {
this.maxConnections = isPremium ? MAX_CONCURRENT_CONNECTIONS_PREMIUM : MAX_CONCURRENT_CONNECTIONS;
this.maxActiveSize = isPremium ? MAX_ACTIVE_REQUEST_SIZE_PREMIUM : MAX_ACTIVE_REQUEST_SIZE;
}
async requestWorker(isPriority: boolean, requestSize: number): Promise<number> {
// Check if adding this request would exceed the dcId size limit
if (this.activeRequestSize + requestSize > this.maxActiveSize) {
const deferred = new Deferred();
const queuedRequest = { deferred, requestSize };
if (isPriority) {
this.priorityQueuedRequests.push(queuedRequest);
} else {
this.queuedRequests.push(queuedRequest);
}
await deferred.promise;
// After being dequeued, select and request a foreman
const foremanIndex = this.getFreeForemanIndex(requestSize);
const foreman = this.foremans[foremanIndex];
await foreman.requestWorker(requestSize, isPriority);
return foremanIndex;
}
const foremanIndex = this.getFreeForemanIndex(requestSize);
const foreman = this.foremans[foremanIndex];
await foreman.requestWorker(requestSize, isPriority);
this.activeRequestSize += requestSize;
return foremanIndex;
}
releaseWorker(foremanIndex: number, requestSize: number) {
this.activeRequestSize -= requestSize;
this.foremans[foremanIndex].releaseWorker(requestSize);
// Try to process queued requests
this.processQueue();
}
private processQueue() {
while (true) {
const queuedRequest = this.priorityQueuedRequests[0] || this.queuedRequests[0];
if (!queuedRequest) {
return;
}
if (this.activeRequestSize + queuedRequest.requestSize > this.maxActiveSize) {
return;
}
const request = (this.priorityQueuedRequests.shift() || this.queuedRequests.shift())!;
this.activeRequestSize += request.requestSize;
request.deferred.resolve();
}
}
private getFreeForemanIndex(requestSize: number): number {
let minTotalHeapSize = Infinity;
let minHeapForemanIndex = 0;
for (let i = 0; i < this.maxConnections; i++) {
const foreman = this.foremans[i];
if (foreman.canAccept(requestSize)) {
return i; // Prefer filling up free foremans to avoid unnecessary connections
}
const totalHeapSize = foreman.activeRequestHeapSize + foreman.queuedRequestHeapSize;
if (totalHeapSize < minTotalHeapSize) {
minTotalHeapSize = totalHeapSize;
minHeapForemanIndex = i;
}
}
// If every foreman is busy, use the one with the smallest total heap size
return minHeapForemanIndex;
}
getForeman(index: number): Foreman {
return this.foremans[index];
}
get queueLength() {
return this.queuedRequests.length + this.priorityQueuedRequests.length;
}
}

View File

@ -1,40 +0,0 @@
import Deferred from './Deferred';
export class Foreman {
private deferreds: Deferred[] = [];
private priorityDeferreds: Deferred[] = [];
activeWorkers = 0;
constructor(private maxWorkers: number) {
}
requestWorker(isPriority?: boolean) {
if (this.activeWorkers === this.maxWorkers) {
const deferred = new Deferred();
if (isPriority) {
this.priorityDeferreds.push(deferred);
} else {
this.deferreds.push(deferred);
}
return deferred.promise;
}
this.activeWorkers++;
return Promise.resolve();
}
releaseWorker() {
if (this.queueLength) {
const deferred = (this.priorityDeferreds.shift() || this.deferreds.shift())!;
deferred.resolve();
} else {
this.activeWorkers--;
}
}
get queueLength() {
return this.deferreds.length + this.priorityDeferreds.length;
}
}

View File

@ -104,6 +104,7 @@ export function cancelProgress(progressCallback: ApiOnProgress) {
cancelApiProgress(parentCallback); cancelApiProgress(parentCallback);
cancellableCallbacks.delete(url); cancellableCallbacks.delete(url);
progressCallbacks.delete(url); progressCallbacks.delete(url);
return;
} }
}); });
}); });
@ -185,7 +186,10 @@ function makeOnProgress(url: string) {
const onProgress: ApiOnProgress = (progress: number) => { const onProgress: ApiOnProgress = (progress: number) => {
progressCallbacks.get(url)?.forEach((callback) => { progressCallbacks.get(url)?.forEach((callback) => {
callback(progress); callback(progress);
if (callback.isCanceled) onProgress.isCanceled = true; if (callback.isCanceled) {
onProgress.isCanceled = true;
memoryCache.delete(url);
}
}); });
}; };

View File

@ -1,4 +1,3 @@
import type { OldLangFn } from '../hooks/useOldLang';
import type { LangFn } from './localization'; import type { LangFn } from './localization';
import EMOJI_REGEX from '../lib/twemojiRegex'; import EMOJI_REGEX from '../lib/twemojiRegex';
@ -44,15 +43,16 @@ export const getFirstLetters = withCache((phrase: string, count = 2) => {
.join(''); .join('');
}); });
const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB']; const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB'] as const;
export function formatFileSize(lang: OldLangFn, bytes: number, decimals = 1): string { export function formatFileSize(lang: LangFn, bytes: number, decimals = 1): string {
if (bytes === 0) { if (bytes === 0) {
return lang('FileSize.B', 0); return lang('MediaSizeB', { size: 0 }, { pluralValue: 0 });
} }
const k = 1024; const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
const value = (bytes / (k ** i)).toFixed(Math.max(decimals, 0)); const v = (bytes / (k ** i));
const value = v.toFixed(Math.max(decimals, 0));
return lang(`FileSize.${FILE_SIZE_UNITS[i]}`, value); return lang(`MediaSize${FILE_SIZE_UNITS[i]}`, { size: value }, { pluralValue: v });
} }