@travetto/runtime
Version:
Runtime for travetto applications.
198 lines (178 loc) • 5.78 kB
text/typescript
import crypto from 'node:crypto';
import timers from 'node:timers/promises';
import { castTo, hasToJSON } from './types.ts';
import { AppError } from './error.ts';
type MapFn<T, U> = (val: T, i: number) => U | Promise<U>;
/**
* Grab bag of common utilities
*/
export class Util {
static #match<T, K extends unknown[]>(
rules: { value: T, positive: boolean }[],
compare: (rule: T, ...compareInput: K) => boolean,
unmatchedValue: boolean,
...input: K
): boolean {
for (const rule of rules) {
if (compare(rule.value, ...input)) {
return rule.positive;
}
}
return unmatchedValue;
}
static #allowDenyRuleInput<T>(
rule: (string | T | [value: T, positive: boolean] | [value: T]),
convert: (inputRule: string) => T
): { value: T, positive: boolean } {
return typeof rule === 'string' ?
{ value: convert(rule.replace(/^!/, '')), positive: !rule.startsWith('!') } :
Array.isArray(rule) ?
{ value: rule[0], positive: rule[1] ?? true } :
{ value: rule, positive: true };
}
/**
* Generate a random UUID
* @param len The length of the uuid to generate
*/
static uuid(len: number = 32): string {
const bytes = crypto.randomBytes(Math.ceil(len / 2));
if (len === 32) { // Make valid uuid-v4
// eslint-disable-next-line no-bitwise
bytes[6] = (bytes[6] & 0x0f) | 0x40;
// eslint-disable-next-line no-bitwise
bytes[8] = (bytes[8] & 0x3f) | 0x80;
}
return bytes.toString('hex').substring(0, len);
}
/**
* Map an async iterable with various mapping functions
*/
static mapAsyncItr<T, U, V, W>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>, fn3: MapFn<V, W>): AsyncIterable<W>;
static mapAsyncItr<T, U, V>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>): AsyncIterable<V>;
static mapAsyncItr<T, U>(source: AsyncIterable<T>, fn: MapFn<T, U>): AsyncIterable<U>;
static async * mapAsyncItr<T>(source: AsyncIterable<T>, ...fns: MapFn<unknown, unknown>[]): AsyncIterable<unknown> {
let idx = -1;
for await (const el of source) {
if (el !== undefined) {
idx += 1;
let m = el;
for (const fn of fns) {
m = castTo(await fn(m, idx));
}
yield m;
}
}
}
/**
* Non-blocking timeout
*/
static nonBlockingTimeout(time: number): Promise<void> {
return timers.setTimeout(time, undefined, { ref: false }).catch(() => { });
}
/**
* Blocking timeout
*/
static blockingTimeout(time: number): Promise<void> {
return timers.setTimeout(time, undefined, { ref: true }).catch(() => { });
}
/**
* Queue new macro task
*/
static queueMacroTask(): Promise<void> {
return timers.setImmediate(undefined);
}
/**
* Simple check against allow/deny rules
* @param rules
*/
static allowDeny<T, K extends unknown[]>(
rules: string | (string | T | [value: T, positive: boolean])[],
convert: (rule: string) => T,
compare: (rule: T, ...compareInput: K) => boolean,
cacheKey?: (...keyInput: K) => string
): (...input: K) => boolean {
const rawRules = (Array.isArray(rules) ? rules : rules.split(/,/g).map(x => x.trim()));
const convertedRules = rawRules.map(rule => this.#allowDenyRuleInput(rule, convert));
const unmatchedValue = !convertedRules.some(r => r.positive);
if (convertedRules.length) {
if (cacheKey) {
const cache: Record<string, boolean> = {};
return (...input: K) =>
cache[cacheKey(...input)] ??= this.#match(convertedRules, compare, unmatchedValue, ...input);
} else {
return (...input: K) => this.#match(convertedRules, compare, unmatchedValue, ...input);
}
} else {
return () => true;
}
}
/**
* Encode JSON value as base64 encoded string
*/
static encodeSafeJSON<T>(value: T | undefined): string | undefined {
if (value === undefined) {
return;
}
const res = JSON.stringify(value);
return Buffer.from(res, 'utf8').toString('base64');
}
/**
* Decode JSON value from base64 encoded string
*/
static decodeSafeJSON<T>(input: string): T;
static decodeSafeJSON<T>(input?: string | undefined): T | undefined;
static decodeSafeJSON<T>(input?: string | undefined): T | undefined {
if (!input) {
return undefined;
}
let decoded = Buffer.from(input, 'base64').toString('utf8');
// Read from encoded if it happens
if (decoded.startsWith('%')) {
decoded = decodeURIComponent(decoded);
}
return JSON.parse(decoded, undefined);
}
/**
* Serialize to JSON
*/
static serializeToJSON<T>(out: T): string {
return JSON.stringify(out, function (k, v) {
const ov = this[k];
if (ov && ov instanceof Error) {
return {
$: true,
...hasToJSON(ov) ? ov.toJSON() : ov,
name: ov.name,
message: ov.message,
stack: ov.stack,
};
} else if (typeof v === 'bigint') {
return `${v.toString()}$n`;
} else {
return v;
}
});
}
/**
* Deserialize from JSON
*/
static deserializeFromJson<T = unknown>(input: string): T {
return JSON.parse(input, function (k, v) {
if (v && typeof v === 'object' && '$' in v) {
const err = AppError.fromJSON(v) ?? new Error();
if (!(err instanceof AppError)) {
const { $: _, ...rest } = v;
Object.assign(err, rest);
}
err.message = v.message;
err.stack = v.stack;
err.name = v.name;
return err;
} else if (typeof v === 'string' && /^\d+[$]n$/.test(v)) {
return BigInt(v.split('$')[0]);
} else {
return v;
}
});
}
}