@kaciras/utilities
Advanced tools
Comparing version 0.6.3 to 0.6.4
@@ -41,3 +41,3 @@ const htmlEscapes = { | ||
* | ||
* <h1>Alternatives</h1> | ||
* # Alternatives | ||
* If you don't need serialization, use `URL.createObjectURL` for better performance. | ||
@@ -158,219 +158,205 @@ */ function blobToBase64URL(blob) { | ||
* - Division conversion: "90s" to "1.5m". | ||
* - Modulo conversion: "90s" to "1m 30s". | ||
* - Modulo conversion: "90s" to "1m 30s". | ||
* | ||
* The n2s and s2n means "number to string" and "string to number". | ||
* | ||
* All conversion functions take a unit argument, which is an array of | ||
* length 2N containing the N units from smallest to largest, and | ||
* the multiplier to the next unit. | ||
* e.g. | ||
* [ | ||
* "second", 60, // 60 seconds is a minute. | ||
* "minute", 60, // 60 minute is a hour. | ||
* "hour", Infinity, // No more units, the multiplier is infinity. | ||
* ] | ||
*/ function n2sDivision(units, value, uIndex) { | ||
if (!Number.isFinite(value)) { | ||
throw new TypeError(`${value} is not a finite number`); | ||
} | ||
let v = Math.abs(value); | ||
for(; uIndex < units.length; uIndex += 2){ | ||
if (v < units[uIndex + 1]) break; | ||
v /= units[uIndex + 1]; | ||
} | ||
if (value < 0) v = -v; | ||
return `${Number(v.toFixed(2))} ${units[uIndex]}`; | ||
} | ||
*/ //@formatter:off | ||
const SIZE_UNITS = [ | ||
"B", | ||
"KB", | ||
"MB", | ||
"GB", | ||
"TB", | ||
"PB", | ||
"EB", | ||
"ZB", | ||
"YB" | ||
]; | ||
const SIZE_FRACTIONS_SI = [ | ||
1, | ||
1e3, | ||
1e6, | ||
1e9, | ||
1e12, | ||
1e15, | ||
1e18, | ||
1e21, | ||
1e24 | ||
]; | ||
const SIZE_FRACTIONS_IEC = [ | ||
1, | ||
2 ** 10, | ||
2 ** 20, | ||
2 ** 30, | ||
2 ** 40, | ||
2 ** 50, | ||
2 ** 60, | ||
2 ** 70, | ||
2 ** 80 | ||
]; | ||
const TIME_UNITS = [ | ||
"ns", | ||
"us", | ||
"ms", | ||
"s", | ||
"m", | ||
"h", | ||
"d" | ||
]; | ||
const TIME_FRACTIONS = [ | ||
1, | ||
1e3, | ||
1e6, | ||
1e9, | ||
6e10, | ||
36e11, | ||
864e11 | ||
]; | ||
// @formatter:on | ||
const divRE = /^([-+0-9.]+)\s*(\w+)$/; | ||
function s2nDivision(name, units, value) { | ||
const match = divRE.exec(value); | ||
if (!match) { | ||
throw new Error(`Can not convert "${value}" to ${name}`); | ||
const groupRE = /\d+([a-z]+)\s*/gi; | ||
class UnitConvertor { | ||
constructor(name, units, fractions){ | ||
this.name = name; | ||
this.units = units; | ||
this.fractions = fractions; | ||
} | ||
const [, v, unit] = match; | ||
let result = Number(v); | ||
for(let i = 0; i < units.length; i += 2){ | ||
if (units[i] === unit) { | ||
return result; | ||
} else { | ||
result *= units[i + 1]; | ||
/** | ||
* Get the fraction of the unit, throw error if the unit is invalid. | ||
*/ getFraction(unit) { | ||
if (unit === undefined) { | ||
return 1; | ||
} | ||
const { units , name , fractions } = this; | ||
const i = units.indexOf(unit); | ||
if (i !== -1) { | ||
return fractions[i]; | ||
} | ||
throw new Error(`Unknown ${name} unit: ${unit}`); | ||
} | ||
throw new Error(`Unknown ${name} unit: ${unit}`); | ||
} | ||
function n2sModulo(name, units, value, unit, parts = 2) { | ||
if (!Number.isFinite(value)) { | ||
throw new TypeError(`${value} is not a finite number`); | ||
/** | ||
* Find the index of the largest fraction that is less than the value. | ||
*/ largest(value) { | ||
const s = this.fractions; | ||
let i = 0; | ||
// When i outbound, (s[i] <= value) is false. | ||
while(s[i] <= value)i++; | ||
// (i = 0) when (value < 1), fallback to the minimum unit. | ||
return Math.max(0, i - 1); | ||
} | ||
if (value < 0) { | ||
throw new Error(`value (${value}) can not be negative`); | ||
/** | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param precision The number of digits to appear after the decimal point. | ||
*/ n2sDivision(value, unit, precision = 2) { | ||
if (!Number.isFinite(value)) { | ||
throw new TypeError(`${value} is not a finite number`); | ||
} | ||
const { units , fractions } = this; | ||
let v = value * this.getFraction(unit); | ||
const i = this.largest(Math.abs(v)); | ||
v /= fractions[i]; | ||
return `${Number(v.toFixed(precision))} ${units[i]}`; | ||
} | ||
let i = units.indexOf(unit); | ||
let d = 1; | ||
if (i === -1) { | ||
throw new Error(`Unknown ${name} unit: ${unit}`); | ||
/** | ||
* Convert string to number in specified unit. | ||
* | ||
* @example | ||
* durationConvertor.s2nModulo("10000d", "d"); // 10000 | ||
* durationConvertor.s2nModulo("0h", "s"); // 0 | ||
* durationConvertor.s2nModulo("0.5m", "s"); // 30 | ||
* durationConvertor.s2nModulo("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
*/ s2nDivision(value, unit) { | ||
const match = divRE.exec(value); | ||
if (!match) { | ||
throw new Error(`Can not convert "${value}" to ${this.name}`); | ||
} | ||
const [, v, u] = match; | ||
return Number(v) * this.getFraction(u) / this.getFraction(unit); | ||
} | ||
// Find index of the largest unit. | ||
for(;; i += 2){ | ||
const x = d * units[i + 1]; | ||
if (value < x) break; | ||
else d = x; | ||
} | ||
const groups = []; | ||
// Backtrace to calculate each group. | ||
for(; // 1.16e-14 = 1/24/60/60/1000/1000/1000 | ||
i >= 0 && parts > 0 && value > 1.16e-14; i -= 2, parts -= 1){ | ||
const t = Math.floor(value / d); | ||
// Avoid leading zeros. | ||
if (groups.length || t !== 0) { | ||
/** | ||
* Formats a value and unit. | ||
* | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @example | ||
* durationConvertor.n2sModulo(0.1, "ns"); // "0ns" | ||
* durationConvertor.n2sModulo(0, "s"); // "0s" | ||
* durationConvertor.n2sModulo(10000, "d"); // "10000d" | ||
* durationConvertor.n2sModulo(97215, "s", 4); // "1d 3h 0m 15s" | ||
* durationConvertor.n2sModulo(0.522, "h"); // "31m 19s" | ||
* durationConvertor.n2sModulo(0.522, "h", 1); // "31m" | ||
* durationConvertor.n2sModulo(0.522, "h", 999); // "31m 19s 200ms" | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param parts Maximum number of groups in result. | ||
*/ n2sModulo(value, unit, parts = 2) { | ||
if (!Number.isFinite(value)) { | ||
throw new TypeError(`${value} is not a finite number`); | ||
} | ||
if (value < 0) { | ||
throw new Error(`value (${value}) can not be negative`); | ||
} | ||
const { units , fractions } = this; | ||
value *= this.getFraction(unit); | ||
const groups = []; | ||
let i = this.largest(value); | ||
// Backtrace to calculate each group. | ||
for(; i >= 0 && parts > 0; i--, parts -= 1){ | ||
const f = fractions[i]; | ||
// Avoid tailing zeros. | ||
if (value * f < 1) break; | ||
const t = Math.floor(value / f); | ||
value %= f; | ||
groups.push(`${t}${units[i]}`); | ||
} | ||
value %= d; | ||
d /= units[i - 1]; | ||
return groups.length ? groups.join(" ") : `0${unit}`; | ||
} | ||
return groups.length ? groups.join(" ") : `0${unit}`; | ||
} | ||
const groupRE = /\d+([a-z]+)\s*/gi; | ||
function s2nModulo(name, units, value, unit) { | ||
const i = units.indexOf(unit); | ||
if (i === -1) { | ||
throw new Error(`Unknown ${name} unit: ${unit}`); | ||
} | ||
let k = units.length - 1; | ||
let seen = 0; | ||
let result = 0; | ||
for (const [matched, u] of value.matchAll(groupRE)){ | ||
const j = units.lastIndexOf(u, k); | ||
k = j - 2; | ||
if (j === -1) { | ||
throw new Error(units.includes(u) ? "Units must be ordered from largest to smallest" : `Unknown ${name} unit: ${u}`); | ||
} | ||
/* | ||
* Similar performance compared to prebuilt table. | ||
* See benchmark/format.js | ||
*/ let n = parseFloat(matched); | ||
if (j > i) { | ||
for(let k = i; k < j; k += 2){ | ||
n *= units[k + 1]; | ||
/** | ||
* Convert string to number in specified unit. | ||
* | ||
* @example | ||
* durationConvertor.s2nModulo("10000d", "d"); // 10000 | ||
* durationConvertor.s2nModulo("0h", "s"); // 0 | ||
* durationConvertor.s2nModulo("0.5m", "s"); // 30 | ||
* durationConvertor.s2nModulo("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
*/ s2nModulo(value, unit) { | ||
const { name , units , fractions } = this; | ||
let k = Infinity; | ||
let seen = 0; | ||
let result = 0; | ||
for (const [matched, u] of value.matchAll(groupRE)){ | ||
const i = units.lastIndexOf(u, k); | ||
k = i - 1; | ||
if (i === -1) { | ||
throw new Error(units.includes(u) ? "Units must be ordered from largest to smallest" : `Unknown ${name} unit: ${u}`); | ||
} | ||
} else { | ||
for(let k = j; k < i; k += 2){ | ||
n /= units[k + 1]; | ||
} | ||
seen += matched.length; | ||
result += parseFloat(matched) * fractions[i]; | ||
} | ||
result += n; | ||
seen += matched.length; | ||
if (seen === value.length && seen > 0) { | ||
return result / this.getFraction(unit); | ||
} | ||
throw new Error(`Can not convert "${value}" to ${name}`); | ||
} | ||
if (seen === value.length && seen > 0) { | ||
return result; | ||
} | ||
throw new Error(`Can not convert "${value}" to ${name}`); | ||
} | ||
const TIME_UNITS = [ | ||
"ns", | ||
1000, | ||
"us", | ||
1000, | ||
"ms", | ||
1000, | ||
"s", | ||
60, | ||
"m", | ||
60, | ||
"h", | ||
24, | ||
"d", | ||
Infinity | ||
]; | ||
/** | ||
* Convert the given duration in to a human-readable format. | ||
* Convert between duration and human-readable string. | ||
* Support units from nanoseconds to days. | ||
* | ||
* @example | ||
* formatDuration(0, "s"); // "0s" | ||
* formatDuration(10000, "d"); // "10000d" | ||
* formatDuration(97215, "s", 4); // "1d 3h 0m 15s" | ||
* formatDuration(0.522, "h"); // "31m 19s" | ||
* formatDuration(0.522, "h", 1); // "31m" | ||
* formatDuration(0.522, "h", 999); // "31m 19s 200ms" | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param parts Maximum number of groups in result. | ||
*/ function formatDuration(value, unit, parts = 2) { | ||
return n2sModulo("time", TIME_UNITS, value, unit, parts); | ||
} | ||
*/ const durationConvertor = new UnitConvertor("time", TIME_UNITS, TIME_FRACTIONS); | ||
/** | ||
* Convert duration string to number in specified unit. | ||
* | ||
* @example | ||
* parseDuration("10000d", "d"); // 10000 | ||
* parseDuration("0h", "s"); // 0 | ||
* parseDuration("0.5m", "s"); // 30 | ||
* parseDuration("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
*/ function parseDuration(value, unit) { | ||
return s2nModulo("time", TIME_UNITS, value, unit); | ||
} | ||
const SIZE_UNITS_SI = [ | ||
"B", | ||
1000, | ||
"KB", | ||
1000, | ||
"MB", | ||
1000, | ||
"GB", | ||
1000, | ||
"TB", | ||
1000, | ||
"PB", | ||
1000, | ||
"EB", | ||
1000, | ||
"ZB", | ||
1000, | ||
"YB", | ||
Infinity | ||
]; | ||
const SIZE_UNITS_IEC = [ | ||
"B", | ||
1024, | ||
"KB", | ||
1024, | ||
"MB", | ||
1024, | ||
"GB", | ||
1024, | ||
"TB", | ||
1024, | ||
"PB", | ||
1024, | ||
"EB", | ||
1024, | ||
"ZB", | ||
1024, | ||
"YB", | ||
Infinity | ||
]; | ||
* Convert between bytes and human-readable string using SI prefixes. | ||
*/ const dataSizeSI = new UnitConvertor("data size", SIZE_UNITS, SIZE_FRACTIONS_SI); | ||
/** | ||
* Convert bytes to a human-readable string. | ||
* | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @param value The number to format. | ||
* @param fraction 1000 for SI or 1024 for IEC. | ||
*/ function formatSize(value, unit, fraction = 1024) { | ||
return `${n2sDivision(fraction === 1024 ? SIZE_UNITS_IEC : SIZE_UNITS_SI, value, 0)}`; | ||
} | ||
* Convert between bytes and human-readable string using IEC prefixes. | ||
*/ const dataSizeIEC = new UnitConvertor("data size", SIZE_UNITS, SIZE_FRACTIONS_IEC); | ||
/** | ||
* Parse size string to number of bytes. | ||
* | ||
* @param value The size string. | ||
* @param fraction 1000 for SI or 1024 for IEC. | ||
*/ function parseSize(value, fraction = 1024) { | ||
return s2nDivision("data size", fraction === 1024 ? SIZE_UNITS_IEC : SIZE_UNITS_SI, value); | ||
} | ||
/** | ||
* A simple string template engine, only support replace placeholders. | ||
@@ -479,3 +465,5 @@ * | ||
} | ||
class FetchClientError extends Error { | ||
/** | ||
* The HTTP request was successful but failed the application layer checks. | ||
*/ class FetchClientError extends Error { | ||
constructor(response){ | ||
@@ -649,2 +637,9 @@ super(`Fetch failed with status: ${response.status}`); | ||
/** | ||
* Call a function silently. returns undefined if any error occurs. | ||
*/ function silentCall(fn) { | ||
try { | ||
return fn(); | ||
} catch {} | ||
} | ||
/** | ||
* Silence a Promise-like object. This is useful for avoiding non-harmful, | ||
@@ -694,2 +689,19 @@ * but potentially confusing "uncaught play promise" rejection error messages. | ||
} | ||
/** | ||
* Create a new instance with the `parent` as prototype and the `value` as child. | ||
* | ||
* # NOTES | ||
* If the parent is a constructor, it will not be called and just use its `prototype`. | ||
* | ||
* Does not support override getter-only properties. | ||
* | ||
* This function does not use `Object.setPrototypeOf` because it has bad performance. | ||
* | ||
* @param parent Prototype of returned object. | ||
* @param value Provide properties for returned object. | ||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf | ||
*/ function inherit(parent, value) { | ||
const proto = typeof parent === "function" ? parent.prototype : parent; | ||
return Object.assign(Object.create(proto), value); | ||
} | ||
@@ -835,4 +847,25 @@ /** | ||
const transferCache = new WeakMap(); | ||
function transfer(obj, transfers) { | ||
/** | ||
* Because RPC should keep the signature of the remote function, we cannot add a parameter | ||
* for transfers to remote function. | ||
* | ||
* We track transferable values with a map. This brings a new problem that | ||
* we cannot ensure that all the values added to the map are used. | ||
* | ||
* This has the potential for memory leaks, so use WeakMap instead of Map. | ||
*/ const transferCache = new WeakMap(); | ||
/** | ||
* By default, every function parameter, return value and object property value is copied, | ||
* in the sense of structured cloning. | ||
* | ||
* If you want a value to be transferred rather than copied, | ||
* you can wrap the value in a transfer() call and provide a list of transferable values: | ||
* | ||
* @example | ||
* const data = new Uint8Array([1, 2, 3, 4, 5]); | ||
* await client.someFunction(transfer(data, [data.buffer])); | ||
* | ||
* @param obj The object contains transferable values. | ||
* @param transfers List of values to transfer. | ||
*/ function transfer(obj, transfers) { | ||
transferCache.set(obj, transfers); | ||
@@ -1085,2 +1118,2 @@ return obj; | ||
export { AbortError, Composite, FetchClient, FetchClientError, LRUCache, MultiEventEmitter, MultiMap, NeverAbort, rpc as RPC, ResponseFacade, SingleEventEmitter, base64url, blobToBase64URL, compositor, dragSortContext, escapeHTML, fetchFile, formatDuration, formatSize, identity, isPointerInside, noop, nthInChildren, parseDuration, parseSize, saveFile, selectFile, sha256, silencePromise, sleep, svgToUrl, swapElements, uniqueId }; | ||
export { AbortError, Composite, FetchClient, FetchClientError, LRUCache, MultiEventEmitter, MultiMap, NeverAbort, rpc as RPC, ResponseFacade, SingleEventEmitter, UnitConvertor, base64url, blobToBase64URL, compositor, dataSizeIEC, dataSizeSI, dragSortContext, durationConvertor, escapeHTML, fetchFile, identity, inherit, isPointerInside, noop, nthInChildren, saveFile, selectFile, sha256, silencePromise, silentCall, sleep, svgToUrl, swapElements, uniqueId }; |
@@ -24,3 +24,3 @@ /// <reference types="node" resolution-mode="require"/> | ||
* | ||
* <h1>Alternatives</h1> | ||
* # Alternatives | ||
* If you don't need serialization, use `URL.createObjectURL` for better performance. | ||
@@ -27,0 +27,0 @@ */ |
@@ -8,2 +8,5 @@ /** | ||
export declare function fetchFile(request: RequestInfo, init?: RequestInit): Promise<File>; | ||
/** | ||
* The HTTP request was successful but failed the application layer checks. | ||
*/ | ||
export declare class FetchClientError extends Error { | ||
@@ -10,0 +13,0 @@ private readonly response; |
@@ -1,48 +0,81 @@ | ||
type TimeUnit = "ns" | "ms" | "s" | "m" | "h" | "d"; | ||
export declare class UnitConvertor<T extends readonly string[]> { | ||
private readonly name; | ||
private readonly units; | ||
private readonly fractions; | ||
constructor(name: string, units: T, fractions: number[]); | ||
/** | ||
* Get the fraction of the unit, throw error if the unit is invalid. | ||
*/ | ||
private getFraction; | ||
/** | ||
* Find the index of the largest fraction that is less than the value. | ||
*/ | ||
private largest; | ||
/** | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param precision The number of digits to appear after the decimal point. | ||
*/ | ||
n2sDivision(value: number, unit?: T[number], precision?: number): string; | ||
/** | ||
* Convert string to number in specified unit. | ||
* | ||
* @example | ||
* durationConvertor.s2nModulo("10000d", "d"); // 10000 | ||
* durationConvertor.s2nModulo("0h", "s"); // 0 | ||
* durationConvertor.s2nModulo("0.5m", "s"); // 30 | ||
* durationConvertor.s2nModulo("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
*/ | ||
s2nDivision(value: string, unit?: T[number]): number; | ||
/** | ||
* Formats a value and unit. | ||
* | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @example | ||
* durationConvertor.n2sModulo(0.1, "ns"); // "0ns" | ||
* durationConvertor.n2sModulo(0, "s"); // "0s" | ||
* durationConvertor.n2sModulo(10000, "d"); // "10000d" | ||
* durationConvertor.n2sModulo(97215, "s", 4); // "1d 3h 0m 15s" | ||
* durationConvertor.n2sModulo(0.522, "h"); // "31m 19s" | ||
* durationConvertor.n2sModulo(0.522, "h", 1); // "31m" | ||
* durationConvertor.n2sModulo(0.522, "h", 999); // "31m 19s 200ms" | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param parts Maximum number of groups in result. | ||
*/ | ||
n2sModulo(value: number, unit?: T[number], parts?: number): string; | ||
/** | ||
* Convert string to number in specified unit. | ||
* | ||
* @example | ||
* durationConvertor.s2nModulo("10000d", "d"); // 10000 | ||
* durationConvertor.s2nModulo("0h", "s"); // 0 | ||
* durationConvertor.s2nModulo("0.5m", "s"); // 30 | ||
* durationConvertor.s2nModulo("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
*/ | ||
s2nModulo(value: string, unit?: T[number]): number; | ||
} | ||
/** | ||
* Convert the given duration in to a human-readable format. | ||
* Convert between duration and human-readable string. | ||
* Support units from nanoseconds to days. | ||
* | ||
* @example | ||
* formatDuration(0, "s"); // "0s" | ||
* formatDuration(10000, "d"); // "10000d" | ||
* formatDuration(97215, "s", 4); // "1d 3h 0m 15s" | ||
* formatDuration(0.522, "h"); // "31m 19s" | ||
* formatDuration(0.522, "h", 1); // "31m" | ||
* formatDuration(0.522, "h", 999); // "31m 19s 200ms" | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param parts Maximum number of groups in result. | ||
*/ | ||
export declare function formatDuration(value: number, unit: TimeUnit, parts?: number): string; | ||
export declare const durationConvertor: UnitConvertor<readonly ["ns", "us", "ms", "s", "m", "h", "d"]>; | ||
/** | ||
* Convert duration string to number in specified unit. | ||
* | ||
* @example | ||
* parseDuration("10000d", "d"); // 10000 | ||
* parseDuration("0h", "s"); // 0 | ||
* parseDuration("0.5m", "s"); // 30 | ||
* parseDuration("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
* Convert between bytes and human-readable string using SI prefixes. | ||
*/ | ||
export declare function parseDuration(value: string, unit: TimeUnit): number; | ||
export declare const dataSizeSI: UnitConvertor<readonly ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]>; | ||
/** | ||
* Convert bytes to a human-readable string. | ||
* | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @param value The number to format. | ||
* @param fraction 1000 for SI or 1024 for IEC. | ||
* Convert between bytes and human-readable string using IEC prefixes. | ||
*/ | ||
export declare function formatSize(value: number, unit: string, fraction?: 1024 | 1000): string; | ||
/** | ||
* Parse size string to number of bytes. | ||
* | ||
* @param value The size string. | ||
* @param fraction 1000 for SI or 1024 for IEC. | ||
*/ | ||
export declare function parseSize(value: string, fraction?: 1024 | 1000): number; | ||
export declare const dataSizeIEC: UnitConvertor<readonly ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]>; | ||
type Placeholders = Record<string, string | RegExp>; | ||
@@ -49,0 +82,0 @@ /** |
export type Awaitable<T> = T | Promise<T>; | ||
export declare const noop: () => void; | ||
export declare const identity: <T>(v: T) => T; | ||
type Constructor<T> = Function & { | ||
prototype: T; | ||
}; | ||
/** | ||
@@ -26,2 +29,6 @@ * An AbortSignal that never aborts. | ||
/** | ||
* Call a function silently. returns undefined if any error occurs. | ||
*/ | ||
export declare function silentCall<T>(fn: () => T): T | undefined; | ||
/** | ||
* Silence a Promise-like object. This is useful for avoiding non-harmful, | ||
@@ -43,1 +50,17 @@ * but potentially confusing "uncaught play promise" rejection error messages. | ||
} | ||
/** | ||
* Create a new instance with the `parent` as prototype and the `value` as child. | ||
* | ||
* # NOTES | ||
* If the parent is a constructor, it will not be called and just use its `prototype`. | ||
* | ||
* Does not support override getter-only properties. | ||
* | ||
* This function does not use `Object.setPrototypeOf` because it has bad performance. | ||
* | ||
* @param parent Prototype of returned object. | ||
* @param value Provide properties for returned object. | ||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf | ||
*/ | ||
export declare function inherit<P extends object | null, C>(parent: P | Constructor<P>, value: C): P extends null ? C : P & C; | ||
export {}; |
435
dist/node.js
@@ -45,3 +45,3 @@ import process from 'process'; | ||
* | ||
* <h1>Alternatives</h1> | ||
* # Alternatives | ||
* If you don't need serialization, use `URL.createObjectURL` for better performance. | ||
@@ -181,3 +181,5 @@ */ function blobToBase64URL(blob) { | ||
} | ||
class FetchClientError extends Error { | ||
/** | ||
* The HTTP request was successful but failed the application layer checks. | ||
*/ class FetchClientError extends Error { | ||
constructor(response){ | ||
@@ -310,219 +312,205 @@ super(`Fetch failed with status: ${response.status}`); | ||
* - Division conversion: "90s" to "1.5m". | ||
* - Modulo conversion: "90s" to "1m 30s". | ||
* - Modulo conversion: "90s" to "1m 30s". | ||
* | ||
* The n2s and s2n means "number to string" and "string to number". | ||
* | ||
* All conversion functions take a unit argument, which is an array of | ||
* length 2N containing the N units from smallest to largest, and | ||
* the multiplier to the next unit. | ||
* e.g. | ||
* [ | ||
* "second", 60, // 60 seconds is a minute. | ||
* "minute", 60, // 60 minute is a hour. | ||
* "hour", Infinity, // No more units, the multiplier is infinity. | ||
* ] | ||
*/ function n2sDivision(units, value, uIndex) { | ||
if (!Number.isFinite(value)) { | ||
throw new TypeError(`${value} is not a finite number`); | ||
} | ||
let v = Math.abs(value); | ||
for(; uIndex < units.length; uIndex += 2){ | ||
if (v < units[uIndex + 1]) break; | ||
v /= units[uIndex + 1]; | ||
} | ||
if (value < 0) v = -v; | ||
return `${Number(v.toFixed(2))} ${units[uIndex]}`; | ||
} | ||
*/ //@formatter:off | ||
const SIZE_UNITS = [ | ||
"B", | ||
"KB", | ||
"MB", | ||
"GB", | ||
"TB", | ||
"PB", | ||
"EB", | ||
"ZB", | ||
"YB" | ||
]; | ||
const SIZE_FRACTIONS_SI = [ | ||
1, | ||
1e3, | ||
1e6, | ||
1e9, | ||
1e12, | ||
1e15, | ||
1e18, | ||
1e21, | ||
1e24 | ||
]; | ||
const SIZE_FRACTIONS_IEC = [ | ||
1, | ||
2 ** 10, | ||
2 ** 20, | ||
2 ** 30, | ||
2 ** 40, | ||
2 ** 50, | ||
2 ** 60, | ||
2 ** 70, | ||
2 ** 80 | ||
]; | ||
const TIME_UNITS = [ | ||
"ns", | ||
"us", | ||
"ms", | ||
"s", | ||
"m", | ||
"h", | ||
"d" | ||
]; | ||
const TIME_FRACTIONS = [ | ||
1, | ||
1e3, | ||
1e6, | ||
1e9, | ||
6e10, | ||
36e11, | ||
864e11 | ||
]; | ||
// @formatter:on | ||
const divRE = /^([-+0-9.]+)\s*(\w+)$/; | ||
function s2nDivision(name, units, value) { | ||
const match = divRE.exec(value); | ||
if (!match) { | ||
throw new Error(`Can not convert "${value}" to ${name}`); | ||
const groupRE = /\d+([a-z]+)\s*/gi; | ||
class UnitConvertor { | ||
constructor(name, units, fractions){ | ||
this.name = name; | ||
this.units = units; | ||
this.fractions = fractions; | ||
} | ||
const [, v, unit] = match; | ||
let result = Number(v); | ||
for(let i = 0; i < units.length; i += 2){ | ||
if (units[i] === unit) { | ||
return result; | ||
} else { | ||
result *= units[i + 1]; | ||
/** | ||
* Get the fraction of the unit, throw error if the unit is invalid. | ||
*/ getFraction(unit) { | ||
if (unit === undefined) { | ||
return 1; | ||
} | ||
const { units , name , fractions } = this; | ||
const i = units.indexOf(unit); | ||
if (i !== -1) { | ||
return fractions[i]; | ||
} | ||
throw new Error(`Unknown ${name} unit: ${unit}`); | ||
} | ||
throw new Error(`Unknown ${name} unit: ${unit}`); | ||
} | ||
function n2sModulo(name, units, value, unit, parts = 2) { | ||
if (!Number.isFinite(value)) { | ||
throw new TypeError(`${value} is not a finite number`); | ||
/** | ||
* Find the index of the largest fraction that is less than the value. | ||
*/ largest(value) { | ||
const s = this.fractions; | ||
let i = 0; | ||
// When i outbound, (s[i] <= value) is false. | ||
while(s[i] <= value)i++; | ||
// (i = 0) when (value < 1), fallback to the minimum unit. | ||
return Math.max(0, i - 1); | ||
} | ||
if (value < 0) { | ||
throw new Error(`value (${value}) can not be negative`); | ||
/** | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param precision The number of digits to appear after the decimal point. | ||
*/ n2sDivision(value, unit, precision = 2) { | ||
if (!Number.isFinite(value)) { | ||
throw new TypeError(`${value} is not a finite number`); | ||
} | ||
const { units , fractions } = this; | ||
let v = value * this.getFraction(unit); | ||
const i = this.largest(Math.abs(v)); | ||
v /= fractions[i]; | ||
return `${Number(v.toFixed(precision))} ${units[i]}`; | ||
} | ||
let i = units.indexOf(unit); | ||
let d = 1; | ||
if (i === -1) { | ||
throw new Error(`Unknown ${name} unit: ${unit}`); | ||
/** | ||
* Convert string to number in specified unit. | ||
* | ||
* @example | ||
* durationConvertor.s2nModulo("10000d", "d"); // 10000 | ||
* durationConvertor.s2nModulo("0h", "s"); // 0 | ||
* durationConvertor.s2nModulo("0.5m", "s"); // 30 | ||
* durationConvertor.s2nModulo("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
*/ s2nDivision(value, unit) { | ||
const match = divRE.exec(value); | ||
if (!match) { | ||
throw new Error(`Can not convert "${value}" to ${this.name}`); | ||
} | ||
const [, v, u] = match; | ||
return Number(v) * this.getFraction(u) / this.getFraction(unit); | ||
} | ||
// Find index of the largest unit. | ||
for(;; i += 2){ | ||
const x = d * units[i + 1]; | ||
if (value < x) break; | ||
else d = x; | ||
} | ||
const groups = []; | ||
// Backtrace to calculate each group. | ||
for(; // 1.16e-14 = 1/24/60/60/1000/1000/1000 | ||
i >= 0 && parts > 0 && value > 1.16e-14; i -= 2, parts -= 1){ | ||
const t = Math.floor(value / d); | ||
// Avoid leading zeros. | ||
if (groups.length || t !== 0) { | ||
/** | ||
* Formats a value and unit. | ||
* | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @example | ||
* durationConvertor.n2sModulo(0.1, "ns"); // "0ns" | ||
* durationConvertor.n2sModulo(0, "s"); // "0s" | ||
* durationConvertor.n2sModulo(10000, "d"); // "10000d" | ||
* durationConvertor.n2sModulo(97215, "s", 4); // "1d 3h 0m 15s" | ||
* durationConvertor.n2sModulo(0.522, "h"); // "31m 19s" | ||
* durationConvertor.n2sModulo(0.522, "h", 1); // "31m" | ||
* durationConvertor.n2sModulo(0.522, "h", 999); // "31m 19s 200ms" | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param parts Maximum number of groups in result. | ||
*/ n2sModulo(value, unit, parts = 2) { | ||
if (!Number.isFinite(value)) { | ||
throw new TypeError(`${value} is not a finite number`); | ||
} | ||
if (value < 0) { | ||
throw new Error(`value (${value}) can not be negative`); | ||
} | ||
const { units , fractions } = this; | ||
value *= this.getFraction(unit); | ||
const groups = []; | ||
let i = this.largest(value); | ||
// Backtrace to calculate each group. | ||
for(; i >= 0 && parts > 0; i--, parts -= 1){ | ||
const f = fractions[i]; | ||
// Avoid tailing zeros. | ||
if (value * f < 1) break; | ||
const t = Math.floor(value / f); | ||
value %= f; | ||
groups.push(`${t}${units[i]}`); | ||
} | ||
value %= d; | ||
d /= units[i - 1]; | ||
return groups.length ? groups.join(" ") : `0${unit}`; | ||
} | ||
return groups.length ? groups.join(" ") : `0${unit}`; | ||
} | ||
const groupRE = /\d+([a-z]+)\s*/gi; | ||
function s2nModulo(name, units, value, unit) { | ||
const i = units.indexOf(unit); | ||
if (i === -1) { | ||
throw new Error(`Unknown ${name} unit: ${unit}`); | ||
} | ||
let k = units.length - 1; | ||
let seen = 0; | ||
let result = 0; | ||
for (const [matched, u] of value.matchAll(groupRE)){ | ||
const j = units.lastIndexOf(u, k); | ||
k = j - 2; | ||
if (j === -1) { | ||
throw new Error(units.includes(u) ? "Units must be ordered from largest to smallest" : `Unknown ${name} unit: ${u}`); | ||
} | ||
/* | ||
* Similar performance compared to prebuilt table. | ||
* See benchmark/format.js | ||
*/ let n = parseFloat(matched); | ||
if (j > i) { | ||
for(let k = i; k < j; k += 2){ | ||
n *= units[k + 1]; | ||
/** | ||
* Convert string to number in specified unit. | ||
* | ||
* @example | ||
* durationConvertor.s2nModulo("10000d", "d"); // 10000 | ||
* durationConvertor.s2nModulo("0h", "s"); // 0 | ||
* durationConvertor.s2nModulo("0.5m", "s"); // 30 | ||
* durationConvertor.s2nModulo("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
*/ s2nModulo(value, unit) { | ||
const { name , units , fractions } = this; | ||
let k = Infinity; | ||
let seen = 0; | ||
let result = 0; | ||
for (const [matched, u] of value.matchAll(groupRE)){ | ||
const i = units.lastIndexOf(u, k); | ||
k = i - 1; | ||
if (i === -1) { | ||
throw new Error(units.includes(u) ? "Units must be ordered from largest to smallest" : `Unknown ${name} unit: ${u}`); | ||
} | ||
} else { | ||
for(let k = j; k < i; k += 2){ | ||
n /= units[k + 1]; | ||
} | ||
seen += matched.length; | ||
result += parseFloat(matched) * fractions[i]; | ||
} | ||
result += n; | ||
seen += matched.length; | ||
if (seen === value.length && seen > 0) { | ||
return result / this.getFraction(unit); | ||
} | ||
throw new Error(`Can not convert "${value}" to ${name}`); | ||
} | ||
if (seen === value.length && seen > 0) { | ||
return result; | ||
} | ||
throw new Error(`Can not convert "${value}" to ${name}`); | ||
} | ||
const TIME_UNITS = [ | ||
"ns", | ||
1000, | ||
"us", | ||
1000, | ||
"ms", | ||
1000, | ||
"s", | ||
60, | ||
"m", | ||
60, | ||
"h", | ||
24, | ||
"d", | ||
Infinity | ||
]; | ||
/** | ||
* Convert the given duration in to a human-readable format. | ||
* Convert between duration and human-readable string. | ||
* Support units from nanoseconds to days. | ||
* | ||
* @example | ||
* formatDuration(0, "s"); // "0s" | ||
* formatDuration(10000, "d"); // "10000d" | ||
* formatDuration(97215, "s", 4); // "1d 3h 0m 15s" | ||
* formatDuration(0.522, "h"); // "31m 19s" | ||
* formatDuration(0.522, "h", 1); // "31m" | ||
* formatDuration(0.522, "h", 999); // "31m 19s 200ms" | ||
* | ||
* @param value Numeric value to use. | ||
* @param unit Unit ot the value. | ||
* @param parts Maximum number of groups in result. | ||
*/ function formatDuration(value, unit, parts = 2) { | ||
return n2sModulo("time", TIME_UNITS, value, unit, parts); | ||
} | ||
*/ const durationConvertor = new UnitConvertor("time", TIME_UNITS, TIME_FRACTIONS); | ||
/** | ||
* Convert duration string to number in specified unit. | ||
* | ||
* @example | ||
* parseDuration("10000d", "d"); // 10000 | ||
* parseDuration("0h", "s"); // 0 | ||
* parseDuration("0.5m", "s"); // 30 | ||
* parseDuration("1d 3h 0m 15s", "s"); // 97215 | ||
* | ||
* @param value The string to parse. | ||
* @param unit Target unit to converted to. | ||
*/ function parseDuration(value, unit) { | ||
return s2nModulo("time", TIME_UNITS, value, unit); | ||
} | ||
const SIZE_UNITS_SI = [ | ||
"B", | ||
1000, | ||
"KB", | ||
1000, | ||
"MB", | ||
1000, | ||
"GB", | ||
1000, | ||
"TB", | ||
1000, | ||
"PB", | ||
1000, | ||
"EB", | ||
1000, | ||
"ZB", | ||
1000, | ||
"YB", | ||
Infinity | ||
]; | ||
const SIZE_UNITS_IEC = [ | ||
"B", | ||
1024, | ||
"KB", | ||
1024, | ||
"MB", | ||
1024, | ||
"GB", | ||
1024, | ||
"TB", | ||
1024, | ||
"PB", | ||
1024, | ||
"EB", | ||
1024, | ||
"ZB", | ||
1024, | ||
"YB", | ||
Infinity | ||
]; | ||
* Convert between bytes and human-readable string using SI prefixes. | ||
*/ const dataSizeSI = new UnitConvertor("data size", SIZE_UNITS, SIZE_FRACTIONS_SI); | ||
/** | ||
* Convert bytes to a human-readable string. | ||
* | ||
* The result may lose precision and cannot be converted back. | ||
* | ||
* @param value The number to format. | ||
* @param fraction 1000 for SI or 1024 for IEC. | ||
*/ function formatSize(value, unit, fraction = 1024) { | ||
return `${n2sDivision(fraction === 1024 ? SIZE_UNITS_IEC : SIZE_UNITS_SI, value, 0)}`; | ||
} | ||
* Convert between bytes and human-readable string using IEC prefixes. | ||
*/ const dataSizeIEC = new UnitConvertor("data size", SIZE_UNITS, SIZE_FRACTIONS_IEC); | ||
/** | ||
* Parse size string to number of bytes. | ||
* | ||
* @param value The size string. | ||
* @param fraction 1000 for SI or 1024 for IEC. | ||
*/ function parseSize(value, fraction = 1024) { | ||
return s2nDivision("data size", fraction === 1024 ? SIZE_UNITS_IEC : SIZE_UNITS_SI, value); | ||
} | ||
/** | ||
* A simple string template engine, only support replace placeholders. | ||
@@ -655,2 +643,9 @@ * | ||
/** | ||
* Call a function silently. returns undefined if any error occurs. | ||
*/ function silentCall(fn) { | ||
try { | ||
return fn(); | ||
} catch {} | ||
} | ||
/** | ||
* Silence a Promise-like object. This is useful for avoiding non-harmful, | ||
@@ -700,5 +695,43 @@ * but potentially confusing "uncaught play promise" rejection error messages. | ||
} | ||
/** | ||
* Create a new instance with the `parent` as prototype and the `value` as child. | ||
* | ||
* # NOTES | ||
* If the parent is a constructor, it will not be called and just use its `prototype`. | ||
* | ||
* Does not support override getter-only properties. | ||
* | ||
* This function does not use `Object.setPrototypeOf` because it has bad performance. | ||
* | ||
* @param parent Prototype of returned object. | ||
* @param value Provide properties for returned object. | ||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf | ||
*/ function inherit(parent, value) { | ||
const proto = typeof parent === "function" ? parent.prototype : parent; | ||
return Object.assign(Object.create(proto), value); | ||
} | ||
const transferCache = new WeakMap(); | ||
function transfer(obj, transfers) { | ||
/** | ||
* Because RPC should keep the signature of the remote function, we cannot add a parameter | ||
* for transfers to remote function. | ||
* | ||
* We track transferable values with a map. This brings a new problem that | ||
* we cannot ensure that all the values added to the map are used. | ||
* | ||
* This has the potential for memory leaks, so use WeakMap instead of Map. | ||
*/ const transferCache = new WeakMap(); | ||
/** | ||
* By default, every function parameter, return value and object property value is copied, | ||
* in the sense of structured cloning. | ||
* | ||
* If you want a value to be transferred rather than copied, | ||
* you can wrap the value in a transfer() call and provide a list of transferable values: | ||
* | ||
* @example | ||
* const data = new Uint8Array([1, 2, 3, 4, 5]); | ||
* await client.someFunction(transfer(data, [data.buffer])); | ||
* | ||
* @param obj The object contains transferable values. | ||
* @param transfers List of values to transfer. | ||
*/ function transfer(obj, transfers) { | ||
transferCache.set(obj, transfers); | ||
@@ -982,2 +1015,2 @@ return obj; | ||
export { AbortError, Composite, FetchClient, FetchClientError, LRUCache, MultiEventEmitter, MultiMap, NeverAbort, rpc as RPC, ResponseFacade, SingleEventEmitter, base64url, blobToBase64URL, compositor, escapeHTML, fetchFile, formatDuration, formatSize, identity, noop, onExit, parseDuration, parseSize, sha256, silencePromise, sleep, svgToUrl, uniqueId }; | ||
export { AbortError, Composite, FetchClient, FetchClientError, LRUCache, MultiEventEmitter, MultiMap, NeverAbort, rpc as RPC, ResponseFacade, SingleEventEmitter, UnitConvertor, base64url, blobToBase64URL, compositor, dataSizeIEC, dataSizeSI, durationConvertor, escapeHTML, fetchFile, identity, inherit, noop, onExit, sha256, silencePromise, silentCall, sleep, svgToUrl, uniqueId }; |
@@ -14,2 +14,16 @@ export type Respond = (resp: ResponseMessage, transfer?: Transferable[]) => void; | ||
} | ||
/** | ||
* By default, every function parameter, return value and object property value is copied, | ||
* in the sense of structured cloning. | ||
* | ||
* If you want a value to be transferred rather than copied, | ||
* you can wrap the value in a transfer() call and provide a list of transferable values: | ||
* | ||
* @example | ||
* const data = new Uint8Array([1, 2, 3, 4, 5]); | ||
* await client.someFunction(transfer(data, [data.buffer])); | ||
* | ||
* @param obj The object contains transferable values. | ||
* @param transfers List of values to transfer. | ||
*/ | ||
export declare function transfer<T>(obj: T, transfers: Transferable[]): T; | ||
@@ -16,0 +30,0 @@ /** |
{ | ||
"name": "@kaciras/utilities", | ||
"version": "0.6.3", | ||
"version": "0.6.4", | ||
"license": "MIT", | ||
@@ -22,3 +22,3 @@ "description": "A set of common JS functions for node and browser.", | ||
"devDependencies": { | ||
"@jest/globals": "^29.3.1", | ||
"@jest/globals": "^29.4.1", | ||
"@kaciras/eslint-config-core": "^2.5.0", | ||
@@ -30,11 +30,11 @@ "@kaciras/eslint-config-jest": "^2.5.0", | ||
"@stryker-mutator/jest-runner": "^6.3.1", | ||
"@swc/core": "^1.3.26", | ||
"@swc/core": "^1.3.32", | ||
"@swc/jest": "^0.2.24", | ||
"@types/node": "^18.11.18", | ||
"eslint": "^8.32.0", | ||
"is-builtin-module": "^3.2.0", | ||
"jest": "^29.3.1", | ||
"mockttp": "^3.6.2", | ||
"rollup": "^3.10.0", | ||
"typescript": "^4.9.4" | ||
"eslint": "^8.33.0", | ||
"is-builtin-module": "^3.2.1", | ||
"jest": "^29.4.1", | ||
"mockttp": "^3.6.3", | ||
"rollup": "^3.12.1", | ||
"typescript": "^4.9.5" | ||
}, | ||
@@ -41,0 +41,0 @@ "scripts": { |
89254
2687