Browse Source

Better error handling

Fix rare FILE_REFERENCE_EXPIRED case
Native file downloading
master
Eduard Kuzmenko 2 years ago
parent
commit
a02387efa1
  1. 4
      src/components/chat/bubbles.ts
  2. 2
      src/components/dialogsContextMenu.ts
  3. 3
      src/components/lazyLoadQueueBase.ts
  4. 2
      src/components/popups/payment.ts
  5. 1
      src/components/popups/paymentCardConfirmation.ts
  6. 7
      src/components/popups/paymentShipping.ts
  7. 75
      src/components/wrappers/document.ts
  8. 2
      src/config/databases/index.ts
  9. 2
      src/config/databases/state.ts
  10. 29
      src/global.d.ts
  11. 12
      src/helpers/blob/blobConstruct.ts
  12. 5
      src/helpers/fileName.ts
  13. 7
      src/helpers/makeError.ts
  14. 4
      src/helpers/string/fileNameRFC.ts
  15. 2
      src/helpers/toggleStorages.ts
  16. 114
      src/lib/appManagers/appDownloadManager.ts
  17. 2
      src/lib/appManagers/appImManager.ts
  18. 3
      src/lib/appManagers/appMessagesManager.ts
  19. 2
      src/lib/appManagers/utils/download/getDownloadMediaDetails.ts
  20. 72
      src/lib/fileManager.ts
  21. 23
      src/lib/files/cacheStorage.ts
  22. 13
      src/lib/files/fileStorage.ts
  23. 11
      src/lib/files/idb.ts
  24. 58
      src/lib/files/memoryWriter.ts
  25. 14
      src/lib/files/streamWriter.ts
  26. 399
      src/lib/mtproto/apiFileManager.ts
  27. 13
      src/lib/mtproto/apiManager.ts
  28. 1
      src/lib/mtproto/api_methods.ts
  29. 1
      src/lib/mtproto/authorizer.ts
  30. 1
      src/lib/mtproto/mtproto_config.ts
  31. 4
      src/lib/mtproto/mtprotoworker.ts
  32. 9
      src/lib/mtproto/networker.ts
  33. 3
      src/lib/mtproto/referenceDatabase.ts
  34. 154
      src/lib/serviceWorker/index.service.ts
  35. 2
      src/lib/serviceWorker/push.ts
  36. 14
      src/lib/serviceWorker/serviceMessagePort.ts
  37. 6
      src/lib/serviceWorker/stream.ts
  38. 7
      src/lib/storage.ts
  39. 3
      src/lib/storages/filters.ts
  40. 1
      src/pages/pageSignQR.ts
  41. 7
      src/scss/partials/_chatBubble.scss
  42. 70
      src/scss/partials/_document.scss

4
src/components/chat/bubbles.ts

@ -503,7 +503,7 @@ export default class ChatBubbles { @@ -503,7 +503,7 @@ export default class ChatBubbles {
div.replaceWith(newDiv);
if(timeSpan) {
newDiv.querySelector('.document-size').append(timeSpan);
newDiv.querySelector('.document').append(timeSpan);
}
});
}
@ -4127,7 +4127,7 @@ export default class ChatBubbles { @@ -4127,7 +4127,7 @@ export default class ChatBubbles {
nameContainer = newNameContainer;
}
const lastContainer = messageDiv.lastElementChild.querySelector('.document-message, .document-size, .audio');
const lastContainer = messageDiv.lastElementChild.querySelector('.document-message, .document, .audio');
// lastContainer && lastContainer.append(timeSpan.cloneNode(true));
lastContainer && lastContainer.append(timeSpan);

2
src/components/dialogsContextMenu.ts

@ -108,7 +108,7 @@ export default class DialogsContextMenu { @@ -108,7 +108,7 @@ export default class DialogsContextMenu {
};
private onPinClick = () => {
this.managers.appMessagesManager.toggleDialogPin(this.selectedId, this.filterId).catch(async(err) => {
this.managers.appMessagesManager.toggleDialogPin(this.selectedId, this.filterId).catch(async(err: ApiError) => {
if(err.type === 'PINNED_DIALOGS_TOO_MUCH') {
if(this.filterId >= 1) {
toastNew({langPackKey: 'PinFolderLimitReached'});

3
src/components/lazyLoadQueueBase.ts

@ -80,7 +80,8 @@ export default class LazyLoadQueueBase { @@ -80,7 +80,8 @@ export default class LazyLoadQueueBase {
//await item.load(item.div);
await this.loadItem(item);
} catch(err) {
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(err as string)) {
const ignoreErrors: Set<ErrorType> = new Set(['NO_ENTRY_FOUND', 'STORAGE_OFFLINE']);
if(!ignoreErrors.has((err as ApiError)?.type)) {
this.log.error('loadMediaQueue error:', err/* , item */);
}
}

2
src/components/popups/payment.ts

@ -13,7 +13,6 @@ import { detectUnifiedCardBrand } from "../../helpers/cards/cardBrands"; @@ -13,7 +13,6 @@ import { detectUnifiedCardBrand } from "../../helpers/cards/cardBrands";
import { attachClickEvent, simulateClickEvent } from "../../helpers/dom/clickEvent";
import findUpAsChild from "../../helpers/dom/findUpAsChild";
import findUpClassName from "../../helpers/dom/findUpClassName";
import loadScript from "../../helpers/dom/loadScript";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
import { renderImageFromUrlPromise } from "../../helpers/dom/renderImageFromUrl";
import replaceContent from "../../helpers/dom/replaceContent";
@ -25,7 +24,6 @@ import ScrollSaver from "../../helpers/scrollSaver"; @@ -25,7 +24,6 @@ import ScrollSaver from "../../helpers/scrollSaver";
import tsNow from "../../helpers/tsNow";
import { AccountTmpPassword, InputInvoice, InputPaymentCredentials, LabeledPrice, Message, MessageMedia, PaymentRequestedInfo, PaymentSavedCredentials, PaymentsPaymentForm, PaymentsPaymentReceipt, PaymentsValidatedRequestedInfo, PostAddress, ShippingOption } from "../../layer";
import I18n, { i18n, LangPackKey, _i18n } from "../../lib/langPack";
import { ApiError } from "../../lib/mtproto/apiManager";
import wrapEmojiText from "../../lib/richTextProcessor/wrapEmojiText";
import wrapRichText from "../../lib/richTextProcessor/wrapRichText";
import rootScope from "../../lib/rootScope";

1
src/components/popups/paymentCardConfirmation.ts

@ -7,7 +7,6 @@ @@ -7,7 +7,6 @@
import PopupElement from ".";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
import { AccountPassword, AccountTmpPassword } from "../../layer";
import { ApiError } from "../../lib/mtproto/apiManager";
import { InputState } from "../inputField";
import PasswordInputField from "../passwordInputField";
import { SettingSection } from "../sidebarLeft";

7
src/components/popups/paymentShipping.ts

@ -5,14 +5,9 @@ @@ -5,14 +5,9 @@
*/
import PopupElement from ".";
import { attachClickEvent } from "../../helpers/dom/clickEvent";
import placeCaretAtEnd from "../../helpers/dom/placeCaretAtEnd";
import toggleDisability from "../../helpers/dom/toggleDisability";
import { InputInvoice, Message, PaymentRequestedInfo, PaymentsPaymentForm, PaymentsValidatedRequestedInfo } from "../../layer";
import getServerMessageId from "../../lib/appManagers/utils/messageId/getServerMessageId";
import { ApiError } from "../../lib/mtproto/apiManager";
import { InputInvoice, PaymentRequestedInfo, PaymentsPaymentForm, PaymentsValidatedRequestedInfo } from "../../layer";
import matchEmail from "../../lib/richTextProcessor/matchEmail";
import Button from "../button";
import CheckboxField from "../checkboxField";
import CountryInputField from "../countryInputField";
import InputField from "../inputField";

75
src/components/wrappers/document.ts

@ -21,6 +21,7 @@ import { AppManagers } from "../../lib/appManagers/managers"; @@ -21,6 +21,7 @@ import { AppManagers } from "../../lib/appManagers/managers";
import getDownloadMediaDetails from "../../lib/appManagers/utils/download/getDownloadMediaDetails";
import choosePhotoSize from "../../lib/appManagers/utils/photos/choosePhotoSize";
import { joinElementsWith } from "../../lib/langPack";
import { MAX_FILE_SAVE_SIZE } from "../../lib/mtproto/mtproto_config";
import wrapPlainText from "../../lib/richTextProcessor/wrapPlainText";
import rootScope from "../../lib/rootScope";
import type { ThumbCache } from "../../lib/storages/thumbs";
@ -94,6 +95,7 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -94,6 +95,7 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
const icoDiv = document.createElement('div');
icoDiv.classList.add('document-ico');
let icoTextEl: HTMLElement;
const hadContext = !!cacheContext;
const getCacheContext = () => {
@ -101,8 +103,10 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -101,8 +103,10 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
};
cacheContext = await getCacheContext();
let hasThumb = false;
if((doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo'))/* && doc.mime_type !== 'image/gif' */) {
docDiv.classList.add('document-with-thumb');
hasThumb = true;
let imgs: (HTMLImageElement | HTMLCanvasElement)[] = [];
// ! WARNING, use thumbs for check when thumb will be generated for media
@ -131,14 +135,20 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -131,14 +135,20 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
imgs.forEach((img) => img.classList.add('document-thumb'));
} else {
icoDiv.innerText = ext;
icoTextEl = document.createElement('span');
icoTextEl.classList.add('document-ico-text');
icoTextEl.innerText = ext;
icoDiv.append(icoTextEl);
}
//let fileName = stringMiddleOverflow(doc.file_name || 'Unknown.file', 26);
let fileName = doc.file_name ? wrapPlainText(doc.file_name) : 'Unknown.file';
const descriptionEl = document.createElement('div');
descriptionEl.classList.add('document-description');
const bytesContainer = document.createElement('span');
const bytesEl = formatBytes(doc.size);
const bytesJoiner = ' / ';
const descriptionParts: (HTMLElement | string | DocumentFragment)[] = [bytesEl];
if(withTime) {
@ -149,8 +159,16 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -149,8 +159,16 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
descriptionParts.push(await wrapSenderToPeer(message));
}
if(!withTime && !showSender) {
const b = document.createElement('span');
const bytesMaxEl = formatBytes(doc.size);
b.append(bytesJoiner, bytesMaxEl);
b.style.visibility = 'hidden';
descriptionParts.push(b);
}
docDiv.innerHTML = `
${(cacheContext.downloaded && !uploadFileName) || !message.mid ? '' : `<div class="document-download"></div>`}
${(cacheContext.downloaded && !uploadFileName) || !message.mid || !hasThumb ? '' : `<div class="document-download"></div>`}
<div class="document-name"></div>
<div class="document-size"></div>
`;
@ -169,7 +187,8 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -169,7 +187,8 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
}
const sizeDiv = docDiv.querySelector('.document-size') as HTMLElement;
sizeDiv.append(...joinElementsWith(descriptionParts, ' · '));
bytesContainer.append(...joinElementsWith(descriptionParts, ' · '));
sizeDiv.append(bytesContainer);
docDiv.prepend(icoDiv);
@ -179,12 +198,20 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -179,12 +198,20 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
let downloadDiv: HTMLElement, preloader: ProgressivePreloader = null;
const onLoad = () => {
if(doc.size <= MAX_FILE_SAVE_SIZE) {
docDiv.classList.add('downloaded');
}
docDiv.classList.remove('downloading');
if(downloadDiv) {
downloadDiv.classList.add('downloaded');
const _downloadDiv = downloadDiv;
setTimeout(() => {
_downloadDiv.remove();
}, 200);
if(downloadDiv !== icoDiv) {
const _downloadDiv = downloadDiv;
setTimeout(() => {
_downloadDiv.remove();
}, 200);
}
downloadDiv = null;
}
@ -194,17 +221,26 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -194,17 +221,26 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
};
const addByteProgress = (promise: CancellablePromise<any>) => {
docDiv.classList.add('downloading');
const sizeContainer = document.createElement('span');
promise.then(() => {
onLoad();
sizeContainer.replaceWith(bytesEl);
}, () => {
replaceContent(sizeContainer, bytesEl);
const _bytesContainer = formatBytes(doc.size);
sizeContainer.style.position = 'absolute';
sizeContainer.style.left = '0';
promise.then(onLoad, noop).finally(() => {
// sizeContainer.replaceWith(bytesContainer);
bytesContainer.style.visibility = '';
sizeContainer.remove();
// b && b.classList.remove('hide');
});
// b && b.classList.add('hide');
let d = formatBytes(0);
bytesEl.replaceWith(sizeContainer);
sizeContainer.append(d, ' / ', bytesEl);
bytesContainer.style.visibility = 'hidden';
// bytesContainer.replaceWith(sizeContainer);
sizeContainer.append(d, bytesJoiner, _bytesContainer);
bytesContainer.parentElement.append(sizeContainer);
promise.addNotifyListener((progress: Progress) => {
const _d = formatBytes(progress.done);
d.replaceWith(_d);
@ -236,6 +272,10 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -236,6 +272,10 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
download = appDownloadManager.downloadToDisc({media: doc, queueId});
}
download.catch(() => {
docDiv.classList.remove('downloading');
});
if(downloadDiv) {
preloader.attach(downloadDiv, true, download);
addByteProgress(download);
@ -244,14 +284,15 @@ export default async function wrapDocument({message, withTime, fontWeight, voice @@ -244,14 +284,15 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
const {fileName: downloadFileName} = getDownloadMediaDetails({media: doc});
if(await managers.apiFileManager.isDownloading(downloadFileName)) {
downloadDiv = docDiv.querySelector('.document-download');
downloadDiv = docDiv.querySelector('.document-download') || icoDiv;
const promise = appDownloadManager.downloadMediaVoid({media: doc});
preloader = new ProgressivePreloader();
preloader.attach(downloadDiv, false, promise);
preloader.setDownloadFunction(load);
addByteProgress(promise);
} else if(!cacheContext.downloaded || uploadFileName) {
downloadDiv = docDiv.querySelector('.document-download');
downloadDiv = docDiv.querySelector('.document-download') || icoDiv;
preloader = new ProgressivePreloader({
isUpload: !!uploadFileName
});

2
src/config/databases/index.ts

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import { IDBStore } from "../../lib/idb";
import { IDBStore } from "../../lib/files/idb";
export type DatabaseStore<StoreName extends string> = Omit<IDBStore, 'name'> & {name: StoreName};
export type Database<StoreName extends string> = {

2
src/config/databases/state.ts

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
*/
import type { Database } from '.';
import type { IDBIndex } from '../../lib/idb';
import type { IDBIndex } from '../../lib/files/idb';
const DATABASE_STATE: Database<'session' | 'stickerSets' | 'users' | 'chats' | 'messages' | 'dialogs'> = {
name: 'tweb',

29
src/global.d.ts vendored

@ -23,13 +23,36 @@ declare global { @@ -23,13 +23,36 @@ declare global {
type Long = string | number;
type MTLong = string;
type LocalErrorType = 'DOWNLOAD_CANCELED';
type ServerErrorType = 'FILE_REFERENCE_EXPIRED';
type ApiFileManagerError = 'DOWNLOAD_CANCELED' | 'UPLOAD_CANCELED' | 'FILE_TOO_BIG' | 'REFERENCE_IS_NOT_REFRESHED';
type StorageError = 'STORAGE_OFFLINE' | 'NO_ENTRY_FOUND' | 'IDB_CREATE_TIMEOUT';
type ReferenceError = 'NO_NEW_CONTEXT';
type NetworkerError = 'NETWORK_BAD_RESPONSE';
type FiltersError = 'PINNED_DIALOGS_TOO_MUCH';
type LocalFileError = ApiFileManagerError | ReferenceError | StorageError;
type LocalErrorType = LocalFileError | NetworkerError | FiltersError | 'UNKNOWN';
type ServerErrorType = 'FILE_REFERENCE_EXPIRED' | 'SESSION_REVOKED' | 'AUTH_KEY_DUPLICATED' |
'SESSION_PASSWORD_NEEDED' | 'CONNECTION_NOT_INITED' | 'ERROR_EMPTY' | 'MTPROTO_CLUSTER_INVALID' |
'BOT_PRECHECKOUT_TIMEOUT' | 'TMP_PASSWORD_INVALID' | 'PASSWORD_HASH_INVALID' | 'CHANNEL_PRIVATE';
type ErrorType = LocalErrorType | ServerErrorType;
interface Error {
type?: LocalErrorType | ServerErrorType;
type?: ErrorType;
}
type ApiError = Partial<{
code: number,
type: ErrorType,
description: string,
originalError: any,
stack: string,
handled: boolean,
input: string,
message: ApiError
}>;
declare const electronHelpers: {
openExternal(url): void;
} | undefined;

12
src/helpers/blob/blobConstruct.ts

@ -16,17 +16,7 @@ export default function blobConstruct<T extends Uint8Array | string>(blobParts: @@ -16,17 +16,7 @@ export default function blobConstruct<T extends Uint8Array | string>(blobParts:
blobParts = [blobParts];
}
let blob;
const safeMimeType = blobSafeMimeType(mimeType);
try {
blob = new Blob(blobParts, {type: safeMimeType});
} catch(e) {
// @ts-ignore
let bb = new BlobBuilder;
blobParts.forEach((blobPart: any) => {
bb.append(blobPart);
});
blob = bb.getBlob(safeMimeType);
}
const blob = new Blob(blobParts, {type: safeMimeType});
return blob;
}

5
src/helpers/fileName.ts

@ -10,7 +10,8 @@ import type { DownloadOptions } from "../lib/mtproto/apiFileManager"; @@ -10,7 +10,8 @@ import type { DownloadOptions } from "../lib/mtproto/apiFileManager";
const FILENAME_JOINER = '_';
export function getFileNameByLocation(location: InputFileLocation | InputWebFileLocation, options?: Partial<{
fileName: string
fileName: string,
downloadId: string
}>) {
const fileName = '';//(options?.fileName || '').split('.');
const ext = fileName[fileName.length - 1] || '';
@ -57,7 +58,7 @@ export function getFileNameByLocation(location: InputFileLocation | InputWebFile @@ -57,7 +58,7 @@ export function getFileNameByLocation(location: InputFileLocation | InputWebFile
}
}
return str + (ext ? '.' + ext : ext);
return str + (options.downloadId || '') + (ext ? '.' + ext : ext);
}
export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download';

7
src/helpers/makeError.ts

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
export default function makeError(type: Error['type']) {
const error: ApiError = {
type
};
return error;
}

4
src/helpers/string/fileNameRFC.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export default function fileNameRFC(fileName: string) {
// Make filename RFC5987 compatible
return encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A');
}

2
src/helpers/toggleStorages.ts

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import CacheStorageController from "../lib/cacheStorage";
import CacheStorageController from "../lib/files/cacheStorage";
import AppStorage from "../lib/storage";
import sessionStorage from "../lib/sessionStorage";
import noop from "./noop";

114
src/lib/appManagers/appDownloadManager.ts

@ -4,21 +4,20 @@ @@ -4,21 +4,20 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type { ApiFileManager, DownloadMediaOptions, DownloadOptions } from "../mtproto/apiFileManager";
import type { DownloadMediaOptions, DownloadOptions } from "../mtproto/apiFileManager";
import type { AppMessagesManager } from "./appMessagesManager";
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
import { Document, InputFile, Photo, PhotoSize, WebDocument } from "../../layer";
import { getFileNameByLocation } from "../../helpers/fileName";
import { InputFile, Photo, PhotoSize } from "../../layer";
import getFileNameForUpload from "../../helpers/getFileNameForUpload";
import { AppManagers } from "./managers";
import rootScope from "../rootScope";
import { MOUNT_CLASS_TO } from "../../config/debug";
import getDocumentDownloadOptions from "./utils/docs/getDocumentDownloadOptions";
import getPhotoDownloadOptions from "./utils/photos/getPhotoDownloadOptions";
import createDownloadAnchor from "../../helpers/dom/createDownloadAnchor";
import noop from "../../helpers/noop";
import getDownloadMediaDetails from "./utils/download/getDownloadMediaDetails";
import getDownloadFileNameFromOptions from "./utils/download/getDownloadFileNameFromOptions";
import { AppMessagesManager } from "./appMessagesManager";
import indexOfAndSplice from "../../helpers/array/indexOfAndSplice";
import { MAX_FILE_SAVE_SIZE } from "../mtproto/mtproto_config";
import createDownloadAnchor from "../../helpers/dom/createDownloadAnchor";
export type ResponseMethodBlob = 'blob';
export type ResponseMethodJson = 'json';
@ -35,10 +34,10 @@ export type Download = DownloadBlob | DownloadUrl/* | DownloadJson */; @@ -35,10 +34,10 @@ export type Download = DownloadBlob | DownloadUrl/* | DownloadJson */;
export type Progress = {done: number, fileName: string, total: number, offset: number};
export type ProgressCallback = (details: Progress) => void;
type DownloadType = 'url' | 'blob' | 'void';
type DownloadType = 'url' | 'blob' | 'void' | 'disc';
export class AppDownloadManager {
private downloads: {[fileName: string]: {main: Download, url?: Download, blob?: Download, void?: Download}} = {};
private downloads: {[fileName: string]: {main: Download} & {[type in DownloadType]?: Download}} = {};
private progress: {[fileName: string]: Progress} = {};
// private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {};
private managers: AppManagers;
@ -84,7 +83,7 @@ export class AppDownloadManager { @@ -84,7 +83,7 @@ export class AppDownloadManager {
};
deferred.catch(() => {
this.clearDownload(fileName);
this.clearDownload(fileName, type);
}).finally(() => {
delete this.progress[fileName];
// delete this.progressCallbacks[fileName];
@ -101,6 +100,13 @@ export class AppDownloadManager { @@ -101,6 +100,13 @@ export class AppDownloadManager {
});
}
const haveToClear = type === 'disc';
if(haveToClear) {
deferred.catch(noop).finally(() => {
this.clearDownload(fileName, type);
});
}
return download[type] = deferred as any;
}
@ -115,8 +121,18 @@ export class AppDownloadManager { @@ -115,8 +121,18 @@ export class AppDownloadManager {
return deferred as CancellablePromise<Awaited<T>>;
}
private clearDownload(fileName: string) {
delete this.downloads[fileName];
private clearDownload(fileName: string, type?: DownloadType) {
const downloads = this.downloads[fileName];
if(!downloads) {
return;
}
delete downloads[type];
const length = Object.keys(downloads).length;
if(!length || (downloads.main && length === 1)) {
delete this.downloads[fileName];
}
}
public getUpload(fileName: string): ReturnType<AppMessagesManager['sendFile']>['promise'] {
@ -161,7 +177,15 @@ export class AppDownloadManager { @@ -161,7 +177,15 @@ export class AppDownloadManager {
const {downloadOptions, fileName} = getDownloadMediaDetails(options);
return this.d(fileName, () => {
const cb = type === 'url' ? this.managers.apiFileManager.downloadMediaURL : (type === 'void' ? this.managers.apiFileManager.downloadMediaVoid : this.managers.apiFileManager.downloadMedia);
let cb: any;
if(type === 'url') {
cb = this.managers.apiFileManager.downloadMediaURL;
} else if(type === 'void' || type === 'disc') {
cb = this.managers.apiFileManager.downloadMediaVoid;
} else if(type === 'blob') {
cb = this.managers.apiFileManager.downloadMedia;
}
return cb(options);
}, type) as any;
}
@ -207,20 +231,58 @@ export class AppDownloadManager { @@ -207,20 +231,58 @@ export class AppDownloadManager {
if(!isDocument && !options.thumb) {
options.thumb = (media as Photo.photo).sizes.slice().pop() as PhotoSize.photoSize;
}
const promise = this.downloadMedia(options);
promise.then((blob) => {
const url = URL.createObjectURL(blob);
const downloadOptions = isDocument ?
getDocumentDownloadOptions(media) :
getPhotoDownloadOptions(media as any, options.thumb);
const fileName = (options.media as Document.document).file_name || getFileNameByLocation(downloadOptions.location);
createDownloadAnchor(url, fileName, () => {
URL.revokeObjectURL(url);
});
}, noop);
return promise;
const {downloadOptions, fileName} = getDownloadMediaDetails(options);
if(downloadOptions.size && downloadOptions.size > MAX_FILE_SAVE_SIZE) {
const id = '' + (Math.random() * 0x7FFFFFFF | 0);
const url = `/download/${id}`;
options.downloadId = id;
const promise = this.downloadMedia(options, 'disc');
let iframe: HTMLIFrameElement;
const onProgress = () => {
iframe = document.createElement('iframe');
iframe.hidden = true;
// iframe.src = sw.scope + fileName;
iframe.src = url;
document.body.append(iframe);
indexOfAndSplice(promise.listeners, onProgress);
};
promise.addNotifyListener(onProgress);
promise.catch(noop).finally(() => {
setTimeout(() => {
iframe?.remove();
}, 1000);
});
return promise;
} else {
const promise = this.downloadMedia(options, 'blob');
promise.then((blob) => {
const url = URL.createObjectURL(blob);
createDownloadAnchor(url, downloadOptions.fileName || fileName, () => {
URL.revokeObjectURL(url);
});
});
return promise;
}
// const promise = this.downloadMedia(options);
// promise.then((blob) => {
// const url = URL.createObjectURL(blob);
// const downloadOptions = isDocument ?
// getDocumentDownloadOptions(media) :
// getPhotoDownloadOptions(media as any, options.thumb);
// const fileName = (options.media as Document.document).file_name || getFileNameByLocation(downloadOptions.location);
// createDownloadAnchor(url, fileName, () => {
// URL.revokeObjectURL(url);
// });
// }, noop);
// return promise;
}
}

2
src/lib/appManagers/appImManager.ts

@ -76,7 +76,7 @@ import generateMessageId from './utils/messageId/generateMessageId'; @@ -76,7 +76,7 @@ import generateMessageId from './utils/messageId/generateMessageId';
import getUserStatusString from '../../components/wrappers/getUserStatusString';
import getChatMembersString from '../../components/wrappers/getChatMembersString';
import { STATE_INIT } from '../../config/state';
import CacheStorageController from '../cacheStorage';
import CacheStorageController from '../files/cacheStorage';
import themeController from '../../helpers/themeController';
import overlayCounter from '../../helpers/overlayCounter';
import appDialogsManager from './appDialogsManager';

3
src/lib/appManagers/appMessagesManager.ts

@ -59,6 +59,7 @@ import appTabsManager from "./appTabsManager"; @@ -59,6 +59,7 @@ import appTabsManager from "./appTabsManager";
import MTProtoMessagePort from "../mtproto/mtprotoMessagePort";
import getAlbumText from "./utils/messages/getAlbumText";
import pause from "../../helpers/schedulers/pause";
import makeError from "../../helpers/makeError";
//console.trace('include');
// TODO: если удалить диалог находясь в папке, то он не удалится из папки и будет виден в настройках
@ -3077,7 +3078,7 @@ export class AppMessagesManager extends AppManager { @@ -3077,7 +3078,7 @@ export class AppMessagesManager extends AppManager {
(this.rootScope.premium ? appConfig.dialogs_folder_pinned_limit_premium : appConfig.dialogs_folder_pinned_limit_default) :
(this.rootScope.premium ? appConfig.dialogs_pinned_limit_premium : appConfig.dialogs_pinned_limit_default);
if(this.dialogsStorage.getPinnedOrders(filterId).length >= max) {
return Promise.reject({type: 'PINNED_DIALOGS_TOO_MUCH'});
return Promise.reject(makeError('PINNED_DIALOGS_TOO_MUCH'));
}
}

2
src/lib/appManagers/utils/download/getDownloadMediaDetails.ts

@ -19,6 +19,8 @@ export default function getDownloadMediaDetails(options: DownloadMediaOptions) { @@ -19,6 +19,8 @@ export default function getDownloadMediaDetails(options: DownloadMediaOptions) {
else if(media._ === 'photo') downloadOptions = getPhotoDownloadOptions(media, thumb, queueId, onlyCache);
else if(isWebDocument(media)) downloadOptions = getWebDocumentDownloadOptions(media);
downloadOptions.downloadId = options.downloadId;
const fileName = getDownloadFileNameFromOptions(downloadOptions);
return {fileName, downloadOptions};
}

72
src/lib/fileManager.ts

@ -1,72 +0,0 @@ @@ -1,72 +0,0 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from:
* https://github.com/zhukov/webogram
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import blobConstruct from "../helpers/blob/blobConstruct";
export class FileManager {
private blobSupported = true;
constructor() {
try {
blobConstruct([], '');
} catch(e) {
this.blobSupported = false;
}
}
public isAvailable() {
return this.blobSupported;
}
public getFakeFileWriter(mimeType: string, size: number, saveFileCallback?: (blob: Blob) => Promise<Blob>) {
let bytes: Uint8Array = new Uint8Array(size);
const fakeFileWriter = {
write: async(part: Uint8Array, offset: number) => {
if(!this.blobSupported) {
throw false;
}
// sometimes file size can be bigger than the prov
const endOffset = offset + part.byteLength;
if(endOffset > bytes.byteLength) {
const newBytes = new Uint8Array(endOffset);
newBytes.set(bytes, 0);
bytes = newBytes;
}
bytes.set(part, offset);
},
truncate: () => {
bytes = new Uint8Array();
},
trim: (size: number) => {
bytes = bytes.slice(0, size);
},
finalize: (saveToStorage = true) => {
const blob = blobConstruct(bytes, mimeType);
if(saveToStorage && saveFileCallback) {
saveFileCallback(blob);
}
return blob;
},
getParts: () => bytes,
replaceParts: (parts: typeof bytes) => {
bytes = parts;
}
};
return fakeFileWriter;
}
}
export default new FileManager();

23
src/lib/cacheStorage.ts → src/lib/files/cacheStorage.ts

@ -4,15 +4,16 @@ @@ -4,15 +4,16 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import Modes from '../config/modes';
import blobConstruct from '../helpers/blob/blobConstruct';
import FileManager from './fileManager';
//import { MOUNT_CLASS_TO } from './mtproto/mtproto_config';
//import { logger } from './polyfill';
import Modes from '../../config/modes';
import blobConstruct from '../../helpers/blob/blobConstruct';
import MemoryWriter from './memoryWriter';
import FileManager from './memoryWriter';
import FileStorage from './fileStorage';
import makeError from '../../helpers/makeError';
export type CacheStorageDbName = 'cachedFiles' | 'cachedStreamChunks' | 'cachedAssets';
export default class CacheStorageController {
export default class CacheStorageController implements FileStorage {
private static STORAGES: CacheStorageController[] = [];
private openDbPromise: Promise<Cache>;
@ -64,7 +65,7 @@ export default class CacheStorageController { @@ -64,7 +65,7 @@ export default class CacheStorageController {
return this.get(fileName).then((response) => {
if(!response) {
//console.warn('getFile:', response, fileName);
throw 'NO_ENTRY_FOUND';
throw makeError('NO_ENTRY_FOUND');
}
const promise = response[method]();
@ -92,7 +93,7 @@ export default class CacheStorageController { @@ -92,7 +93,7 @@ export default class CacheStorageController {
public timeoutOperation<T>(callback: (cache: Cache) => Promise<T>) {
if(!this.useStorage) {
return Promise.reject('STORAGE_OFFLINE');
return Promise.reject(makeError('STORAGE_OFFLINE'));
}
return new Promise<T>(async(resolve, reject) => {
@ -123,12 +124,12 @@ export default class CacheStorageController { @@ -123,12 +124,12 @@ export default class CacheStorageController {
});
}
public getFileWriter(fileName: string, fileSize: number, mimeType: string) {
const fakeWriter = FileManager.getFakeFileWriter(mimeType, fileSize, (blob) => {
public getWriter(fileName: string, fileSize: number, mimeType: string) {
const writer = new MemoryWriter(mimeType, fileSize, (blob) => {
return this.saveFile(fileName, blob).catch(() => blob);
});
return Promise.resolve(fakeWriter);
return Promise.resolve(writer);
}
public static toggleStorage(enabled: boolean, clearWrite: boolean) {

13
src/lib/files/fileStorage.ts

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import StreamWriter from "./streamWriter";
export default abstract class FileStorage {
public abstract getFile(fileName: string): Promise<any>;
public abstract getWriter(fileName: string, fileSize: number, mimeType: string): Promise<StreamWriter>;
}

11
src/lib/idb.ts → src/lib/files/idb.ts

@ -9,10 +9,11 @@ @@ -9,10 +9,11 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import { Database } from '../config/databases';
import Modes from '../config/modes';
import safeAssign from '../helpers/object/safeAssign';
import { logger } from './logger';
import { Database } from '../../config/databases';
import Modes from '../../config/modes';
import makeError from '../../helpers/makeError';
import safeAssign from '../../helpers/object/safeAssign';
import { logger } from '../logger';
/**
* https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/createIndex
@ -111,7 +112,7 @@ export class IDB { @@ -111,7 +112,7 @@ export class IDB {
let finished = false;
setTimeout(() => {
if(!finished) {
request.onerror({type: 'IDB_CREATE_TIMEOUT'} as Event);
request.onerror(makeError('IDB_CREATE_TIMEOUT') as Event);
}
}, 3000);

58
src/lib/files/memoryWriter.ts

@ -0,0 +1,58 @@ @@ -0,0 +1,58 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import blobConstruct from "../../helpers/blob/blobConstruct";
import StreamWriter from './streamWriter';
export default class MemoryWriter implements StreamWriter {
private bytes: Uint8Array;
constructor(
private mimeType: string,
private size: number,
private saveFileCallback?: (blob: Blob) => Promise<Blob>
) {
this.bytes = new Uint8Array(size);
}
public async write(part: Uint8Array, offset: number) {
// sometimes file size can be bigger than the prov
const endOffset = offset + part.byteLength;
if(endOffset > this.bytes.byteLength) {
const newBytes = new Uint8Array(endOffset);
newBytes.set(this.bytes, 0);
this.bytes = newBytes;
}
this.bytes.set(part, offset);
};
public truncate() {
this.bytes = new Uint8Array();
}
public trim(size: number) {
this.bytes = this.bytes.slice(0, size);
}
public finalize(saveToStorage = true) {
const blob = blobConstruct(this.bytes, this.mimeType);
if(saveToStorage && this.saveFileCallback) {
this.saveFileCallback(blob);
}
return blob;
}
public getParts() {
return this.bytes;
}
public replaceParts(parts: Uint8Array) {
this.bytes = parts;
}
}

14
src/lib/files/streamWriter.ts

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
export default abstract class StreamWriter {
public abstract write(part: Uint8Array, offset?: number): Promise<any>;
public abstract truncate?(): void;
public abstract trim?(size: number): void;
public abstract finalize(saveToStorage?: boolean): Promise<Blob> | Blob;
public abstract getParts?(): Uint8Array;
public abstract replaceParts?(parts: Uint8Array): void;
}

399
src/lib/mtproto/apiFileManager.ts

@ -12,12 +12,10 @@ @@ -12,12 +12,10 @@
import type { ReferenceBytes } from "./referenceDatabase";
import Modes from "../../config/modes";
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
import { getFileNameByLocation } from "../../helpers/fileName";
import { randomLong } from "../../helpers/random";
import { Document, InputFile, InputFileLocation, InputWebFileLocation, Photo, PhotoSize, UploadFile, UploadWebFile, WebDocument } from "../../layer";
import { DcId } from "../../types";
import CacheStorageController from "../cacheStorage";
import fileManager from "../fileManager";
import CacheStorageController from "../files/cacheStorage";
import { logger, LogTypes } from "../logger";
import assumeType from "../../helpers/assumeType";
import ctx from "../../environment/ctx";
@ -33,12 +31,19 @@ import getFileNameForUpload from "../../helpers/getFileNameForUpload"; @@ -33,12 +31,19 @@ import getFileNameForUpload from "../../helpers/getFileNameForUpload";
import type { Progress } from "../appManagers/appDownloadManager";
import getDownloadMediaDetails from "../appManagers/utils/download/getDownloadMediaDetails";
import networkStats from "./networkStats";
import pause from "../../helpers/schedulers/pause";
import getDownloadFileNameFromOptions from "../appManagers/utils/download/getDownloadFileNameFromOptions";
import { getServiceMessagePort } from "./mtproto.worker";
import StreamWriter from "../files/streamWriter";
import FileStorage from "../files/fileStorage";
import fileNameRFC from "../../helpers/string/fileNameRFC";
import { MAX_FILE_SAVE_SIZE } from "./mtproto_config";
import throttle from "../../helpers/schedulers/throttle";
import makeError from "../../helpers/makeError";
type Delayed = {
offset: number,
writeFilePromise: CancellablePromise<void>,
writeFileDeferred: CancellablePromise<void>
writePromise: CancellablePromise<void>,
writeDeferred: CancellablePromise<void>
};
export type DownloadOptions = {
@ -50,6 +55,7 @@ export type DownloadOptions = { @@ -50,6 +55,7 @@ export type DownloadOptions = {
limitPart?: number,
queueId?: number,
onlyCache?: boolean,
downloadId?: string
// getFileMethod: Parameters<CacheStorageController['getFile']>[1]
};
@ -57,7 +63,8 @@ export type DownloadMediaOptions = { @@ -57,7 +63,8 @@ export type DownloadMediaOptions = {
media: Photo.photo | Document.document | WebDocument,
thumb?: PhotoSize,
queueId?: number,
onlyCache?: boolean
onlyCache?: boolean,
downloadId?: string
};
type DownloadPromise = CancellablePromise<Blob>;
@ -75,11 +82,18 @@ export type MyUploadFile = UploadFile.uploadFile | UploadWebFile.uploadWebFile; @@ -75,11 +82,18 @@ export type MyUploadFile = UploadFile.uploadFile | UploadWebFile.uploadWebFile;
// originalPayload: ReferenceBytes
// };
const MAX_FILE_SAVE_SIZE = 20 * 1024 * 1024;
const MAX_FILE_PART_SIZE = 1 * 1024 * 1024;
const REGULAR_DOWNLOAD_DELTA = 36;
const PREMIUM_DOWNLOAD_DELTA = 72;
const IGNORE_ERRORS: Set<ErrorType> = new Set([
'DOWNLOAD_CANCELED',
'UPLOAD_CANCELED',
'UNKNOWN',
'NO_NEW_CONTEXT'
]);
export class ApiFileManager extends AppManager {
private cacheStorage = new CacheStorageController('cachedFiles');
@ -108,7 +122,7 @@ export class ApiFileManager extends AppManager { @@ -108,7 +122,7 @@ export class ApiFileManager extends AppManager {
public refreshReferencePromises: {
[referenceHex: string]: {
deferred: CancellablePromise<ReferenceBytes>,
timeout: number
timeout?: number
}
} = {};
@ -117,6 +131,9 @@ export class ApiFileManager extends AppManager { @@ -117,6 +131,9 @@ export class ApiFileManager extends AppManager {
private queueId = 0;
private debug = Modes.debug;
private maxUploadParts = 4000;
private maxDownloadParts = 8000;
protected after() {
setInterval(() => { // clear old promises
for(const hex in this.refreshReferencePromises) {
@ -126,6 +143,11 @@ export class ApiFileManager extends AppManager { @@ -126,6 +143,11 @@ export class ApiFileManager extends AppManager {
}
}
}, 1800e3);
this.rootScope.addEventListener('app_config', (appConfig) => {
this.maxUploadParts = this.rootScope.premium ? appConfig.upload_max_fileparts_premium : appConfig.upload_max_fileparts_default;
this.maxDownloadParts = appConfig.upload_max_fileparts_premium;
});
}
private downloadRequest(dcId: 'upload', id: number, cb: () => Promise<void>, activeDelta: number, queueId?: number): Promise<void>;
@ -154,12 +176,12 @@ export class ApiFileManager extends AppManager { @@ -154,12 +176,12 @@ export class ApiFileManager extends AppManager {
const downloadLimit = dcId === 'upload' ? 24 : (this.rootScope.premium ? PREMIUM_DOWNLOAD_DELTA : REGULAR_DOWNLOAD_DELTA);
//const downloadLimit = Infinity;
if(this.downloadActives[dcId] >= downloadLimit || !downloadPull || !downloadPull.length) {
if(this.downloadActives[dcId] >= downloadLimit || !downloadPull?.length) {
return false;
}
//const data = downloadPull.shift();
const data = findAndSplice(downloadPull, d => d.queueId === 0) || findAndSplice(downloadPull, d => d.queueId === this.queueId) || downloadPull.shift();
const data = findAndSplice(downloadPull, (d) => d.queueId === 0) || findAndSplice(downloadPull, (d) => d.queueId === this.queueId) || downloadPull.shift();
const activeDelta = data.activeDelta || 1;
this.downloadActives[dcId] += activeDelta;
@ -174,9 +196,8 @@ export class ApiFileManager extends AppManager { @@ -174,9 +196,8 @@ export class ApiFileManager extends AppManager {
this.downloadCheck(dcId);
networkPromise.resolve();
}, (error: Error) => {
// @ts-ignore
if(!error || !error.type || (error.type !== 'DOWNLOAD_CANCELED' && error.type !== 'UPLOAD_CANCELED')) {
}, (error: ApiError) => {
if(!error?.type || !IGNORE_ERRORS.has(error.type)) {
this.log.error('downloadCheck error:', error);
}
@ -234,6 +255,9 @@ export class ApiFileManager extends AppManager { @@ -234,6 +255,9 @@ export class ApiFileManager extends AppManager {
const invoke = async(): Promise<MyUploadFile> => {
checkCancel && checkCancel(); // do not remove async, because checkCancel will throw an error
// * IMPORTANT: reference can be changed in previous request
const reference = (location as InputFileLocation.inputDocumentFileLocation).file_reference?.slice();
const promise = /* pause(1000).then(() => */this.apiManager.invokeApi('upload.getFile', {
location,
offset,
@ -243,9 +267,9 @@ export class ApiFileManager extends AppManager { @@ -243,9 +267,9 @@ export class ApiFileManager extends AppManager {
fileDownload: true
}) as Promise<MyUploadFile>/* ) */;
return promise.catch((err) => {
return promise.catch((err: ApiError) => {
if(err.type === 'FILE_REFERENCE_EXPIRED') {
return this.refreshReference(location).then(invoke);
return this.refreshReference(location as InputFileLocation.inputDocumentFileLocation, reference).then(invoke);
}
throw err;
@ -258,7 +282,7 @@ export class ApiFileManager extends AppManager { @@ -258,7 +282,7 @@ export class ApiFileManager extends AppManager {
location.checkedReference = true;
const hex = bytesToHex(reference);
if(this.refreshReferencePromises[hex]) {
return this.refreshReference(location).then(invoke);
return this.refreshReference(location, reference).then(invoke);
}
}
@ -274,14 +298,16 @@ export class ApiFileManager extends AppManager { @@ -274,14 +298,16 @@ export class ApiFileManager extends AppManager {
return bytes / 1024 / 128;
}
private getLimitPart(size: number): number {
private getLimitPart(size: number, isUpload: boolean): number {
if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments)
return 512 * 1024;
}
let bytes = 128 * 1024;
while((size / bytes) > 2000) {
const maxParts = isUpload ? this.maxUploadParts : this.maxDownloadParts;
// usually it will stick to 512Kb size if the file is too big
while((size / bytes) > maxParts && bytes < MAX_FILE_PART_SIZE) {
bytes *= 2;
}
/* if(size < 1e6 || !size) bytes = 512;
@ -323,38 +349,41 @@ export class ApiFileManager extends AppManager { @@ -323,38 +349,41 @@ export class ApiFileManager extends AppManager {
return instance.invoke('convertOpus', {fileName, bytes});
};
private refreshReference(inputFileLocation: InputFileLocation) {
const reference = (inputFileLocation as InputFileLocation.inputDocumentFileLocation).file_reference;
const hex = bytesToHex(reference);
private refreshReference(
inputFileLocation: InputFileLocation.inputDocumentFileLocation,
reference: typeof inputFileLocation['file_reference'],
hex = bytesToHex(reference)
) {
let r = this.refreshReferencePromises[hex];
if(!r) {
const deferred = deferredPromise<ReferenceBytes>();
r = this.refreshReferencePromises[hex] = {
deferred,
timeout: ctx.setTimeout(() => {
this.log.error('Didn\'t refresh the reference:', inputFileLocation);
deferred.reject('REFERENCE_IS_NOT_REFRESHED');
}, 60000)
// ! I don't remember what it was for...
// timeout: ctx.setTimeout(() => {
// this.log.error('Didn\'t refresh the reference:', inputFileLocation);
// deferred.reject(makeError('REFERENCE_IS_NOT_REFRESHED'));
// }, 60000)
};
deferred.catch(noop).finally(() => {
clearTimeout(r.timeout);
});
// deferred.catch(noop).finally(() => {
// clearTimeout(r.timeout);
// });
this.referenceDatabase.refreshReference(reference).then((reference) => {
if(hex === bytesToHex(reference)) {
deferred.reject(makeError('REFERENCE_IS_NOT_REFRESHED'));
}
this.referenceDatabase.refreshReference(reference).then(deferred.resolve, deferred.reject);
// const task = {type: 'refreshReference', payload: reference};
// notifySomeone(task);
deferred.resolve(reference);
}, deferred.reject);
}
// have to replace file_reference in any way, because location can be different everytime if it's stream
return r.deferred.then((reference) => {
if(hex === bytesToHex(reference)) {
throw 'REFERENCE_IS_NOT_REFRESHED';
}
(inputFileLocation as InputFileLocation.inputDocumentFileLocation).file_reference = reference;
inputFileLocation.file_reference = reference;
});
}
@ -371,16 +400,14 @@ export class ApiFileManager extends AppManager { @@ -371,16 +400,14 @@ export class ApiFileManager extends AppManager {
}
public download(options: DownloadOptions): DownloadPromise {
if(!fileManager.isAvailable()) {
return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'});
}
const size = options.size ?? 0;
const {dcId, location} = options;
const {dcId, location, downloadId} = options;
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp'];
if(options.mimeType === 'application/x-tgwallpattern') {
if(downloadId) {
} else if(options.mimeType === 'application/x-tgwallpattern') {
process = this.uncompressTGV;
options.mimeType = 'image/svg+xml';
} else if(options.mimeType === 'image/webp' && !getEnvironment().IS_WEBP_SUPPORTED) {
@ -394,9 +421,9 @@ export class ApiFileManager extends AppManager { @@ -394,9 +421,9 @@ export class ApiFileManager extends AppManager {
options.mimeType = 'audio/wav';
}
const fileName = getFileNameByLocation(location, {fileName: options.fileName});
const cachedPromise = this.downloadPromises[fileName];
const fileStorage = this.getFileStorage();
const fileName = getDownloadFileNameFromOptions(options);
const cachedPromise = options.downloadId ? undefined : this.downloadPromises[fileName];
let fileStorage: FileStorage = this.getFileStorage();
this.debug && this.log('downloadFile', fileName, size, location, options.mimeType);
@ -429,28 +456,77 @@ export class ApiFileManager extends AppManager { @@ -429,28 +456,77 @@ export class ApiFileManager extends AppManager {
const deferred: DownloadPromise = deferredPromise();
const mimeType = options.mimeType || 'image/jpeg';
let error: Error;
let error: ApiError;
let resolved = false;
let cacheFileWriter: ReturnType<typeof fileManager['getFakeFileWriter']>;
let errorHandler = (_error: Error) => {
let cacheWriter: StreamWriter;
let errorHandler = (_error: typeof error) => {
error = _error;
delete this.downloadPromises[fileName];
deferred.reject(error);
errorHandler = () => {};
if(cacheFileWriter && (!error || error.type !== 'DOWNLOAD_CANCELED')) {
cacheFileWriter.truncate();
if(cacheWriter && (!error || error.type !== 'DOWNLOAD_CANCELED')) {
cacheWriter.truncate?.();
}
};
const id = this.tempId++;
if(downloadId) {
const headers = {
'Content-Type': 'application/octet-stream; charset=utf-8',
'Content-Disposition': "attachment; filename*=UTF-8''" + fileNameRFC(options.fileName),
// 'Content-Disposition': `attachment; filename="${options.fileName}"`,
// 'Content-Type': 'application/octet-stream; charset=utf-8',
...(size ? {'Content-Length': size} : {})
};
const serviceMessagePort = getServiceMessagePort();
const promise = serviceMessagePort.invoke('download', {
fileName,
headers,
id: downloadId
});
promise.catch(errorHandler);
deferred.catch(() => {
getServiceMessagePort().invoke('downloadCancel', downloadId);
});
class f implements StreamWriter {
constructor() {
}
public async write(part: Uint8Array, offset?: number) {
return serviceMessagePort.invoke('downloadChunk', {
id: downloadId,
chunk: part
});
}
public finalize(saveToStorage?: boolean): Promise<Blob> {
return serviceMessagePort.invoke('downloadFinalize', downloadId).then(() => null);
}
}
class d implements FileStorage {
public getFile(fileName: string): Promise<any> {
return Promise.reject();
}
public getWriter(fileName: string, fileSize: number, mimeType: string): Promise<StreamWriter> {
return Promise.resolve(new f());
}
}
fileStorage = new d();
}
fileStorage.getFile(fileName).then(async(blob: Blob) => {
//this.log('maybe cached', fileName);
//throw '';
if(blob.size < size) {
//this.log('downloadFile need to deleteFile 2, wrong size:', blob.size, size);
if(!options.onlyCache) {
await this.delete(fileName);
}
@ -459,118 +535,121 @@ export class ApiFileManager extends AppManager { @@ -459,118 +535,121 @@ export class ApiFileManager extends AppManager {
}
deferred.resolve(blob);
}).catch((err) => {
}).catch(async(err: ApiError) => {
if(options.onlyCache) {
errorHandler(err);
return;
}
//this.log('not cached', fileName);
const limit = options.limitPart || this.getLimitPart(size);
const fileWriterPromise = fileStorage.getFileWriter(fileName, size || limit, mimeType);
fileWriterPromise.then((fileWriter) => {
cacheFileWriter = fileWriter;
let offset: number;
let startOffset = 0;
let writeFilePromise: CancellablePromise<void> = Promise.resolve(),
writeFileDeferred: CancellablePromise<void>;
//const maxRequests = 13107200 / limit; // * 100 Mb speed
const maxRequests = Infinity;
//console.error('maxRequests', maxRequests);
const processDownloaded = async(bytes: Uint8Array) => {
if(process) {
//const perf = performance.now();
const processed = await process(bytes, fileName);
//this.log('downloadFile process downloaded time', performance.now() - perf, mimeType, process);
return processed;
}
return bytes;
};
const r = location._ === 'inputWebFileLocation' ? this.requestWebFilePart.bind(this) : this.requestFilePart.bind(this);
const delayed: Delayed[] = [];
offset = startOffset;
do {
////this.log('offset:', startOffset);
writeFileDeferred = deferredPromise<void>();
delayed.push({offset, writeFilePromise, writeFileDeferred});
writeFilePromise = writeFileDeferred;
offset += limit;
} while(offset < size);
let done = 0;
const superpuper = async() => {
//if(!delayed.length) return;
const {offset, writeFilePromise, writeFileDeferred} = delayed.shift();
try {
checkCancel();
const limit = options.limitPart || this.getLimitPart(size, false);
const writerPromise = fileStorage.getWriter(fileName, size || limit, mimeType);
// @ts-ignore
const result = await r(dcId, location as any, offset, limit, id, options.queueId, checkCancel);
const writer = cacheWriter = await writerPromise;
let offset: number;
let startOffset = 0;
let writePromise: CancellablePromise<void> = Promise.resolve(),
writeDeferred: CancellablePromise<void>;
//const maxRequests = 13107200 / limit; // * 100 Mb speed
const maxRequests = Infinity;
const processDownloaded = async(bytes: Uint8Array) => {
if(process) {
//const perf = performance.now();
const processed = await process(bytes, fileName);
//this.log('downloadFile process downloaded time', performance.now() - perf, mimeType, process);
return processed;
}
const bytes = result.bytes;
return bytes;
};
if(delayed.length) {
superpuper();
}
const r = location._ === 'inputWebFileLocation' ? this.requestWebFilePart.bind(this) : this.requestFilePart.bind(this);
const delayed: Delayed[] = [];
offset = startOffset;
do {
writeDeferred = deferredPromise<void>();
delayed.push({offset, writePromise, writeDeferred});
writePromise = writeDeferred;
offset += limit;
} while(offset < size);
const progress: Progress = {done: 0, offset, total: size, fileName};
const dispatchProgress = () => {
progress.done = done;
deferred.notify?.(progress);
};
this.debug && this.log('downloadFile requestFilePart result:', fileName, result);
const isFinal = (offset + limit) >= size || !bytes.byteLength;
if(bytes.byteLength) {
//done += limit;
done += bytes.byteLength;
const throttledDispatchProgress = throttle(dispatchProgress, 50, true);
//if(!isFinal) {
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);
const progress: Progress = {done, offset, total: size, fileName};
deferred.notify(progress);
//}
let done = 0;
const superpuper = async() => {
//if(!delayed.length) return;
await writeFilePromise;
checkCancel();
const {offset, writePromise, writeDeferred} = delayed.shift();
try {
checkCancel();
await fileWriter.write(bytes, offset);
}
// @ts-ignore
const result = await r(dcId, location as any, offset, limit, id, options.queueId, checkCancel);
if(isFinal && process) {
const bytes = fileWriter.getParts();
const processedResult = await processDownloaded(bytes);
checkCancel();
const bytes = result.bytes;
fileWriter.replaceParts(processedResult);
}
if(delayed.length) {
superpuper();
}
writeFileDeferred.resolve();
const byteLength = bytes.byteLength;
this.debug && this.log('downloadFile requestFilePart result:', fileName, result);
const isFinal = (offset + limit) >= size || !byteLength;
if(byteLength) {
done += byteLength;
if(isFinal) {
resolved = true;
dispatchProgress();
} else {
throttledDispatchProgress();
}
const realSize = size || bytes.byteLength;
if(!size) {
fileWriter.trim(realSize);
}
await writePromise;
checkCancel();
deferred.resolve(fileWriter.finalize(realSize < MAX_FILE_SAVE_SIZE));
}
} catch(err) {
errorHandler(err as Error);
// const perf = performance.now();
await writer.write(bytes, offset);
checkCancel();
// downloadId && this.log('write time', performance.now() - perf);
}
};
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) {
superpuper();
}
}).catch((err) => {
if(!['STORAGE_OFFLINE'].includes(err)) {
this.log.error('saveFile error:', err);
if(isFinal && process) {
const bytes = writer.getParts();
const processedResult = await processDownloaded(bytes);
checkCancel();
writer.replaceParts(processedResult);
}
writeDeferred.resolve();
if(isFinal) {
resolved = true;
const realSize = size || byteLength;
if(!size) {
writer.trim(realSize);
}
deferred.resolve(await writer.finalize(realSize <= MAX_FILE_SAVE_SIZE));
}
} catch(err) {
errorHandler(err as ApiError);
}
});
};
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) {
superpuper();
}
});
const checkCancel = () => {
@ -581,8 +660,7 @@ export class ApiFileManager extends AppManager { @@ -581,8 +660,7 @@ export class ApiFileManager extends AppManager {
deferred.cancel = () => {
if(!error && !resolved) {
const error = new Error('Canceled');
error.type = 'DOWNLOAD_CANCELED';
const error = makeError('DOWNLOAD_CANCELED');
errorHandler(error);
}
};
@ -610,8 +688,8 @@ export class ApiFileManager extends AppManager { @@ -610,8 +688,8 @@ export class ApiFileManager extends AppManager {
// get original instance with correct file_reference instead of using copies
const isDocument = media._ === 'document';
// const isWebDocument = media._ === 'webDocument';
if(isDocument) media = this.appDocsManager.getDoc((media as Photo.photo).id);
else if(isPhoto) media = this.appPhotosManager.getPhoto((media as Document.document).id);
if(isDocument) media = this.appDocsManager.getDoc((media as Document.document).id);
else if(isPhoto) media = this.appPhotosManager.getPhoto((media as Photo.photo).id);
const {fileName, downloadOptions} = getDownloadMediaDetails(options);
@ -653,7 +731,6 @@ export class ApiFileManager extends AppManager { @@ -653,7 +731,6 @@ export class ApiFileManager extends AppManager {
}
private delete(fileName: string) {
//this.log('will delete file:', fileName);
delete this.downloadPromises[fileName];
return this.getFileStorage().delete(fileName);
}
@ -665,16 +742,7 @@ export class ApiFileManager extends AppManager { @@ -665,16 +742,7 @@ export class ApiFileManager extends AppManager {
let canceled = false,
resolved = false,
doneParts = 0,
partSize = 262144; // 256 Kb
/* if(fileSize > (524288 * 3000)) {
partSize = 1024 * 1024;
activeDelta = 8;
} else */if(fileSize > 67108864) {
partSize = 524288;
} else if(fileSize < 102400) {
partSize = 32768;
}
partSize = this.getLimitPart(fileSize, true);
fileName ||= getFileNameForUpload(file);
@ -694,12 +762,12 @@ export class ApiFileManager extends AppManager { @@ -694,12 +762,12 @@ export class ApiFileManager extends AppManager {
};
const deferred = deferredPromise<typeof resultInputFile>();
if(totalParts > 4000) {
deferred.reject({type: 'FILE_TOO_BIG'});
if(totalParts > this.maxUploadParts) {
deferred.reject(makeError('FILE_TOO_BIG'));
return deferred;
}
let errorHandler = (error: any) => {
let errorHandler = (error: ApiError) => {
if(error?.type !== 'UPLOAD_CANCELED') {
this.log.error('Up Error', error);
}
@ -713,10 +781,6 @@ export class ApiFileManager extends AppManager { @@ -713,10 +781,6 @@ export class ApiFileManager extends AppManager {
const id = this.tempId++;
/* setInterval(() => {
console.log(file);
}, 1e3); */
const self = this;
function* generator() {
for(let offset = 0; offset < fileSize; offset += partSize) {
@ -726,7 +790,7 @@ export class ApiFileManager extends AppManager { @@ -726,7 +790,7 @@ export class ApiFileManager extends AppManager {
return readBlobAsArrayBuffer(blob).then((buffer) => {
if(canceled) {
throw {type: 'UPLOAD_CANCELED'};
throw makeError('UPLOAD_CANCELED');
}
self.debug && self.log('Upload file part, isBig:', isBigFile, part, buffer.byteLength, new Uint8Array(buffer).length, new Uint8Array(buffer).slice().length);
@ -800,10 +864,9 @@ export class ApiFileManager extends AppManager { @@ -800,10 +864,9 @@ export class ApiFileManager extends AppManager {
}
deferred.cancel = () => {
//this.log('cancel upload', canceled, resolved);
if(!canceled && !resolved) {
canceled = true;
errorHandler({type: 'UPLOAD_CANCELED'});
errorHandler(makeError('UPLOAD_CANCELED'));
}
};

13
src/lib/mtproto/apiManager.ts

@ -24,7 +24,7 @@ import type { MethodDeclMap } from '../../layer'; @@ -24,7 +24,7 @@ import type { MethodDeclMap } from '../../layer';
import deferredPromise, { CancellablePromise } from '../../helpers/cancellablePromise';
import App from '../../config/app';
import { MOUNT_CLASS_TO } from '../../config/debug';
import { IDB } from '../idb';
import { IDB } from '../files/idb';
import CryptoWorker from "../crypto/cryptoMessagePort";
import ctx from '../../environment/ctx';
import noop from '../../helpers/noop';
@ -38,17 +38,6 @@ import { getEnvironment } from '../../environment/utils'; @@ -38,17 +38,6 @@ import { getEnvironment } from '../../environment/utils';
import toggleStorages from '../../helpers/toggleStorages';
import type TcpObfuscated from './transports/tcpObfuscated';
export type ApiError = Partial<{
code: number,
type: string,
description: string,
originalError: any,
stack: string,
handled: boolean,
input: string,
message: ApiError
}>;
/* class RotatableArray<T> {
public array: Array<T> = [];
private lastIndex = -1;

1
src/lib/mtproto/api_methods.ts

@ -9,7 +9,6 @@ import { ignoreRestrictionReasons } from "../../helpers/restrictions"; @@ -9,7 +9,6 @@ import { ignoreRestrictionReasons } from "../../helpers/restrictions";
import { MethodDeclMap, User } from "../../layer";
import { InvokeApiOptions } from "../../types";
import { AppManager } from "../appManagers/manager";
import { ApiError } from "./apiManager";
import { MTAppConfig } from "./appConfig";
import { UserAuth } from "./mtproto_config";
import { MTMessage } from "./networker";

1
src/lib/mtproto/authorizer.ts

@ -22,7 +22,6 @@ import CryptoWorker from "../crypto/cryptoMessagePort"; @@ -22,7 +22,6 @@ import CryptoWorker from "../crypto/cryptoMessagePort";
import { logger, LogTypes } from "../logger";
import DEBUG from "../../config/debug";
import { Awaited, DcId } from "../../types";
import { ApiError } from "./apiManager";
import addPadding from "../../helpers/bytes/addPadding";
import bytesCmp from "../../helpers/bytes/bytesCmp";
import bytesFromHex from "../../helpers/bytes/bytesFromHex";

1
src/lib/mtproto/mtproto_config.ts

@ -18,6 +18,7 @@ export const REPLIES_HIDDEN_CHANNEL_ID: ChatId = 777; @@ -18,6 +18,7 @@ export const REPLIES_HIDDEN_CHANNEL_ID: ChatId = 777;
export const SERVICE_PEER_ID: PeerId = 777000;
export const MUTE_UNTIL = 0x7FFFFFFF;
export const BOT_START_PARAM = '';
export const MAX_FILE_SAVE_SIZE = 20 * 1024 * 1024;
export const FOLDER_ID_ALL: REAL_FOLDER_ID = 0;
export const FOLDER_ID_ARCHIVE: REAL_FOLDER_ID = 1;

4
src/lib/mtproto/mtprotoworker.ts

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
*/
import type { Awaited } from '../../types';
import type { CacheStorageDbName } from '../cacheStorage';
import type { CacheStorageDbName } from '../files/cacheStorage';
import type { State } from '../../config/state';
import type { Message, MessagePeerReaction, PeerNotifySettings } from '../../layer';
import { CryptoMethods } from '../crypto/crypto_methods';
@ -247,6 +247,8 @@ class ApiManagerProxy extends MTProtoMessagePort { @@ -247,6 +247,8 @@ class ApiManagerProxy extends MTProtoMessagePort {
const worker = navigator.serviceWorker;
this._registerServiceWorker();
// worker.startMessages();
worker.addEventListener('controllerchange', () => {
this.log.warn('controllerchange');

9
src/lib/mtproto/networker.ts

@ -207,7 +207,7 @@ export default class MTPNetworker { @@ -207,7 +207,7 @@ export default class MTPNetworker {
const suffix = this.isFileUpload ? '-U' : this.isFileDownload ? '-D' : '';
this.name = 'NET-' + dcId + suffix;
//this.log = logger(this.name, this.upload && this.dcId === 2 ? LogLevels.debug | LogLevels.warn | LogLevels.log | LogLevels.error : LogLevels.error);
this.log = logger(this.name, LogTypes.Log | LogTypes.Debug | LogTypes.Error | LogTypes.Warn, undefined);
this.log = logger(this.name, LogTypes.Log /* | LogTypes.Debug */ | LogTypes.Error | LogTypes.Warn);
this.log('constructor'/* , this.authKey, this.authKeyID, this.serverSalt */);
// Test resend after bad_server_salt
@ -1310,9 +1310,10 @@ export default class MTPNetworker { @@ -1310,9 +1310,10 @@ export default class MTPNetworker {
if(!(this.transport instanceof HTTP)) return promise;
/// #endif
const baseError = {
const baseError: ApiError = {
code: 406,
type: 'NETWORK_BAD_RESPONSE',
// @ts-ignore
transport: this.transport
};
@ -1596,13 +1597,13 @@ export default class MTPNetworker { @@ -1596,13 +1597,13 @@ export default class MTPNetworker {
}
}
private processError(rawError: {error_message: string, error_code: number}) {
private processError(rawError: {error_message: string, error_code: number}): ApiError {
const matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || [];
rawError.error_code = rawError.error_code;
return {
code: !rawError.error_code || rawError.error_code <= 0 ? 500 : rawError.error_code,
type: matches[1] || 'UNKNOWN',
type: matches[1] as any || 'UNKNOWN',
description: matches[3] || ('CODE#' + rawError.error_code + ' ' + rawError.error_message),
originalError: rawError
};

3
src/lib/mtproto/referenceDatabase.ts

@ -9,6 +9,7 @@ import { logger } from "../logger"; @@ -9,6 +9,7 @@ import { logger } from "../logger";
import bytesToHex from "../../helpers/bytes/bytesToHex";
import deepEqual from "../../helpers/object/deepEqual";
import { AppManager } from "../appManagers/manager";
import makeError from "../../helpers/makeError";
export type ReferenceContext = ReferenceContext.referenceContextProfilePhoto | ReferenceContext.referenceContextMessage | ReferenceContext.referenceContextEmojiesSounds | ReferenceContext.referenceContextReactions | ReferenceContext.referenceContextUserFull;
export namespace ReferenceContext {
@ -173,7 +174,7 @@ export class ReferenceDatabase extends AppManager { @@ -173,7 +174,7 @@ export class ReferenceDatabase extends AppManager {
this.log.error('refreshReference: no new context, reference before:', hex, 'after:', newHex, context);
throw 'NO_NEW_CONTEXT';
throw makeError('NO_NEW_CONTEXT');
});
}

154
src/lib/serviceWorker/index.service.ts

@ -12,12 +12,14 @@ import { logger, LogTypes } from '../logger'; @@ -12,12 +12,14 @@ import { logger, LogTypes } from '../logger';
import { CACHE_ASSETS_NAME, requestCache } from './cache';
import onStreamFetch from './stream';
import { closeAllNotifications, onPing } from './push';
import CacheStorageController from '../cacheStorage';
import CacheStorageController from '../files/cacheStorage';
import { IS_SAFARI } from '../../environment/userAgent';
import ServiceMessagePort from './serviceMessagePort';
import ServiceMessagePort, { ServiceDownloadTaskPayload } from './serviceMessagePort';
import listenMessagePort from '../../helpers/listenMessagePort';
import { getWindowClients } from '../../helpers/context';
import { MessageSendPort } from '../mtproto/superMessagePort';
import noop from '../../helpers/noop';
import makeError from '../../helpers/makeError';
export const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn);
const ctx = self as any as ServiceWorkerGlobalScope;
@ -33,17 +35,35 @@ const sendMessagePort = (source: MessageSendPort) => { @@ -33,17 +35,35 @@ const sendMessagePort = (source: MessageSendPort) => {
};
const sendMessagePortIfNeeded = (source: MessageSendPort) => {
if(!connectedWindows && !_mtprotoMessagePort) {
if(!connectedWindows.size && !_mtprotoMessagePort) {
sendMessagePort(source);
}
};
const onWindowConnected = (source: MessageSendPort) => {
const onWindowConnected = (source: WindowClient) => {
log('window connected', source.id);
if(source.frameType === 'none') {
log.warn('maybe a bugged Safari starting window', source.id);
return;
}
sendMessagePortIfNeeded(source);
connectedWindows.add(source.id);
};
++connectedWindows;
log('window connected');
type DownloadType = Uint8Array;
type DownloadItem = ServiceDownloadTaskPayload & {
transformStream: TransformStream<DownloadType, DownloadType>,
readableStream: ReadableStream<DownloadType>,
writableStream: WritableStream<DownloadType>,
writer: WritableStreamDefaultWriter<DownloadType>,
// controller: TransformStreamDefaultController<DownloadType>,
// promise: CancellablePromise<void>,
used?: boolean
};
const downloadMap: Map<string, DownloadItem> = new Map();
const DOWNLOAD_ERROR = makeError('UNKNOWN');
export const serviceMessagePort = new ServiceMessagePort<false>();
serviceMessagePort.addMultipleEventsListeners({
@ -58,32 +78,117 @@ serviceMessagePort.addMultipleEventsListeners({ @@ -58,32 +78,117 @@ serviceMessagePort.addMultipleEventsListeners({
},
hello: (payload, source) => {
onWindowConnected(source);
onWindowConnected(source as any as WindowClient);
},
download: (payload) => {
const {id} = payload;
if(downloadMap.has(id)) {
return;
}
// const writableStrategy = new ByteLengthQueuingStrategy({highWaterMark: 1024 * 1024});
// let controller: TransformStreamDefaultController<DownloadType>;
const transformStream = new TransformStream<DownloadType, DownloadType>(/* {
start: (_controller) => controller = _controller,
}, {
highWaterMark: 1,
size: (chunk) => chunk.byteLength
}, new CountQueuingStrategy({highWaterMark: 4}) */);
const {readable, writable} = transformStream;
const writer = writable.getWriter();
// const promise = deferredPromise<void>();
// promise.catch(noop).finally(() => {
// downloadMap.delete(id);
// });
// writer.closed.then(promise.resolve, promise.reject);
writer.closed.catch(noop).finally(() => {
log.error('closed writer');
downloadMap.delete(id);
});
const item: DownloadItem = {
...payload,
transformStream,
readableStream: readable,
writableStream: writable,
writer,
// promise,
// controller
};
downloadMap.set(id, item);
return writer.closed.catch(() => {throw DOWNLOAD_ERROR;});
// return promise;
},
downloadChunk: ({id, chunk}) => {
const item = downloadMap.get(id);
if(!item) {
return Promise.reject();
}
// return item.controller.enqueue(chunk);
return item.writer.write(chunk);
},
downloadFinalize: (id) => {
const item = downloadMap.get(id);
if(!item) {
return Promise.reject();
}
// item.promise.resolve();
// return item.controller.terminate();
return item.writer.close();
},
downloadCancel: (id) => {
const item = downloadMap.get(id);
if(!item) {
return;
}
// item.promise.reject();
// return item.controller.error();
return item.writer.abort();
}
});
// * service worker can be killed, so won't get 'hello' event
getWindowClients().then((windowClients) => {
log(`got ${windowClients.length} windows from the start`);
windowClients.forEach((windowClient) => {
onWindowConnected(windowClient);
});
});
let connectedWindows = 0;
let connectedWindows: Set<string> = new Set();
listenMessagePort(serviceMessagePort, undefined, (source) => {
if(source === _mtprotoMessagePort) {
const isWindowClient = source instanceof WindowClient;
if(!isWindowClient || !connectedWindows.has(source.id)) {
return;
}
log('window disconnected');
connectedWindows = Math.max(0, connectedWindows - 1);
if(!connectedWindows) {
connectedWindows.delete(source.id);
if(!connectedWindows.size) {
log.warn('no windows left');
if(_mtprotoMessagePort) {
serviceMessagePort.detachPort(_mtprotoMessagePort);
_mtprotoMessagePort = undefined;
}
if(downloadMap.size) {
for(const [id, item] of downloadMap) {
item.writer.abort().catch(noop);
}
}
}
});
/// #endif
@ -102,15 +207,28 @@ const onFetch = (event: FetchEvent): void => { @@ -102,15 +207,28 @@ const onFetch = (event: FetchEvent): void => {
try {
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
//log.debug('[fetch]:', event);
// log.debug('[fetch]:', event);
switch(scope) {
case 'stream': {
onStreamFetch(event, params);
break;
}
case 'download': {
const item = downloadMap.get(params);
if(!item || item.used) {
break;
}
item.used = true;
const response = new Response(item.transformStream.readable, {headers: item.headers});
event.respondWith(response);
break;
}
}
} catch(err) {
log.error('fetch error', err);
event.respondWith(new Response('', {
status: 500,
statusText: 'Internal Server Error',
@ -133,13 +251,13 @@ ctx.addEventListener('activate', (event) => { @@ -133,13 +251,13 @@ ctx.addEventListener('activate', (event) => {
event.waitUntil(ctx.clients.claim());
});
ctx.onerror = (error) => {
log.error('error:', error);
};
// ctx.onerror = (error) => {
// log.error('error:', error);
// };
ctx.onunhandledrejection = (error) => {
log.error('onunhandledrejection:', error);
};
// ctx.onunhandledrejection = (error) => {
// log.error('onunhandledrejection:', error);
// };
ctx.onoffline = ctx.ononline = onChangeState;

2
src/lib/serviceWorker/push.ts

@ -13,7 +13,7 @@ import { Database } from "../../config/databases"; @@ -13,7 +13,7 @@ import { Database } from "../../config/databases";
import DATABASE_STATE from "../../config/databases/state";
import { IS_FIREFOX } from "../../environment/userAgent";
import deepEqual from "../../helpers/object/deepEqual";
import IDBStorage from "../idb";
import IDBStorage from "../files/idb";
import { log, serviceMessagePort } from "./index.service";
import { ServicePushPingTaskPayload } from "./serviceMessagePort";

14
src/lib/serviceWorker/serviceMessagePort.ts

@ -27,6 +27,12 @@ export type ServiceRequestFilePartTaskPayload = { @@ -27,6 +27,12 @@ export type ServiceRequestFilePartTaskPayload = {
limit: number
};
export type ServiceDownloadTaskPayload = {
fileName: string,
headers: any,
id: string,
};
export type ServiceEvent = {
port: (payload: void, source: MessageEventSource, event: MessageEvent) => void
};
@ -36,7 +42,13 @@ export default class ServiceMessagePort<Master extends boolean = false> extends @@ -36,7 +42,13 @@ export default class ServiceMessagePort<Master extends boolean = false> extends
notificationsClear: () => void,
toggleStorages: (payload: {enabled: boolean, clearWrite: boolean}) => void,
pushPing: (payload: ServicePushPingTaskPayload, source: MessageEventSource, event: MessageEvent) => void,
hello: (payload: void, source: MessageEventSource, event: MessageEvent) => void
hello: (payload: void, source: MessageEventSource, event: MessageEvent) => void,
// from mtproto worker
download: (payload: ServiceDownloadTaskPayload) => void,
downloadChunk: (payload: {id: ServiceDownloadTaskPayload['id'], chunk: Uint8Array}) => void
downloadFinalize: (payload: ServiceDownloadTaskPayload['id']) => void,
downloadCancel: (payload: ServiceDownloadTaskPayload['id']) => void
}, {
// to main thread
pushClick: (payload: PushNotificationObject) => void,

6
src/lib/serviceWorker/stream.ts

@ -8,7 +8,7 @@ import readBlobAsUint8Array from "../../helpers/blob/readBlobAsUint8Array"; @@ -8,7 +8,7 @@ import readBlobAsUint8Array from "../../helpers/blob/readBlobAsUint8Array";
import deferredPromise, { CancellablePromise } from "../../helpers/cancellablePromise";
import debounce from "../../helpers/schedulers/debounce";
import { InputFileLocation } from "../../layer";
import CacheStorageController from "../cacheStorage";
import CacheStorageController from "../files/cacheStorage";
import { DownloadOptions, MyUploadFile } from "../mtproto/apiFileManager";
import { getMtprotoMessagePort, log, serviceMessagePort } from "./index.service";
import { ServiceRequestFilePartTaskPayload } from "./serviceMessagePort";
@ -140,8 +140,8 @@ class Stream { @@ -140,8 +140,8 @@ class Stream {
const key = this.getChunkKey(alignedOffset, limit);
return cacheStorage.getFile(key).then((blob: Blob) => {
return fromPreload ? new Uint8Array() : readBlobAsUint8Array(blob);
}, (error) => {
if(error === 'NO_ENTRY_FOUND') {
}, (error: ApiError) => {
if(error.type === 'NO_ENTRY_FOUND') {
return;
}
});

7
src/lib/storage.ts

@ -16,7 +16,7 @@ import deferredPromise, { CancellablePromise } from "../helpers/cancellablePromi @@ -16,7 +16,7 @@ import deferredPromise, { CancellablePromise } from "../helpers/cancellablePromi
import { IS_WORKER } from "../helpers/context";
import throttle from "../helpers/schedulers/throttle";
//import { WorkerTaskTemplate } from "../types";
import IDBStorage from "./idb";
import IDBStorage from "./files/idb";
function noop() {}
@ -166,8 +166,9 @@ export default class AppStorage< @@ -166,8 +166,9 @@ export default class AppStorage<
}
// console.log('[AS]: get time', keys, performance.now() - perf);
}, (error) => {
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(error)) {
}, (error: ApiError) => {
const ignoreErrors: Set<ErrorType> = new Set(['NO_ENTRY_FOUND', 'STORAGE_OFFLINE']);
if(!ignoreErrors.has(error.type)) {
this.useStorage = false;
console.error('[AS]: get error:', error, keys, storeName);
}

3
src/lib/storages/filters.ts

@ -12,6 +12,7 @@ import { AppManager } from "../appManagers/manager"; @@ -12,6 +12,7 @@ import { AppManager } from "../appManagers/manager";
import findAndSplice from "../../helpers/array/findAndSplice";
import assumeType from "../../helpers/assumeType";
import { FOLDER_ID_ALL, FOLDER_ID_ARCHIVE, REAL_FOLDERS, REAL_FOLDER_ID, START_LOCAL_ID } from "../mtproto/mtproto_config";
import makeError from "../../helpers/makeError";
export type MyDialogFilter = DialogFilter.dialogFilter;
@ -313,7 +314,7 @@ export default class FiltersStorage extends AppManager { @@ -313,7 +314,7 @@ export default class FiltersStorage extends AppManager {
if(!wasPinned) {
if(filter.pinned_peers.length >= (await this.apiManager.getConfig()).pinned_infolder_count_max) {
return Promise.reject({type: 'PINNED_DIALOGS_TOO_MUCH'});
return Promise.reject(makeError('PINNED_DIALOGS_TOO_MUCH'));
}
filter.pinned_peers.unshift(this.appPeersManager.getInputPeerById(peerId));

1
src/pages/pageSignQR.ts

@ -5,7 +5,6 @@ @@ -5,7 +5,6 @@
*/
import type { DcId } from '../types';
import type { ApiError } from '../lib/mtproto/apiManager';
import Page from './page';
import { AuthAuthorization, AuthLoginToken } from '../layer';
import App from '../config/app';

7
src/scss/partials/_chatBubble.scss

@ -1490,6 +1490,13 @@ $bubble-beside-button-width: 38px; @@ -1490,6 +1490,13 @@ $bubble-beside-button-width: 38px;
}
}
.document {
.time {
height: 0;
align-self: flex-start;
}
}
/* .document,
.audio {
.time.tgico {

70
src/scss/partials/_document.scss

@ -23,6 +23,14 @@ @@ -23,6 +23,14 @@
line-height: 1;
text-align: center;
&-text {
opacity: 0;
@include animation-level(2) {
transition: opacity .2s ease-in-out;
}
}
.document:not(.document-with-thumb) & {
padding: 1.5625rem .25rem 0 .25rem;
@ -43,8 +51,8 @@ @@ -43,8 +51,8 @@
position: absolute;
top: 0;
right: 0;
width: var(--size);
height: var(--size);
// width: var(--size);
// height: var(--size);
border-bottom-left-radius: .25rem;
border-style: solid;
border-width: calc(var(--size) / 2);
@ -52,25 +60,54 @@ @@ -52,25 +60,54 @@
border-bottom-color: rgba(0, 0, 0, .25);
border-top-color: var(--message-background-color);
border-right-color: var(--message-background-color);
@include animation-level(2) {
transition: border-width .2s ease-in-out;
}
}
}
&:not(.downloaded) {
@include hover() {
.document-ico:after {
border-width: 0;
}
.document-ico-text {
opacity: 0;
}
.preloader-container {
opacity: 1 !important;
}
}
}
&:not(.downloading) {
.document-ico-text {
opacity: 1;
}
.preloader-container {
opacity: 0 !important;
}
}
&.downloading {
.document-ico:after {
border-width: 0;
}
}
&-ico,
&-download {
&-ico {
font-size: 1.125rem;
background-size: contain;
}
&-ico,
&-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-download {
background-color: var(--background-color);
border-radius: $border-radius;
&-name,
&-size {
@include text-overflow();
}
&.ext-zip {
@ -95,7 +132,7 @@ @@ -95,7 +132,7 @@
}
.document-download {
background-color: rgba(0, 0, 0, .15);
background-color: rgba(0, 0, 0, .2);
}
.preloader-circular {
@ -123,13 +160,12 @@ @@ -123,13 +160,12 @@
}
&-size {
white-space: nowrap;
color: var(--secondary-text-color);
font-size: var(--font-size-14);
line-height: var(--line-height-14);
//padding-right: 32px;
text-overflow: ellipsis;
overflow: hidden;
pointer-events: none;
position: relative;
}
.preloader-container {

Loading…
Cancel
Save