Browse Source

Better downloading

master
Eduard Kuzmenko 2 years ago
parent
commit
439d2ce684
  1. 6
      .eslintrc.js
  2. 1
      src/components/chat/bubbles.ts
  3. 18
      src/components/wrappers/document.ts
  4. 2
      src/global.d.ts
  5. 2
      src/helpers/fileName.ts
  6. 14
      src/helpers/formatBytes.ts
  7. 9
      src/lib/appManagers/appDocsManager.ts
  8. 96
      src/lib/appManagers/appDownloadManager.ts
  9. 2
      src/lib/appManagers/utils/download/getDownloadFileNameFromOptions.ts
  10. 2
      src/lib/appManagers/utils/webDocs/getWebDocumentDownloadOptions.ts
  11. 10
      src/lib/files/cacheStorage.ts
  12. 55
      src/lib/files/downloadStorage.ts
  13. 29
      src/lib/files/downloadWriter.ts
  14. 4
      src/lib/files/fileStorage.ts
  15. 487
      src/lib/mtproto/apiFileManager.ts
  16. 62
      src/lib/mtproto/api_methods.ts
  17. 2
      src/lib/mtproto/dcConfigurator.ts
  18. 4
      src/lib/mtproto/transports/websocket.ts
  19. 3
      src/lib/rootScope.ts
  20. 162
      src/lib/serviceWorker/download.ts
  21. 124
      src/lib/serviceWorker/index.service.ts
  22. 3
      src/lib/serviceWorker/serviceMessagePort.ts
  23. 4
      src/scss/partials/_document.scss

6
.eslintrc.js

@ -6,9 +6,9 @@ module.exports = {
extends: [], extends: [],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
// 'ecmaVersion': 'latest', 'ecmaVersion': 'latest',
// 'sourceType': 'module' 'sourceType': 'module'
project: ['./tsconfig.json'] // project: ['./tsconfig.json']
}, },
plugins: [ plugins: [
'@typescript-eslint' '@typescript-eslint'

1
src/components/chat/bubbles.ts

@ -522,6 +522,7 @@ export default class ChatBubbles {
(element as any).onLoad(true); (element as any).onLoad(true);
} else { } else {
element.dataset.docId = '' + doc.id; element.dataset.docId = '' + doc.id;
(element as any).doc = doc;
} }
} }
} else if(poll) { } else if(poll) {

18
src/components/wrappers/document.ts

@ -90,6 +90,7 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
const docDiv = document.createElement('div'); const docDiv = document.createElement('div');
docDiv.classList.add('document', `ext-${ext}`); docDiv.classList.add('document', `ext-${ext}`);
docDiv.dataset.docId = '' + doc.id; docDiv.dataset.docId = '' + doc.id;
(docDiv as any).doc = doc;
// return docDiv; // return docDiv;
@ -236,25 +237,28 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
// b && b.classList.add('hide'); // b && b.classList.add('hide');
let d = formatBytes(0); const format = (bytes: number) => formatBytes(bytes);
let d = format(0);
bytesContainer.style.visibility = 'hidden'; bytesContainer.style.visibility = 'hidden';
// bytesContainer.replaceWith(sizeContainer); // bytesContainer.replaceWith(sizeContainer);
sizeContainer.append(d, bytesJoiner, _bytesContainer); sizeContainer.append(d, bytesJoiner, _bytesContainer);
bytesContainer.parentElement.append(sizeContainer); bytesContainer.parentElement.append(sizeContainer);
promise.addNotifyListener((progress: Progress) => { promise.addNotifyListener((progress: Progress) => {
const _d = formatBytes(progress.done); const _d = format(progress.done);
d.replaceWith(_d); d.replaceWith(_d);
d = _d; d = _d;
}); });
}; };
const load = async(e?: Event) => { // ! DO NOT USE ASYNC/AWAIT HERE ! SAFARI WON'T LET DOWNLOAD THE FILE BECAUSE OF ASYNC
const load = (e?: Event) => {
const save = !e || e.isTrusted; const save = !e || e.isTrusted;
const doc = await managers.appDocsManager.getDoc(docDiv.dataset.docId); const doc = (docDiv as any).doc;
// const doc = await managers.appDocsManager.getDoc(docDiv.dataset.docId);
let download: CancellablePromise<any>; let download: CancellablePromise<any>;
const queueId = appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : undefined; const queueId = appImManager.chat.bubbles ? appImManager.chat.bubbles.lazyLoadQueue.queueId : undefined;
if(!save) { if(!save) {
download = appDownloadManager.downloadMediaVoid({media: doc, queueId}); download = appDownloadManager.downloadToDisc({media: doc, queueId}, true);
} else if(doc.type === 'pdf') { } else if(doc.type === 'pdf') {
const canOpenAfter = /* managers.appDocsManager.downloading.has(doc.id) || */!preloader || preloader.detached; const canOpenAfter = /* managers.appDocsManager.downloading.has(doc.id) || */!preloader || preloader.detached;
download = appDownloadManager.downloadMediaURL({media: doc, queueId}); download = appDownloadManager.downloadMediaURL({media: doc, queueId});
@ -282,10 +286,10 @@ export default async function wrapDocument({message, withTime, fontWeight, voice
} }
}; };
const {fileName: downloadFileName} = getDownloadMediaDetails({media: doc}); const {fileName: downloadFileName} = getDownloadMediaDetails({media: doc, downloadId: '1'});
if(await managers.apiFileManager.isDownloading(downloadFileName)) { if(await managers.apiFileManager.isDownloading(downloadFileName)) {
downloadDiv = docDiv.querySelector('.document-download') || icoDiv; downloadDiv = docDiv.querySelector('.document-download') || icoDiv;
const promise = appDownloadManager.downloadMediaVoid({media: doc}); const promise = appDownloadManager.downloadToDisc({media: doc}, true);
preloader = new ProgressivePreloader(); preloader = new ProgressivePreloader();
preloader.attach(downloadDiv, false, promise); preloader.attach(downloadDiv, false, promise);

2
src/global.d.ts vendored

@ -30,7 +30,7 @@ declare global {
type FiltersError = 'PINNED_DIALOGS_TOO_MUCH'; type FiltersError = 'PINNED_DIALOGS_TOO_MUCH';
type LocalFileError = ApiFileManagerError | ReferenceError | StorageError; type LocalFileError = ApiFileManagerError | ReferenceError | StorageError;
type LocalErrorType = LocalFileError | NetworkerError | FiltersError | 'UNKNOWN'; type LocalErrorType = LocalFileError | NetworkerError | FiltersError | 'UNKNOWN' | 'NO_DOC';
type ServerErrorType = 'FILE_REFERENCE_EXPIRED' | 'SESSION_REVOKED' | 'AUTH_KEY_DUPLICATED' | type ServerErrorType = 'FILE_REFERENCE_EXPIRED' | 'SESSION_REVOKED' | 'AUTH_KEY_DUPLICATED' |
'SESSION_PASSWORD_NEEDED' | 'CONNECTION_NOT_INITED' | 'ERROR_EMPTY' | 'MTPROTO_CLUSTER_INVALID' | 'SESSION_PASSWORD_NEEDED' | 'CONNECTION_NOT_INITED' | 'ERROR_EMPTY' | 'MTPROTO_CLUSTER_INVALID' |

2
src/helpers/fileName.ts

@ -58,7 +58,7 @@ export function getFileNameByLocation(location: InputFileLocation | InputWebFile
} }
} }
return str + (options.downloadId || '') + (ext ? '.' + ext : ext); return str + (options.downloadId ? '_download' : '') + (ext ? '.' + ext : ext);
} }
export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download'; export type FileURLType = 'photo' | 'thumb' | 'document' | 'stream' | 'download';

14
src/helpers/formatBytes.ts

@ -6,14 +6,18 @@
import {i18n, LangPackKey} from '../lib/langPack'; import {i18n, LangPackKey} from '../lib/langPack';
export default function formatBytes(bytes: number, decimals = 2) { export default function formatBytes(bytes: number, decimals: number | 'auto' = 'auto') {
if(bytes === 0) return i18n('FileSize.B', [0]); if(bytes === 0) return i18n('FileSize.B', [0]);
const k = 1024; const strictDecimals = decimals === 'auto';
const dm = decimals < 0 ? 0 : decimals;
const sizes: LangPackKey[] = ['FileSize.B', 'FileSize.KB', 'FileSize.MB', 'FileSize.GB'];
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
const _decimals = decimals === 'auto' ? Math.max(0, i - 1) : decimals;
const dm = Math.max(0, _decimals);
const sizes: LangPackKey[] = ['FileSize.B', 'FileSize.KB', 'FileSize.MB', 'FileSize.GB'];
return i18n(sizes[i], [parseFloat((bytes / Math.pow(k, i)).toFixed(dm))]); const fixed = (bytes / Math.pow(k, i)).toFixed(dm);
return i18n(sizes[i], [strictDecimals ? fixed : parseFloat(fixed)]);
} }

9
src/lib/appManagers/appDocsManager.ts

@ -9,7 +9,7 @@
* https://github.com/zhukov/webogram/blob/master/LICENSE * https://github.com/zhukov/webogram/blob/master/LICENSE
*/ */
import {AccountWallPapers, Document, MessagesSavedGifs, PhotoSize, WallPaper} from '../../layer'; import {AccountWallPapers, Document, DocumentAttribute, MessagesSavedGifs, PhotoSize, WallPaper} from '../../layer';
import {ReferenceContext} from '../mtproto/referenceDatabase'; import {ReferenceContext} from '../mtproto/referenceDatabase';
import {getFullDate} from '../../helpers/date'; import {getFullDate} from '../../helpers/date';
import isObject from '../../helpers/object/isObject'; import isObject from '../../helpers/object/isObject';
@ -23,6 +23,7 @@ import MTProtoMessagePort from '../mtproto/mtprotoMessagePort';
import getDocumentInput from './utils/docs/getDocumentInput'; import getDocumentInput from './utils/docs/getDocumentInput';
import getDocumentURL from './utils/docs/getDocumentURL'; import getDocumentURL from './utils/docs/getDocumentURL';
import type {ThumbCache} from '../storages/thumbs'; import type {ThumbCache} from '../storages/thumbs';
import makeError from '../../helpers/makeError';
export type MyDocument = Document.document; export type MyDocument = Document.document;
@ -216,7 +217,10 @@ export class AppDocsManager extends AppManager {
if(doc.type === 'voice' || doc.type === 'round') { if(doc.type === 'voice' || doc.type === 'round') {
// browser will identify extension // browser will identify extension
doc.file_name = doc.type + '_' + getFullDate(new Date(doc.date * 1000), {monthAsNumber: true, leadingZero: true}).replace(/[:\.]/g, '-').replace(', ', '_'); const attribute = doc.attributes.find((attribute) => attribute._ === 'documentAttributeFilename') as DocumentAttribute.documentAttributeFilename;
const ext = attribute && attribute.file_name.split('.').pop();
const date = getFullDate(new Date(doc.date * 1000), {monthAsNumber: true, leadingZero: true}).replace(/[:\.]/g, '-').replace(', ', '_');
doc.file_name = `${doc.type}_${date}${ext ? '.' + ext : ''}`;
} }
if(isServiceWorkerOnline()) { if(isServiceWorkerOnline()) {
@ -400,6 +404,7 @@ export class AppDocsManager extends AppManager {
public requestDocPart(docId: DocId, dcId: number, offset: number, limit: number) { public requestDocPart(docId: DocId, dcId: number, offset: number, limit: number) {
const doc = this.getDoc(docId); const doc = this.getDoc(docId);
if(!doc) return Promise.reject(makeError('NO_DOC'));
return this.apiFileManager.requestFilePart(dcId, getDocumentInput(doc), offset, limit); return this.apiFileManager.requestFilePart(dcId, getDocumentInput(doc), offset, limit);
} }
} }

96
src/lib/appManagers/appDownloadManager.ts

@ -16,8 +16,7 @@ import noop from '../../helpers/noop';
import getDownloadMediaDetails from './utils/download/getDownloadMediaDetails'; import getDownloadMediaDetails from './utils/download/getDownloadMediaDetails';
import getDownloadFileNameFromOptions from './utils/download/getDownloadFileNameFromOptions'; import getDownloadFileNameFromOptions from './utils/download/getDownloadFileNameFromOptions';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice'; import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
import {MAX_FILE_SAVE_SIZE} from '../mtproto/mtproto_config'; import makeError from '../../helpers/makeError';
import createDownloadAnchor from '../../helpers/dom/createDownloadAnchor';
export type ResponseMethodBlob = 'blob'; export type ResponseMethodBlob = 'blob';
export type ResponseMethodJson = 'json'; export type ResponseMethodJson = 'json';
@ -38,6 +37,7 @@ type DownloadType = 'url' | 'blob' | 'void' | 'disc';
export class AppDownloadManager { export class AppDownloadManager {
private downloads: {[fileName: string]: {main: Download} & {[type in DownloadType]?: Download}} = {}; private downloads: {[fileName: string]: {main: Download} & {[type in DownloadType]?: Download}} = {};
// private downloadsToDisc: {[fileName: string]: Download} = {};
private progress: {[fileName: string]: Progress} = {}; private progress: {[fileName: string]: Progress} = {};
// private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {}; // private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {};
private managers: AppManagers; private managers: AppManagers;
@ -45,15 +45,14 @@ export class AppDownloadManager {
public construct(managers: AppManagers) { public construct(managers: AppManagers) {
this.managers = managers; this.managers = managers;
rootScope.addEventListener('download_progress', (details) => { rootScope.addEventListener('download_progress', (details) => {
this.progress[details.fileName] = details;
// const callbacks = this.progressCallbacks[details.fileName]; // const callbacks = this.progressCallbacks[details.fileName];
// if(callbacks) { // if(callbacks) {
// callbacks.forEach((callback) => callback(details)); // callbacks.forEach((callback) => callback(details));
// } // }
const download = this.downloads[details.fileName]; const download = this.downloads[details.fileName];
if(download) { if(download?.main?.notifyAll) {
this.progress[details.fileName] = details;
download.main.notifyAll(details); download.main.notifyAll(details);
} }
}); });
@ -69,17 +68,12 @@ export class AppDownloadManager {
}; };
deferred.cancel = () => { deferred.cancel = () => {
// try { const error = makeError('DOWNLOAD_CANCELED');
const error = new Error('Download canceled');
error.name = 'AbortError';
this.managers.apiFileManager.cancelDownload(fileName); this.managers.apiFileManager.cancelDownload(fileName);
deferred.reject(error); deferred.reject(error);
deferred.cancel = () => {}; deferred.cancel = noop;
/* } catch(err) {
} */
}; };
deferred.catch(() => { deferred.catch(() => {
@ -225,50 +219,92 @@ export class AppDownloadManager {
// } // }
// } // }
public downloadToDisc(options: DownloadMediaOptions) { public downloadToDisc(options: DownloadMediaOptions, justAttach?: boolean) {
const media = options.media; const media = options.media;
const isDocument = media._ === 'document'; const isDocument = media._ === 'document';
if(!isDocument && !options.thumb) { if(!isDocument && !options.thumb) {
options.thumb = (media as Photo.photo).sizes.slice().pop() as PhotoSize.photoSize; options.thumb = (media as Photo.photo).sizes.slice().pop() as PhotoSize.photoSize;
} }
const {downloadOptions, fileName} = getDownloadMediaDetails(options); // const {fileName: cacheFileName} = getDownloadMediaDetails(options);
if(downloadOptions.size && downloadOptions.size > MAX_FILE_SAVE_SIZE) { // if(justAttach) {
// const promise = this.downloadsToDisc[cacheFileName];
// if(promise) {
// return promise;
// }
// }
// const {downloadOptions, fileName} = getDownloadMediaDetails(options);
// if(downloadOptions.size && downloadOptions.size > MAX_FILE_SAVE_SIZE) {
const id = '' + (Math.random() * 0x7FFFFFFF | 0); const id = '' + (Math.random() * 0x7FFFFFFF | 0);
// const id = 'test';
const url = `/download/${id}`; const url = `/download/${id}`;
options.downloadId = id; options.downloadId = id;
const promise = this.downloadMedia(options, 'disc'); const promise = this.downloadMedia(options, 'disc');
// this.downloadsToDisc[cacheFileName] = promise;
let iframe: HTMLIFrameElement; if(justAttach) {
const onProgress = () => { return promise;
iframe = document.createElement('iframe'); }
const iframe = document.createElement('iframe');
iframe.hidden = true; iframe.hidden = true;
// iframe.src = sw.scope + fileName;
iframe.src = url; iframe.src = url;
document.body.append(iframe); document.body.append(iframe);
// createDownloadAnchor(url, 'asd.txt');
// const events = [
// 'emptied',
// 'abort',
// 'suspend',
// 'reset',
// 'error',
// 'ended',
// 'load'
// ].forEach((event) => {
// iframe.addEventListener(event, () => alert(event));
// iframe.contentWindow.addEventListener(event, () => alert(event));
// });
let element: HTMLElement, hadProgress = false;
const onProgress = () => {
if(hadProgress) {
return;
}
hadProgress = true;
element = iframe;
indexOfAndSplice(promise.listeners, onProgress); indexOfAndSplice(promise.listeners, onProgress);
}; };
promise.addNotifyListener(onProgress); promise.addNotifyListener(onProgress);
promise.catch(noop).finally(() => { promise.catch(noop).finally(() => {
if(!hadProgress) {
onProgress();
}
setTimeout(() => { setTimeout(() => {
iframe?.remove(); element?.remove();
}, 1000); }, 1000);
});
return promise; // if(this.downloadsToDisc[cacheFileName] === promise) {
} else { // delete this.downloadsToDisc[cacheFileName];
const promise = this.downloadMedia(options, 'blob'); // }
promise.then((blob) => {
const url = URL.createObjectURL(blob);
createDownloadAnchor(url, downloadOptions.fileName || fileName, () => {
URL.revokeObjectURL(url);
});
}); });
return promise; 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); // const promise = this.downloadMedia(options);
// promise.then((blob) => { // promise.then((blob) => {

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

@ -8,5 +8,5 @@ import {getFileNameByLocation} from '../../../../helpers/fileName';
import {DownloadOptions} from '../../../mtproto/apiFileManager'; import {DownloadOptions} from '../../../mtproto/apiFileManager';
export default function getDownloadFileNameFromOptions(options: DownloadOptions) { export default function getDownloadFileNameFromOptions(options: DownloadOptions) {
return getFileNameByLocation(options.location, {fileName: options.fileName}); return getFileNameByLocation(options.location, options);
} }

2
src/lib/appManagers/utils/webDocs/getWebDocumentDownloadOptions.ts

@ -3,7 +3,7 @@ import {DownloadOptions} from '../../../mtproto/apiFileManager';
export default function getWebDocumentDownloadOptions(webDocument: WebDocument): DownloadOptions { export default function getWebDocumentDownloadOptions(webDocument: WebDocument): DownloadOptions {
return { return {
dcId: 4, dcId: 0,
location: { location: {
_: 'inputWebFileLocation', _: 'inputWebFileLocation',
access_hash: (webDocument as WebDocument.webDocument).access_hash, access_hash: (webDocument as WebDocument.webDocument).access_hash,

10
src/lib/files/cacheStorage.ts

@ -10,6 +10,7 @@ import MemoryWriter from './memoryWriter';
import FileManager from './memoryWriter'; import FileManager from './memoryWriter';
import FileStorage from './fileStorage'; import FileStorage from './fileStorage';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
import deferredPromise from '../../helpers/cancellablePromise';
export type CacheStorageDbName = 'cachedFiles' | 'cachedStreamChunks' | 'cachedAssets'; export type CacheStorageDbName = 'cachedFiles' | 'cachedStreamChunks' | 'cachedAssets';
@ -124,12 +125,17 @@ export default class CacheStorageController implements FileStorage {
}); });
} }
public getWriter(fileName: string, fileSize: number, mimeType: string) { public prepareWriting(fileName: string, fileSize: number, mimeType: string) {
return {
deferred: deferredPromise<Blob>(),
getWriter: () => {
const writer = new MemoryWriter(mimeType, fileSize, (blob) => { const writer = new MemoryWriter(mimeType, fileSize, (blob) => {
return this.saveFile(fileName, blob).catch(() => blob); return this.saveFile(fileName, blob).catch(() => blob);
}); });
return Promise.resolve(writer); return writer;
}
};
} }
public static toggleStorage(enabled: boolean, clearWrite: boolean) { public static toggleStorage(enabled: boolean, clearWrite: boolean) {

55
src/lib/files/downloadStorage.ts

@ -0,0 +1,55 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import deferredPromise from '../../helpers/cancellablePromise';
import makeError from '../../helpers/makeError';
import fileNameRFC from '../../helpers/string/fileNameRFC';
import {getServiceMessagePort} from '../mtproto/mtproto.worker';
import DownloadWriter from './downloadWriter';
import FileStorage from './fileStorage';
import StreamWriter from './streamWriter';
export default class DownloadStorage implements FileStorage {
public getFile(fileName: string): Promise<any> {
return Promise.reject(makeError('NO_ENTRY_FOUND'));
}
public prepareWriting({fileName, downloadId, size}: {
fileName: string,
downloadId: string,
size: number
}) {
const headers = {
'Content-Type': 'application/octet-stream; charset=utf-8',
'Content-Disposition': 'attachment; filename*=UTF-8\'\'' + fileNameRFC(fileName),
...(size ? {'Content-Length': size} : {})
};
const serviceMessagePort = getServiceMessagePort();
const promise = serviceMessagePort.invoke('download', {
headers,
id: downloadId
});
const deferred = deferredPromise<void>();
deferred.cancel = () => {
deferred.reject(makeError('DOWNLOAD_CANCELED'));
};
deferred.catch(() => {
getServiceMessagePort().invoke('downloadCancel', downloadId);
});
promise.then(deferred.resolve, deferred.reject);
return {
deferred,
getWriter: () => {
return new DownloadWriter(serviceMessagePort, downloadId);
}
};
}
}

29
src/lib/files/downloadWriter.ts

@ -0,0 +1,29 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import {getServiceMessagePort} from '../mtproto/mtproto.worker';
import ServiceMessagePort from '../serviceWorker/serviceMessagePort';
import StreamWriter from './streamWriter';
export default class DownloadWriter implements StreamWriter {
constructor(
private serviceMessagePort: ServiceMessagePort<true>,
private downloadId: string
) {
this.serviceMessagePort = getServiceMessagePort();
}
public async write(part: Uint8Array, offset?: number) {
return this.serviceMessagePort.invoke('downloadChunk', {
id: this.downloadId,
chunk: part
});
}
public finalize(saveToStorage?: boolean): Promise<Blob> {
return this.serviceMessagePort.invoke('downloadFinalize', this.downloadId).then(() => undefined);
}
}

4
src/lib/files/fileStorage.ts

@ -4,10 +4,10 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import {CancellablePromise} from '../../helpers/cancellablePromise';
import StreamWriter from './streamWriter'; import StreamWriter from './streamWriter';
export default abstract class FileStorage { export default abstract class FileStorage {
public abstract getFile(fileName: string): Promise<any>; public abstract getFile(fileName: string): Promise<any>;
public abstract prepareWriting(...args: any[]): {deferred: CancellablePromise<any>, getWriter: () => StreamWriter};
public abstract getWriter(fileName: string, fileSize: number, mimeType: string): Promise<StreamWriter>;
} }

487
src/lib/mtproto/apiFileManager.ts

@ -18,7 +18,6 @@ import {DcId} from '../../types';
import CacheStorageController from '../files/cacheStorage'; import CacheStorageController from '../files/cacheStorage';
import {logger, LogTypes} from '../logger'; import {logger, LogTypes} from '../logger';
import assumeType from '../../helpers/assumeType'; import assumeType from '../../helpers/assumeType';
import ctx from '../../environment/ctx';
import noop from '../../helpers/noop'; import noop from '../../helpers/noop';
import readBlobAsArrayBuffer from '../../helpers/blob/readBlobAsArrayBuffer'; import readBlobAsArrayBuffer from '../../helpers/blob/readBlobAsArrayBuffer';
import bytesToHex from '../../helpers/bytes/bytesToHex'; import bytesToHex from '../../helpers/bytes/bytesToHex';
@ -32,13 +31,15 @@ import type {Progress} from '../appManagers/appDownloadManager';
import getDownloadMediaDetails from '../appManagers/utils/download/getDownloadMediaDetails'; import getDownloadMediaDetails from '../appManagers/utils/download/getDownloadMediaDetails';
import networkStats from './networkStats'; import networkStats from './networkStats';
import getDownloadFileNameFromOptions from '../appManagers/utils/download/getDownloadFileNameFromOptions'; import getDownloadFileNameFromOptions from '../appManagers/utils/download/getDownloadFileNameFromOptions';
import {getServiceMessagePort} from './mtproto.worker';
import StreamWriter from '../files/streamWriter'; import StreamWriter from '../files/streamWriter';
import FileStorage from '../files/fileStorage'; import FileStorage from '../files/fileStorage';
import fileNameRFC from '../../helpers/string/fileNameRFC';
import {MAX_FILE_SAVE_SIZE} from './mtproto_config'; import {MAX_FILE_SAVE_SIZE} from './mtproto_config';
import throttle from '../../helpers/schedulers/throttle'; import throttle from '../../helpers/schedulers/throttle';
import makeError from '../../helpers/makeError'; import makeError from '../../helpers/makeError';
import readBlobAsUint8Array from '../../helpers/blob/readBlobAsUint8Array';
import DownloadStorage from '../files/downloadStorage';
import copy from '../../helpers/object/copy';
import indexOfAndSplice from '../../helpers/array/indexOfAndSplice';
type Delayed = { type Delayed = {
offset: number, offset: number,
@ -82,10 +83,13 @@ export type MyUploadFile = UploadFile.uploadFile | UploadWebFile.uploadWebFile;
// originalPayload: ReferenceBytes // originalPayload: ReferenceBytes
// }; // };
const MAX_FILE_PART_SIZE = 1 * 1024 * 1024; const MAX_DOWNLOAD_FILE_PART_SIZE = 1 * 1024 * 1024;
const MAX_UPLOAD_FILE_PART_SIZE = 512 * 1024;
const MIN_PART_SIZE = 128 * 1024;
const AVG_PART_SIZE = 512 * 1024;
const REGULAR_DOWNLOAD_DELTA = 36; const REGULAR_DOWNLOAD_DELTA = (9 * 512 * 1024) / MIN_PART_SIZE;
const PREMIUM_DOWNLOAD_DELTA = 72; const PREMIUM_DOWNLOAD_DELTA = REGULAR_DOWNLOAD_DELTA * 2;
const IGNORE_ERRORS: Set<ErrorType> = new Set([ const IGNORE_ERRORS: Set<ErrorType> = new Set([
'DOWNLOAD_CANCELED', 'DOWNLOAD_CANCELED',
@ -96,11 +100,16 @@ const IGNORE_ERRORS: Set<ErrorType> = new Set([
export class ApiFileManager extends AppManager { export class ApiFileManager extends AppManager {
private cacheStorage = new CacheStorageController('cachedFiles'); private cacheStorage = new CacheStorageController('cachedFiles');
private downloadStorage = new DownloadStorage();
private downloadPromises: { private downloadPromises: {
[fileName: string]: DownloadPromise [fileName: string]: DownloadPromise
} = {}; } = {};
// private downloadToDiscPromises: {
// [fileName: string]: DownloadPromise
// } = {};
private uploadPromises: { private uploadPromises: {
[fileName: string]: CancellablePromise<InputFile> [fileName: string]: CancellablePromise<InputFile>
} = {}; } = {};
@ -133,6 +142,7 @@ export class ApiFileManager extends AppManager {
private maxUploadParts = 4000; private maxUploadParts = 4000;
private maxDownloadParts = 8000; private maxDownloadParts = 8000;
private webFileDcId: DcId;
protected after() { protected after() {
setInterval(() => { // clear old promises setInterval(() => { // clear old promises
@ -144,6 +154,10 @@ export class ApiFileManager extends AppManager {
} }
}, 1800e3); }, 1800e3);
this.rootScope.addEventListener('config', (config) => {
this.webFileDcId = config.webfile_dc_id;
});
this.rootScope.addEventListener('app_config', (appConfig) => { this.rootScope.addEventListener('app_config', (appConfig) => {
this.maxUploadParts = this.rootScope.premium ? appConfig.upload_max_fileparts_premium : appConfig.upload_max_fileparts_default; this.maxUploadParts = this.rootScope.premium ? appConfig.upload_max_fileparts_premium : appConfig.upload_max_fileparts_default;
this.maxDownloadParts = appConfig.upload_max_fileparts_premium; this.maxDownloadParts = appConfig.upload_max_fileparts_premium;
@ -173,7 +187,7 @@ export class ApiFileManager extends AppManager {
private downloadCheck(dcId: string | number) { private downloadCheck(dcId: string | number) {
const downloadPull = this.downloadPulls[dcId]; const downloadPull = this.downloadPulls[dcId];
const downloadLimit = dcId === 'upload' ? 24 : (this.rootScope.premium ? PREMIUM_DOWNLOAD_DELTA : REGULAR_DOWNLOAD_DELTA); const downloadLimit = /* dcId === 'upload' ? 24 : */(this.rootScope.premium ? PREMIUM_DOWNLOAD_DELTA : REGULAR_DOWNLOAD_DELTA);
// const downloadLimit = Infinity; // const downloadLimit = Infinity;
if(this.downloadActives[dcId] >= downloadLimit || !downloadPull?.length) { if(this.downloadActives[dcId] >= downloadLimit || !downloadPull?.length) {
@ -187,7 +201,7 @@ export class ApiFileManager extends AppManager {
this.downloadActives[dcId] += activeDelta; this.downloadActives[dcId] += activeDelta;
const promise = data.cb(); const promise = data.cb();
const networkPromise = networkStats.waitForChunk(dcId as DcId, activeDelta * 1024 * 128); const networkPromise = networkStats.waitForChunk(dcId as DcId, activeDelta * MIN_PART_SIZE);
Promise.race([ Promise.race([
promise, promise,
networkPromise networkPromise
@ -235,7 +249,7 @@ export class ApiFileManager extends AppManager {
public requestWebFilePart(dcId: DcId, location: InputWebFileLocation, offset: number, limit: number, id = 0, queueId = 0, checkCancel?: () => void) { public requestWebFilePart(dcId: DcId, location: InputWebFileLocation, offset: number, limit: number, id = 0, queueId = 0, checkCancel?: () => void) {
return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error
checkCancel && checkCancel(); checkCancel?.();
return this.apiManager.invokeApi('upload.getWebFile', { return this.apiManager.invokeApi('upload.getWebFile', {
location, location,
@ -248,17 +262,26 @@ export class ApiFileManager extends AppManager {
}, this.getDelta(limit), queueId); }, this.getDelta(limit), queueId);
} }
public requestFilePart(dcId: DcId, location: InputFileLocation, offset: number, limit: number, id = 0, queueId = 0, checkCancel?: () => void) { public requestFilePart(
dcId: DcId,
location: InputFileLocation,
offset: number,
limit: number,
id = 0,
queueId = 0,
checkCancel?: () => void
) {
return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error return this.downloadRequest(dcId, id, async() => { // do not remove async, because checkCancel will throw an error
checkCancel && checkCancel(); checkCancel?.();
const invoke = async(): Promise<MyUploadFile> => { const invoke = async(): Promise<MyUploadFile> => {
checkCancel && checkCancel(); // do not remove async, because checkCancel will throw an error checkCancel?.(); // do not remove async, because checkCancel will throw an error
// * IMPORTANT: reference can be changed in previous request // * IMPORTANT: reference can be changed in previous request
const reference = (location as InputFileLocation.inputDocumentFileLocation).file_reference?.slice(); const reference = (location as InputFileLocation.inputDocumentFileLocation).file_reference?.slice();
const promise = /* pause(1000).then(() => */this.apiManager.invokeApi('upload.getFile', { const promise = // pause(offset > (100 * 1024 * 1024) ? 10000000 : 0).then(() =>
this.apiManager.invokeApi('upload.getFile', {
location, location,
offset, offset,
limit limit
@ -268,6 +291,8 @@ export class ApiFileManager extends AppManager {
}) as Promise<MyUploadFile>/* ) */; }) as Promise<MyUploadFile>/* ) */;
return promise.catch((err: ApiError) => { return promise.catch((err: ApiError) => {
checkCancel?.();
if(err.type === 'FILE_REFERENCE_EXPIRED') { if(err.type === 'FILE_REFERENCE_EXPIRED') {
return this.refreshReference(location as InputFileLocation.inputDocumentFileLocation, reference).then(invoke); return this.refreshReference(location as InputFileLocation.inputDocumentFileLocation, reference).then(invoke);
} }
@ -295,19 +320,22 @@ export class ApiFileManager extends AppManager {
} */ } */
private getDelta(bytes: number) { private getDelta(bytes: number) {
return bytes / 1024 / 128; return bytes / MIN_PART_SIZE;
} }
private getLimitPart(size: number, isUpload: boolean): number { private getLimitPart(size: number, isUpload: boolean): number {
if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments) if(!size) { // * sometimes size can be 0 (e.g. avatars, webDocuments)
return 512 * 1024; return AVG_PART_SIZE;
} }
let bytes = 128 * 1024; // return 1 * 1024 * 1024;
let bytes = MIN_PART_SIZE;
const maxParts = isUpload ? this.maxUploadParts : this.maxDownloadParts; const maxParts = isUpload ? this.maxUploadParts : this.maxDownloadParts;
const maxPartSize = isUpload ? MAX_UPLOAD_FILE_PART_SIZE : MAX_DOWNLOAD_FILE_PART_SIZE;
// usually it will stick to 512Kb size if the file is too big // usually it will stick to 512Kb size if the file is too big
while((size / bytes) > maxParts && bytes < MAX_FILE_PART_SIZE) { while((size / bytes) > maxParts && bytes < maxPartSize) {
bytes *= 2; bytes *= 2;
} }
/* if(size < 1e6 || !size) bytes = 512; /* if(size < 1e6 || !size) bytes = 512;
@ -399,201 +427,230 @@ export class ApiFileManager extends AppManager {
return this.uploadPromises[fileName]; return this.uploadPromises[fileName];
} }
public download(options: DownloadOptions): DownloadPromise { private getConvertMethod(mimeType: string) {
const size = options.size ?? 0;
const {dcId, location, downloadId} = options;
let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp']; let process: ApiFileManager['uncompressTGS'] | ApiFileManager['convertWebp'];
if(mimeType === 'application/x-tgwallpattern') {
if(downloadId) {
} else if(options.mimeType === 'application/x-tgwallpattern') {
process = this.uncompressTGV; process = this.uncompressTGV;
options.mimeType = 'image/svg+xml'; mimeType = 'image/svg+xml';
} else if(options.mimeType === 'image/webp' && !getEnvironment().IS_WEBP_SUPPORTED) { } else if(mimeType === 'image/webp' && !getEnvironment().IS_WEBP_SUPPORTED) {
process = this.convertWebp; process = this.convertWebp;
options.mimeType = 'image/png'; mimeType = 'image/png';
} else if(options.mimeType === 'application/x-tgsticker') { } else if(mimeType === 'application/x-tgsticker') {
process = this.uncompressTGS; process = this.uncompressTGS;
options.mimeType = 'application/json'; mimeType = 'application/json';
} else if(options.mimeType === 'audio/ogg' && !getEnvironment().IS_OPUS_SUPPORTED) { } else if(mimeType === 'audio/ogg' && !getEnvironment().IS_OPUS_SUPPORTED) {
process = this.convertOpus; process = this.convertOpus;
options.mimeType = 'audio/wav'; mimeType = 'audio/wav';
} }
const fileName = getDownloadFileNameFromOptions(options); return {mimeType, process};
const cachedPromise = options.downloadId ? undefined : this.downloadPromises[fileName]; }
let fileStorage: FileStorage = this.getFileStorage();
this.debug && this.log('downloadFile', fileName, size, location, options.mimeType); private allocateDeferredPromises(startOffset: number, size: number, limitPart: number) {
const delayed: Delayed[] = [];
let offset = startOffset;
let writePromise: CancellablePromise<void> = Promise.resolve(),
writeDeferred: CancellablePromise<void>;
do {
writeDeferred = deferredPromise<void>();
delayed.push({offset, writePromise, writeDeferred});
writePromise = writeDeferred;
offset += limitPart;
} while(offset < size);
/* if(options.queueId) { return delayed;
this.log.error('downloadFile queueId:', fileName, options.queueId); }
} */
if(cachedPromise) { public download(options: DownloadOptions): DownloadPromise {
// this.log('downloadFile cachedPromise'); const size = options.size ?? 0;
const {dcId, location, downloadId} = options;
if(size) { const originalMimeType = options.mimeType;
return cachedPromise.then((blob) => { const convertMethod = this.getConvertMethod(originalMimeType);
if(blob instanceof Blob && blob.size < size) { const {process} = convertMethod;
this.debug && this.log('downloadFile need to deleteFile, wrong size:', blob.size, size); options.mimeType = convertMethod.mimeType || 'image/jpeg';
return this.delete(fileName).then(() => { const fileName = getDownloadFileNameFromOptions(options);
return this.download(options); const cacheFileName = downloadId ? getDownloadFileNameFromOptions({...copy(options), downloadId: undefined}) : fileName;
}).catch(() => { const cacheStorage: FileStorage = this.getFileStorage();
return this.download(options); const downloadStorage: FileStorage = downloadId ? this.downloadStorage : undefined;
}); let deferred: DownloadPromise = downloadId ? undefined : this.downloadPromises[fileName];
} else {
return blob; this.debug && this.log('downloadFile', fileName, options);
}
}); if(deferred) {
} else { return deferred;
return cachedPromise;
}
} }
const deferred: DownloadPromise = deferredPromise(); // if(deferred) {
const mimeType = options.mimeType || 'image/jpeg'; // if(size) {
// return deferred.then(async(blob) => {
// if(blob instanceof Blob && blob.size < size) {
// this.debug && this.log('downloadFile need to deleteFile, wrong size:', blob.size, size);
// try {
// await this.delete(fileName);
// } finally {
// return this.download(options);
// }
// } else {
// return blob;
// }
// });
// } else {
// return deferred;
// }
// }
let error: ApiError; const errorHandler = (item: typeof cachePrepared, error: ApiError) => {
let resolved = false; if(item?.error) {
let cacheWriter: StreamWriter; return;
let errorHandler = (_error: typeof error) => { }
error = _error;
delete this.downloadPromises[fileName];
deferred.reject(error);
errorHandler = () => {};
if(cacheWriter && (!error || error.type !== 'DOWNLOAD_CANCELED')) { for(const p of prepared) {
cacheWriter.truncate?.(); if(item && item !== p) {
continue;
}
p.error = error;
p.deferred.reject(error);
} }
}; };
const id = this.tempId++; const id = this.tempId++;
const limitPart = options.limitPart || this.getLimitPart(size, false);
if(downloadId) { let getFile: FileStorage['getFile'] = cacheStorage.getFile.bind(cacheStorage);
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(); let cachePrepared: ReturnType<FileStorage['prepareWriting']> & {writer?: StreamWriter, error?: ApiError},
const promise = serviceMessagePort.invoke('download', { downloadPrepared: typeof cachePrepared;
fileName, const prepared: (typeof cachePrepared)[] = [];
headers, const possibleSize = size || limitPart;
id: downloadId
});
promise.catch(errorHandler); const getErrorsCount = () => prepared.reduce((acc, item) => acc + +!!item.error, 0);
deferred.catch(() => {
getServiceMessagePort().invoke('downloadCancel', downloadId);
});
class f implements StreamWriter { const attach = (item: typeof cachePrepared, fileName: string) => {
constructor() { const {deferred} = item;
const _errorHandler = errorHandler.bind(null, item);
deferred.cancel = () => deferred.reject(makeError('DOWNLOAD_CANCELED'));
deferred.catch((error) => {
_errorHandler(error);
item.writer?.truncate?.();
}).finally(() => {
if(this.downloadPromises[fileName] === deferred) {
delete this.downloadPromises[fileName];
} }
public async write(part: Uint8Array, offset?: number) { delete item.writer;
return serviceMessagePort.invoke('downloadChunk', { indexOfAndSplice(prepared, item);
id: downloadId,
chunk: part
}); });
}
public finalize(saveToStorage?: boolean): Promise<Blob> { this.downloadPromises[fileName] = deferred;
return serviceMessagePort.invoke('downloadFinalize', downloadId).then(() => null);
}
}
class d implements FileStorage { prepared.push(item);
public getFile(fileName: string): Promise<any> { };
return Promise.reject();
}
public getWriter(fileName: string, fileSize: number, mimeType: string): Promise<StreamWriter> { if(cacheStorage && (!downloadStorage || possibleSize <= MAX_FILE_SAVE_SIZE)) {
return Promise.resolve(new f()); cachePrepared = cacheStorage.prepareWriting(cacheFileName, possibleSize, options.mimeType)
attach(cachePrepared, cacheFileName);
} }
if(downloadStorage) {
downloadPrepared = downloadStorage.prepareWriting({
fileName: options.fileName, // it's doc file_name
downloadId,
size: possibleSize
});
attach(downloadPrepared, fileName);
if(cachePrepared) { // cancel cache too
downloadPrepared.deferred.catch((err) => cachePrepared.deferred.reject(err));
} }
fileStorage = new d(); // this.downloadToDiscPromises[cacheFileName] = deferred;
// deferred.catch(noop).finally(() => {
// if(this.downloadToDiscPromises[cacheFileName] === deferred) {
// delete this.downloadToDiscPromises[cacheFileName];
// }
// });
} }
fileStorage.getFile(fileName).then(async(blob: Blob) => { deferred = downloadPrepared?.deferred ?? cachePrepared.deferred;
// throw '';
if(blob.size < size) { if(downloadStorage && process) { // then have to load file again
if(!options.onlyCache) { getFile = downloadStorage.getFile.bind(downloadStorage);
await this.delete(fileName);
} }
throw false; getFile(cacheFileName).then(async(blob: Blob) => {
checkCancel();
// if(blob.size < size) {
// if(!options.onlyCache) {
// await this.delete(cacheFileName);
// checkCancel();
// }
// throw makeError('NO_ENTRY_FOUND');
// }
if(downloadPrepared) {
const writer = downloadPrepared.writer = downloadPrepared.getWriter();
checkCancel();
const arr = await readBlobAsUint8Array(blob);
checkCancel();
await writer.write(arr);
checkCancel();
downloadPrepared.deferred.resolve(await writer.finalize());
} }
deferred.resolve(blob); if(cachePrepared) {
cachePrepared.deferred.resolve(blob);
}
}).catch(async(err: ApiError) => { }).catch(async(err: ApiError) => {
if(options.onlyCache) { if(options.onlyCache) {
errorHandler(err); errorHandler(null, err);
return; return;
} }
// this.log('not cached', fileName); prepared.forEach((p) => {
const limit = options.limitPart || this.getLimitPart(size, false); p.writer = p.getWriter();
const writerPromise = fileStorage.getWriter(fileName, size || limit, mimeType); });
const writer = cacheWriter = await writerPromise;
let offset: number;
const startOffset = 0;
let writePromise: CancellablePromise<void> = Promise.resolve(),
writeDeferred: CancellablePromise<void>;
// const maxRequests = 13107200 / limit; // * 100 Mb speed
const maxRequests = Infinity; const maxRequests = Infinity;
const processDownloaded = async(bytes: Uint8Array) => { const isWebFile = location._ === 'inputWebFileLocation';
if(process) { const requestPart = (isWebFile ? this.requestWebFilePart : this.requestFilePart).bind(this);
// 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; if(isWebFile && this.webFileDcId === undefined) {
}; await this.apiManager.getConfig();
checkCancel();
}
const r = location._ === 'inputWebFileLocation' ? this.requestWebFilePart.bind(this) : this.requestFilePart.bind(this); const delayed = this.allocateDeferredPromises(0, size, limitPart);
const delayed: Delayed[] = []; const progress: Progress = {done: 0, offset: 0, total: size, fileName};
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 = () => { const dispatchProgress = () => {
try {
checkCancel();
progress.done = done; progress.done = done;
deferred.notify?.(progress); this.rootScope.dispatchEvent('download_progress', progress);
} catch(err) {}
}; };
const throttledDispatchProgress = throttle(dispatchProgress, 50, true); const throttledDispatchProgress = throttle(dispatchProgress, 50, true);
let done = 0; let done = 0;
const superpuper = async() => { const superpuper = async() => {
// if(!delayed.length) return;
const {offset, writePromise, writeDeferred} = delayed.shift(); const {offset, writePromise, writeDeferred} = delayed.shift();
try { try {
checkCancel(); checkCancel();
// @ts-ignore const requestPerf = performance.now();
const result = await r(dcId, location as any, offset, limit, id, options.queueId, checkCancel); const result = await requestPart(dcId, location as any, offset, limitPart, id, options.queueId, checkCancel);
const requestTime = performance.now() - requestPerf;
const bytes = result.bytes; const bytes = result.bytes;
@ -603,7 +660,7 @@ export class ApiFileManager extends AppManager {
const byteLength = bytes.byteLength; const byteLength = bytes.byteLength;
this.debug && this.log('downloadFile requestFilePart result:', fileName, result); this.debug && this.log('downloadFile requestFilePart result:', fileName, result);
const isFinal = (offset + limit) >= size || !byteLength; const isFinal = (offset + limitPart) >= size || !byteLength;
if(byteLength) { if(byteLength) {
done += byteLength; done += byteLength;
@ -613,68 +670,65 @@ export class ApiFileManager extends AppManager {
throttledDispatchProgress(); throttledDispatchProgress();
} }
const writeQueuePerf = performance.now();
await writePromise; await writePromise;
checkCancel(); checkCancel();
const writeQueueTime = performance.now() - writeQueuePerf;
// const perf = performance.now(); const perf = performance.now();
await writer.write(bytes, offset); await Promise.all(prepared.map(({writer}) => writer?.write(bytes, offset)));
checkCancel(); checkCancel();
// downloadId && this.log('write time', performance.now() - perf); // downloadId && this.log('write time', performance.now() - perf, 'request time', requestTime, 'queue time', writeQueueTime);
} }
if(isFinal && process) { if(isFinal && process) {
const promises = prepared
.filter(({writer}) => writer?.getParts && writer.replaceParts)
.map(async({writer}) => {
const bytes = writer.getParts(); const bytes = writer.getParts();
const processedResult = await processDownloaded(bytes); const processedResult = await process(bytes, cacheFileName);
checkCancel();
writer.replaceParts(processedResult); writer.replaceParts(processedResult);
});
await Promise.all(promises);
checkCancel();
} }
writeDeferred.resolve(); writeDeferred.resolve();
if(isFinal) { if(isFinal) {
resolved = true;
const realSize = size || byteLength; const realSize = size || byteLength;
if(!size) { if(!size || byteLength < size) {
writer.trim(realSize); prepared.forEach(({writer}) => writer?.trim?.(realSize));
}
const saveToStorage = realSize <= MAX_FILE_SAVE_SIZE;
prepared.forEach((item) => {
const {deferred, writer} = item;
if(deferred.isFulfilled || deferred.isRejected || !writer) {
return;
} }
deferred.resolve(await writer.finalize(realSize <= MAX_FILE_SAVE_SIZE)); const result = writer.finalize(saveToStorage);
deferred.resolve(result);
});
} }
} catch(err) { } catch(err) {
errorHandler(err as ApiError); errorHandler(null, err as ApiError);
} }
}; };
for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) { for(let i = 0, length = Math.min(maxRequests, delayed.length); i < length; ++i) {
superpuper(); superpuper();
} }
}); }).catch(noop);
const checkCancel = () => { const checkCancel = () => {
if(error) { if(getErrorsCount() === prepared.length) {
throw error; throw prepared[0].error;
} }
}; };
deferred.cancel = () => {
if(!error && !resolved) {
const error = makeError('DOWNLOAD_CANCELED');
errorHandler(error);
}
};
deferred.notify = (progress: Progress) => {
this.rootScope.dispatchEvent('download_progress', progress);
};
this.downloadPromises[fileName] = deferred;
deferred.catch(noop).finally(() => {
delete this.downloadPromises[fileName];
});
return deferred; return deferred;
} }
@ -736,23 +790,14 @@ export class ApiFileManager extends AppManager {
} }
public upload({file, fileName}: {file: Blob | File, fileName?: string}) { public upload({file, fileName}: {file: Blob | File, fileName?: string}) {
const fileSize = file.size,
isBigFile = fileSize >= 10485760;
let canceled = false,
resolved = false,
doneParts = 0;
const partSize = this.getLimitPart(fileSize, true);
fileName ||= getFileNameForUpload(file); fileName ||= getFileNameForUpload(file);
const fileSize = file.size;
const isBigFile = fileSize >= 10485760;
const partSize = this.getLimitPart(fileSize, true);
const activeDelta = this.getDelta(partSize); const activeDelta = this.getDelta(partSize);
const totalParts = Math.ceil(fileSize / partSize); const totalParts = Math.ceil(fileSize / partSize);
const fileId = randomLong(); const fileId = randomLong();
let _part = 0;
const resultInputFile: InputFile = { const resultInputFile: InputFile = {
_: isBigFile ? 'inputFileBig' : 'inputFile', _: isBigFile ? 'inputFileBig' : 'inputFile',
id: fileId as any, id: fileId as any,
@ -767,6 +812,7 @@ export class ApiFileManager extends AppManager {
return deferred; return deferred;
} }
let canceled = false, resolved = false;
let errorHandler = (error: ApiError) => { let errorHandler = (error: ApiError) => {
if(error?.type !== 'UPLOAD_CANCELED') { if(error?.type !== 'UPLOAD_CANCELED') {
this.log.error('Up Error', error); this.log.error('Up Error', error);
@ -774,58 +820,32 @@ export class ApiFileManager extends AppManager {
deferred.reject(error); deferred.reject(error);
canceled = true; canceled = true;
errorHandler = () => {}; errorHandler = noop;
}; };
const method = isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart'; const method = isBigFile ? 'upload.saveBigFilePart' : 'upload.saveFilePart';
const id = this.tempId++; const id = this.tempId++;
const self = this; const self = this;
function* generator() { function* generator() {
let _part = 0, doneParts = 0;
for(let offset = 0; offset < fileSize; offset += partSize) { for(let offset = 0; offset < fileSize; offset += partSize) {
const part = _part++; // 0, 1 const part = _part++; // 0, 1
yield self.downloadRequest('upload', id, () => { yield self.downloadRequest('upload', id, async() => {
const blob = file.slice(offset, offset + partSize); checkCancel();
return readBlobAsArrayBuffer(blob).then((buffer) => { const blob = file.slice(offset, offset + partSize);
if(canceled) { const buffer = await readBlobAsArrayBuffer(blob);
throw makeError('UPLOAD_CANCELED'); checkCancel();
}
self.debug && self.log('Upload file part, isBig:', isBigFile, part, buffer.byteLength, new Uint8Array(buffer).length, new Uint8Array(buffer).slice().length); self.debug && self.log('Upload file part, isBig:', isBigFile, part, buffer.byteLength, new Uint8Array(buffer).length, new Uint8Array(buffer).slice().length);
/* const u = new Uint8Array(buffer.byteLength);
for(let i = 0; i < u.length; ++i) {
//u[i] = Math.random() * 255 | 0;
u[i] = 0;
}
buffer = u.buffer; */
/* setTimeout(() => {
doneParts++;
uploadResolve();
//////this.log('Progress', doneParts * partSize / fileSize);
self.log('done part', part, doneParts);
deferred.notify({done: doneParts * partSize, total: fileSize});
if(doneParts >= totalParts) {
deferred.resolve(resultInputFile);
resolved = true;
}
}, 1250);
return; */
return self.apiManager.invokeApi(method, { return self.apiManager.invokeApi(method, {
file_id: fileId, file_id: fileId,
file_part: part, file_part: part,
file_total_parts: totalParts, file_total_parts: totalParts,
bytes: buffer/* new Uint8Array(buffer) */ bytes: buffer
} as any, { } as any, {
// startMaxLength: partSize + 256,
fileUpload: true fileUpload: true
}).then(() => { }).then(() => {
if(canceled) { if(canceled) {
@ -841,7 +861,6 @@ export class ApiFileManager extends AppManager {
resolved = true; resolved = true;
} }
}, errorHandler); }, errorHandler);
});
}, activeDelta).catch(errorHandler); }, activeDelta).catch(errorHandler);
} }
} }
@ -855,14 +874,16 @@ export class ApiFileManager extends AppManager {
}; };
const maxRequests = Infinity; const maxRequests = Infinity;
// const maxRequests = 10;
/* for(let i = 0; i < 10; ++i) {
process();
} */
for(let i = 0, length = Math.min(maxRequests, totalParts); i < length; ++i) { for(let i = 0, length = Math.min(maxRequests, totalParts); i < length; ++i) {
process(); process();
} }
const checkCancel = () => {
if(canceled) {
throw makeError('UPLOAD_CANCELED');
}
};
deferred.cancel = () => { deferred.cancel = () => {
if(!canceled && !resolved) { if(!canceled && !resolved) {
canceled = true; canceled = true;
@ -875,7 +896,9 @@ export class ApiFileManager extends AppManager {
}; };
deferred.finally(() => { deferred.finally(() => {
if(this.uploadPromises[fileName] === deferred) {
delete this.uploadPromises[fileName]; delete this.uploadPromises[fileName];
}
}); });
return this.uploadPromises[fileName] = deferred; return this.uploadPromises[fileName] = deferred;

62
src/lib/mtproto/api_methods.ts

@ -6,7 +6,7 @@
import ctx from '../../environment/ctx'; import ctx from '../../environment/ctx';
import {ignoreRestrictionReasons} from '../../helpers/restrictions'; import {ignoreRestrictionReasons} from '../../helpers/restrictions';
import {MethodDeclMap, User} from '../../layer'; import {Config, MethodDeclMap, User} from '../../layer';
import {InvokeApiOptions} from '../../types'; import {InvokeApiOptions} from '../../types';
import {AppManager} from '../appManagers/manager'; import {AppManager} from '../appManagers/manager';
import {MTAppConfig} from './appConfig'; import {MTAppConfig} from './appConfig';
@ -43,8 +43,8 @@ export default abstract class ApiManagerMethods extends AppManager {
} }
} = {}; } = {};
private config: Config;
private appConfig: MTAppConfig; private appConfig: MTAppConfig;
private getAppConfigPromise: Promise<MTAppConfig>;
constructor() { constructor() {
super(); super();
@ -140,7 +140,7 @@ export default abstract class ApiManagerMethods extends AppManager {
processResult: (response: MethodDeclMap[T]['res']) => R, processResult: (response: MethodDeclMap[T]['res']) => R,
processError?: (error: ApiError) => any, processError?: (error: ApiError) => any,
params?: MethodDeclMap[T]['req'], params?: MethodDeclMap[T]['req'],
options?: InvokeApiOptions & {cacheKey?: string} options?: InvokeApiOptions & {cacheKey?: string, overwrite?: boolean}
}): Promise<Awaited<R>> { }): Promise<Awaited<R>> {
o.params ??= {}; o.params ??= {};
o.options ??= {}; o.options ??= {};
@ -154,10 +154,32 @@ export default abstract class ApiManagerMethods extends AppManager {
return oldPromise; return oldPromise;
} }
const getNewPromise = () => {
const promise = map.get(cacheKey);
return promise === p ? undefined : promise;
}
const originalPromise = this.invokeApi(method, params, options); const originalPromise = this.invokeApi(method, params, options);
const newPromise: Promise<Awaited<R>> = originalPromise.then(processResult, processError); const newPromise: Promise<Awaited<R>> = originalPromise.then((result) => {
return getNewPromise() || processResult(result);
}, (error) => {
const promise = getNewPromise();
if(promise) {
return promise;
}
if(!processError) {
throw error;
}
return processError(error);
});
const p = newPromise.finally(() => { const p = newPromise.finally(() => {
if(map.get(cacheKey) !== p) {
return;
}
map.delete(cacheKey); map.delete(cacheKey);
if(!map.size) { if(!map.size) {
delete cache[method]; delete cache[method];
@ -226,24 +248,38 @@ export default abstract class ApiManagerMethods extends AppManager {
} }
} }
public getConfig() { public getConfig(overwrite?: boolean) {
return this.invokeApiCacheable('help.getConfig'); if(this.config && !overwrite) {
return this.config;
}
return this.invokeApiSingleProcess({
method: 'help.getConfig',
params: {},
processResult: (config) => {
this.config = config;
this.rootScope.dispatchEvent('config', config);
return config;
},
options: {overwrite}
});
} }
public getAppConfig(overwrite?: boolean) { public getAppConfig(overwrite?: boolean) {
if(this.appConfig && !overwrite) return this.appConfig; if(this.appConfig && !overwrite) {
if(this.getAppConfigPromise && !overwrite) return this.getAppConfigPromise; return this.appConfig;
const promise: Promise<MTAppConfig> = this.getAppConfigPromise = this.invokeApi('help.getAppConfig').then((config: MTAppConfig) => {
if(this.getAppConfigPromise !== promise) {
return this.getAppConfigPromise;
} }
return this.invokeApiSingleProcess({
method: 'help.getAppConfig',
params: {},
processResult: (config: MTAppConfig) => {
this.appConfig = config; this.appConfig = config;
ignoreRestrictionReasons(config.ignore_restriction_reasons ?? []); ignoreRestrictionReasons(config.ignore_restriction_reasons ?? []);
this.rootScope.dispatchEvent('app_config', config); this.rootScope.dispatchEvent('app_config', config);
return config; return config;
},
options: {overwrite}
}); });
return promise;
} }
} }

2
src/lib/mtproto/dcConfigurator.ts

@ -51,7 +51,7 @@ export function getTelegramConnectionSuffix(connectionType: ConnectionType) {
// #if MTPROTO_HAS_WS // #if MTPROTO_HAS_WS
export function constructTelegramWebSocketUrl(dcId: DcId, connectionType: ConnectionType, premium?: boolean) { export function constructTelegramWebSocketUrl(dcId: DcId, connectionType: ConnectionType, premium?: boolean) {
const suffix = getTelegramConnectionSuffix(connectionType); const suffix = getTelegramConnectionSuffix(connectionType);
const path = connectionType !== 'client' ? 'apiws' + (premium ? PREMIUM_SUFFIX : TEST_SUFFIX) : ('apiws' + TEST_SUFFIX); const path = connectionType !== 'client' ? 'apiws' + TEST_SUFFIX + (premium ? PREMIUM_SUFFIX : '') : ('apiws' + TEST_SUFFIX);
const chosenServer = `wss://${App.suffix.toLowerCase()}ws${dcId}${suffix}.web.telegram.org/${path}`; const chosenServer = `wss://${App.suffix.toLowerCase()}ws${dcId}${suffix}.web.telegram.org/${path}`;
return chosenServer; return chosenServer;

4
src/lib/mtproto/transports/websocket.ts

@ -86,8 +86,8 @@ export default class Socket extends EventListenerBase<{
this.close(); this.close();
}; };
private handleClose = () => { private handleClose = (e?: CloseEvent) => {
this.log('closed'/* , event, this.pending, this.ws.bufferedAmount */); this.log('closed', e/* , this.pending, this.ws.bufferedAmount */);
this.removeListeners(); this.removeListeners();
this.dispatchEvent('close'); this.dispatchEvent('close');

3
src/lib/rootScope.ts

@ -4,7 +4,7 @@
* https://github.com/morethanwords/tweb/blob/master/LICENSE * https://github.com/morethanwords/tweb/blob/master/LICENSE
*/ */
import type {Message, StickerSet, Update, NotifyPeer, PeerNotifySettings, PollResults, Poll, WebPage, GroupCall, GroupCallParticipant, ReactionCount, MessagePeerReaction, PhoneCall} from '../layer'; import type {Message, StickerSet, Update, NotifyPeer, PeerNotifySettings, PollResults, Poll, WebPage, GroupCall, GroupCallParticipant, ReactionCount, MessagePeerReaction, PhoneCall, Config} from '../layer';
import type {AppMessagesManager, Dialog, MessagesStorageKey, MyMessage} from './appManagers/appMessagesManager'; import type {AppMessagesManager, Dialog, MessagesStorageKey, MyMessage} from './appManagers/appMessagesManager';
import type {MyDialogFilter} from './storages/filters'; import type {MyDialogFilter} from './storages/filters';
import type {Folder} from './storages/dialogs'; import type {Folder} from './storages/dialogs';
@ -143,6 +143,7 @@ export type BroadcastEvents = {
'premium_toggle': boolean, 'premium_toggle': boolean,
'config': Config,
'app_config': MTAppConfig 'app_config': MTAppConfig
}; };

162
src/lib/serviceWorker/download.ts

@ -0,0 +1,162 @@
/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
import type {ServiceDownloadTaskPayload} from './serviceMessagePort';
import type ServiceMessagePort from './serviceMessagePort';
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise';
import makeError from '../../helpers/makeError';
import pause from '../../helpers/schedulers/pause';
type DownloadType = Uint8Array;
type DownloadItem = ServiceDownloadTaskPayload & {
// transformStream: TransformStream<DownloadType, DownloadType>,
readableStream: ReadableStream<DownloadType>,
// writableStream: WritableStream<DownloadType>,
// writer: WritableStreamDefaultWriter<DownloadType>,
// controller: TransformStreamDefaultController<DownloadType>,
controller: ReadableStreamController<Uint8Array>,
promise: CancellablePromise<void>,
// downloadPromise: Promise<void>,
used?: boolean
};
const downloadMap: Map<string, DownloadItem> = new Map();
const DOWNLOAD_ERROR = makeError('UNKNOWN');
const DOWNLOAD_TEST = false;
type A = Parameters<ServiceMessagePort<false>['addMultipleEventsListeners']>[0];
const events: A = {
download: (payload) => {
const {id} = payload;
if(downloadMap.has(id)) {
return Promise.reject(DOWNLOAD_ERROR);
}
// const y = (20 * 1024 * 1024) / payload.limitPart;
// const strategy = new ByteLengthQueuingStrategy({highWaterMark: y});
// let controller: TransformStreamDefaultController<DownloadType>;
const strategy = new CountQueuingStrategy({highWaterMark: 1});
// const transformStream = new TransformStream<DownloadType, DownloadType>(/* {
// start: (_controller) => controller = _controller,
// }, */undefined, strategy, strategy);
// const {readable, writable} = transformStream;
// const writer = writable.getWriter();
const promise = deferredPromise<void>();
promise.then(() => {
setTimeout(() => {
downloadMap.delete(id);
}, 5e3);
}, () => {
downloadMap.delete(id);
});
// writer.closed.then(promise.resolve, promise.reject);
let controller: ReadableStreamController<any>;
const readable = new ReadableStream({
start: (_controller) => {
controller = _controller;
},
cancel: (reason) => {
promise.reject(DOWNLOAD_ERROR);
}
}, strategy);
// writer.closed.catch(noop).finally(() => {
// log.error('closed writer');
// onEnd();
// });
// const downloadPromise = writer.closed.catch(() => {throw DOWNLOAD_ERROR;});
const item: DownloadItem = {
...payload,
// transformStream,
readableStream: readable,
// writableStream: writable,
// writer,
// downloadPromise,
promise,
controller
};
downloadMap.set(id, item);
// return downloadPromise;
return promise.catch(() => {throw DOWNLOAD_ERROR});
},
downloadChunk: ({id, chunk}) => {
const item = downloadMap.get(id);
if(!item) {
return Promise.reject();
}
// return item.controller.enqueue(chunk);
// return item.writer.write(chunk);
return item.controller.enqueue(chunk);
},
downloadFinalize: (id) => {
const item = downloadMap.get(id);
if(!item) {
return Promise.reject();
}
item.promise.resolve();
// return item.controller.terminate();
// return item.writer.close();
return item.controller.close();
},
downloadCancel: (id) => {
const item = downloadMap.get(id);
if(!item) {
return;
}
item.promise.reject();
// return item.controller.error();
// return item.writer.abort();
return item.controller.error();
}
};
export default function handleDownload(serviceMessagePort: ServiceMessagePort<false>) {
serviceMessagePort.addMultipleEventsListeners(events);
return {
onDownloadFetch,
onClosedWindows: cancelAllDownloads
};
}
function onDownloadFetch(event: FetchEvent, params: string) {
event.respondWith(pause(100).then(() => {
const item = downloadMap.get(params);
if(!item || (item.used && !DOWNLOAD_TEST)) {
return;
}
item.used = true;
const stream = item.readableStream;
const response = new Response(stream, {headers: item.headers});
return response;
}));
// event.respondWith(response);
}
function cancelAllDownloads() {
if(downloadMap.size) {
for(const [id, item] of downloadMap) {
// item.writer.abort().catch(noop);
item.controller.error();
}
}
}

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

@ -14,12 +14,11 @@ import onStreamFetch from './stream';
import {closeAllNotifications, onPing} from './push'; import {closeAllNotifications, onPing} from './push';
import CacheStorageController from '../files/cacheStorage'; import CacheStorageController from '../files/cacheStorage';
import {IS_SAFARI} from '../../environment/userAgent'; import {IS_SAFARI} from '../../environment/userAgent';
import ServiceMessagePort, {ServiceDownloadTaskPayload} from './serviceMessagePort'; import ServiceMessagePort from './serviceMessagePort';
import listenMessagePort from '../../helpers/listenMessagePort'; import listenMessagePort from '../../helpers/listenMessagePort';
import {getWindowClients} from '../../helpers/context'; import {getWindowClients} from '../../helpers/context';
import {MessageSendPort} from '../mtproto/superMessagePort'; import {MessageSendPort} from '../mtproto/superMessagePort';
import noop from '../../helpers/noop'; import handleDownload from './download';
import makeError from '../../helpers/makeError';
export const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn); export const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn);
const ctx = self as any as ServiceWorkerGlobalScope; const ctx = self as any as ServiceWorkerGlobalScope;
@ -52,19 +51,6 @@ const onWindowConnected = (source: WindowClient) => {
connectedWindows.add(source.id); connectedWindows.add(source.id);
}; };
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>(); export const serviceMessagePort = new ServiceMessagePort<false>();
serviceMessagePort.addMultipleEventsListeners({ serviceMessagePort.addMultipleEventsListeners({
notificationsClear: closeAllNotifications, notificationsClear: closeAllNotifications,
@ -79,86 +65,14 @@ serviceMessagePort.addMultipleEventsListeners({
hello: (payload, source) => { hello: (payload, source) => {
onWindowConnected(source as any as WindowClient); 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();
} }
}); });
const {
onDownloadFetch,
onClosedWindows: onDownloadClosedWindows
} = handleDownload(serviceMessagePort);
// * service worker can be killed, so won't get 'hello' event // * service worker can be killed, so won't get 'hello' event
getWindowClients().then((windowClients) => { getWindowClients().then((windowClients) => {
log(`got ${windowClients.length} windows from the start`); log(`got ${windowClients.length} windows from the start`);
@ -184,11 +98,7 @@ listenMessagePort(serviceMessagePort, undefined, (source) => {
_mtprotoMessagePort = undefined; _mtprotoMessagePort = undefined;
} }
if(downloadMap.size) { onDownloadClosedWindows();
for(const [id, item] of downloadMap) {
item.writer.abort().catch(noop);
}
}
} }
}); });
// #endif // #endif
@ -216,14 +126,7 @@ const onFetch = (event: FetchEvent): void => {
} }
case 'download': { case 'download': {
const item = downloadMap.get(params); onDownloadFetch(event, params);
if(!item || item.used) {
break;
}
item.used = true;
const response = new Response(item.transformStream.readable, {headers: item.headers});
event.respondWith(response);
break; break;
} }
} }
@ -231,7 +134,8 @@ const onFetch = (event: FetchEvent): void => {
log.error('fetch error', err); log.error('fetch error', err);
event.respondWith(new Response('', { event.respondWith(new Response('', {
status: 500, status: 500,
statusText: 'Internal Server Error' statusText: 'Internal Server Error',
headers: {'Cache-Control': 'no-cache'}
})); }));
} }
}; };
@ -242,13 +146,13 @@ const onChangeState = () => {
ctx.addEventListener('install', (event) => { ctx.addEventListener('install', (event) => {
log('installing'); log('installing');
event.waitUntil(ctx.skipWaiting()); // Activate worker immediately event.waitUntil(ctx.skipWaiting().then(() => log('skipped waiting'))); // Activate worker immediately
}); });
ctx.addEventListener('activate', (event) => { ctx.addEventListener('activate', (event) => {
log('activating', ctx); log('activating', ctx);
event.waitUntil(ctx.caches.delete(CACHE_ASSETS_NAME)); event.waitUntil(ctx.caches.delete(CACHE_ASSETS_NAME).then(() => log('cleared assets cache')));
event.waitUntil(ctx.clients.claim()); event.waitUntil(ctx.clients.claim().then(() => log('claimed clients')));
}); });
// ctx.onerror = (error) => { // ctx.onerror = (error) => {

3
src/lib/serviceWorker/serviceMessagePort.ts

@ -28,9 +28,8 @@ export type ServiceRequestFilePartTaskPayload = {
}; };
export type ServiceDownloadTaskPayload = { export type ServiceDownloadTaskPayload = {
fileName: string,
headers: any, headers: any,
id: string, id: string
}; };
export type ServiceEvent = { export type ServiceEvent = {

4
src/scss/partials/_document.scss

@ -23,6 +23,10 @@
line-height: 1; line-height: 1;
text-align: center; text-align: center;
html.is-safari & {
-webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%); // fix safari overflow
}
&-text { &-text {
opacity: 0; opacity: 0;

Loading…
Cancel
Save