| b69ab31 | | | 1 | /** |
| b69ab31 | | | 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. |
| b69ab31 | | | 3 | * |
| b69ab31 | | | 4 | * This source code is licensed under the MIT license found in the |
| b69ab31 | | | 5 | * LICENSE file in the root directory of this source tree. |
| b69ab31 | | | 6 | */ |
| b69ab31 | | | 7 | |
| b69ab31 | | | 8 | import type {Json} from './typeUtils'; |
| b69ab31 | | | 9 | |
| b69ab31 | | | 10 | export function notEmpty<T>(value: T | null | undefined): value is T { |
| b69ab31 | | | 11 | return value !== null && value !== undefined; |
| b69ab31 | | | 12 | } |
| b69ab31 | | | 13 | |
| b69ab31 | | | 14 | /** |
| b69ab31 | | | 15 | * Throw if value is `null` or `undefined`. |
| b69ab31 | | | 16 | */ |
| b69ab31 | | | 17 | export function nullthrows<T>(value: T | undefined | null): T { |
| b69ab31 | | | 18 | if (value == null) { |
| b69ab31 | | | 19 | throw new Error(`expected value not to be ${value}`); |
| b69ab31 | | | 20 | } |
| b69ab31 | | | 21 | return value; |
| b69ab31 | | | 22 | } |
| b69ab31 | | | 23 | |
| b69ab31 | | | 24 | /** |
| b69ab31 | | | 25 | * generate a small random ID string via time in ms + random number encoded as a [0-9a-z]+ string |
| b69ab31 | | | 26 | * This should not be used for cryptographic purposes or if universal uniqueness is absolutely necessary |
| b69ab31 | | | 27 | */ |
| b69ab31 | | | 28 | export function randomId(): string { |
| b69ab31 | | | 29 | return Date.now().toString(36) + Math.random().toString(36); |
| b69ab31 | | | 30 | } |
| b69ab31 | | | 31 | |
| b69ab31 | | | 32 | export type Deferred<T> = { |
| b69ab31 | | | 33 | promise: Promise<T>; |
| b69ab31 | | | 34 | resolve: (t: T) => void; |
| b69ab31 | | | 35 | reject: (e: Error) => void; |
| b69ab31 | | | 36 | }; |
| b69ab31 | | | 37 | /** |
| b69ab31 | | | 38 | * Wraps `new Promise<T>()`, so you can access resolve/reject outside of the callback. |
| b69ab31 | | | 39 | * Useful for externally resolving promises in tests. |
| b69ab31 | | | 40 | */ |
| b69ab31 | | | 41 | export function defer<T>(): Deferred<T> { |
| b69ab31 | | | 42 | const deferred = { |
| b69ab31 | | | 43 | promise: undefined as unknown as Promise<T>, |
| b69ab31 | | | 44 | resolve: undefined as unknown as (t: T) => void, |
| b69ab31 | | | 45 | reject: undefined as unknown as (e: Error) => void, |
| b69ab31 | | | 46 | }; |
| b69ab31 | | | 47 | deferred.promise = new Promise<T>((resolve: (t: T) => void, reject: (e: Error) => void) => { |
| b69ab31 | | | 48 | deferred.resolve = resolve; |
| b69ab31 | | | 49 | deferred.reject = reject; |
| b69ab31 | | | 50 | }); |
| b69ab31 | | | 51 | return deferred; |
| b69ab31 | | | 52 | } |
| b69ab31 | | | 53 | |
| b69ab31 | | | 54 | /** |
| b69ab31 | | | 55 | * Returns the part of the string after the last occurrence of delimiter, |
| b69ab31 | | | 56 | * or the entire string if no matches are found. |
| b69ab31 | | | 57 | * (default delimiter is '/') |
| b69ab31 | | | 58 | * |
| b69ab31 | | | 59 | * ``` |
| b69ab31 | | | 60 | * basename('/path/to/foo.txt', '/') -> 'foo.txt' |
| b69ab31 | | | 61 | * basename('foo.txt', '/') -> 'foo.txt' |
| b69ab31 | | | 62 | * basename('/path/', '/') -> '' |
| b69ab31 | | | 63 | * ``` |
| b69ab31 | | | 64 | */ |
| b69ab31 | | | 65 | export function basename(s: string, delimiter = '/') { |
| b69ab31 | | | 66 | const foundIndex = s.lastIndexOf(delimiter); |
| b69ab31 | | | 67 | if (foundIndex === -1) { |
| b69ab31 | | | 68 | return s; |
| b69ab31 | | | 69 | } |
| b69ab31 | | | 70 | return s.slice(foundIndex + 1); |
| b69ab31 | | | 71 | } |
| b69ab31 | | | 72 | |
| b69ab31 | | | 73 | /** |
| b69ab31 | | | 74 | * Given a multi-line string, return the first line excluding '\n'. |
| b69ab31 | | | 75 | * If no newlines in the string, return the whole string. |
| b69ab31 | | | 76 | */ |
| b69ab31 | | | 77 | export function firstLine(s: string): string { |
| b69ab31 | | | 78 | return s.split('\n', 1)[0]; |
| b69ab31 | | | 79 | } |
| b69ab31 | | | 80 | |
| b69ab31 | | | 81 | /** |
| b69ab31 | | | 82 | * Applies a function to each key & value in an Object. |
| b69ab31 | | | 83 | * ``` |
| b69ab31 | | | 84 | * mapObject( |
| b69ab31 | | | 85 | * {foo: 1, bar: 2}, |
| b69ab31 | | | 86 | * ([key, value]) => ['_' + key, value + 1] |
| b69ab31 | | | 87 | * ) |
| b69ab31 | | | 88 | * => {_foo: 2, _bar: 3} |
| b69ab31 | | | 89 | * ``` |
| b69ab31 | | | 90 | */ |
| b69ab31 | | | 91 | export function mapObject<K1 extends string | number, V1, K2 extends string | number, V2>( |
| b69ab31 | | | 92 | o: Record<K1, V1>, |
| b69ab31 | | | 93 | func: (param: [K1, V1]) => [K2, V2], |
| b69ab31 | | | 94 | ): Record<K2, V2> { |
| b69ab31 | | | 95 | return Object.fromEntries((Object.entries(o) as Array<[K1, V1]>).map(func)) as Record<K2, V2>; |
| b69ab31 | | | 96 | } |
| b69ab31 | | | 97 | |
| b69ab31 | | | 98 | /** |
| b69ab31 | | | 99 | * Test if a generator yields the given value. |
| b69ab31 | | | 100 | * `value` can be either a value to test equality, or a function to customize the equality test. |
| b69ab31 | | | 101 | */ |
| b69ab31 | | | 102 | export function generatorContains<V>( |
| b69ab31 | | | 103 | gen: IterableIterator<V>, |
| b69ab31 | | | 104 | value: V | ((v: V) => boolean), |
| b69ab31 | | | 105 | ): boolean { |
| b69ab31 | | | 106 | const test = typeof value === 'function' ? (value as (v: V) => boolean) : (v: V) => v === value; |
| b69ab31 | | | 107 | for (const v of gen) { |
| b69ab31 | | | 108 | if (test(v)) { |
| b69ab31 | | | 109 | return true; |
| b69ab31 | | | 110 | } |
| b69ab31 | | | 111 | } |
| b69ab31 | | | 112 | return false; |
| b69ab31 | | | 113 | } |
| b69ab31 | | | 114 | |
| b69ab31 | | | 115 | /** |
| b69ab31 | | | 116 | * Zip 2 iterators. |
| b69ab31 | | | 117 | */ |
| b69ab31 | | | 118 | export function* zip<T, U>(iter1: Iterable<T>, iter2: Iterable<U>): IterableIterator<[T, U]> { |
| b69ab31 | | | 119 | const iterator1 = iter1[Symbol.iterator](); |
| b69ab31 | | | 120 | const iterator2 = iter2[Symbol.iterator](); |
| b69ab31 | | | 121 | while (true) { |
| b69ab31 | | | 122 | const result1 = iterator1.next(); |
| b69ab31 | | | 123 | const result2 = iterator2.next(); |
| b69ab31 | | | 124 | if (result1.done || result2.done) { |
| b69ab31 | | | 125 | break; |
| b69ab31 | | | 126 | } |
| b69ab31 | | | 127 | yield [result1.value, result2.value]; |
| b69ab31 | | | 128 | } |
| b69ab31 | | | 129 | } |
| b69ab31 | | | 130 | |
| b69ab31 | | | 131 | /** Truncate a long string. */ |
| b69ab31 | | | 132 | export function truncate(text: string, maxLength = 100): string { |
| b69ab31 | | | 133 | return text.length > maxLength ? text.substring(0, Math.max(0, maxLength - 1)) + '…' : text; |
| b69ab31 | | | 134 | } |
| b69ab31 | | | 135 | |
| b69ab31 | | | 136 | export function isPromise<T>(o: unknown): o is Promise<T> { |
| b69ab31 | | | 137 | return typeof (o as {then?: () => void})?.then === 'function'; |
| b69ab31 | | | 138 | } |
| b69ab31 | | | 139 | |
| b69ab31 | | | 140 | export function tryJsonParse(s: string): Json | undefined { |
| b69ab31 | | | 141 | try { |
| b69ab31 | | | 142 | return JSON.parse(s); |
| b69ab31 | | | 143 | } catch { |
| b69ab31 | | | 144 | return undefined; |
| b69ab31 | | | 145 | } |
| b69ab31 | | | 146 | } |
| b69ab31 | | | 147 | |
| b69ab31 | | | 148 | /** |
| b69ab31 | | | 149 | * Like Array.filter, but separates elements that pass from those that don't pass and return both arrays. |
| b69ab31 | | | 150 | * For example, partition([1, 2, 3], n => n % 2 === 0) returns [[2], [1, 3]] |
| b69ab31 | | | 151 | */ |
| b69ab31 | | | 152 | export function partition<T>(a: Array<T>, predicate: (item: T) => boolean): [Array<T>, Array<T>] { |
| b69ab31 | | | 153 | const [passed, failed] = [[], []] as [Array<T>, Array<T>]; |
| b69ab31 | | | 154 | for (const item of a) { |
| b69ab31 | | | 155 | (predicate(item) ? passed : failed).push(item); |
| b69ab31 | | | 156 | } |
| b69ab31 | | | 157 | return [passed, failed]; |
| b69ab31 | | | 158 | } |
| b69ab31 | | | 159 | |
| b69ab31 | | | 160 | /** |
| b69ab31 | | | 161 | * Like Array.filter, but separates elements that pass from those that don't pass and return both arrays. |
| b69ab31 | | | 162 | * For example, partition([1, 2, 3], n => n % 2 === 0) returns [[2], [1, 3]] |
| b69ab31 | | | 163 | */ |
| b69ab31 | | | 164 | export function group<ArrayType, BucketType extends string | number>( |
| b69ab31 | | | 165 | a: ReadonlyArray<ArrayType>, |
| b69ab31 | | | 166 | bucket: (item: ArrayType) => BucketType, |
| b69ab31 | | | 167 | ): Record<BucketType, Array<ArrayType> | undefined> { |
| b69ab31 | | | 168 | const result = {} as Record<BucketType, Array<ArrayType>>; |
| b69ab31 | | | 169 | for (const item of a) { |
| b69ab31 | | | 170 | const b = bucket(item); |
| b69ab31 | | | 171 | const existing = result[b] ?? []; |
| b69ab31 | | | 172 | existing.push(item); |
| b69ab31 | | | 173 | result[b] = existing; |
| b69ab31 | | | 174 | } |
| b69ab31 | | | 175 | return result; |
| b69ab31 | | | 176 | } |
| b69ab31 | | | 177 | |
| b69ab31 | | | 178 | /** |
| b69ab31 | | | 179 | * Split string `s` with the `sep` once. |
| b69ab31 | | | 180 | * If `s` does not contain `sep`, return undefined. |
| b69ab31 | | | 181 | */ |
| b69ab31 | | | 182 | export function splitOnce(s: string, sep: string): [string, string] | undefined { |
| b69ab31 | | | 183 | const index = s.indexOf(sep); |
| b69ab31 | | | 184 | if (index < 0) { |
| b69ab31 | | | 185 | return undefined; |
| b69ab31 | | | 186 | } |
| b69ab31 | | | 187 | return [s.substring(0, index), s.substring(index + sep.length)]; |
| b69ab31 | | | 188 | } |
| b69ab31 | | | 189 | |
| b69ab31 | | | 190 | /** |
| b69ab31 | | | 191 | * Like Array's .map() but for iterators. |
| b69ab31 | | | 192 | * Returns a new iterator applying a function to each value in the input. |
| b69ab31 | | | 193 | */ |
| b69ab31 | | | 194 | export function* mapIterable<T, R>(iterable: Iterable<T>, mapFn: (t: T) => R): IterableIterator<R> { |
| b69ab31 | | | 195 | for (const item of iterable) { |
| b69ab31 | | | 196 | yield mapFn(item); |
| b69ab31 | | | 197 | } |
| b69ab31 | | | 198 | } |
| b69ab31 | | | 199 | |
| b69ab31 | | | 200 | export function base64Decode(data: string): ArrayBuffer { |
| b69ab31 | | | 201 | return Buffer.from(data, 'base64'); |
| b69ab31 | | | 202 | } |
| b69ab31 | | | 203 | |
| b69ab31 | | | 204 | /** Deduplicate items in an array. */ |
| b69ab31 | | | 205 | export function dedup<T>(arr: Array<T>): Array<T> { |
| b69ab31 | | | 206 | return Array.from(new Set(arr)); |
| b69ab31 | | | 207 | } |