Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01e799274a |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ztimson/utils",
|
||||
"version": "0.30.8",
|
||||
"version": "0.30.9",
|
||||
"description": "Utility library",
|
||||
"author": "Zak Timson",
|
||||
"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)
|
||||
*
|
||||
* @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: {
|
||||
url: string;
|
||||
@@ -102,7 +102,7 @@ export function uploadWithProgress<T>(options: {
|
||||
options.files.forEach(f => formData.append('files', f));
|
||||
|
||||
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('error', () => rej(JSONAttemptParse(xhr.responseText)));
|
||||
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
|
||||
* ```js
|
||||
* const promise = new Promise((resolve, reject, progress) => {
|
||||
* const max = 10;
|
||||
* for(let i = 0; i < max; i++) progress(i / max);
|
||||
* const promise = new PromiseProgress((resolve, reject, progress) => {
|
||||
* const total = 100;
|
||||
* for(let i = 0; i <= total; i++) progress(i, total);
|
||||
* 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)
|
||||
* .catch(console.error)
|
||||
* .finally(...);
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
export class PromiseProgress<T> extends Promise<T> {
|
||||
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 progress() { return this._progress; }
|
||||
set progress(p: number) {
|
||||
if(p == this._progress) return;
|
||||
this._progress = p;
|
||||
this.listeners.forEach(l => l(p));
|
||||
get meta() { return this._meta; }
|
||||
get start() { return this._meta.start; }
|
||||
get end() { return this._meta.end; }
|
||||
get elapsed() { return this._meta.elapsed; }
|
||||
get speed() { return this._meta.speed; }
|
||||
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(
|
||||
(value: T) => resolve(value),
|
||||
(reason: any) => reject(reason),
|
||||
(progress: number) => this.progress = progress,
|
||||
(value: T) => { this._meta = {...this._meta, end: Date.now()}; resolve(value); },
|
||||
(reason: any) => { this._meta = {...this._meta, end: Date.now()}; reject(reason); },
|
||||
(size: number, total?: number) => this.update(size, total ?? this._meta.total),
|
||||
));
|
||||
}
|
||||
|
||||
static from<T>(promise: Promise<T>): PromiseProgress<T> {
|
||||
if(promise instanceof PromiseProgress) return promise;
|
||||
return new PromiseProgress<T>((res, rej) => promise
|
||||
.then((...args) => res(...args))
|
||||
.catch((...args) => rej(...args)));
|
||||
return new PromiseProgress<T>((res, rej) => promise.then(res).catch(rej));
|
||||
}
|
||||
|
||||
private from(promise: Promise<T>): PromiseProgress<T> {
|
||||
const newPromise = PromiseProgress.from(promise);
|
||||
this.onProgress(p => newPromise.progress = p);
|
||||
this.onProgress(m => newPromise.update(m.size, m.total));
|
||||
return newPromise;
|
||||
}
|
||||
|
||||
onProgress(callback: ProgressCallback) {
|
||||
this.listeners.push(callback);
|
||||
(this.listeners ||= []).push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
then(res?: (v: T) => any, rej?: (err: any) => any): PromiseProgress<any> {
|
||||
const resp = super.then(res, rej);
|
||||
return this.from(resp);
|
||||
return this.from(super.then(res, rej));
|
||||
}
|
||||
|
||||
catch(rej?: (err: any) => any): PromiseProgress<any> {
|
||||
|
||||
Reference in New Issue
Block a user