Added download with progress
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ztimson/utils",
|
"name": "@ztimson/utils",
|
||||||
"version": "0.30.8",
|
"version": "0.30.9",
|
||||||
"description": "Utility library",
|
"description": "Utility library",
|
||||||
"author": "Zak Timson",
|
"author": "Zak Timson",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
+39
-3
@@ -82,11 +82,11 @@ export function timestampFilename(name?: string, date: Date | number | string =
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload file to URL with progress callback using PromiseProgress
|
* Upload file to URL with progress (size/total/speed/eta/...) via PromiseProgress
|
||||||
* Works in both browser (with progress) and Node.js (fallback without progress)
|
* Works in both browser (with progress) and Node.js (fallback without progress)
|
||||||
*
|
*
|
||||||
* @param {{url: string, files: File[], headers?: {[p: string]: string}, withCredentials?: boolean}} options
|
* @param {{url: string, files: File[], headers?: {[p: string]: string}, withCredentials?: boolean}} options
|
||||||
* @return {PromiseProgress<T>} Promise of request with `onProgress` callback
|
* @return {PromiseProgress<T>} Promise of request with `onProgress` callback & meta getters
|
||||||
*/
|
*/
|
||||||
export function uploadWithProgress<T>(options: {
|
export function uploadWithProgress<T>(options: {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -102,7 +102,7 @@ export function uploadWithProgress<T>(options: {
|
|||||||
options.files.forEach(f => formData.append('files', f));
|
options.files.forEach(f => formData.append('files', f));
|
||||||
|
|
||||||
xhr.withCredentials = !!options.withCredentials;
|
xhr.withCredentials = !!options.withCredentials;
|
||||||
xhr.upload.addEventListener('progress', (event) => event.lengthComputable ? prog(event.loaded / event.total) : null);
|
xhr.upload.addEventListener('progress', (event) => event.lengthComputable ? prog(event.loaded, event.total) : null);
|
||||||
xhr.addEventListener('loadend', () => res(<T>JSONAttemptParse(xhr.responseText)));
|
xhr.addEventListener('loadend', () => res(<T>JSONAttemptParse(xhr.responseText)));
|
||||||
xhr.addEventListener('error', () => rej(JSONAttemptParse(xhr.responseText)));
|
xhr.addEventListener('error', () => rej(JSONAttemptParse(xhr.responseText)));
|
||||||
xhr.addEventListener('timeout', () => rej({error: 'Request timed out'}));
|
xhr.addEventListener('timeout', () => rej({error: 'Request timed out'}));
|
||||||
@@ -131,3 +131,39 @@ export function uploadWithProgress<T>(options: {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download file from URL with progress (size/total/speed/eta/...) via PromiseProgress
|
||||||
|
* Streams the response body for progress when supported, falls back to a plain blob otherwise
|
||||||
|
*
|
||||||
|
* @param {{url: string, name?: string, headers?: {[p: string]: string}}} options name - if set, triggers a file save via `downloadFile`
|
||||||
|
* @return {PromiseProgress<Blob>} Promise of downloaded blob with `onProgress` callback & meta getters
|
||||||
|
*/
|
||||||
|
export function downloadWithProgress(options: {url: string; name?: string; headers?: {[key: string]: string}}): PromiseProgress<Blob> {
|
||||||
|
return new PromiseProgress<Blob>(async (res, rej, prog) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(options.url, {headers: options.headers || {}});
|
||||||
|
if(!response.ok) return rej(JSONAttemptParse(await response.text()));
|
||||||
|
const total = Number(response.headers.get('content-length')) || 0;
|
||||||
|
if(!response.body?.getReader) {
|
||||||
|
const blob = await response.blob();
|
||||||
|
options.name && downloadFile(blob, options.name);
|
||||||
|
return res(blob);
|
||||||
|
}
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
let size = 0;
|
||||||
|
while(true) {
|
||||||
|
const {done, value} = await reader.read();
|
||||||
|
if(done) break;
|
||||||
|
chunks.push(value);
|
||||||
|
prog(size += value.length, total);
|
||||||
|
}
|
||||||
|
const blob = new Blob(chunks as BlobPart[]);
|
||||||
|
options.name && downloadFile(blob, options.name);
|
||||||
|
res(blob);
|
||||||
|
} catch (error) {
|
||||||
|
rej(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+43
-25
@@ -1,65 +1,83 @@
|
|||||||
export type ProgressCallback = (progress: number) => any;
|
export interface ProgressMeta {
|
||||||
|
start: number;
|
||||||
|
end: number | null;
|
||||||
|
elapsed: number; // seconds
|
||||||
|
speed: number; // bytes/sec
|
||||||
|
progress: number; // 0-1
|
||||||
|
size: number; // bytes transferred
|
||||||
|
total: number; // total bytes
|
||||||
|
eta: number | null; // seconds remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProgressCallback = (meta: ProgressMeta) => any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A promise that fires the `onProgress` callback on incremental progress
|
* A promise that tracks transfer progress (size/total/speed/eta/elapsed) via `onProgress` callback & direct getters
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```js
|
* ```js
|
||||||
* const promise = new Promise((resolve, reject, progress) => {
|
* const promise = new PromiseProgress((resolve, reject, progress) => {
|
||||||
* const max = 10;
|
* const total = 100;
|
||||||
* for(let i = 0; i < max; i++) progress(i / max);
|
* for(let i = 0; i <= total; i++) progress(i, total);
|
||||||
* resolve(1);
|
* resolve(1);
|
||||||
* });
|
* });
|
||||||
*
|
*
|
||||||
* console.log(promise.progress);
|
* console.log(promise.progress, promise.speed, promise.eta);
|
||||||
*
|
*
|
||||||
* promise.onProgress(console.log)
|
* promise.onProgress(m => console.log(m.progress, m.eta))
|
||||||
* .then(console.log)
|
* .then(console.log)
|
||||||
* .catch(console.error)
|
* .catch(console.error)
|
||||||
* .finally(...);
|
* .finally(...);
|
||||||
*
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class PromiseProgress<T> extends Promise<T> {
|
export class PromiseProgress<T> extends Promise<T> {
|
||||||
private listeners: ProgressCallback[] = [];
|
private listeners: ProgressCallback[] = [];
|
||||||
|
private _meta: ProgressMeta = {start: Date.now(), end: null, elapsed: 0, speed: 0, progress: 0, size: 0, total: 0, eta: null};
|
||||||
|
|
||||||
private _progress = 0;
|
get meta() { return this._meta; }
|
||||||
get progress() { return this._progress; }
|
get start() { return this._meta.start; }
|
||||||
set progress(p: number) {
|
get end() { return this._meta.end; }
|
||||||
if(p == this._progress) return;
|
get elapsed() { return this._meta.elapsed; }
|
||||||
this._progress = p;
|
get speed() { return this._meta.speed; }
|
||||||
this.listeners.forEach(l => l(p));
|
get progress() { return this._meta.progress; }
|
||||||
|
get size() { return this._meta.size; }
|
||||||
|
get total() { return this._meta.total; }
|
||||||
|
get eta() { return this._meta.eta; }
|
||||||
|
|
||||||
|
private update(size: number, total: number) {
|
||||||
|
const elapsed = (Date.now() - this._meta.start) / 1000;
|
||||||
|
const speed = elapsed ? size / elapsed : 0;
|
||||||
|
const progress = total ? size / total : this._meta.progress;
|
||||||
|
this._meta = {...this._meta, size, total, progress, elapsed, speed, eta: speed && total ? (total - size) / speed : null};
|
||||||
|
(this.listeners ||= []).forEach(l => l(this._meta));
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(executor: (resolve: (value: T) => any, reject: (reason: any) => void, progress: (progress: number) => any) => void) {
|
constructor(executor: (resolve: (value: T) => any, reject: (reason: any) => void, progress: (size: number, total?: number) => any) => void) {
|
||||||
super((resolve, reject) => executor(
|
super((resolve, reject) => executor(
|
||||||
(value: T) => resolve(value),
|
(value: T) => { this._meta = {...this._meta, end: Date.now()}; resolve(value); },
|
||||||
(reason: any) => reject(reason),
|
(reason: any) => { this._meta = {...this._meta, end: Date.now()}; reject(reason); },
|
||||||
(progress: number) => this.progress = progress,
|
(size: number, total?: number) => this.update(size, total ?? this._meta.total),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static from<T>(promise: Promise<T>): PromiseProgress<T> {
|
static from<T>(promise: Promise<T>): PromiseProgress<T> {
|
||||||
if(promise instanceof PromiseProgress) return promise;
|
if(promise instanceof PromiseProgress) return promise;
|
||||||
return new PromiseProgress<T>((res, rej) => promise
|
return new PromiseProgress<T>((res, rej) => promise.then(res).catch(rej));
|
||||||
.then((...args) => res(...args))
|
|
||||||
.catch((...args) => rej(...args)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private from(promise: Promise<T>): PromiseProgress<T> {
|
private from(promise: Promise<T>): PromiseProgress<T> {
|
||||||
const newPromise = PromiseProgress.from(promise);
|
const newPromise = PromiseProgress.from(promise);
|
||||||
this.onProgress(p => newPromise.progress = p);
|
this.onProgress(m => newPromise.update(m.size, m.total));
|
||||||
return newPromise;
|
return newPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress(callback: ProgressCallback) {
|
onProgress(callback: ProgressCallback) {
|
||||||
this.listeners.push(callback);
|
(this.listeners ||= []).push(callback);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
then(res?: (v: T) => any, rej?: (err: any) => any): PromiseProgress<any> {
|
then(res?: (v: T) => any, rej?: (err: any) => any): PromiseProgress<any> {
|
||||||
const resp = super.then(res, rej);
|
return this.from(super.then(res, rej));
|
||||||
return this.from(resp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catch(rej?: (err: any) => any): PromiseProgress<any> {
|
catch(rej?: (err: any) => any): PromiseProgress<any> {
|
||||||
|
|||||||
Reference in New Issue
Block a user