/** * Represents a tuple, or a homogenous array of a fixed length. * * const deck: Tuple = [Card, Card, Card, ...]; */ export type Tuple< T, N extends number, R extends readonly T[] = [], > = R['length'] extends N ? R : Tuple; /** * Chooses a random element from an array * * choose(document.all).click() * @since 2020-06-29 */ export function choose(arr: T[]): T { if (!arr.length) throw new Error('Cannot choose from an empty array') return arr[Math.floor(Math.random() * arr.length)] } /** * Creates an iterable that always yields the given value. * @author MindfulMinun * @since 2022-04-21 */ export function* fill(value = 0 as unknown as T, length = Infinity): Generator { for (let i = 0; i < length; i++) yield value } /** * Creates an iterable that yields numbers between a range. * @author MindfulMinun * @since 2022-04-27 */ export function range(end: number): Generator export function range(start: number, end: number): Generator export function range(start: number, end: number, step: number): Generator export function* range(a: number, b?: number, c?: number) { const [start, end, step] = ( typeof b === "undefined" ? [0, a, 1] : typeof c === "undefined" ? [a, b, 1] : [a, b, Math.abs(c)] ) const delta = end < start ? -step : step for (let i = start; i < end; i += delta) yield i } /** * Given an iterable, yields the first `count` items, then returns. * @author MindfulMinun * @since 2022-07-07 */ export function* limit(count: number, iterable: Iterable) { let i = 0 for (const value of iterable) { if (i < count) { yield value i++ } else break } return iterable } /** * Given an async iterable, yields the first `count` items, then returns. * @author MindfulMinun * @since 2022-07-07 */ export async function* limitAsync(count: number, iterable: AsyncIterable) { let i = 0 for await (const value of iterable) { if (i < count) { yield value i++ } else break } return iterable } /** * Removes elements from an array. Non-destructive, as all array operations should be :D * * // Removes 'b', at index 1 * removeFromArraybyIndexes(['a', 'b', 'c'], 1) // ['a', 'c'] * // Removes 'b' and 'd', at indexes 1 and 3 * removeFromArraybyIndexes(['a', 'b', 'c', 'd'], [1, 3]) // ['a', 'c'] * @since 2020-07-12 */ export function removeFromArraybyIndexes( arr: T[], indexes: number | number[] ): T[] { if (Array.isArray(indexes)) { return arr.filter((_, i) => indexes.indexOf(i) === -1) } else { return arr.filter((_, i) => i !== indexes) } } /** * Divides an array into two by a given predicate function. Those that pass are put into the first, the rest are put in the second. * * @author MindfulMinun * @since 2022-06-28 */ export function divide(list: T[], predicate: (this: typeof list, value: T, index: number, array: typeof list) => boolean): [T[], T[]] { const pass: T[] = [] const fail: T[] = [] list.forEach((v, i, arr) => { const passed = predicate.apply(list, [v, i, arr]) ;(passed ? pass : fail).push(v) }) return [pass, fail] } /** * Shuffles an array. Note that this function swaps the elements * *in place*, meaning the original array is modified. * @since 2020-07-19 */ export function shuffle(arr: T[]): T[] { let j, i for (i = arr.length - 1; i > 0; i--) { j = Math.floor(Math.random() * (i + 1)) swap(arr, i, j) } return arr } /** * Swaps two elements of an array. Note that this function swaps * the elements *in place*, meaning the original array is modified. * @since 2020-07-19 */ export function swap(arr: T[], i: number, j: number): void { const carry = arr[i] arr[i] = arr[j] arr[j] = carry } /** * Helper class to handle events and turn them into async iterables * * ```ts * const stream = new EventStream() * * document.body.addEventListener('click', ev => stream.emit(ev)) * wait(10e3).then(() => stream.end()) * * for await (const event of stream) { * alert("Clicked!") * } * ``` * @since 2021-08-07 */ export class EventStream implements AsyncIterable { #done: boolean #events: T[] #resolve: () => void #promise!: Promise constructor() { this.#done = false this.#events = [] this.#resolve = () => { } this.#defer() } #defer() { this.#promise = new Promise(r => this.#resolve = r) } async*[Symbol.asyncIterator]() { while (!this.#done) { await this.#promise yield* this.#events this.#events = [] } } /** * Dispatches an event. For-await-of listeners of this class instance will recieve this event. * Note that once an event is emitted, it cannot be cancelled. * @since 2021-08-07 */ emit(event: T) { this.#events.push(event) this.#resolve() this.#defer() } /** * Waits for a specific event to be emitted according to a predicate. * * @example * const stream = new EventStream() * // Some events happen... * const click = stream.once(ev => ev.type === 'click') * @since 2021-12-22 */ once(predicate: (value: T) => boolean) { return once(this, predicate) } /** * Stops the iterator. This terminates for-await-of loops listening for events, * and any newly dispatched events will not be sent to the iterator. * @since 2021-08-07 */ end() { this.#done = true this.#resolve() } } /** * Wait for any async iterable to yield a specific value according to a predicate. * * @example * const stream = new EventStream() * // Some events happen... * const click = once(stream, ev => ev.type === 'click') * @since 2021-12-22 */ export async function once(source: AsyncIterable, predicate: (value: T) => boolean) { for await (const value of source) { if (predicate(value)) return value } } /** * A "destructured" Promise, useful for waiting for callbacks. * @example * // When awaited, this function returns a promise that resolves to * // an *OPEN* WebSocket * () => { * const sock = new WebSocket('wss://wss.example.com') * const [p, res, rej] = pinkyPromise() * sock.onerror = err => rej(err) * sock.onopen = () => res(sock) * return p * } * * @author MindfulMinun * @since 2022-06-05 */ export function pinkyPromise(): [Promise, () => void, (reason?: unknown) => void] export function pinkyPromise(): [Promise, (value: T) => void, (reason?: unknown) => void] export function pinkyPromise(): [ Promise, (value?: T | PromiseLike) => void, (reason?: unknown) => void ] { let resolve: (value?: T | PromiseLike) => void let reject: (error: unknown) => void // This works because callbacks to the Promise constructor are called synchronously. const p = new Promise((yay, nay) => { resolve = value => yay(value) reject = reason => nay(reason) }) // HACK: This works because callbacks to the Promise constructor are called synchronously. // In other words, they're immediately invoked, so they're available immediately after // the assignment to `p` return [p, resolve!, reject!] }