@travetto/runtime
Version:
Runtime for travetto applications.
121 lines (108 loc) • 3.82 kB
text/typescript
import crypto from 'node:crypto';
import timers from 'node:timers/promises';
import { castTo } from './types.ts';
type MapFn<T, U> = (value: 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 length The length of the uuid to generate
*/
static uuid(length: number = 32): string {
const bytes = crypto.randomBytes(Math.ceil(length / 2));
if (length === 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, length);
}
/**
* Map an async iterable with various mapping functions
*/
static mapAsyncIterable<T, U, V, W>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>, fn3: MapFn<V, W>): AsyncIterable<W>;
static mapAsyncIterable<T, U, V>(source: AsyncIterable<T>, fn1: MapFn<T, U>, fn2: MapFn<U, V>): AsyncIterable<V>;
static mapAsyncIterable<T, U>(source: AsyncIterable<T>, fn: MapFn<T, U>): AsyncIterable<U>;
static async * mapAsyncIterable<T>(input: AsyncIterable<T>, ...fns: MapFn<unknown, unknown>[]): AsyncIterable<unknown> {
let idx = -1;
for await (const item of input) {
if (item !== undefined) {
idx += 1;
let result = item;
for (const fn of fns) {
result = castTo(await fn(result, idx));
}
yield result;
}
}
}
/**
* 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(rule => rule.trim()));
const convertedRules = rawRules.map(rule => this.#allowDenyRuleInput(rule, convert));
const unmatchedValue = !convertedRules.some(rule => rule.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;
}
}
}