5.8 KB208 lines
Blame
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
8import type {Json} from './typeUtils';
9
10export 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 */
17export 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 */
28export function randomId(): string {
29 return Date.now().toString(36) + Math.random().toString(36);
30}
31
32export 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 */
41export 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 */
65export 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 */
77export 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 */
91export 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 */
102export 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 */
118export 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. */
132export function truncate(text: string, maxLength = 100): string {
133 return text.length > maxLength ? text.substring(0, Math.max(0, maxLength - 1)) + '…' : text;
134}
135
136export function isPromise<T>(o: unknown): o is Promise<T> {
137 return typeof (o as {then?: () => void})?.then === 'function';
138}
139
140export 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 */
152export 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 */
164export 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 */
182export 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 */
194export 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
200export function base64Decode(data: string): ArrayBuffer {
201 return Buffer.from(data, 'base64');
202}
203
204/** Deduplicate items in an array. */
205export function dedup<T>(arr: Array<T>): Array<T> {
206 return Array.from(new Set(arr));
207}
208