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