tweb/src/lib/mtproto/mtproto.service.ts

287 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* https://github.com/morethanwords/tweb
* Copyright (C) 2019-2021 Eduard Kuzmenko
* https://github.com/morethanwords/tweb/blob/master/LICENSE
*/
/// #if MTPROTO_SW
import './mtproto.worker';
/// #endif
//import CacheStorageController from '../cacheStorage';
import { isSafari } from '../../helpers/userAgent';
import { logger, LogTypes } from '../logger';
import type { DownloadOptions } from './apiFileManager';
import type { WorkerTaskTemplate } from '../../types';
import { notifySomeone } from '../../helpers/context';
import type { InputFileLocation, UploadFile } from '../../layer';
import { CancellablePromise, deferredPromise } from '../../helpers/cancellablePromise';
const log = logger('SW', LogTypes.Error | LogTypes.Debug | LogTypes.Log | LogTypes.Warn);
const ctx = self as any as ServiceWorkerGlobalScope;
const deferredPromises: {[taskId: number]: CancellablePromise<any>} = {};
export interface ServiceWorkerTask extends WorkerTaskTemplate {
type: 'requestFilePart',
payload: [number, InputFileLocation, number, number]
};
export interface ServiceWorkerTaskResponse extends WorkerTaskTemplate {
type: 'requestFilePart',
payload?: UploadFile.uploadFile,
originalPayload?: ServiceWorkerTask['payload']
};
/// #if !MTPROTO_SW
ctx.addEventListener('message', (e) => {
const task = e.data as ServiceWorkerTaskResponse;
const promise = deferredPromises[task.id];
if(task.error) {
promise.reject(task.error);
} else {
promise.resolve(task.payload);
}
delete deferredPromises[task.id];
});
/// #endif
//const cacheStorage = new CacheStorageController('cachedAssets');
let taskId = 0;
function isCorrectResponse(response: Response) {
return response.ok && response.status === 200;
}
async function requestCache(event: FetchEvent) {
try {
const cache = await ctx.caches.open('cachedAssets');
const file = await cache.match(event.request);
if(file && isCorrectResponse(file)) {
return file;
}
let response = await fetch(event.request);
if(isCorrectResponse(response)) {
cache.put(event.request, response.clone());
} else if(response.status === 304) { // possible fix for 304 in Safari
const url = event.request.url.replace(/\?.+$/, '') + '?' + (Math.random() * 100000 | 0);
const request = new Request(url);
response = await fetch(request);
if(isCorrectResponse(response)) {
cache.put(request, response.clone());
}
}
return response;
} catch(err) {
return fetch(event.request);
}
}
const onFetch = (event: FetchEvent): void => {
if(event.request.url.indexOf(location.origin + '/') === 0 && event.request.url.match(/\.(js|css|jpe?g|json|wasm|png|mp3|svg|tgs|ico|woff2?|ttf|webmanifest?)(?:\?.*)?$/)) {
return event.respondWith(requestCache(event));
}
try {
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
//log.debug('[fetch]:', event);
switch(scope) {
case 'stream': {
const range = parseRange(event.request.headers.get('Range'));
let [offset, end] = range;
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
//const fileName = getFileNameByLocation(info.location);
// ! если грузить очень большое видео чанками по 512Кб в мобильном Safari, то стрим не запустится
const limitPart = info.size > (75 * 1024 * 1024) ? STREAM_CHUNK_UPPER_LIMIT : STREAM_CHUNK_MIDDLE_LIMIT;
/* if(info.size > limitPart && isSafari && offset === limitPart) {
//end = info.size - 1;
//offset = info.size - 1 - limitPart;
offset = info.size - (info.size % limitPart);
} */
//log.debug('[stream]', url, offset, end);
event.respondWith(Promise.race([
timeout(45 * 1000),
new Promise<Response>((resolve, reject) => {
// safari workaround
const possibleResponse = responseForSafariFirstRange(range, info.mimeType, info.size);
if(possibleResponse) {
return resolve(possibleResponse);
}
const limit = end && end < limitPart ? alignLimit(end - offset + 1) : limitPart;
const alignedOffset = alignOffset(offset, limit);
//log.debug('[stream] requestFilePart:', /* info.dcId, info.location, */ alignedOffset, limit);
const task: ServiceWorkerTask = {
type: 'requestFilePart',
id: taskId++,
payload: [info.dcId, info.location, alignedOffset, limit]
};
const deferred = deferredPromises[task.id] = deferredPromise<UploadFile.uploadFile>();
deferred.then(result => {
let ab = result.bytes as Uint8Array;
//log.debug('[stream] requestFilePart result:', result);
const headers: Record<string, string> = {
'Accept-Ranges': 'bytes',
'Content-Range': `bytes ${alignedOffset}-${alignedOffset + ab.byteLength - 1}/${info.size || '*'}`,
'Content-Length': `${ab.byteLength}`,
};
if(info.mimeType) headers['Content-Type'] = info.mimeType;
if(isSafari) {
ab = ab.slice(offset - alignedOffset, end - alignedOffset + 1);
headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`;
headers['Content-Length'] = `${ab.byteLength}`;
}
// simulate slow connection
//setTimeout(() => {
resolve(new Response(ab, {
status: 206,
statusText: 'Partial Content',
headers,
}));
//}, 2.5e3);
}).catch(err => {});
notifySomeone(task);
})
]));
break;
}
}
} catch(err) {
event.respondWith(new Response('', {
status: 500,
statusText: 'Internal Server Error',
}));
}
};
const onChangeState = () => {
ctx.onfetch = onFetch;
};
ctx.addEventListener('install', (event) => {
log('installing');
/* initCache();
event.waitUntil(
initNetwork(),
); */
event.waitUntil(ctx.skipWaiting()); // Activate worker immediately
});
ctx.addEventListener('activate', (event) => {
log('activating', ctx);
/* if (!ctx.cache) initCache();
if (!ctx.network) initNetwork(); */
event.waitUntil(ctx.caches.delete('cachedAssets'));
event.waitUntil(ctx.clients.claim());
});
function timeout(delay: number): Promise<Response> {
return new Promise(((resolve) => {
setTimeout(() => {
resolve(new Response('', {
status: 408,
statusText: 'Request timed out.',
}));
}, delay);
}));
}
function responseForSafariFirstRange(range: [number, number], mimeType: string, size: number): Response {
if(range[0] === 0 && range[1] === 1) {
return new Response(new Uint8Array(2).buffer, {
status: 206,
statusText: 'Partial Content',
headers: {
'Accept-Ranges': 'bytes',
'Content-Range': `bytes 0-1/${size || '*'}`,
'Content-Length': '2',
'Content-Type': mimeType || 'video/mp4',
},
});
}
return null;
}
ctx.onerror = (error) => {
log.error('error:', error);
};
ctx.onunhandledrejection = (error) => {
log.error('onunhandledrejection:', error);
};
ctx.onoffline = ctx.ononline = onChangeState;
onChangeState();
/* const STREAM_CHUNK_UPPER_LIMIT = 256 * 1024;
const SMALLEST_CHUNK_LIMIT = 256 * 4; */
/* const STREAM_CHUNK_UPPER_LIMIT = 1024 * 1024;
const SMALLEST_CHUNK_LIMIT = 1024 * 4; */
const STREAM_CHUNK_MIDDLE_LIMIT = 512 * 1024;
const STREAM_CHUNK_UPPER_LIMIT = 1024 * 1024;
const SMALLEST_CHUNK_LIMIT = 512 * 4;
function parseRange(header: string): [number, number] {
if(!header) return [0, 0];
const [, chunks] = header.split('=');
const ranges = chunks.split(', ');
const [offset, end] = ranges[0].split('-');
return [+offset, +end || 0];
}
function alignOffset(offset: number, base = SMALLEST_CHUNK_LIMIT) {
return offset - (offset % base);
}
function alignLimit(limit: number) {
return 2 ** Math.ceil(Math.log(limit) / Math.log(2));
}
/* ctx.addEventListener('push', (event) => {
console.log('[Service Worker] Push Received.');
console.log(`[Service Worker] Push had this data: "${event.data.text()}"`, event, event.data);
const title = 'Push Push Push';
const options = {};
// const options = {
// body: 'Yay it works.',
// icon: 'images/icon.png',
// badge: 'images/badge.png'
// };
event.waitUntil(ctx.registration.showNotification(title, options));
}); */
//export default () => {};
//MOUNT_CLASS_TO.onFetch = onFetch;