@welshman/lib 0.1.1 → 0.2.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/{build/src → dist}/Deferred.d.ts +1 -0
  3. package/dist/Deferred.d.ts.map +1 -0
  4. package/dist/Deferred.js.map +1 -0
  5. package/{build/src → dist}/Emitter.d.ts +1 -0
  6. package/dist/Emitter.d.ts.map +1 -0
  7. package/dist/Emitter.js.map +1 -0
  8. package/{build/src → dist}/LRUCache.d.ts +1 -0
  9. package/dist/LRUCache.d.ts.map +1 -0
  10. package/dist/LRUCache.js.map +1 -0
  11. package/{build/src → dist}/TaskQueue.d.ts +3 -0
  12. package/dist/TaskQueue.d.ts.map +1 -0
  13. package/{build/src → dist}/TaskQueue.js +10 -0
  14. package/dist/TaskQueue.js.map +1 -0
  15. package/{build/src → dist}/Tools.d.ts +428 -338
  16. package/dist/Tools.d.ts.map +1 -0
  17. package/{build/src → dist}/Tools.js +716 -500
  18. package/dist/Tools.js.map +1 -0
  19. package/{build/src → dist}/index.d.ts +1 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/normalize-url/index.d.ts +286 -0
  23. package/dist/normalize-url/index.d.ts.map +1 -0
  24. package/{build/src → dist}/normalize-url/index.js +53 -51
  25. package/dist/normalize-url/index.js.map +1 -0
  26. package/package.json +14 -17
  27. package/README.md +0 -13
  28. package/build/src/Deferred.js.map +0 -1
  29. package/build/src/Emitter.js.map +0 -1
  30. package/build/src/LRUCache.js.map +0 -1
  31. package/build/src/TaskQueue.js.map +0 -1
  32. package/build/src/Tools.js.map +0 -1
  33. package/build/src/index.js.map +0 -1
  34. package/build/src/normalize-url/index.d.ts +0 -285
  35. package/build/src/normalize-url/index.js.map +0 -1
  36. package/build/tsconfig.tsbuildinfo +0 -1
  37. /package/{build/src → dist}/Deferred.js +0 -0
  38. /package/{build/src → dist}/Emitter.js +0 -0
  39. /package/{build/src → dist}/LRUCache.js +0 -0
  40. /package/{build/src → dist}/index.js +0 -0
@@ -1,33 +1,12 @@
1
1
  import { bech32, utf8 } from "@scure/base";
2
- /** Checks if a value is null or undefined */
3
- export const isNil = (x) => [null, undefined].includes(x);
4
- /**
5
- * Executes a function if the value is defined
6
- * @param x - The value to check
7
- * @param f - Function to execute if x is defined
8
- * @returns Result of f(x) if x is defined, undefined otherwise
9
- */
10
- export const ifLet = (x, f) => x === undefined ? undefined : f(x);
2
+ export const isNil = (x, ...args) => x === undefined || x === null;
3
+ export const isNotNil = (x, ...args) => x !== undefined && x !== null;
4
+ export const assertNotNil = (x, ...args) => x;
5
+ // ----------------------------------------------------------------------------
6
+ // Basic functional programming utilities
7
+ // ----------------------------------------------------------------------------
11
8
  /** Function that does nothing and returns undefined */
12
9
  export const noop = (...args) => undefined;
13
- /**
14
- * Returns the first element of an array
15
- * @param xs - The array
16
- * @returns First element or undefined
17
- */
18
- export const first = (xs, ...args) => xs[0];
19
- /**
20
- * Returns the first element of the first array in a nested array
21
- * @param xs - Array of arrays
22
- * @returns First element of first array or undefined
23
- */
24
- export const ffirst = (xs, ...args) => xs[0][0];
25
- /**
26
- * Returns the last element of an array
27
- * @param xs - The array
28
- * @returns Last element or undefined
29
- */
30
- export const last = (xs, ...args) => xs[xs.length - 1];
31
10
  /**
32
11
  * Returns the input value unchanged
33
12
  * @param x - Any value
@@ -46,9 +25,60 @@ export const always = (x, ...args) => () => x;
46
25
  * @returns !x
47
26
  */
48
27
  export const not = (x, ...args) => !x;
49
- /** Returns a function that returns the boolean negation of the given function */
50
- export const complement = (f) => (...args) => !f(...args);
51
- /** Converts a `Maybe<number>` to a number, defaulting to 0 */
28
+ /**
29
+ * Deep equality comparison
30
+ * @param a - First value
31
+ * @param b - Second value
32
+ * @returns True if values are deeply equal
33
+ */
34
+ export const equals = (a, b) => {
35
+ if (a === b)
36
+ return true;
37
+ if (a instanceof Set && b instanceof Set) {
38
+ a = Array.from(a);
39
+ b = Array.from(b);
40
+ }
41
+ if (a instanceof Set) {
42
+ if (!(b instanceof Set) || a.size !== b.size) {
43
+ return false;
44
+ }
45
+ return Array.from(a).every(x => b.has(x));
46
+ }
47
+ if (Array.isArray(a)) {
48
+ if (!Array.isArray(b) || a.length !== b.length) {
49
+ return false;
50
+ }
51
+ for (let i = 0; i < a.length; i++) {
52
+ if (!equals(a[i], b[i])) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+ if (isPojo(a)) {
59
+ if (!isPojo(b)) {
60
+ return false;
61
+ }
62
+ const aKeys = Object.keys(a);
63
+ const bKeys = Object.keys(b);
64
+ if (aKeys.length !== bKeys.length) {
65
+ return false;
66
+ }
67
+ for (const k of aKeys) {
68
+ if (!equals(a[k], b[k])) {
69
+ return false;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+ return false;
75
+ };
76
+ // ----------------------------------------------------------------------------
77
+ // Numbers
78
+ // ----------------------------------------------------------------------------
79
+ /** Converts string or number to number */
80
+ export const ensureNumber = (x) => parseFloat(x);
81
+ /** Converts a `number | undefined` to a number, defaulting to 0 */
52
82
  export const num = (x) => x || 0;
53
83
  /** Adds two numbers, handling undefined values */
54
84
  export const add = (x, y) => num(x) + num(y);
@@ -74,7 +104,7 @@ export const gte = (x, y) => num(x) >= num(y);
74
104
  export const max = (xs) => xs.reduce((a, b) => Math.max(num(a), num(b)), 0);
75
105
  /** Returns minimum value in array, handling undefined values */
76
106
  export const min = (xs) => {
77
- const [head, ...tail] = xs.filter(x => !isNil(x));
107
+ const [head, ...tail] = xs.filter(x => x !== undefined);
78
108
  if (tail.length === 0)
79
109
  return head || 0;
80
110
  return tail.reduce((a, b) => Math.min(a, b), head);
@@ -84,217 +114,211 @@ export const sum = (xs) => xs.reduce((a, b) => add(a, b), 0);
84
114
  /** Returns average of array values, handling undefined values */
85
115
  export const avg = (xs) => sum(xs) / xs.length;
86
116
  /**
87
- * Returns array with first n elements removed
88
- * @param n - Number of elements to drop
89
- * @param xs - Input array
90
- * @returns Array with first n elements removed
117
+ * Checks if a number is between two values (exclusive)
118
+ * @param bounds - Lower and upper bounds
119
+ * @param n - Number to check
120
+ * @returns True if n is between low and high
91
121
  */
92
- export const drop = (n, xs) => xs.slice(n);
122
+ export const between = ([low, high], n) => n > low && n < high;
93
123
  /**
94
- * Returns first n elements of array
95
- * @param n - Number of elements to take
96
- * @param xs - Input array
97
- * @returns Array of first n elements
124
+ * Checks if a number is between two values (inclusive)
125
+ * @param bounds - Lower and upper bounds
126
+ * @param n - Number to check
127
+ * @returns True if n is between low and high
98
128
  */
99
- export const take = (n, xs) => xs.slice(0, n);
129
+ export const within = ([low, high], n) => n >= low && n <= high;
100
130
  /**
101
- * Creates new object with specified keys removed
102
- * @param ks - Keys to remove
103
- * @param x - Source object
104
- * @returns New object without specified keys
131
+ * Constrains number between min and max values
132
+ * @param bounds - Minimum and maximum allowed values
133
+ * @param n - Number to clamp
134
+ * @returns Clamped value
105
135
  */
106
- export const omit = (ks, x) => {
107
- const r = { ...x };
108
- for (const k of ks) {
109
- delete r[k];
110
- }
111
- return r;
112
- };
136
+ export const clamp = ([min, max], n) => Math.min(max, Math.max(min, n));
113
137
  /**
114
- * Creates new object excluding entries with specified values
115
- * @param xs - Values to exclude
116
- * @param x - Source object
117
- * @returns New object without entries containing specified values
138
+ * Round a number to the nearest float precision
139
+ * @param precision - Number of decimal places
140
+ * @param x - Number to round
141
+ * @returns Formatted number
118
142
  */
119
- export const omitVals = (xs, x) => {
120
- const r = {};
121
- for (const [k, v] of Object.entries(x)) {
122
- if (!xs.includes(v)) {
123
- r[k] = v;
124
- }
125
- }
126
- return r;
127
- };
143
+ export const round = (precision, x) => Math.round(x * Math.pow(10, precision)) / Math.pow(10, precision);
144
+ // ----------------------------------------------------------------------------
145
+ // Timestamps
146
+ // ----------------------------------------------------------------------------
147
+ /** One minute in seconds */
148
+ export const MINUTE = 60;
149
+ /** One hour in seconds */
150
+ export const HOUR = 60 * MINUTE;
151
+ /** One day in seconds */
152
+ export const DAY = 24 * HOUR;
153
+ /** One week in seconds */
154
+ export const WEEK = 7 * DAY;
155
+ /** One month in seconds (approximate) */
156
+ export const MONTH = 30 * DAY;
157
+ /** One quarter in seconds (approximate) */
158
+ export const QUARTER = 90 * DAY;
159
+ /** One year in seconds (approximate) */
160
+ export const YEAR = 365 * DAY;
161
+ /** User's default locale */
162
+ export const LOCALE = new Intl.DateTimeFormat().resolvedOptions().locale;
163
+ /** User's default timezone */
164
+ export const TIMEZONE = new Date().toString().match(/GMT[^\s]+/)[0];
128
165
  /**
129
- * Creates new object with only specified keys
130
- * @param ks - Keys to keep
131
- * @param x - Source object
132
- * @returns New object with only specified keys
166
+ * Multiplies time unit by count
167
+ * @param unit - Time unit in seconds
168
+ * @param count - Number of units
169
+ * @returns Total seconds
133
170
  */
134
- export const pick = (ks, x) => {
135
- const r = { ...x };
136
- for (const k of Object.keys(x)) {
137
- if (!ks.includes(k)) {
138
- delete r[k];
139
- }
140
- }
141
- return r;
142
- };
171
+ export const int = (unit, count = 1) => unit * count;
172
+ /** Returns current Unix timestamp in seconds */
173
+ export const now = () => Math.round(Date.now() / 1000);
143
174
  /**
144
- * Generates sequence of numbers from a to b
145
- * @param a - Start number (inclusive)
146
- * @param b - End number (exclusive)
147
- * @param step - Increment between numbers
148
- * @yields Numbers in sequence
175
+ * Returns Unix timestamp from specified time ago
176
+ * @param unit - Time unit in seconds
177
+ * @param count - Number of units
178
+ * @returns Timestamp in seconds
149
179
  */
150
- export function* range(a, b, step = 1) {
151
- for (let i = a; i < b; i += step) {
152
- yield i;
153
- }
154
- }
180
+ export const ago = (unit, count = 1) => now() - int(unit, count);
155
181
  /**
156
- * Yields indexed items
157
- * @param items - A collection of items
158
- * @yields tuples of [index, item]
182
+ * Converts seconds to milliseconds
183
+ * @param seconds - Time in seconds
184
+ * @returns Time in milliseconds
159
185
  */
160
- export function* enumerate(items) {
161
- for (let i = 0; i < items.length; i += 1) {
162
- yield [i, items[i]];
163
- }
164
- }
186
+ export const ms = (seconds) => seconds * 1000;
165
187
  /**
166
- * Creates new object with transformed keys
167
- * @param f - Function to transform keys
168
- * @param x - Source object
169
- * @returns Object with transformed keys
188
+ * Converts seconds to date
189
+ * @param seconds - Time in seconds
190
+ * @returns Date object
170
191
  */
171
- export const mapKeys = (f, x) => {
172
- const r = {};
173
- for (const [k, v] of Object.entries(x)) {
174
- r[f(k)] = v;
175
- }
176
- return r;
177
- };
192
+ export const secondsToDate = (seconds) => new Date(seconds * 1000);
178
193
  /**
179
- * Creates new object with transformed values
180
- * @param f - Function to transform values
181
- * @param x - Source object
182
- * @returns Object with transformed values
194
+ * Converts date object to seconds
195
+ * @param date - Date object
196
+ * @returns timestamp in seconds
183
197
  */
184
- export const mapVals = (f, x) => {
185
- const r = {};
186
- for (const [k, v] of Object.entries(x)) {
187
- r[k] = f(v);
188
- }
189
- return r;
190
- };
198
+ export const dateToSeconds = (date) => Math.round(date.valueOf() / 1000);
191
199
  /**
192
- * Merges two objects, with left object taking precedence
193
- * @param a - Left object
194
- * @param b - Right object
195
- * @returns Merged object with a"s properties overriding b"s
200
+ * Creates a local date from a date string
201
+ * @param dateString - date string
202
+ * @param timezone - timezone string
203
+ * @returns timezone-aware Date object
196
204
  */
197
- export const mergeLeft = (a, b) => ({
198
- ...b,
199
- ...a,
205
+ export const createLocalDate = (dateString, timezone = TIMEZONE) => new Date(`${dateString} ${timezone}`);
206
+ /** Formatter for date+time */
207
+ export const dateTimeFormatter = new Intl.DateTimeFormat(LOCALE, {
208
+ dateStyle: "short",
209
+ timeStyle: "short",
200
210
  });
201
211
  /**
202
- * Merges two objects, with right object taking precedence
203
- * @param a - Left object
204
- * @param b - Right object
205
- * @returns Merged object with b"s properties overriding a"s
212
+ * Formats seconds as a datetime
213
+ * @param seconds - timestamp in seconds
214
+ * @returns datetime string
206
215
  */
207
- export const mergeRight = (a, b) => ({
208
- ...a,
209
- ...b,
216
+ export const formatTimestamp = (seconds) => dateTimeFormatter.format(secondsToDate(seconds));
217
+ /** Formatter for date */
218
+ export const dateFormatter = new Intl.DateTimeFormat(LOCALE, {
219
+ year: "numeric",
220
+ month: "long",
221
+ day: "numeric",
210
222
  });
211
- /** Deep merge two objects, prioritizing the first argument. */
212
- export const deepMergeLeft = (a, b) => deepMergeRight(b, a);
213
- /** Deep merge two objects, prioritizing the second argument. */
214
- export const deepMergeRight = (a, b) => {
215
- a = { ...a };
216
- for (const [k, v] of Object.entries(b)) {
217
- if (isPojo(v) && isPojo(a[k])) {
218
- a[k] = deepMergeRight(a[k], v);
219
- }
220
- else {
221
- a[k] = v;
222
- }
223
- }
224
- return a;
225
- };
226
223
  /**
227
- * Checks if a number is between two values (exclusive)
228
- * @param bounds - Lower and upper bounds
229
- * @param n - Number to check
230
- * @returns True if n is between low and high
224
+ * Formats seconds as a date
225
+ * @param seconds - timestamp in seconds
226
+ * @returns date string
231
227
  */
232
- export const between = ([low, high], n) => n > low && n < high;
228
+ export const formatTimestampAsDate = (ts) => dateFormatter.format(secondsToDate(ts));
229
+ /** Formatter for time */
230
+ export const timeFormatter = new Intl.DateTimeFormat(LOCALE, {
231
+ timeStyle: "short",
232
+ });
233
233
  /**
234
- * Checks if a number is between two values (inclusive)
235
- * @param bounds - Lower and upper bounds
236
- * @param n - Number to check
237
- * @returns True if n is between low and high
234
+ * Formats seconds as a time
235
+ * @param seconds - timestamp in seconds
236
+ * @returns time string
238
237
  */
239
- export const within = ([low, high], n) => n >= low && n <= high;
238
+ export const formatTimestampAsTime = (ts) => timeFormatter.format(secondsToDate(ts));
240
239
  /**
241
- * Generates random integer between min and max (inclusive)
242
- * @param min - Minimum value
243
- * @param max - Maximum value
244
- * @returns Random integer
240
+ * Formats seconds as a relative date (x minutes ago)
241
+ * @param seconds - timestamp in seconds
242
+ * @returns relative date string
245
243
  */
246
- export const randomInt = (min = 0, max = 9) => min + Math.round(Math.random() * (max - min));
244
+ export const formatTimestampRelative = (ts) => {
245
+ let unit;
246
+ let delta = now() - ts;
247
+ if (delta < int(MINUTE)) {
248
+ unit = "second";
249
+ }
250
+ else if (delta < int(HOUR)) {
251
+ unit = "minute";
252
+ delta = Math.round(delta / int(MINUTE));
253
+ }
254
+ else if (delta < int(DAY, 2)) {
255
+ unit = "hour";
256
+ delta = Math.round(delta / int(HOUR));
257
+ }
258
+ else {
259
+ unit = "day";
260
+ delta = Math.round(delta / int(DAY));
261
+ }
262
+ const locale = new Intl.RelativeTimeFormat().resolvedOptions().locale;
263
+ const formatter = new Intl.RelativeTimeFormat(locale, {
264
+ numeric: "auto",
265
+ });
266
+ return formatter.format(-delta, unit);
267
+ };
268
+ // ----------------------------------------------------------------------------
269
+ // Sequences
270
+ // ----------------------------------------------------------------------------
247
271
  /**
248
- * Generates random string ID
249
- * @returns Random string suitable for use as an ID
272
+ * Returns the first element of an array
273
+ * @param xs - The array
274
+ * @returns First element or undefined
250
275
  */
251
- export const randomId = () => Math.random().toString().slice(2);
276
+ export const first = (xs, ...args) => {
277
+ for (const x of xs) {
278
+ return x;
279
+ }
280
+ };
252
281
  /**
253
- * Removes protocol (http://, https://, etc) from URL
254
- * @param url - URL to process
255
- * @returns URL without protocol
282
+ * Returns the first element of the first array in a nested array
283
+ * @param xs - Array of arrays
284
+ * @returns First element of first array or undefined
256
285
  */
257
- export const stripProtocol = (url) => url.replace(/.*:\/\//, "");
286
+ export const ffirst = (xs, ...args) => {
287
+ for (const chunk of xs) {
288
+ for (const x of chunk) {
289
+ return x;
290
+ }
291
+ }
292
+ };
258
293
  /**
259
- * Formats URL for display by removing protocol, www, and trailing slash
260
- * @param url - URL to format
261
- * @returns Formatted URL
294
+ * Returns the last element of an array
295
+ * @param xs - The array
296
+ * @returns Last element or undefined
262
297
  */
263
- export const displayUrl = (url) => stripProtocol(url)
264
- .replace(/^(www\.)?/i, "")
265
- .replace(/\/$/, "");
298
+ export const last = (xs, ...args) => {
299
+ const a = Array.from(xs);
300
+ return a[a.length - 1];
301
+ };
266
302
  /**
267
- * Extracts and formats domain from URL
268
- * @param url - URL to process
269
- * @returns Formatted domain name
303
+ * Returns array with first n elements removed
304
+ * @param n - Number of elements to drop
305
+ * @param xs - Input array
306
+ * @returns Array with first n elements removed
270
307
  */
271
- export const displayDomain = (url) => displayUrl(first(url.split(/[\/\?]/)));
308
+ export const drop = (n, xs) => Array.from(xs).slice(n);
272
309
  /**
273
- * Creates a promise that resolves after specified time
274
- * @param t - Time in milliseconds
275
- * @returns Promise that resolves after t milliseconds
310
+ * Returns first n elements of array
311
+ * @param n - Number of elements to take
312
+ * @param xs - Input array
313
+ * @returns Array of first n elements
276
314
  */
277
- export const sleep = (t) => new Promise(resolve => setTimeout(resolve, t));
278
- /**
279
- * Creates a microtask that yields to other tasks in the event loop
280
- * @returns Promise that resolves after yielding
281
- */
282
- export const yieldThread = () => {
283
- if (typeof window !== "undefined" &&
284
- "scheduler" in window &&
285
- "yield" in window.scheduler) {
286
- return window.scheduler.yield();
287
- }
288
- return new Promise(resolve => {
289
- setTimeout(resolve, 0);
290
- });
291
- };
315
+ export const take = (n, xs) => Array.from(xs).slice(0, n);
292
316
  /**
293
317
  * Concatenates multiple arrays, filtering out null/undefined
294
318
  * @param xs - Arrays to concatenate
295
319
  * @returns Combined array
296
320
  */
297
- export const concat = (...xs) => xs.flatMap(x => (isNil(x) ? [] : x));
321
+ export const concat = (...xs) => xs.flatMap(x => (x === undefined ? [] : x));
298
322
  /**
299
323
  * Appends element to array
300
324
  * @param x - Element to append
@@ -336,6 +360,13 @@ export const difference = (a, b) => {
336
360
  * @returns New array with element removed
337
361
  */
338
362
  export const remove = (a, xs) => xs.filter(x => x !== a);
363
+ /**
364
+ * Removes element at index
365
+ * @param i - Index to remove
366
+ * @param xs - Source array
367
+ * @returns New array with element removed
368
+ */
369
+ export const removeAt = (i, xs) => [...xs.slice(0, i), ...xs.slice(i + 1)];
339
370
  /**
340
371
  * Returns elements from second array not present in first
341
372
  * @param a - Array of elements to exclude
@@ -351,195 +382,27 @@ export const without = (a, b) => b.filter(x => !a.includes(x));
351
382
  */
352
383
  export const toggle = (x, xs) => (xs.includes(x) ? remove(x, xs) : append(x, xs));
353
384
  /**
354
- * Constrains number between min and max values
355
- * @param bounds - Minimum and maximum allowed values
356
- * @param n - Number to clamp
357
- * @returns Clamped value
358
- */
359
- export const clamp = ([min, max], n) => Math.min(max, Math.max(min, n));
360
- /**
361
- * Round a number to the nearest float precision
362
- * @param precision - Number of decimal places
363
- * @param x - Number to round
364
- * @returns Formatted number
365
- */
366
- export const round = (precision, x) => Math.round(x * Math.pow(10, precision)) / Math.pow(10, precision);
367
- /**
368
- * Safely parses JSON string
369
- * @param json - JSON string to parse
370
- * @returns Parsed object or null if invalid
371
- */
372
- export const parseJson = (json) => {
373
- if (!json)
374
- return undefined;
375
- try {
376
- return JSON.parse(json);
377
- }
378
- catch (e) {
379
- return undefined;
380
- }
381
- };
382
- /**
383
- * Gets and parses JSON from localStorage
384
- * @param k - Storage key
385
- * @returns Parsed value or undefined if invalid/missing
386
- */
387
- export const getJson = (k) => parseJson(localStorage.getItem(k) || "");
388
- /**
389
- * Stringifies and stores value in localStorage
390
- * @param k - Storage key
391
- * @param v - Value to store
392
- */
393
- export const setJson = (k, v) => localStorage.setItem(k, JSON.stringify(v));
394
- /**
395
- * Safely executes function and handles errors
396
- * @param f - Function to execute
397
- * @param onError - Optional error handler
398
- * @returns Function result or undefined if error
399
- */
400
- export const tryCatch = (f, onError) => {
401
- try {
402
- const r = f();
403
- if (r instanceof Promise) {
404
- r.catch(e => onError?.(e));
405
- }
406
- return r;
407
- }
408
- catch (e) {
409
- onError?.(e);
410
- }
411
- return undefined;
412
- };
413
- /**
414
- * Truncates string to length, breaking at word boundaries
415
- * @param s - String to truncate
416
- * @param l - Maximum length
417
- * @param suffix - String to append if truncated
418
- * @returns Truncated string
419
- */
420
- export const ellipsize = (s, l, suffix = "...") => {
421
- if (s.length < l * 1.1) {
422
- return s;
423
- }
424
- while (s.length > l && s.includes(" ")) {
425
- s = s.split(" ").slice(0, -1).join(" ");
426
- }
427
- return s + suffix;
428
- };
429
- /**
430
- * Checks if value is a plain object
431
- * @param obj - Value to check
432
- * @returns True if value is a plain object
433
- */
434
- export const isPojo = (obj) => {
435
- if (obj === null || typeof obj !== "object") {
436
- return false;
437
- }
438
- return Object.getPrototypeOf(obj) === Object.prototype;
439
- };
440
- /**
441
- * Deep equality comparison
442
- * @param a - First value
443
- * @param b - Second value
444
- * @returns True if values are deeply equal
445
- */
446
- export const equals = (a, b) => {
447
- if (a === b)
448
- return true;
449
- if (a instanceof Set && b instanceof Set) {
450
- a = Array.from(a);
451
- b = Array.from(b);
452
- }
453
- if (a instanceof Set) {
454
- if (!(b instanceof Set) || a.size !== b.size) {
455
- return false;
456
- }
457
- return Array.from(a).every(x => b.has(x));
458
- }
459
- if (Array.isArray(a)) {
460
- if (!Array.isArray(b) || a.length !== b.length) {
461
- return false;
462
- }
463
- for (let i = 0; i < a.length; i++) {
464
- if (!equals(a[i], b[i])) {
465
- return false;
466
- }
467
- }
468
- return true;
469
- }
470
- if (isPojo(a)) {
471
- if (!isPojo(b)) {
472
- return false;
473
- }
474
- const aKeys = Object.keys(a);
475
- const bKeys = Object.keys(b);
476
- if (aKeys.length !== bKeys.length) {
477
- return false;
478
- }
479
- for (const k of aKeys) {
480
- if (!equals(a[k], b[k])) {
481
- return false;
482
- }
483
- }
484
- return true;
485
- }
486
- return false;
487
- };
488
- // Curried utils
489
- /** Returns a function that gets the nth element of an array */
490
- export const nth = (i) => (xs, ...args) => xs[i];
491
- /** Returns a function that checks if nth element equals value */
492
- export const nthEq = (i, v) => (xs, ...args) => xs[i] === v;
493
- /** Returns a function that checks if nth element does not equal value */
494
- export const nthNe = (i, v) => (xs, ...args) => xs[i] !== v;
495
- /** Returns a function that checks if key/value pairs of x match all pairs in spec */
496
- export const spec = (values) => (x, ...args) => {
497
- if (Array.isArray(values)) {
498
- for (let i = 0; i < values.length; i++) {
499
- if (x[i] !== values[i]) {
500
- return false;
501
- }
502
- }
503
- }
504
- else {
505
- for (const [k, v] of Object.entries(values)) {
506
- if (x[k] !== v)
507
- return false;
508
- }
509
- }
510
- return true;
511
- };
512
- /** Returns a function that checks equality with value */
513
- export const eq = (v) => (x) => x === v;
514
- /** Returns a function that checks inequality with value */
515
- export const ne = (v) => (x) => x !== v;
516
- /** Returns a function that gets property value from object */
517
- export const prop = (k) => (x) => x[k];
518
- /** Returns a function that adds/updates a property on object */
519
- export const assoc = (k, v) => (o) => ({ ...o, [k]: v });
520
- /** Returns a function that removes a property on object */
521
- export const dissoc = (k) => (o) => omit([k], o);
522
- /** Generates a hash string from input string */
523
- export const hash = (s) => Math.abs(s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)).toString();
524
- // Collections
525
- /** Splits array into two parts at index */
526
- export const splitAt = (n, xs) => [xs.slice(0, n), xs.slice(n)];
527
- /** Inserts element into array at index */
528
- export const insert = (n, x, xs) => [...xs.slice(0, n), x, ...xs.slice(n)];
529
- /** Returns random element from array */
530
- export const choice = (xs) => xs[Math.floor(xs.length * Math.random())];
531
- /** Returns shuffled copy of iterable */
532
- export const shuffle = (xs) => Array.from(xs).sort(() => (Math.random() > 0.5 ? 1 : -1));
533
- /** Returns n random elements from array */
534
- export const sample = (n, xs) => shuffle(xs).slice(0, n);
535
- /** Checks if value is iterable */
536
- export const isIterable = (x) => Symbol.iterator in Object(x);
537
- /** Ensures value is iterable by wrapping in array if needed */
538
- export const toIterable = (x) => (isIterable(x) ? x : [x]);
539
- /** Ensures value is array by wrapping if needed */
540
- export const ensurePlural = (x) => (x instanceof Array ? x : [x]);
541
- /** Converts string or number to number */
542
- export const ensureNumber = (x) => parseFloat(x);
385
+ * Generates sequence of numbers from a to b
386
+ * @param a - Start number (inclusive)
387
+ * @param b - End number (exclusive)
388
+ * @param step - Increment between numbers
389
+ * @yields Numbers in sequence
390
+ */
391
+ export function* range(a, b, step = 1) {
392
+ for (let i = a; i < b; i += step) {
393
+ yield i;
394
+ }
395
+ }
396
+ /**
397
+ * Yields indexed items
398
+ * @param items - A collection of items
399
+ * @yields tuples of [index, item]
400
+ */
401
+ export function* enumerate(items) {
402
+ for (let i = 0; i < items.length; i += 1) {
403
+ yield [i, items[i]];
404
+ }
405
+ }
543
406
  /** Returns a function that gets property value from object */
544
407
  export const pluck = (k, xs) => xs.map(x => x[k]);
545
408
  /**
@@ -556,21 +419,6 @@ export const fromPairs = (pairs) => {
556
419
  }
557
420
  return r;
558
421
  };
559
- /**
560
- * Filters object values based on predicate
561
- * @param f - Function to test values
562
- * @param x - Object to filter
563
- * @returns Object with only values that pass predicate
564
- */
565
- export const filterVals = (f, x) => {
566
- const r = {};
567
- for (const k in x) {
568
- if (f(x[k])) {
569
- r[k] = x[k];
570
- }
571
- }
572
- return r;
573
- };
574
422
  /**
575
423
  * Flattens array of arrays into single array
576
424
  * @param xs - Array of arrays to flatten
@@ -657,6 +505,19 @@ export const groupBy = (f, xs) => {
657
505
  }
658
506
  return r;
659
507
  };
508
+ /**
509
+ * Counts array elements by key function
510
+ * @param f - Function to generate group key
511
+ * @param xs - Array to count entries
512
+ * @returns Map of counts
513
+ */
514
+ export const countBy = (f, xs) => {
515
+ const r = new Map();
516
+ for (const [k, items] of groupBy(f, xs)) {
517
+ r.set(k, items.length);
518
+ }
519
+ return r;
520
+ };
660
521
  /**
661
522
  * Creates map from array using key function
662
523
  * @param f - Function to generate key
@@ -719,6 +580,190 @@ export const chunks = (n, xs) => {
719
580
  }
720
581
  return result;
721
582
  };
583
+ /** Splits array into two parts at index */
584
+ export const splitAt = (n, xs) => [xs.slice(0, n), xs.slice(n)];
585
+ /** Inserts element into array at index */
586
+ export const insertAt = (n, x, xs) => [...xs.slice(0, n), x, ...xs.slice(n)];
587
+ /** Replaces array element at index */
588
+ export const replaceAt = (n, x, xs) => [...xs.slice(0, n), x, ...xs.slice(n + 1)];
589
+ /** Returns random element from array */
590
+ export const choice = (xs) => xs[Math.floor(xs.length * Math.random())];
591
+ /** Returns shuffled copy of iterable */
592
+ export const shuffle = (xs) => Array.from(xs).sort(() => (Math.random() > 0.5 ? 1 : -1));
593
+ /** Returns n random elements from array */
594
+ export const sample = (n, xs) => shuffle(xs).slice(0, n);
595
+ /** Checks if value is iterable */
596
+ export const isIterable = (x) => Symbol.iterator in Object(x);
597
+ /** Ensures value is iterable by wrapping in array if needed */
598
+ export const toIterable = (x) => (isIterable(x) ? x : [x]);
599
+ /** Ensures value is array by wrapping if needed */
600
+ export const ensurePlural = (x) => (x instanceof Array ? x : [x]);
601
+ /** Ensures values are not undefined */
602
+ export const removeNil = (xs) => xs.filter(isNotNil).map(assertNotNil);
603
+ // ----------------------------------------------------------------------------
604
+ // Objects
605
+ // ----------------------------------------------------------------------------
606
+ /**
607
+ * Checks if value is a plain object
608
+ * @param obj - Value to check
609
+ * @returns True if value is a plain object
610
+ */
611
+ export const isPojo = (obj) => {
612
+ if (obj === null || typeof obj !== "object") {
613
+ return false;
614
+ }
615
+ return Object.getPrototypeOf(obj) === Object.prototype;
616
+ };
617
+ /**
618
+ * Creates new object with only specified keys
619
+ * @param ks - Keys to keep
620
+ * @param x - Source object
621
+ * @returns New object with only specified keys
622
+ */
623
+ export const pick = (ks, x) => {
624
+ const r = { ...x };
625
+ for (const k of Object.keys(x)) {
626
+ if (!ks.includes(k)) {
627
+ delete r[k];
628
+ }
629
+ }
630
+ return r;
631
+ };
632
+ /**
633
+ * Creates new object with specified keys removed
634
+ * @param ks - Keys to remove
635
+ * @param x - Source object
636
+ * @returns New object without specified keys
637
+ */
638
+ export const omit = (ks, x) => {
639
+ const r = { ...x };
640
+ for (const k of ks) {
641
+ delete r[k];
642
+ }
643
+ return r;
644
+ };
645
+ /**
646
+ * Creates new object excluding entries with specified values
647
+ * @param xs - Values to exclude
648
+ * @param x - Source object
649
+ * @returns New object without entries containing specified values
650
+ */
651
+ export const omitVals = (xs, x) => {
652
+ const r = {};
653
+ for (const [k, v] of Object.entries(x)) {
654
+ if (!xs.includes(v)) {
655
+ r[k] = v;
656
+ }
657
+ }
658
+ return r;
659
+ };
660
+ /**
661
+ * Filters object values based on predicate
662
+ * @param f - Function to test values
663
+ * @param x - Object to filter
664
+ * @returns Object with only values that pass predicate
665
+ */
666
+ export const filterVals = (f, x) => {
667
+ const r = {};
668
+ for (const k in x) {
669
+ if (f(x[k])) {
670
+ r[k] = x[k];
671
+ }
672
+ }
673
+ return r;
674
+ };
675
+ /**
676
+ * Creates new object with transformed keys
677
+ * @param f - Function to transform keys
678
+ * @param x - Source object
679
+ * @returns Object with transformed keys
680
+ */
681
+ export const mapKeys = (f, x) => {
682
+ const r = {};
683
+ for (const [k, v] of Object.entries(x)) {
684
+ r[f(k)] = v;
685
+ }
686
+ return r;
687
+ };
688
+ /**
689
+ * Creates new object with transformed values
690
+ * @param f - Function to transform values
691
+ * @param x - Source object
692
+ * @returns Object with transformed values
693
+ */
694
+ export const mapVals = (f, x) => {
695
+ const r = {};
696
+ for (const [k, v] of Object.entries(x)) {
697
+ r[k] = f(v);
698
+ }
699
+ return r;
700
+ };
701
+ /**
702
+ * Merges two objects, with left object taking precedence
703
+ * @param a - Left object
704
+ * @param b - Right object
705
+ * @returns Merged object with a"s properties overriding b"s
706
+ */
707
+ export const mergeLeft = (a, b) => ({
708
+ ...b,
709
+ ...a,
710
+ });
711
+ /**
712
+ * Merges two objects, with right object taking precedence
713
+ * @param a - Left object
714
+ * @param b - Right object
715
+ * @returns Merged object with b"s properties overriding a"s
716
+ */
717
+ export const mergeRight = (a, b) => ({
718
+ ...a,
719
+ ...b,
720
+ });
721
+ /** Deep merge two objects, prioritizing the first argument. */
722
+ export const deepMergeLeft = (a, b) => deepMergeRight(b, a);
723
+ /** Deep merge two objects, prioritizing the second argument. */
724
+ export const deepMergeRight = (a, b) => {
725
+ a = { ...a };
726
+ for (const [k, v] of Object.entries(b)) {
727
+ if (isPojo(v) && isPojo(a[k])) {
728
+ a[k] = deepMergeRight(a[k], v);
729
+ }
730
+ else {
731
+ a[k] = v;
732
+ }
733
+ }
734
+ return a;
735
+ };
736
+ /**
737
+ * Switches on key in object, with default fallback
738
+ * @param k - Key to look up
739
+ * @param m - Object with values and optional default
740
+ * @returns Value at key or default value
741
+ */
742
+ export const switcher = (k, m) => m[k] === undefined ? m.default : m[k];
743
+ // ----------------------------------------------------------------------------
744
+ // Combinators
745
+ // ----------------------------------------------------------------------------
746
+ /** Returns a function that returns the boolean negation of the given function */
747
+ export const complement = (f) => (...args) => !f(...args);
748
+ /**
749
+ * Safely executes function and handles errors
750
+ * @param f - Function to execute
751
+ * @param onError - Optional error handler
752
+ * @returns Function result or undefined if error
753
+ */
754
+ export const tryCatch = (f, onError) => {
755
+ try {
756
+ const r = f();
757
+ if (r instanceof Promise) {
758
+ r.catch(e => onError?.(e));
759
+ }
760
+ return r;
761
+ }
762
+ catch (e) {
763
+ onError?.(e);
764
+ }
765
+ return undefined;
766
+ };
722
767
  /**
723
768
  * Creates function that only executes once
724
769
  * @param f - Function to wrap
@@ -740,20 +785,82 @@ export const once = (f) => {
740
785
  */
741
786
  export const call = (f, ...args) => f();
742
787
  /**
743
- * Memoizes function results based on arguments
744
- * @param f - Function to memoize
745
- * @returns Memoized function
788
+ * Memoizes function results based on arguments
789
+ * @param f - Function to memoize
790
+ * @returns Memoized function
791
+ */
792
+ export const memoize = (f) => {
793
+ let prevArgs;
794
+ let result;
795
+ return (...args) => {
796
+ if (!equals(prevArgs, args)) {
797
+ prevArgs = args;
798
+ result = f(...args);
799
+ }
800
+ return result;
801
+ };
802
+ };
803
+ /**
804
+ * Executes a function if the value is defined
805
+ * @param x - The value to check
806
+ * @param f - Function to execute if x is defined
807
+ * @returns Result of f(x) if x is defined, undefined otherwise
808
+ */
809
+ export const ifLet = (x, f) => x === undefined ? undefined : f(x);
810
+ // ----------------------------------------------------------------------------
811
+ // Randomness
812
+ // ----------------------------------------------------------------------------
813
+ /**
814
+ * Generates random integer between min and max (inclusive)
815
+ * @param min - Minimum value
816
+ * @param max - Maximum value
817
+ * @returns Random integer
818
+ */
819
+ export const randomInt = (min = 0, max = 9) => min + Math.round(Math.random() * (max - min));
820
+ /**
821
+ * Generates random string ID
822
+ * @returns Random string suitable for use as an ID
823
+ */
824
+ export const randomId = () => Math.random().toString().slice(2);
825
+ // ----------------------------------------------------------------------------
826
+ // Async
827
+ // ----------------------------------------------------------------------------
828
+ /**
829
+ * Creates a promise that resolves after specified time
830
+ * @param t - Time in milliseconds
831
+ * @returns Promise that resolves after t milliseconds
832
+ */
833
+ export const sleep = (t) => new Promise(resolve => setTimeout(resolve, t));
834
+ /**
835
+ * Creates a promise that resolves after the condition completes or timeout
836
+ * @param options - PollOptions
837
+ * @returns void Promise
746
838
  */
747
- export const memoize = (f) => {
748
- let prevArgs;
749
- let result;
750
- return (...args) => {
751
- if (!equals(prevArgs, args)) {
752
- prevArgs = args;
753
- result = f(...args);
839
+ export const poll = ({ interval = 300, condition, signal }) => new Promise(resolve => {
840
+ const int = setInterval(() => {
841
+ if (condition()) {
842
+ resolve();
843
+ clearInterval(int);
754
844
  }
755
- return result;
756
- };
845
+ }, interval);
846
+ signal.addEventListener("abort", () => {
847
+ resolve();
848
+ clearInterval(int);
849
+ });
850
+ });
851
+ /**
852
+ * Creates a microtask that yields to other tasks in the event loop
853
+ * @returns Promise that resolves after yielding
854
+ */
855
+ export const yieldThread = () => {
856
+ if (typeof window !== "undefined" &&
857
+ "scheduler" in window &&
858
+ "yield" in window.scheduler) {
859
+ return window.scheduler.yield();
860
+ }
861
+ return new Promise(resolve => {
862
+ setTimeout(resolve, 0);
863
+ });
757
864
  };
758
865
  /**
759
866
  * Creates throttled version of function
@@ -845,107 +952,80 @@ export const batcher = (t, execute) => {
845
952
  });
846
953
  };
847
954
  /**
848
- * Adds value to Set at key in object
849
- * @param m - Object mapping keys to Sets
850
- * @param k - Key to add to
851
- * @param v - Value to add
955
+ * Returns a promise that resolves after some proportion of promises complete
956
+ * @param threshold - number between 0 and 1 for how many promises to wait for
957
+ * @param promises - array of promises
958
+ * @returns promise
852
959
  */
853
- export const addToKey = (m, k, v) => {
854
- const s = m[k] || new Set();
855
- s.add(v);
856
- m[k] = s;
960
+ export const race = (threshold, promises) => {
961
+ let count = 0;
962
+ if (threshold === 0) {
963
+ return Promise.resolve();
964
+ }
965
+ return new Promise((resolve, reject) => {
966
+ promises.forEach(p => {
967
+ p.then(() => {
968
+ count++;
969
+ if (count >= threshold * promises.length) {
970
+ resolve();
971
+ }
972
+ }).catch(reject);
973
+ });
974
+ });
857
975
  };
976
+ // ----------------------------------------------------------------------------
977
+ // URLs
978
+ // ----------------------------------------------------------------------------
858
979
  /**
859
- * Pushes value to array at key in object
860
- * @param m - Object mapping keys to arrays
861
- * @param k - Key to push to
862
- * @param v - Value to push
980
+ * Removes protocol (http://, https://, etc) from URL
981
+ * @param url - URL to process
982
+ * @returns URL without protocol
863
983
  */
864
- export const pushToKey = (m, k, v) => {
865
- const a = m[k] || [];
866
- a.push(v);
867
- m[k] = a;
868
- };
984
+ export const stripProtocol = (url) => url.replace(/.*:\/\//, "");
869
985
  /**
870
- * Adds value to Set at key in Map
871
- * @param m - Map of Sets
872
- * @param k - Key to add to
873
- * @param v - Value to add
986
+ * Formats URL for display by removing protocol, www, and trailing slash
987
+ * @param url - URL to format
988
+ * @returns Formatted URL
874
989
  */
875
- export const addToMapKey = (m, k, v) => {
876
- const s = m.get(k) || new Set();
877
- s.add(v);
878
- m.set(k, s);
879
- };
990
+ export const displayUrl = (url) => stripProtocol(url)
991
+ .replace(/^(www\.)?/i, "")
992
+ .replace(/\/$/, "");
880
993
  /**
881
- * Pushes value to array at key in Map
882
- * @param m - Map of arrays
883
- * @param k - Key to push to
884
- * @param v - Value to push
994
+ * Extracts and formats domain from URL
995
+ * @param url - URL to process
996
+ * @returns Formatted domain name
885
997
  */
886
- export const pushToMapKey = (m, k, v) => {
887
- const a = m.get(k) || [];
888
- a.push(v);
889
- m.set(k, a);
890
- };
998
+ export const displayDomain = (url) => displayUrl(first(url.split(/[\/\?]/)) || "");
999
+ // ----------------------------------------------------------------------------
1000
+ // JSON, localStorage, fetch, event emitters, etc
1001
+ // ----------------------------------------------------------------------------
891
1002
  /**
892
- * A generic type-safe event listener function that auto-detects the appropriate methods
893
- * for adding and removing event listeners.
894
- *
895
- * @param target - The event target object with add/remove listener methods
896
- * @param eventName - The name of the event to listen for
897
- * @param callback - The callback function to execute when the event occurs
898
- * @returns A function that removes the event listener when called
1003
+ * Safely parses JSON string
1004
+ * @param json - JSON string to parse
1005
+ * @returns Parsed object or null if invalid
899
1006
  */
900
- export const on = (target, eventName, callback) => {
901
- target.on(eventName, callback);
902
- return () => {
903
- target.off(eventName, callback);
904
- };
1007
+ export const parseJson = (json) => {
1008
+ if (!json)
1009
+ return undefined;
1010
+ try {
1011
+ return JSON.parse(json);
1012
+ }
1013
+ catch (e) {
1014
+ return undefined;
1015
+ }
905
1016
  };
906
1017
  /**
907
- * Switches on key in object, with default fallback
908
- * @param k - Key to look up
909
- * @param m - Object with values and optional default
910
- * @returns Value at key or default value
911
- */
912
- export const switcher = (k, m) => m[k] === undefined ? m.default : m[k];
913
- /** One minute in seconds */
914
- export const MINUTE = 60;
915
- /** One hour in seconds */
916
- export const HOUR = 60 * MINUTE;
917
- /** One day in seconds */
918
- export const DAY = 24 * HOUR;
919
- /** One week in seconds */
920
- export const WEEK = 7 * DAY;
921
- /** One month in seconds (approximate) */
922
- export const MONTH = 30 * DAY;
923
- /** One quarter in seconds (approximate) */
924
- export const QUARTER = 90 * DAY;
925
- /** One year in seconds (approximate) */
926
- export const YEAR = 365 * DAY;
927
- /**
928
- * Multiplies time unit by count
929
- * @param unit - Time unit in seconds
930
- * @param count - Number of units
931
- * @returns Total seconds
932
- */
933
- export const int = (unit, count = 1) => unit * count;
934
- /** Returns current Unix timestamp in seconds */
935
- export const now = () => Math.round(Date.now() / 1000);
936
- /**
937
- * Returns Unix timestamp from specified time ago
938
- * @param unit - Time unit in seconds
939
- * @param count - Number of units
940
- * @returns Timestamp in seconds
1018
+ * Gets and parses JSON from localStorage
1019
+ * @param k - Storage key
1020
+ * @returns Parsed value or undefined if invalid/missing
941
1021
  */
942
- export const ago = (unit, count = 1) => now() - int(unit, count);
1022
+ export const getJson = (k) => parseJson(localStorage.getItem(k) || "");
943
1023
  /**
944
- * Converts seconds to milliseconds
945
- * @param seconds - Time in seconds
946
- * @returns Time in milliseconds
1024
+ * Stringifies and stores value in localStorage
1025
+ * @param k - Storage key
1026
+ * @param v - Value to store
947
1027
  */
948
- export const ms = (seconds) => seconds * 1000;
1028
+ export const setJson = (k, v) => localStorage.setItem(k, JSON.stringify(v));
949
1029
  /**
950
1030
  * Fetches JSON from URL with options
951
1031
  * @param url - URL to fetch from
@@ -992,6 +1072,142 @@ export const uploadFile = (url, file) => {
992
1072
  body.append("file", file);
993
1073
  return fetchJson(url, { method: "POST", body });
994
1074
  };
1075
+ /**
1076
+ * A generic type-safe event listener function that works with event emitters.
1077
+ *
1078
+ * @param target - The event target object with add/remove listener methods
1079
+ * @param eventName - The name of the event to listen for
1080
+ * @param callback - The callback function to execute when the event occurs
1081
+ * @returns A function that removes the event listener when called
1082
+ */
1083
+ export const on = (target, eventName, callback) => {
1084
+ target.on(eventName, callback);
1085
+ return () => {
1086
+ target.off(eventName, callback);
1087
+ };
1088
+ };
1089
+ // ----------------------------------------------------------------------------
1090
+ // Strings
1091
+ // ----------------------------------------------------------------------------
1092
+ /**
1093
+ * Truncates string to length, breaking at word boundaries
1094
+ * @param s - String to truncate
1095
+ * @param l - Maximum length
1096
+ * @param suffix - String to append if truncated
1097
+ * @returns Truncated string
1098
+ */
1099
+ export const ellipsize = (s, l, suffix = "...") => {
1100
+ if (s.length < l * 1.1) {
1101
+ return s;
1102
+ }
1103
+ while (s.length > l && s.includes(" ")) {
1104
+ s = s.split(" ").slice(0, -1).join(" ");
1105
+ }
1106
+ return s + suffix;
1107
+ };
1108
+ /** Displays a list of items with oxford commas and a chosen conjunction */
1109
+ export const displayList = (xs, conj = "and", n = 6) => {
1110
+ if (xs.length > n + 2) {
1111
+ return `${xs.slice(0, n).join(", ")}, ${conj} ${xs.length - n} others`;
1112
+ }
1113
+ if (xs.length < 3) {
1114
+ return xs.join(` ${conj} `);
1115
+ }
1116
+ return `${xs.slice(0, -1).join(", ")}, ${conj} ${xs.slice(-1).join("")}`;
1117
+ };
1118
+ /** Generates a hash string from input string */
1119
+ export const hash = (s) => Math.abs(s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)).toString();
1120
+ // ----------------------------------------------------------------------------
1121
+ // Curried utilities for working with collections
1122
+ // ----------------------------------------------------------------------------
1123
+ /** Returns a function that gets the nth element of an array */
1124
+ export const nth = (i) => (xs, ...args) => xs[i];
1125
+ /** Returns a function that checks if nth element equals value */
1126
+ export const nthEq = (i, v) => (xs, ...args) => xs[i] === v;
1127
+ /** Returns a function that checks if nth element does not equal value */
1128
+ export const nthNe = (i, v) => (xs, ...args) => xs[i] !== v;
1129
+ /** Returns a function that checks if key/value pairs of x match all pairs in spec */
1130
+ export const spec = (values) => (x, ...args) => {
1131
+ if (Array.isArray(values)) {
1132
+ for (let i = 0; i < values.length; i++) {
1133
+ if (x[i] !== values[i]) {
1134
+ return false;
1135
+ }
1136
+ }
1137
+ }
1138
+ else {
1139
+ for (const [k, v] of Object.entries(values)) {
1140
+ if (x[k] !== v)
1141
+ return false;
1142
+ }
1143
+ }
1144
+ return true;
1145
+ };
1146
+ /** Returns a function that checks equality with value */
1147
+ export const eq = (v) => (x, ...args) => x === v;
1148
+ /** Returns a function that checks inequality with value */
1149
+ export const ne = (v) => (x, ...args) => x !== v;
1150
+ /** Returns a function that gets property value from object */
1151
+ export const prop = (k) => (x) => x[k];
1152
+ /** Returns a function that adds/updates a property on object */
1153
+ export const assoc = (k, v) => (o) => ({ ...o, [k]: v });
1154
+ /** Returns a function that removes a property on object */
1155
+ export const dissoc = (k) => (o) => omit([k], o);
1156
+ /** Returns a function that checks whether a value is in the given sequence */
1157
+ export const member = (xs) => (x) => Array.from(xs).includes(x);
1158
+ // ----------------------------------------------------------------------------
1159
+ // Sets
1160
+ // ----------------------------------------------------------------------------
1161
+ /**
1162
+ * Adds value to Set at key in object
1163
+ * @param m - Object mapping keys to Sets
1164
+ * @param k - Key to add to
1165
+ * @param v - Value to add
1166
+ */
1167
+ export const addToKey = (m, k, v) => {
1168
+ const s = m[k] || new Set();
1169
+ s.add(v);
1170
+ m[k] = s;
1171
+ };
1172
+ /**
1173
+ * Pushes value to array at key in object
1174
+ * @param m - Object mapping keys to arrays
1175
+ * @param k - Key to push to
1176
+ * @param v - Value to push
1177
+ */
1178
+ export const pushToKey = (m, k, v) => {
1179
+ const a = m[k] || [];
1180
+ a.push(v);
1181
+ m[k] = a;
1182
+ };
1183
+ // ----------------------------------------------------------------------------
1184
+ // Maps
1185
+ // ----------------------------------------------------------------------------
1186
+ /**
1187
+ * Adds value to Set at key in Map
1188
+ * @param m - Map of Sets
1189
+ * @param k - Key to add to
1190
+ * @param v - Value to add
1191
+ */
1192
+ export const addToMapKey = (m, k, v) => {
1193
+ const s = m.get(k) || new Set();
1194
+ s.add(v);
1195
+ m.set(k, s);
1196
+ };
1197
+ /**
1198
+ * Pushes value to array at key in Map
1199
+ * @param m - Map of arrays
1200
+ * @param k - Key to push to
1201
+ * @param v - Value to push
1202
+ */
1203
+ export const pushToMapKey = (m, k, v) => {
1204
+ const a = m.get(k) || [];
1205
+ a.push(v);
1206
+ m.set(k, a);
1207
+ };
1208
+ // ----------------------------------------------------------------------------
1209
+ // Bech32 <-> hex encoding
1210
+ // ----------------------------------------------------------------------------
995
1211
  /**
996
1212
  * Converts hex string to bech32 format
997
1213
  * @param prefix - Bech32 prefix