@fuzdev/fuz_util 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/array.d.ts +15 -0
  4. package/dist/array.d.ts.map +1 -0
  5. package/dist/array.js +25 -0
  6. package/dist/async.d.ts +62 -0
  7. package/dist/async.d.ts.map +1 -0
  8. package/dist/async.js +147 -0
  9. package/dist/colors.d.ts +41 -0
  10. package/dist/colors.d.ts.map +1 -0
  11. package/dist/colors.js +106 -0
  12. package/dist/counter.d.ts +7 -0
  13. package/dist/counter.d.ts.map +1 -0
  14. package/dist/counter.js +7 -0
  15. package/dist/deep_equal.d.ts +18 -0
  16. package/dist/deep_equal.d.ts.map +1 -0
  17. package/dist/deep_equal.js +152 -0
  18. package/dist/dom.d.ts +35 -0
  19. package/dist/dom.d.ts.map +1 -0
  20. package/dist/dom.js +95 -0
  21. package/dist/error.d.ts +15 -0
  22. package/dist/error.d.ts.map +1 -0
  23. package/dist/error.js +18 -0
  24. package/dist/fetch.d.ts +81 -0
  25. package/dist/fetch.d.ts.map +1 -0
  26. package/dist/fetch.js +162 -0
  27. package/dist/fs.d.ts +34 -0
  28. package/dist/fs.d.ts.map +1 -0
  29. package/dist/fs.js +73 -0
  30. package/dist/function.d.ts +27 -0
  31. package/dist/function.d.ts.map +1 -0
  32. package/dist/function.js +21 -0
  33. package/dist/git.d.ts +132 -0
  34. package/dist/git.d.ts.map +1 -0
  35. package/dist/git.js +288 -0
  36. package/dist/id.d.ts +18 -0
  37. package/dist/id.d.ts.map +1 -0
  38. package/dist/id.js +18 -0
  39. package/dist/iterator.d.ts +5 -0
  40. package/dist/iterator.d.ts.map +1 -0
  41. package/dist/iterator.js +9 -0
  42. package/dist/json.d.ts +30 -0
  43. package/dist/json.d.ts.map +1 -0
  44. package/dist/json.js +44 -0
  45. package/dist/library_json.d.ts +42 -0
  46. package/dist/library_json.d.ts.map +1 -0
  47. package/dist/library_json.js +76 -0
  48. package/dist/log.d.ts +188 -0
  49. package/dist/log.d.ts.map +1 -0
  50. package/dist/log.js +393 -0
  51. package/dist/map.d.ts +12 -0
  52. package/dist/map.d.ts.map +1 -0
  53. package/dist/map.js +14 -0
  54. package/dist/maths.d.ts +85 -0
  55. package/dist/maths.d.ts.map +1 -0
  56. package/dist/maths.js +87 -0
  57. package/dist/object.d.ts +46 -0
  58. package/dist/object.d.ts.map +1 -0
  59. package/dist/object.js +89 -0
  60. package/dist/package_json.d.ts +90 -0
  61. package/dist/package_json.d.ts.map +1 -0
  62. package/dist/package_json.js +112 -0
  63. package/dist/path.d.ts +63 -0
  64. package/dist/path.d.ts.map +1 -0
  65. package/dist/path.js +83 -0
  66. package/dist/print.d.ts +52 -0
  67. package/dist/print.d.ts.map +1 -0
  68. package/dist/print.js +89 -0
  69. package/dist/process.d.ts +77 -0
  70. package/dist/process.d.ts.map +1 -0
  71. package/dist/process.js +148 -0
  72. package/dist/random.d.ts +25 -0
  73. package/dist/random.d.ts.map +1 -0
  74. package/dist/random.js +35 -0
  75. package/dist/random_alea.d.ts +23 -0
  76. package/dist/random_alea.d.ts.map +1 -0
  77. package/dist/random_alea.js +95 -0
  78. package/dist/regexp.d.ts +12 -0
  79. package/dist/regexp.d.ts.map +1 -0
  80. package/dist/regexp.js +16 -0
  81. package/dist/result.d.ts +64 -0
  82. package/dist/result.d.ts.map +1 -0
  83. package/dist/result.js +48 -0
  84. package/dist/source_json.d.ts +375 -0
  85. package/dist/source_json.d.ts.map +1 -0
  86. package/dist/source_json.js +189 -0
  87. package/dist/string.d.ts +51 -0
  88. package/dist/string.d.ts.map +1 -0
  89. package/dist/string.js +92 -0
  90. package/dist/throttle.d.ts +26 -0
  91. package/dist/throttle.d.ts.map +1 -0
  92. package/dist/throttle.js +53 -0
  93. package/dist/timings.d.ts +33 -0
  94. package/dist/timings.d.ts.map +1 -0
  95. package/dist/timings.js +75 -0
  96. package/dist/types.d.ts +77 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +10 -0
  99. package/dist/url.d.ts +10 -0
  100. package/dist/url.d.ts.map +1 -0
  101. package/dist/url.js +8 -0
  102. package/package.json +125 -0
  103. package/src/lib/array.ts +30 -0
  104. package/src/lib/async.ts +182 -0
  105. package/src/lib/colors.ts +132 -0
  106. package/src/lib/counter.ts +11 -0
  107. package/src/lib/deep_equal.ts +155 -0
  108. package/src/lib/dom.ts +108 -0
  109. package/src/lib/error.ts +22 -0
  110. package/src/lib/fetch.ts +231 -0
  111. package/src/lib/fs.ts +128 -0
  112. package/src/lib/function.ts +32 -0
  113. package/src/lib/git.ts +390 -0
  114. package/src/lib/id.ts +30 -0
  115. package/src/lib/iterator.ts +8 -0
  116. package/src/lib/json.ts +61 -0
  117. package/src/lib/library_json.ts +122 -0
  118. package/src/lib/log.ts +469 -0
  119. package/src/lib/map.ts +18 -0
  120. package/src/lib/maths.ts +91 -0
  121. package/src/lib/object.ts +110 -0
  122. package/src/lib/package_json.ts +135 -0
  123. package/src/lib/path.ts +137 -0
  124. package/src/lib/print.ts +111 -0
  125. package/src/lib/process.ts +207 -0
  126. package/src/lib/random.ts +48 -0
  127. package/src/lib/random_alea.ts +107 -0
  128. package/src/lib/regexp.ts +17 -0
  129. package/src/lib/result.ts +67 -0
  130. package/src/lib/source_json.ts +209 -0
  131. package/src/lib/string.ts +99 -0
  132. package/src/lib/throttle.ts +70 -0
  133. package/src/lib/timings.ts +93 -0
  134. package/src/lib/types.ts +99 -0
  135. package/src/lib/url.ts +14 -0
@@ -0,0 +1,231 @@
1
+ import {z} from 'zod';
2
+
3
+ import type {Flavored} from './types.js';
4
+ import type {Logger} from './log.js';
5
+ import {EMPTY_OBJECT} from './object.js';
6
+ import type {Result} from './result.js';
7
+ import {json_stringify_deterministic} from './json.js';
8
+
9
+ const DEFAULT_GITHUB_API_ACCEPT_HEADER = 'application/vnd.github+json';
10
+ const DEFAULT_GITHUB_API_VERSION_HEADER = '2022-11-28';
11
+
12
+ export interface FetchValueOptions<TValue, TParams = undefined> {
13
+ /**
14
+ * The `request.headers` take precedence over the headers computed from other options.
15
+ */
16
+ request?: RequestInit;
17
+ params?: TParams;
18
+ parse?: (v: any) => TValue;
19
+ token?: string | null;
20
+ cache?: FetchValueCache | null;
21
+ return_early_from_cache?: boolean; // TODO name?
22
+ log?: Logger;
23
+ fetch?: typeof globalThis.fetch;
24
+ }
25
+
26
+ /**
27
+ * Specializes `fetch` with some slightly different behavior and additional features:
28
+ *
29
+ * - throws on ratelimit errors to mitigate unintentional abuse
30
+ * - optional `parse` function called on the return value
31
+ * - optional cache (different from the browser cache,
32
+ * the caller can serialize it so e.g. dev setups can avoid hitting the network)
33
+ * - optional simplified API for authorization and data types
34
+ * (you can still provide headers directly)
35
+ *
36
+ * Unlike `fetch`, this throws on ratelimits (status code 429)
37
+ * to halt whatever is happpening in its tracks to avoid accidental abuse,
38
+ * but returns a `Result` in all other cases.
39
+ * Handling ratelimit headers with more sophistication gets tricky because behavior
40
+ * differs across services.
41
+ * (e.g. Mastodon returns an ISO string for `x-ratelimit-reset`,
42
+ * but GitHub returns `Date.now()/1000`,
43
+ * and other services may do whatever, or even use a different header)
44
+ *
45
+ * It's also stateless to avoid the complexity and bugs,
46
+ * so we don't try to track `x-ratelimit-remaining` per domain.
47
+ *
48
+ * If the `value` is cached, only the cached safe subset of the `headers` are returned.
49
+ * (currently just `etag` and `last-modified`)
50
+ * Otherwise the full `res.headers` are included.
51
+ * @mutates options.cache calls `cache.set()` to store fetched results if cache is provided
52
+ */
53
+ export const fetch_value = async <TValue = any, TParams = undefined>(
54
+ url: string | URL,
55
+ options?: FetchValueOptions<TValue, TParams>,
56
+ ): Promise<Result<{value: TValue; headers: Headers}, {status: number; message: string}>> => {
57
+ const {
58
+ request,
59
+ params,
60
+ parse,
61
+ token,
62
+ cache,
63
+ return_early_from_cache,
64
+ log,
65
+ fetch = globalThis.fetch,
66
+ } = options ?? EMPTY_OBJECT;
67
+
68
+ const url_obj = typeof url === 'string' ? new URL(url) : url;
69
+ const url_str = url_obj.href;
70
+
71
+ const method = request?.method ?? (params ? 'POST' : 'GET');
72
+
73
+ // local cache?
74
+ let cached;
75
+ let key;
76
+ if (cache) {
77
+ key = to_fetch_value_cache_key(url_str, params, method);
78
+ cached = cache.get(key);
79
+ if (return_early_from_cache && cached) {
80
+ log?.info('[fetch_value] cached locally and returning early', url_str);
81
+ log?.debug('[fetch_value] cached value', cached);
82
+ return {ok: true, value: cached.value, headers: to_cached_headers(cached)};
83
+ }
84
+ }
85
+
86
+ const body =
87
+ request?.body ?? (method === 'GET' || method === 'HEAD' ? null : JSON.stringify(params ?? {}));
88
+
89
+ const headers = new Headers(request?.headers);
90
+ if (!headers.has('accept')) {
91
+ headers.set(
92
+ 'accept',
93
+ url_obj.hostname === 'api.github.com' ? DEFAULT_GITHUB_API_ACCEPT_HEADER : 'application/json',
94
+ );
95
+ }
96
+ if (
97
+ headers.get('accept') === DEFAULT_GITHUB_API_ACCEPT_HEADER &&
98
+ !headers.has('x-github-api-version')
99
+ ) {
100
+ headers.set('x-github-api-version', DEFAULT_GITHUB_API_VERSION_HEADER);
101
+ }
102
+ if (body && !headers.has('content-type')) {
103
+ headers.set('content-type', 'application/json');
104
+ }
105
+ if (token && !headers.has('authorization')) {
106
+ headers.set('authorization', 'Bearer ' + token);
107
+ }
108
+ const etag = cached?.etag;
109
+ if (etag && !headers.has('if-none-match')) {
110
+ headers.set('if-none-match', etag);
111
+ } else {
112
+ // fall back to last-modified, ignoring if there's an etag
113
+ const last_modified = cached?.last_modified;
114
+ if (last_modified && !headers.has('if-modified-since')) {
115
+ headers.set('if-modified-since', last_modified);
116
+ }
117
+ }
118
+
119
+ const req = new Request(url_obj, {...request, headers, method, body});
120
+
121
+ log?.info('[fetch_value] fetching url with headers', url);
122
+ log?.debug('[fetch_value] fetching with headers', print_headers(headers));
123
+ const res = await fetch(req); // don't catch network errors
124
+ log?.info('[fetch_value] fetched', url, res.status, print_ratelimit_headers(res.headers));
125
+ log?.debug('[fetch_value] fetched', Object.fromEntries(res.headers.entries()));
126
+
127
+ // throw on ratelimit
128
+ if (res.status === 429) {
129
+ throw Error('ratelimited exceeded fetching url ' + url);
130
+ }
131
+
132
+ // return from cache if it hits
133
+ if (res.status === 304) {
134
+ if (!cached) throw Error('unexpected 304 status without a cached value');
135
+ log?.info('[fetch_value] cache hit', url);
136
+ return {ok: true, value: cached.value, headers: to_cached_headers(cached)};
137
+ }
138
+
139
+ if (!res.ok) {
140
+ return {ok: false, status: res.status, message: res.statusText};
141
+ }
142
+
143
+ const content_type = res.headers.get('content-type');
144
+
145
+ const fetched = await (!content_type || content_type.includes('json') ? res.json() : res.text()); // TODO hacky
146
+
147
+ const parsed = parse ? parse(fetched) : fetched;
148
+ log?.debug('[fetch_value] fetched json', url, parsed);
149
+
150
+ if (key) {
151
+ const result: FetchValueCacheItem = {
152
+ key,
153
+ url: url_str,
154
+ params,
155
+ value: parsed,
156
+ etag: res.headers.get('etag'),
157
+ last_modified: res.headers.get('etag') ? null : res.headers.get('last-modified'), // fall back to last-modified, ignoring if there's an etag
158
+ };
159
+ cache!.set(key, result);
160
+ }
161
+
162
+ return {ok: true, value: parsed, headers: res.headers};
163
+ };
164
+
165
+ /**
166
+ * Returns a subset of headers that are safe to store in a `fetch_value` cache.
167
+ */
168
+ const to_cached_headers = (cached: FetchValueCacheItem): Headers => {
169
+ const headers = new Headers();
170
+ if (cached.etag) {
171
+ headers.set('etag', cached.etag);
172
+ }
173
+ if (cached.last_modified) {
174
+ headers.set('last-modified', cached.last_modified);
175
+ }
176
+ return headers;
177
+ };
178
+
179
+ const print_headers = (headers: Headers): Record<string, string> => {
180
+ const h = Object.fromEntries(headers.entries());
181
+ if (h.authorization) h.authorization = '[REDACTED]';
182
+ return h;
183
+ };
184
+
185
+ const print_ratelimit_headers = (headers: Headers): string => {
186
+ const limit = headers.get('x-ratelimit-limit');
187
+ const remaining = headers.get('x-ratelimit-remaining');
188
+ return limit || remaining ? `ratelimit ${remaining} of ${limit}` : '';
189
+ };
190
+
191
+ export const FetchValueCacheKey = z.string();
192
+ export type FetchValueCacheKey = Flavored<z.infer<typeof FetchValueCacheKey>, 'FetchValueCacheKey'>;
193
+
194
+ export const FetchValueCacheItem = z.object({
195
+ key: FetchValueCacheKey,
196
+ url: z.string(),
197
+ params: z.any(),
198
+ value: z.any(),
199
+ etag: z.string().nullable(),
200
+ last_modified: z.string().nullable(),
201
+ });
202
+ export type FetchValueCacheItem = z.infer<typeof FetchValueCacheItem>;
203
+
204
+ export const FetchValueCache = z.map(FetchValueCacheKey, FetchValueCacheItem);
205
+ export type FetchValueCache = z.infer<typeof FetchValueCache>;
206
+
207
+ const KEY_SEPARATOR = '::';
208
+
209
+ export const to_fetch_value_cache_key = (
210
+ url: string,
211
+ params: any,
212
+ method: string,
213
+ ): FetchValueCacheKey => {
214
+ let key = method + KEY_SEPARATOR + url;
215
+ if (params != null) {
216
+ key += KEY_SEPARATOR + json_stringify_deterministic(params);
217
+ }
218
+ return key;
219
+ };
220
+
221
+ /**
222
+ * Converts `FetchValueCache` to a JSON string.
223
+ */
224
+ export const serialize_cache = (cache: FetchValueCache): string =>
225
+ JSON.stringify(Array.from(cache.entries()));
226
+
227
+ /**
228
+ * Converts a serialized cache string to a `FetchValueCache`.
229
+ */
230
+ export const deserialize_cache = (serialized: string): FetchValueCache =>
231
+ FetchValueCache.parse(new Map(JSON.parse(serialized)));
package/src/lib/fs.ts ADDED
@@ -0,0 +1,128 @@
1
+ import {rm, readdir, access, constants} from 'node:fs/promises';
2
+ import type {RmOptions} from 'node:fs';
3
+ import {join, isAbsolute} from 'node:path';
4
+
5
+ import {EMPTY_OBJECT} from './object.js';
6
+ import {to_array} from './array.js';
7
+ import {ensure_end} from './string.js';
8
+ import type {FileFilter, ResolvedPath, PathFilter} from './path.js';
9
+
10
+ /**
11
+ * Checks if a file or directory exists.
12
+ */
13
+ export const fs_exists = async (path: string): Promise<boolean> => {
14
+ try {
15
+ await access(path, constants.F_OK);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ };
21
+
22
+ /**
23
+ * Empties a directory, recursively by default. If `should_remove` is provided, only entries where it returns `true` are removed.
24
+ */
25
+ export const fs_empty_dir = async (
26
+ dir: string,
27
+ should_remove?: (name: string) => boolean,
28
+ options?: RmOptions,
29
+ ): Promise<void> => {
30
+ const entries = await readdir(dir);
31
+ const to_remove = should_remove ? entries.filter(should_remove) : entries;
32
+ await Promise.all(to_remove.map((name) => rm(join(dir, name), {recursive: true, ...options})));
33
+ };
34
+
35
+ export interface FsSearchOptions {
36
+ /**
37
+ * One or more filter functions, any of which can short-circuit the search by returning `false`.
38
+ */
39
+ filter?: PathFilter | Array<PathFilter>;
40
+ /**
41
+ * One or more file filter functions. Every filter must pass for a file to be included.
42
+ */
43
+ file_filter?: FileFilter | Array<FileFilter>;
44
+ /**
45
+ * Pass `null` or `false` to speed things up at the cost of volatile ordering.
46
+ */
47
+ sort?: boolean | null | ((a: ResolvedPath, b: ResolvedPath) => number);
48
+ /**
49
+ * Set to `true` to include directories. Defaults to `false`.
50
+ */
51
+ include_directories?: boolean;
52
+ /**
53
+ * Sets the cwd for `dir` unless it's an absolute path or `null`.
54
+ */
55
+ cwd?: string | null;
56
+ }
57
+
58
+ export const fs_search = async (
59
+ dir: string,
60
+ options: FsSearchOptions = EMPTY_OBJECT,
61
+ ): Promise<Array<ResolvedPath>> => {
62
+ const {
63
+ filter,
64
+ file_filter,
65
+ sort = default_sort,
66
+ include_directories = false,
67
+ cwd = process.cwd(),
68
+ } = options;
69
+
70
+ const final_dir = ensure_end(cwd && !isAbsolute(dir) ? join(cwd, dir) : dir, '/');
71
+
72
+ const filters =
73
+ !filter || (Array.isArray(filter) && !filter.length) ? undefined : to_array(filter);
74
+ const file_filters =
75
+ !file_filter || (Array.isArray(file_filter) && !file_filter.length)
76
+ ? undefined
77
+ : to_array(file_filter);
78
+
79
+ const paths: Array<ResolvedPath> = []; // mutated
80
+ try {
81
+ await crawl(final_dir, paths, filters, file_filters, include_directories, null);
82
+ } catch (error) {
83
+ if (error.code === 'ENOENT') return [];
84
+ throw error;
85
+ }
86
+
87
+ return sort ? paths.sort(typeof sort === 'boolean' ? default_sort : sort) : paths;
88
+ };
89
+
90
+ const default_sort = (a: ResolvedPath, b: ResolvedPath): number => a.path.localeCompare(b.path);
91
+
92
+ /**
93
+ * Recursively crawls a directory, collecting paths.
94
+ * @mutates paths - appends discovered files and directories
95
+ */
96
+ const crawl = async (
97
+ dir: string,
98
+ paths: Array<ResolvedPath>,
99
+ filters: Array<PathFilter> | undefined,
100
+ file_filters: Array<FileFilter> | undefined,
101
+ include_directories: boolean,
102
+ base_dir: string | null,
103
+ ): Promise<void> => {
104
+ let subdirs: Array<Promise<void>> | undefined;
105
+ const dirents = await readdir(dir, {withFileTypes: true});
106
+ for (const dirent of dirents) {
107
+ const {name, parentPath} = dirent;
108
+ const is_directory = dirent.isDirectory();
109
+ const id = parentPath + name;
110
+
111
+ if (filters && !filters.every((f) => f(id, is_directory))) {
112
+ continue;
113
+ }
114
+
115
+ const path = base_dir === null ? name : base_dir + '/' + name;
116
+
117
+ if (is_directory) {
118
+ const dir_id = id + '/';
119
+ if (include_directories) {
120
+ paths.push({path, id: dir_id, is_directory: true});
121
+ }
122
+ (subdirs ??= []).push(crawl(dir_id, paths, filters, file_filters, include_directories, path));
123
+ } else if (!file_filters || file_filters.every((f) => f(id))) {
124
+ paths.push({path, id, is_directory: false});
125
+ }
126
+ }
127
+ if (subdirs) await Promise.all(subdirs);
128
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Does nothing when called.
3
+ */
4
+ export const noop: (...args: Array<any>) => any = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function
5
+
6
+ /**
7
+ * Async function that returns a resolved promise.
8
+ */
9
+ export const noop_async: (...args: Array<any>) => Promise<any> = () => resolved;
10
+
11
+ /**
12
+ * A singleton resolved `Promise`.
13
+ */
14
+ export const resolved = Promise.resolve();
15
+
16
+ /**
17
+ * Returns the first arg.
18
+ */
19
+ export const identity = <T>(t: T): T => t;
20
+
21
+ /**
22
+ * Represents a value wrapped in a function.
23
+ * Useful for lazy values and bridging reactive boundaries in Svelte.
24
+ */
25
+ export type Thunk<T> = () => T;
26
+
27
+ /**
28
+ * Returns the result of calling `value` if it's a function,
29
+ * otherwise it's like the `identity` function and passes it through.
30
+ */
31
+ export const unthunk = <T>(value: T | Thunk<T>): T =>
32
+ typeof value === 'function' ? (value as any)() : value;