@naturalcycles/js-lib 15.95.0 → 15.97.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 (55) hide show
  1. package/dist/abort.d.ts +3 -3
  2. package/dist/abort.js +1 -1
  3. package/dist/analytics/analytics.model.d.ts +1 -1
  4. package/dist/array/array.util.d.ts +13 -7
  5. package/dist/array/array.util.js +11 -3
  6. package/dist/browser/i18n/translation.service.d.ts +1 -1
  7. package/dist/datetime/localDate.d.ts +4 -4
  8. package/dist/datetime/localTime.d.ts +4 -4
  9. package/dist/decorators/decorator.util.d.ts +1 -1
  10. package/dist/decorators/timeout.decorator.js +5 -2
  11. package/dist/error/error.model.d.ts +1 -1
  12. package/dist/http/fetcher.js +5 -4
  13. package/dist/http/http.model.d.ts +1 -1
  14. package/dist/log/commonLogger.d.ts +1 -1
  15. package/dist/math/math.util.d.ts +8 -8
  16. package/dist/math/math.util.js +2 -2
  17. package/dist/math/stack.util.d.ts +8 -3
  18. package/dist/math/stack.util.js +13 -6
  19. package/dist/number/number.util.d.ts +1 -1
  20. package/dist/object/keySortedMap.d.ts +1 -1
  21. package/dist/object/lazyKeySortedMap.d.ts +1 -1
  22. package/dist/object/object.util.d.ts +1 -1
  23. package/dist/object/sortObject.d.ts +1 -1
  24. package/dist/promise/pTimeout.js +1 -1
  25. package/dist/semver.d.ts +4 -4
  26. package/dist/string/pupa.d.ts +1 -1
  27. package/dist/types.d.ts +11 -0
  28. package/package.json +2 -2
  29. package/src/abort.ts +4 -4
  30. package/src/analytics/analytics.model.ts +1 -1
  31. package/src/array/array.util.ts +28 -10
  32. package/src/browser/i18n/translation.service.ts +1 -1
  33. package/src/browser/imageFitter.ts +1 -1
  34. package/src/datetime/localDate.ts +4 -4
  35. package/src/datetime/localTime.ts +4 -4
  36. package/src/decorators/decorator.util.ts +1 -1
  37. package/src/decorators/timeout.decorator.ts +5 -2
  38. package/src/error/error.model.ts +1 -1
  39. package/src/http/fetcher.ts +6 -5
  40. package/src/http/http.model.ts +1 -1
  41. package/src/log/commonLogger.ts +1 -1
  42. package/src/math/math.util.ts +13 -10
  43. package/src/math/stack.util.ts +16 -8
  44. package/src/number/number.util.ts +1 -1
  45. package/src/object/keySortedMap.ts +1 -1
  46. package/src/object/lazyKeySortedMap.ts +1 -1
  47. package/src/object/object.util.ts +5 -1
  48. package/src/object/sortObject.ts +1 -1
  49. package/src/promise/pMap.ts +2 -2
  50. package/src/promise/pTimeout.ts +1 -1
  51. package/src/semver.ts +4 -4
  52. package/src/string/pupa.ts +5 -1
  53. package/src/string/readingTime.ts +1 -1
  54. package/src/types.ts +12 -0
  55. package/src/ua/ua.ts +1 -1
package/dist/abort.d.ts CHANGED
@@ -40,7 +40,7 @@ export declare function polyfilledAbortSignalTimeout(ms: NumberOfMilliseconds):
40
40
  * Returns AbortSignal.any(signals) is the array (after filtering undefined inputs) is not empty,
41
41
  * otherwise undefined.
42
42
  */
43
- export declare function abortSignalAnyOrUndefined(signals: (AbortSignal | undefined)[]): AbortSignal | undefined;
43
+ export declare function abortSignalAnyOrUndefined(signals: readonly (AbortSignal | undefined)[]): AbortSignal | undefined;
44
44
  /**
45
45
  * Returns an AbortSignal that aborts when any of the given signals abort.
46
46
  * Uses native `AbortSignal.any()` when available, falls back to a polyfill.
@@ -50,5 +50,5 @@ export declare function abortSignalAnyOrUndefined(signals: (AbortSignal | undefi
50
50
  *
51
51
  * If only 1 signal is passed in the input array - that Signal is returned as-is.
52
52
  */
53
- export declare function abortSignalAny(signals: AbortSignal[]): AbortSignal;
54
- export declare function polyfilledAbortSignalAny(signals: AbortSignal[]): AbortSignal;
53
+ export declare function abortSignalAny(signals: readonly AbortSignal[]): AbortSignal;
54
+ export declare function polyfilledAbortSignalAny(signals: readonly AbortSignal[]): AbortSignal;
package/dist/abort.js CHANGED
@@ -58,7 +58,7 @@ export function abortSignalAny(signals) {
58
58
  return signals[0];
59
59
  }
60
60
  return typeof AbortSignal.any === 'function'
61
- ? AbortSignal.any(signals)
61
+ ? AbortSignal.any(signals) // lib.dom types require a mutable array, but it is only read
62
62
  : polyfilledAbortSignalAny(signals);
63
63
  }
64
64
  export function polyfilledAbortSignalAny(signals) {
@@ -60,7 +60,7 @@ export interface AnalyticsClientEvent {
60
60
  userId?: AnalyticsDistinctId;
61
61
  }
62
62
  /** Canonical utm parameters. */
63
- export declare const CANONICAL_UTM_PARAMS: string[];
63
+ export declare const CANONICAL_UTM_PARAMS: readonly string[];
64
64
  /** Keyed by `initial_<canonical utm param>`, to avoid permanently saving non-standard ones. */
65
65
  export type FirstTouchUtms = StringMap<string | null>;
66
66
  /**
@@ -1,4 +1,4 @@
1
- import type { AbortablePredicate, FalsyValue, Mapper, MutateOptions, Predicate, StringMap } from '../types.js';
1
+ import type { AbortablePredicate, FalsyValue, Mapper, MutateOptions, NonEmptyArray, Predicate, StringMap } from '../types.js';
2
2
  /**
3
3
  * Creates an array of elements split into groups the length of size. If collection can’t be split evenly, the
4
4
  * final chunk will be the remaining elements.
@@ -11,7 +11,7 @@ import type { AbortablePredicate, FalsyValue, Mapper, MutateOptions, Predicate,
11
11
  *
12
12
  * Based on: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_chunk
13
13
  */
14
- export declare function _chunk<T>(array: readonly T[], size?: number): T[][];
14
+ export declare function _chunk<T>(array: readonly T[], size?: number): NonEmptyArray<T>[];
15
15
  /**
16
16
  * Removes duplicates from given array.
17
17
  */
@@ -89,7 +89,7 @@ export declare function _mapBy<ITEM, KEY>(items: readonly ITEM[], mapper: Mapper
89
89
  *
90
90
  * Returning `undefined` from the Mapper will EXCLUDE the item.
91
91
  */
92
- export declare function _groupBy<T>(items: readonly T[], mapper: Mapper<T, any>): StringMap<T[]>;
92
+ export declare function _groupBy<T>(items: readonly T[], mapper: Mapper<T, any>): StringMap<NonEmptyArray<T>>;
93
93
  /**
94
94
  * Similar to `Array.find`, but the `predicate` may return `END` to stop the iteration early.
95
95
  *
@@ -138,7 +138,7 @@ export declare function _countBy<T>(items: Iterable<T>, mapper: Mapper<T, any>):
138
138
  * _intersection([2, 1], [2, 3])
139
139
  * // [2]
140
140
  */
141
- export declare function _intersection<T>(a1: T[], a2: T[] | Set<T>): T[];
141
+ export declare function _intersection<T>(a1: readonly T[], a2: readonly T[] | Set<T>): T[];
142
142
  /**
143
143
  * Returns true if there is at least 1 item common between 2 arrays.
144
144
  * Otherwise returns false.
@@ -147,7 +147,7 @@ export declare function _intersection<T>(a1: T[], a2: T[] | Set<T>): T[];
147
147
  *
148
148
  * Passing second array as Set is more performant (it'll skip turning the array into Set in-place).
149
149
  */
150
- export declare function _intersectsWith<T>(a1: T[], a2: T[] | Set<T>): boolean;
150
+ export declare function _intersectsWith<T>(a1: readonly T[], a2: readonly T[] | Set<T>): boolean;
151
151
  /**
152
152
  * Returns array1 minus array2.
153
153
  *
@@ -157,11 +157,11 @@ export declare function _intersectsWith<T>(a1: T[], a2: T[] | Set<T>): boolean;
157
157
  *
158
158
  * Passing second array as Set is more performant (it'll skip turning the array into Set in-place).
159
159
  */
160
- export declare function _difference<T>(a1: T[], a2: T[] | Set<T>): T[];
160
+ export declare function _difference<T>(a1: readonly T[], a2: readonly T[] | Set<T>): T[];
161
161
  /**
162
162
  * Does NOT mutate the array, returns a filtered array instead.
163
163
  */
164
- export declare function _arrayRemove<T>(a: T[], itemToRemove: T): T[];
164
+ export declare function _arrayRemove<T>(a: readonly T[], itemToRemove: T): T[];
165
165
  /**
166
166
  * "Toggles" an item to be present or absent in the array,
167
167
  * based on the predicate. Respects uniqueness.
@@ -217,6 +217,12 @@ export declare function _first<T>(array: readonly T[]): T;
217
217
  * Returns first item of the array (or undefined if array is empty).
218
218
  */
219
219
  export declare function _firstOrUndefined<T>(array: readonly T[]): T | undefined;
220
+ /**
221
+ * Type guard that narrows an array to NonEmptyArray (array with at least one element).
222
+ * Readonly arrays are narrowed to `Readonly<NonEmptyArray<T>>`.
223
+ */
224
+ export declare function _isNonEmptyArray<T>(array: T[]): array is NonEmptyArray<T>;
225
+ export declare function _isNonEmptyArray<T>(array: readonly T[]): array is Readonly<NonEmptyArray<T>>;
220
226
  /**
221
227
  * Returns the first item of a non-empty iterable (Set, Map.values(), generator, etc.).
222
228
  * Throws if the iterable is empty.
@@ -144,9 +144,14 @@ export function _groupBy(items, mapper) {
144
144
  const map = {};
145
145
  for (const item of items) {
146
146
  const key = mapper(item);
147
- if (key !== undefined) {
148
- ;
149
- (map[key] ||= []).push(item);
147
+ if (key === undefined)
148
+ continue;
149
+ const group = map[key];
150
+ if (group) {
151
+ group.push(item);
152
+ }
153
+ else {
154
+ map[key] = [item];
150
155
  }
151
156
  }
152
157
  return map;
@@ -403,6 +408,9 @@ export function _first(array) {
403
408
  export function _firstOrUndefined(array) {
404
409
  return array[0];
405
410
  }
411
+ export function _isNonEmptyArray(array) {
412
+ return array.length > 0;
413
+ }
406
414
  /**
407
415
  * Returns the first item of a non-empty iterable (Set, Map.values(), generator, etc.).
408
416
  * Throws if the iterable is empty.
@@ -37,7 +37,7 @@ export declare class TranslationService {
37
37
  * Loads locale(s) (if not already cached) via configured TranslationLoader.
38
38
  * Resolves promise when done (ready to be used).
39
39
  */
40
- loadLocale(locale: string | string[]): Promise<void>;
40
+ loadLocale(locale: string | readonly string[]): Promise<void>;
41
41
  /**
42
42
  * Will invoke `missingTranslationHandler` on missing tranlation.
43
43
  *
@@ -250,21 +250,21 @@ declare class LocalDateFactory {
250
250
  /**
251
251
  * Returns the earliest (min) LocalDate from the array, or undefined if the array is empty.
252
252
  */
253
- minOrUndefined(items: LocalDateInputNullable[]): LocalDate | undefined;
253
+ minOrUndefined(items: readonly LocalDateInputNullable[]): LocalDate | undefined;
254
254
  /**
255
255
  * Returns the earliest LocalDate from the array.
256
256
  * Throws if the array is empty.
257
257
  */
258
- min(items: LocalDateInputNullable[]): LocalDate;
258
+ min(items: readonly LocalDateInputNullable[]): LocalDate;
259
259
  /**
260
260
  * Returns the latest (max) LocalDate from the array, or undefined if the array is empty.
261
261
  */
262
- maxOrUndefined(items: LocalDateInputNullable[]): LocalDate | undefined;
262
+ maxOrUndefined(items: readonly LocalDateInputNullable[]): LocalDate | undefined;
263
263
  /**
264
264
  * Returns the latest LocalDate from the array.
265
265
  * Throws if the array is empty.
266
266
  */
267
- max(items: LocalDateInputNullable[]): LocalDate;
267
+ max(items: readonly LocalDateInputNullable[]): LocalDate;
268
268
  /**
269
269
  * Returns the range (array) of LocalDates between min and max.
270
270
  * By default, min is included, max is excluded.
@@ -322,10 +322,10 @@ declare class LocalTimeFactory {
322
322
  */
323
323
  isTimezoneValid(tz: string): boolean;
324
324
  sort(items: LocalTime[], dir?: SortDirection, opt?: MutateOptions): LocalTime[];
325
- minOrUndefined(items: LocalTimeInputNullable[]): LocalTime | undefined;
326
- min(items: LocalTimeInputNullable[]): LocalTime;
327
- maxOrUndefined(items: LocalTimeInputNullable[]): LocalTime | undefined;
328
- max(items: LocalTimeInputNullable[]): LocalTime;
325
+ minOrUndefined(items: readonly LocalTimeInputNullable[]): LocalTime | undefined;
326
+ min(items: readonly LocalTimeInputNullable[]): LocalTime;
327
+ maxOrUndefined(items: readonly LocalTimeInputNullable[]): LocalTime | undefined;
328
+ max(items: readonly LocalTimeInputNullable[]): LocalTime;
329
329
  }
330
330
  interface LocalTimeFn extends LocalTimeFactory {
331
331
  (d: LocalTimeInput): LocalTime;
@@ -21,4 +21,4 @@ export declare function _getTargetMethodSignature(target: AnyObject, keyStr: str
21
21
  * returns:
22
22
  * a, b, c
23
23
  */
24
- export declare function _getArgsSignature(args?: any[], logArgs?: boolean): string;
24
+ export declare function _getArgsSignature(args?: readonly any[], logArgs?: boolean): string;
@@ -10,8 +10,11 @@ export function _Timeout(opt) {
10
10
  const keyStr = String(key);
11
11
  descriptor.value = async function (...args) {
12
12
  const ctx = this;
13
- opt.name ||= _getMethodSignature(ctx, keyStr);
14
- return await pTimeout(() => originalFn.apply(this, args), opt);
13
+ // Not mutating the shared decorator options, as the name depends on the instance (ctx)
14
+ return await pTimeout(() => originalFn.apply(this, args), {
15
+ ...opt,
16
+ name: opt.name || _getMethodSignature(ctx, keyStr),
17
+ });
15
18
  };
16
19
  return descriptor;
17
20
  };
@@ -81,7 +81,7 @@ export interface ErrorData {
81
81
  /**
82
82
  * Used in e.g http 403 error.
83
83
  */
84
- adminPermissionsRequired?: string[];
84
+ adminPermissionsRequired?: readonly string[];
85
85
  /**
86
86
  * Open-ended.
87
87
  */
@@ -706,10 +706,6 @@ export class Fetcher {
706
706
  }
707
707
  normalizeCfg(cfg) {
708
708
  const { debug = false, logger = consoleLogger } = cfg;
709
- if (cfg.baseUrl?.endsWith('/')) {
710
- logger.warn(`Fetcher: baseUrl should not end with slash: ${cfg.baseUrl}`);
711
- cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
712
- }
713
709
  const norm = _merge({
714
710
  baseUrl: '',
715
711
  name: this.getFetcherName(cfg),
@@ -746,6 +742,11 @@ export class Fetcher {
746
742
  errorData: {},
747
743
  }, _omit(cfg, ['method', 'credentials', 'headers', 'redirect', 'logger', 'name', 'keepalive']));
748
744
  norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase());
745
+ // Normalizing `norm`, not the input `cfg`, to not mutate the input
746
+ if (norm.baseUrl.endsWith('/')) {
747
+ logger.warn(`Fetcher: baseUrl should not end with slash: ${norm.baseUrl}`);
748
+ norm.baseUrl = norm.baseUrl.slice(0, -1);
749
+ }
749
750
  return norm;
750
751
  }
751
752
  getFetcherName(cfg) {
@@ -4,4 +4,4 @@ export type HttpStatusFamily = 5 | 4 | 3 | 2 | 1;
4
4
  * Number, representing a valid Http status code (e.g 401, 500, etc)
5
5
  */
6
6
  export type HttpStatusCode = number;
7
- export declare const HTTP_METHODS: HttpMethod[];
7
+ export declare const HTTP_METHODS: readonly HttpMethod[];
@@ -74,4 +74,4 @@ export declare function createCommonLoggerAtLevel(logger?: CommonLogger, minLeve
74
74
  /**
75
75
  * Creates a "proxy" CommonLogger that pipes log entries to all provided sub-loggers.
76
76
  */
77
- export declare function commonLoggerPipe(loggers: CommonLogger[]): CommonLogger;
77
+ export declare function commonLoggerPipe(loggers: readonly CommonLogger[]): CommonLogger;
@@ -6,11 +6,11 @@
6
6
  * _average([1, 2, 3, 4])
7
7
  * // 2.5
8
8
  */
9
- export declare function _average(values: number[]): number;
9
+ export declare function _average(values: readonly number[]): number;
10
10
  /**
11
11
  * Same as _average, but safely returns null if input array is empty or nullish.
12
12
  */
13
- export declare function _averageOrNull(values: number[] | undefined | null): number | null;
13
+ export declare function _averageOrNull(values: readonly number[] | undefined | null): number | null;
14
14
  /**
15
15
  * @returns Population standard deviation of the array of numbers
16
16
  *
@@ -19,15 +19,15 @@ export declare function _averageOrNull(values: number[] | undefined | null): num
19
19
  * _stdDev([2, 4, 4, 4, 5, 5, 7, 9])
20
20
  * // 2
21
21
  */
22
- export declare function _stdDev(values: number[]): number;
22
+ export declare function _stdDev(values: readonly number[]): number;
23
23
  /**
24
24
  * Same as _stdDev, but safely returns null if input array is empty or nullish.
25
25
  */
26
- export declare function _stdDevOrNull(values: number[] | undefined | null): number | null;
26
+ export declare function _stdDevOrNull(values: readonly number[] | undefined | null): number | null;
27
27
  /**
28
28
  * valuesArray and weightsArray length is expected to be the same.
29
29
  */
30
- export declare function _averageWeighted(values: number[], weights: number[]): number;
30
+ export declare function _averageWeighted(values: readonly number[], weights: readonly number[]): number;
31
31
  /**
32
32
  * @example
33
33
  *
@@ -40,11 +40,11 @@ export declare function _averageWeighted(values: number[], weights: number[]): n
40
40
  * _percentile([1, 2, 3], 100)
41
41
  * // 3
42
42
  */
43
- export declare function _percentile(values: number[], pc: number): number;
43
+ export declare function _percentile(values: readonly number[], pc: number): number;
44
44
  /**
45
45
  * A tiny bit more efficient function than calling _percentile individually.
46
46
  */
47
- export declare function _percentiles(values: number[], pcs: number[]): Record<number, number>;
47
+ export declare function _percentiles(values: readonly number[], pcs: readonly number[]): Record<number, number>;
48
48
  /**
49
49
  * @example
50
50
  *
@@ -54,4 +54,4 @@ export declare function _percentiles(values: number[], pcs: number[]): Record<nu
54
54
  * _median([1, 2, 3, 4])
55
55
  * // 2.5
56
56
  */
57
- export declare function _median(values: number[]): number;
57
+ export declare function _median(values: readonly number[]): number;
@@ -70,7 +70,7 @@ export function _averageWeighted(values, weights) {
70
70
  * // 3
71
71
  */
72
72
  export function _percentile(values, pc) {
73
- const sorted = values.sort(comparators.numericAsc);
73
+ const sorted = values.toSorted(comparators.numericAsc);
74
74
  // Floating pos in the range of [0; length - 1]
75
75
  const pos = ((values.length - 1) * pc) / 100;
76
76
  const dec = pos % 1;
@@ -83,7 +83,7 @@ export function _percentile(values, pc) {
83
83
  */
84
84
  export function _percentiles(values, pcs) {
85
85
  const r = {};
86
- const sorted = values.sort(comparators.numericAsc);
86
+ const sorted = values.toSorted(comparators.numericAsc);
87
87
  for (const pc of pcs) {
88
88
  // Floating pos in the range of [0; length - 1]
89
89
  const pos = ((values.length - 1) * pc) / 100;
@@ -13,7 +13,12 @@ export declare class Stack<T> {
13
13
  * Currently this slot contains OLDEST item (if any).
14
14
  */
15
15
  private nextIndex;
16
- readonly items: T[];
16
+ private readonly buffer;
17
+ /**
18
+ * Raw "items buffer" as-is (not ordered properly).
19
+ * Readonly, because mutating it (e.g sorting in-place) would corrupt the Stack.
20
+ */
21
+ get items(): readonly T[];
17
22
  push(item: T): this;
18
23
  /**
19
24
  * Fill (overwrite) the whole Stack (all its items) with the passed `item`.
@@ -23,7 +28,7 @@ export declare class Stack<T> {
23
28
  * Returns last items in the right order.
24
29
  * Unlike raw `items` property that returns "items buffer" as-is (not ordered properly).
25
30
  */
26
- get itemsOrdered(): T[];
31
+ get itemsOrdered(): readonly T[];
27
32
  }
28
33
  /**
29
34
  * Fixed-size FILO stack of Numbers.
@@ -46,5 +51,5 @@ export declare class NumberStack extends Stack<number> {
46
51
  * Returns null if Stack is empty.
47
52
  */
48
53
  percentileOrNull(pc: number): number | null;
49
- percentiles(pcs: number[]): Record<number, number>;
54
+ percentiles(pcs: readonly number[]): Record<number, number>;
50
55
  }
@@ -16,9 +16,16 @@ export class Stack {
16
16
  * Currently this slot contains OLDEST item (if any).
17
17
  */
18
18
  nextIndex = 0;
19
- items = [];
19
+ buffer = [];
20
+ /**
21
+ * Raw "items buffer" as-is (not ordered properly).
22
+ * Readonly, because mutating it (e.g sorting in-place) would corrupt the Stack.
23
+ */
24
+ get items() {
25
+ return this.buffer;
26
+ }
20
27
  push(item) {
21
- this.items[this.nextIndex] = item;
28
+ this.buffer[this.nextIndex] = item;
22
29
  this.nextIndex = this.nextIndex === this.size - 1 ? 0 : this.nextIndex + 1;
23
30
  return this;
24
31
  }
@@ -27,7 +34,7 @@ export class Stack {
27
34
  */
28
35
  fill(item) {
29
36
  for (let i = 0; i < this.size; i++) {
30
- this.items[i] = item;
37
+ this.buffer[i] = item;
31
38
  }
32
39
  return this;
33
40
  }
@@ -36,12 +43,12 @@ export class Stack {
36
43
  * Unlike raw `items` property that returns "items buffer" as-is (not ordered properly).
37
44
  */
38
45
  get itemsOrdered() {
39
- if (this.items.length < this.size) {
46
+ if (this.buffer.length < this.size) {
40
47
  // Buffer is not filled yet, just return it as-is
41
- return this.items;
48
+ return this.buffer;
42
49
  }
43
50
  // Buffer was filled and started to "overwrite itself", will need to return 2 slices
44
- return [...this.items.slice(this.nextIndex), ...this.items.slice(0, this.nextIndex)];
51
+ return [...this.buffer.slice(this.nextIndex), ...this.buffer.slice(0, this.nextIndex)];
45
52
  }
46
53
  }
47
54
  /**
@@ -5,7 +5,7 @@ export declare function _randomInt(minIncl: number, maxIncl: number): number;
5
5
  * Should be used on non-empty arrays! (otherwise will return undefined,
6
6
  * which is not reflected in the output type)
7
7
  */
8
- export declare function _randomArrayItem<T>(array: T[]): T;
8
+ export declare function _randomArrayItem<T>(array: readonly T[]): T;
9
9
  /**
10
10
  * Convenience function to "throttle" some code - run it less often.
11
11
  *
@@ -24,7 +24,7 @@ export interface KeySortedMap<K, V> extends Map<K, V> {
24
24
  export declare class KeySortedMap<K, V> {
25
25
  #private;
26
26
  private readonly map;
27
- constructor(entries?: [K, V][], opt?: KeySortedMapOptions<K>);
27
+ constructor(entries?: readonly [K, V][], opt?: KeySortedMapOptions<K>);
28
28
  /**
29
29
  * Convenience way to create KeySortedMap from object.
30
30
  */
@@ -21,7 +21,7 @@ export declare class LazyKeySortedMap<K, V> {
21
21
  private readonly map;
22
22
  private readonly maybeSortedKeys;
23
23
  private keysAreSorted;
24
- constructor(entries?: [K, V][], opt?: LazyKeySortedMapOptions<K>);
24
+ constructor(entries?: readonly [K, V][], opt?: LazyKeySortedMapOptions<K>);
25
25
  /**
26
26
  * Convenience way to create KeySortedMap from object.
27
27
  */
@@ -28,7 +28,7 @@ export declare function _omitWithUndefined<T extends AnyObject, K extends keyof
28
28
  * 'account.updated',
29
29
  * ])
30
30
  */
31
- export declare function _mask<T extends AnyObject>(obj: T, props: string[], opt?: MutateOptions): T;
31
+ export declare function _mask<T extends AnyObject>(obj: T, props: readonly string[], opt?: MutateOptions): T;
32
32
  /**
33
33
  * Removes "falsy" values from the object.
34
34
  */
@@ -4,4 +4,4 @@ import type { AnyObject } from '../types.js';
4
4
  * All keys that are not listed in `keyOrder` go last.
5
5
  * Does not mutate original object.
6
6
  */
7
- export declare function _sortObject<T extends AnyObject>(obj: T, keyOrder: (keyof T)[]): T;
7
+ export declare function _sortObject<T extends AnyObject>(obj: T, keyOrder: readonly (keyof T)[]): T;
@@ -8,7 +8,7 @@ import { _typeCast } from '../types.js';
8
8
  * If the Function rejects - passes this rejection further.
9
9
  */
10
10
  export function pTimeoutFn(fn, opt) {
11
- opt.name ||= fn.name;
11
+ opt = { ...opt, name: opt.name || fn.name }; // copy, to not mutate the input options
12
12
  if (!opt.timeout) {
13
13
  return fn;
14
14
  }
package/dist/semver.d.ts CHANGED
@@ -46,21 +46,21 @@ declare class SemverFactory {
46
46
  /**
47
47
  * Returns the highest (max) Semver from the array, or undefined if the array is empty.
48
48
  */
49
- maxOrUndefined(items: SemverInputNullable[]): Semver | undefined;
49
+ maxOrUndefined(items: readonly SemverInputNullable[]): Semver | undefined;
50
50
  /**
51
51
  * Returns the highest Semver from the array.
52
52
  * Throws if the array is empty.
53
53
  */
54
- max(items: SemverInputNullable[]): Semver;
54
+ max(items: readonly SemverInputNullable[]): Semver;
55
55
  /**
56
56
  * Returns the lowest (min) Semver from the array, or undefined if the array is empty.
57
57
  */
58
- minOrUndefined(items: SemverInputNullable[]): Semver | undefined;
58
+ minOrUndefined(items: readonly SemverInputNullable[]): Semver | undefined;
59
59
  /**
60
60
  * Returns the lowest Semver from the array.
61
61
  * Throws if the array is empty.
62
62
  */
63
- min(items: SemverInputNullable[]): Semver;
63
+ min(items: readonly SemverInputNullable[]): Semver;
64
64
  /**
65
65
  * Sorts an array of Semvers in `dir` order (ascending by default).
66
66
  */
@@ -19,4 +19,4 @@ export interface PupaOptions {
19
19
  /**
20
20
  * API: https://github.com/sindresorhus/pupa
21
21
  */
22
- export declare function pupa(template: string, data: any[] | AnyObject, opt?: PupaOptions): string;
22
+ export declare function pupa(template: string, data: readonly any[] | AnyObject, opt?: PupaOptions): string;
package/dist/types.d.ts CHANGED
@@ -181,6 +181,17 @@ export interface BatchResult<RES = any, ERR = Error> {
181
181
  * type Foo = ValuesOf<typeof arr> // 'a' | 'b'
182
182
  */
183
183
  export type ValuesOf<T extends readonly any[]> = T[number];
184
+ /**
185
+ * Array with at least one element.
186
+ * Index 0 is typed as `T` (not `T | undefined`) under `noUncheckedIndexedAccess`.
187
+ *
188
+ * Use `_isNonEmptyArray` to narrow a regular array to it.
189
+ *
190
+ * Caveats:
191
+ * - Mutating methods (pop, shift, splice, `length = 0`) can empty it without the type noticing.
192
+ * - Non-emptiness is lost after map, filter, slice, concat (they return `T[]`). Spread preserves it.
193
+ */
194
+ export type NonEmptyArray<T> = [T, ...T[]];
184
195
  /**
185
196
  * Based on: https://stackoverflow.com/a/49286056/4919972
186
197
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.95.0",
4
+ "version": "15.97.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2"
7
7
  },
@@ -14,7 +14,7 @@
14
14
  }
15
15
  },
16
16
  "devDependencies": {
17
- "@naturalcycles/dev-lib": "0.0.0",
17
+ "@naturalcycles/dev-lib": "20.68.0",
18
18
  "@types/node": "^26",
19
19
  "@types/semver": "^7",
20
20
  "typescript": "^7",
package/src/abort.ts CHANGED
@@ -67,7 +67,7 @@ export function polyfilledAbortSignalTimeout(ms: NumberOfMilliseconds): AbortSig
67
67
  * otherwise undefined.
68
68
  */
69
69
  export function abortSignalAnyOrUndefined(
70
- signals: (AbortSignal | undefined)[],
70
+ signals: readonly (AbortSignal | undefined)[],
71
71
  ): AbortSignal | undefined {
72
72
  const filtered = signals.filter(_isTruthy)
73
73
  return filtered.length ? abortSignalAny(filtered) : undefined
@@ -82,17 +82,17 @@ export function abortSignalAnyOrUndefined(
82
82
  *
83
83
  * If only 1 signal is passed in the input array - that Signal is returned as-is.
84
84
  */
85
- export function abortSignalAny(signals: AbortSignal[]): AbortSignal {
85
+ export function abortSignalAny(signals: readonly AbortSignal[]): AbortSignal {
86
86
  if (signals.length === 1) {
87
87
  return signals[0]!
88
88
  }
89
89
 
90
90
  return typeof AbortSignal.any === 'function'
91
- ? AbortSignal.any(signals)
91
+ ? AbortSignal.any(signals as AbortSignal[]) // lib.dom types require a mutable array, but it is only read
92
92
  : polyfilledAbortSignalAny(signals)
93
93
  }
94
94
 
95
- export function polyfilledAbortSignalAny(signals: AbortSignal[]): AbortSignal {
95
+ export function polyfilledAbortSignalAny(signals: readonly AbortSignal[]): AbortSignal {
96
96
  const ac = new AbortController()
97
97
 
98
98
  for (const signal of signals) {
@@ -70,7 +70,7 @@ export interface AnalyticsClientEvent {
70
70
  }
71
71
 
72
72
  /** Canonical utm parameters. */
73
- export const CANONICAL_UTM_PARAMS = [
73
+ export const CANONICAL_UTM_PARAMS: readonly string[] = [
74
74
  'utm_source',
75
75
  'utm_medium',
76
76
  'utm_campaign',
@@ -4,6 +4,7 @@ import type {
4
4
  FalsyValue,
5
5
  Mapper,
6
6
  MutateOptions,
7
+ NonEmptyArray,
7
8
  Predicate,
8
9
  StringMap,
9
10
  } from '../types.js'
@@ -21,12 +22,12 @@ import { END } from '../types.js'
21
22
  *
22
23
  * Based on: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_chunk
23
24
  */
24
- export function _chunk<T>(array: readonly T[], size = 1): T[][] {
25
+ export function _chunk<T>(array: readonly T[], size = 1): NonEmptyArray<T>[] {
25
26
  const a: T[][] = []
26
27
  for (let i = 0; i < array.length; i += size) {
27
28
  a.push(array.slice(i, i + size))
28
29
  }
29
- return a
30
+ return a as NonEmptyArray<T>[]
30
31
  }
31
32
 
32
33
  /**
@@ -158,13 +159,20 @@ export function _mapBy<ITEM, KEY>(
158
159
  *
159
160
  * Returning `undefined` from the Mapper will EXCLUDE the item.
160
161
  */
161
- export function _groupBy<T>(items: readonly T[], mapper: Mapper<T, any>): StringMap<T[]> {
162
- const map: StringMap<T[]> = {}
162
+ export function _groupBy<T>(
163
+ items: readonly T[],
164
+ mapper: Mapper<T, any>,
165
+ ): StringMap<NonEmptyArray<T>> {
166
+ const map: StringMap<NonEmptyArray<T>> = {}
163
167
 
164
168
  for (const item of items) {
165
169
  const key = mapper(item)
166
- if (key !== undefined) {
167
- ;(map[key] ||= []).push(item)
170
+ if (key === undefined) continue
171
+ const group = map[key]
172
+ if (group) {
173
+ group.push(item)
174
+ } else {
175
+ map[key] = [item]
168
176
  }
169
177
  }
170
178
  return map
@@ -294,7 +302,7 @@ export function _countBy<T>(items: Iterable<T>, mapper: Mapper<T, any>): StringM
294
302
  * _intersection([2, 1], [2, 3])
295
303
  * // [2]
296
304
  */
297
- export function _intersection<T>(a1: T[], a2: T[] | Set<T>): T[] {
305
+ export function _intersection<T>(a1: readonly T[], a2: readonly T[] | Set<T>): T[] {
298
306
  const a2set = a2 instanceof Set ? a2 : new Set(a2)
299
307
  return a1.filter(v => a2set.has(v))
300
308
  }
@@ -307,7 +315,7 @@ export function _intersection<T>(a1: T[], a2: T[] | Set<T>): T[] {
307
315
  *
308
316
  * Passing second array as Set is more performant (it'll skip turning the array into Set in-place).
309
317
  */
310
- export function _intersectsWith<T>(a1: T[], a2: T[] | Set<T>): boolean {
318
+ export function _intersectsWith<T>(a1: readonly T[], a2: readonly T[] | Set<T>): boolean {
311
319
  const a2set = a2 instanceof Set ? a2 : new Set(a2)
312
320
  return a1.some(v => a2set.has(v))
313
321
  }
@@ -321,7 +329,7 @@ export function _intersectsWith<T>(a1: T[], a2: T[] | Set<T>): boolean {
321
329
  *
322
330
  * Passing second array as Set is more performant (it'll skip turning the array into Set in-place).
323
331
  */
324
- export function _difference<T>(a1: T[], a2: T[] | Set<T>): T[] {
332
+ export function _difference<T>(a1: readonly T[], a2: readonly T[] | Set<T>): T[] {
325
333
  const a2set = a2 instanceof Set ? a2 : new Set(a2)
326
334
  return a1.filter(v => !a2set.has(v))
327
335
  }
@@ -329,7 +337,7 @@ export function _difference<T>(a1: T[], a2: T[] | Set<T>): T[] {
329
337
  /**
330
338
  * Does NOT mutate the array, returns a filtered array instead.
331
339
  */
332
- export function _arrayRemove<T>(a: T[], itemToRemove: T): T[] {
340
+ export function _arrayRemove<T>(a: readonly T[], itemToRemove: T): T[] {
333
341
  return a.filter(r => r !== itemToRemove)
334
342
  }
335
343
 
@@ -468,6 +476,16 @@ export function _firstOrUndefined<T>(array: readonly T[]): T | undefined {
468
476
  return array[0]
469
477
  }
470
478
 
479
+ /**
480
+ * Type guard that narrows an array to NonEmptyArray (array with at least one element).
481
+ * Readonly arrays are narrowed to `Readonly<NonEmptyArray<T>>`.
482
+ */
483
+ export function _isNonEmptyArray<T>(array: T[]): array is NonEmptyArray<T>
484
+ export function _isNonEmptyArray<T>(array: readonly T[]): array is Readonly<NonEmptyArray<T>>
485
+ export function _isNonEmptyArray(array: readonly unknown[]): boolean {
486
+ return array.length > 0
487
+ }
488
+
471
489
  /**
472
490
  * Returns the first item of a non-empty iterable (Set, Map.values(), generator, etc.).
473
491
  * Throws if the iterable is empty.
@@ -71,7 +71,7 @@ export class TranslationService {
71
71
  * Loads locale(s) (if not already cached) via configured TranslationLoader.
72
72
  * Resolves promise when done (ready to be used).
73
73
  */
74
- async loadLocale(locale: string | string[]): Promise<void> {
74
+ async loadLocale(locale: string | readonly string[]): Promise<void> {
75
75
  const locales = Array.isArray(locale) ? locale : [locale]
76
76
 
77
77
  await pMap(locales, async locale => {
@@ -74,7 +74,7 @@ export class ImageFitter {
74
74
  this.resizeObserver.disconnect()
75
75
  }
76
76
 
77
- private update(entries: ResizeObserverEntry[]): void {
77
+ private update(entries: readonly ResizeObserverEntry[]): void {
78
78
  const width = Math.floor(entries[0]!.contentRect.width)
79
79
  if (width === this.containerWidth) return // we're only interested in width changes
80
80
  this.containerWidth = width
@@ -730,7 +730,7 @@ class LocalDateFactory {
730
730
  /**
731
731
  * Returns the earliest (min) LocalDate from the array, or undefined if the array is empty.
732
732
  */
733
- minOrUndefined(items: LocalDateInputNullable[]): LocalDate | undefined {
733
+ minOrUndefined(items: readonly LocalDateInputNullable[]): LocalDate | undefined {
734
734
  let min: LocalDate | undefined
735
735
  for (const item of items) {
736
736
  if (!item) continue
@@ -746,7 +746,7 @@ class LocalDateFactory {
746
746
  * Returns the earliest LocalDate from the array.
747
747
  * Throws if the array is empty.
748
748
  */
749
- min(items: LocalDateInputNullable[]): LocalDate {
749
+ min(items: readonly LocalDateInputNullable[]): LocalDate {
750
750
  const min = this.minOrUndefined(items)
751
751
  _assert(min, 'localDate.min called on empty array')
752
752
  return min
@@ -755,7 +755,7 @@ class LocalDateFactory {
755
755
  /**
756
756
  * Returns the latest (max) LocalDate from the array, or undefined if the array is empty.
757
757
  */
758
- maxOrUndefined(items: LocalDateInputNullable[]): LocalDate | undefined {
758
+ maxOrUndefined(items: readonly LocalDateInputNullable[]): LocalDate | undefined {
759
759
  let max: LocalDate | undefined
760
760
  for (const item of items) {
761
761
  if (!item) continue
@@ -771,7 +771,7 @@ class LocalDateFactory {
771
771
  * Returns the latest LocalDate from the array.
772
772
  * Throws if the array is empty.
773
773
  */
774
- max(items: LocalDateInputNullable[]): LocalDate {
774
+ max(items: readonly LocalDateInputNullable[]): LocalDate {
775
775
  const max = this.maxOrUndefined(items)
776
776
  _assert(max, 'localDate.max called on empty array')
777
777
  return max
@@ -1024,7 +1024,7 @@ class LocalTimeFactory {
1024
1024
  return opt.mutate ? items.sort(cmp) : items.toSorted(cmp)
1025
1025
  }
1026
1026
 
1027
- minOrUndefined(items: LocalTimeInputNullable[]): LocalTime | undefined {
1027
+ minOrUndefined(items: readonly LocalTimeInputNullable[]): LocalTime | undefined {
1028
1028
  let min: LocalTime | undefined
1029
1029
  for (const item of items) {
1030
1030
  if (!item) continue
@@ -1036,13 +1036,13 @@ class LocalTimeFactory {
1036
1036
  return min
1037
1037
  }
1038
1038
 
1039
- min(items: LocalTimeInputNullable[]): LocalTime {
1039
+ min(items: readonly LocalTimeInputNullable[]): LocalTime {
1040
1040
  const min = this.minOrUndefined(items)
1041
1041
  _assert(min, 'localTime.min called on empty array')
1042
1042
  return min
1043
1043
  }
1044
1044
 
1045
- maxOrUndefined(items: LocalTimeInputNullable[]): LocalTime | undefined {
1045
+ maxOrUndefined(items: readonly LocalTimeInputNullable[]): LocalTime | undefined {
1046
1046
  let max: LocalTime | undefined
1047
1047
  for (const item of items) {
1048
1048
  if (!item) continue
@@ -1054,7 +1054,7 @@ class LocalTimeFactory {
1054
1054
  return max
1055
1055
  }
1056
1056
 
1057
- max(items: LocalTimeInputNullable[]): LocalTime {
1057
+ max(items: readonly LocalTimeInputNullable[]): LocalTime {
1058
1058
  const max = this.maxOrUndefined(items)
1059
1059
  _assert(max, 'localTime.max called on empty array')
1060
1060
  return max
@@ -34,7 +34,7 @@ export function _getTargetMethodSignature(target: AnyObject, keyStr: string): st
34
34
  * returns:
35
35
  * a, b, c
36
36
  */
37
- export function _getArgsSignature(args: any[] = [], logArgs = true): string {
37
+ export function _getArgsSignature(args: readonly any[] = [], logArgs = true): string {
38
38
  if (!logArgs) return ''
39
39
 
40
40
  return args
@@ -14,8 +14,11 @@ export function _Timeout(opt: PTimeoutOptions): MethodDecorator {
14
14
 
15
15
  descriptor.value = async function (this: typeof _target, ...args: any[]) {
16
16
  const ctx = this
17
- opt.name ||= _getMethodSignature(ctx, keyStr)
18
- return await pTimeout(() => originalFn.apply(this, args), opt)
17
+ // Not mutating the shared decorator options, as the name depends on the instance (ctx)
18
+ return await pTimeout(() => originalFn.apply(this, args), {
19
+ ...opt,
20
+ name: opt.name || _getMethodSignature(ctx, keyStr),
21
+ })
19
22
  } as any
20
23
 
21
24
  return descriptor
@@ -94,7 +94,7 @@ export interface ErrorData {
94
94
  /**
95
95
  * Used in e.g http 403 error.
96
96
  */
97
- adminPermissionsRequired?: string[]
97
+ adminPermissionsRequired?: readonly string[]
98
98
 
99
99
  /**
100
100
  * Open-ended.
@@ -848,11 +848,6 @@ export class Fetcher {
848
848
  private normalizeCfg(cfg: FetcherCfg & FetcherOptions): FetcherNormalizedCfg {
849
849
  const { debug = false, logger = consoleLogger } = cfg
850
850
 
851
- if (cfg.baseUrl?.endsWith('/')) {
852
- logger.warn(`Fetcher: baseUrl should not end with slash: ${cfg.baseUrl}`)
853
- cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1)
854
- }
855
-
856
851
  const norm: FetcherNormalizedCfg = _merge(
857
852
  {
858
853
  baseUrl: '',
@@ -894,6 +889,12 @@ export class Fetcher {
894
889
 
895
890
  norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase())
896
891
 
892
+ // Normalizing `norm`, not the input `cfg`, to not mutate the input
893
+ if (norm.baseUrl.endsWith('/')) {
894
+ logger.warn(`Fetcher: baseUrl should not end with slash: ${norm.baseUrl}`)
895
+ norm.baseUrl = norm.baseUrl.slice(0, -1)
896
+ }
897
+
897
898
  return norm
898
899
  }
899
900
 
@@ -8,4 +8,4 @@ export type HttpStatusFamily = 5 | 4 | 3 | 2 | 1
8
8
  */
9
9
  export type HttpStatusCode = number
10
10
 
11
- export const HTTP_METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']
11
+ export const HTTP_METHODS: readonly HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD']
@@ -142,7 +142,7 @@ export function createCommonLoggerAtLevel(
142
142
  /**
143
143
  * Creates a "proxy" CommonLogger that pipes log entries to all provided sub-loggers.
144
144
  */
145
- export function commonLoggerPipe(loggers: CommonLogger[]): CommonLogger {
145
+ export function commonLoggerPipe(loggers: readonly CommonLogger[]): CommonLogger {
146
146
  return {
147
147
  debug: (...args) => loggers.forEach(logger => logger.debug(...args)),
148
148
  log: (...args) => loggers.forEach(logger => logger.log(...args)),
@@ -9,7 +9,7 @@ import { _assert } from '../error/assert.js'
9
9
  * _average([1, 2, 3, 4])
10
10
  * // 2.5
11
11
  */
12
- export function _average(values: number[]): number {
12
+ export function _average(values: readonly number[]): number {
13
13
  _assert(values.length, '_average is called on empty array')
14
14
  let total = 0
15
15
  for (const n of values) total += n
@@ -19,7 +19,7 @@ export function _average(values: number[]): number {
19
19
  /**
20
20
  * Same as _average, but safely returns null if input array is empty or nullish.
21
21
  */
22
- export function _averageOrNull(values: number[] | undefined | null): number | null {
22
+ export function _averageOrNull(values: readonly number[] | undefined | null): number | null {
23
23
  return values?.length ? _average(values) : null
24
24
  }
25
25
 
@@ -31,7 +31,7 @@ export function _averageOrNull(values: number[] | undefined | null): number | nu
31
31
  * _stdDev([2, 4, 4, 4, 5, 5, 7, 9])
32
32
  * // 2
33
33
  */
34
- export function _stdDev(values: number[]): number {
34
+ export function _stdDev(values: readonly number[]): number {
35
35
  _assert(values.length, '_stdDev is called on empty array')
36
36
  const avg = _average(values)
37
37
  let sumSquaredDiff = 0
@@ -45,14 +45,14 @@ export function _stdDev(values: number[]): number {
45
45
  /**
46
46
  * Same as _stdDev, but safely returns null if input array is empty or nullish.
47
47
  */
48
- export function _stdDevOrNull(values: number[] | undefined | null): number | null {
48
+ export function _stdDevOrNull(values: readonly number[] | undefined | null): number | null {
49
49
  return values?.length ? _stdDev(values) : null
50
50
  }
51
51
 
52
52
  /**
53
53
  * valuesArray and weightsArray length is expected to be the same.
54
54
  */
55
- export function _averageWeighted(values: number[], weights: number[]): number {
55
+ export function _averageWeighted(values: readonly number[], weights: readonly number[]): number {
56
56
  let numerator = 0
57
57
  let denominator = 0
58
58
  for (let i = 0; i < values.length; i++) {
@@ -74,8 +74,8 @@ export function _averageWeighted(values: number[], weights: number[]): number {
74
74
  * _percentile([1, 2, 3], 100)
75
75
  * // 3
76
76
  */
77
- export function _percentile(values: number[], pc: number): number {
78
- const sorted = values.sort(comparators.numericAsc)
77
+ export function _percentile(values: readonly number[], pc: number): number {
78
+ const sorted = values.toSorted(comparators.numericAsc)
79
79
 
80
80
  // Floating pos in the range of [0; length - 1]
81
81
  const pos = ((values.length - 1) * pc) / 100
@@ -89,10 +89,13 @@ export function _percentile(values: number[], pc: number): number {
89
89
  /**
90
90
  * A tiny bit more efficient function than calling _percentile individually.
91
91
  */
92
- export function _percentiles(values: number[], pcs: number[]): Record<number, number> {
92
+ export function _percentiles(
93
+ values: readonly number[],
94
+ pcs: readonly number[],
95
+ ): Record<number, number> {
93
96
  const r = {} as Record<number, number>
94
97
 
95
- const sorted = values.sort(comparators.numericAsc)
98
+ const sorted = values.toSorted(comparators.numericAsc)
96
99
 
97
100
  for (const pc of pcs) {
98
101
  // Floating pos in the range of [0; length - 1]
@@ -116,7 +119,7 @@ export function _percentiles(values: number[], pcs: number[]): Record<number, nu
116
119
  * _median([1, 2, 3, 4])
117
120
  * // 2.5
118
121
  */
119
- export function _median(values: number[]): number {
122
+ export function _median(values: readonly number[]): number {
120
123
  const sorted = values.toSorted(comparators.numericAsc)
121
124
  const len = sorted.length
122
125
  const mid = Math.floor(len / 2)
@@ -16,10 +16,18 @@ export class Stack<T> {
16
16
  */
17
17
  private nextIndex = 0
18
18
 
19
- readonly items: T[] = []
19
+ private readonly buffer: T[] = []
20
+
21
+ /**
22
+ * Raw "items buffer" as-is (not ordered properly).
23
+ * Readonly, because mutating it (e.g sorting in-place) would corrupt the Stack.
24
+ */
25
+ get items(): readonly T[] {
26
+ return this.buffer
27
+ }
20
28
 
21
29
  push(item: T): this {
22
- this.items[this.nextIndex] = item
30
+ this.buffer[this.nextIndex] = item
23
31
  this.nextIndex = this.nextIndex === this.size - 1 ? 0 : this.nextIndex + 1
24
32
  return this
25
33
  }
@@ -29,7 +37,7 @@ export class Stack<T> {
29
37
  */
30
38
  fill(item: T): this {
31
39
  for (let i = 0; i < this.size; i++) {
32
- this.items[i] = item
40
+ this.buffer[i] = item
33
41
  }
34
42
  return this
35
43
  }
@@ -38,14 +46,14 @@ export class Stack<T> {
38
46
  * Returns last items in the right order.
39
47
  * Unlike raw `items` property that returns "items buffer" as-is (not ordered properly).
40
48
  */
41
- get itemsOrdered(): T[] {
42
- if (this.items.length < this.size) {
49
+ get itemsOrdered(): readonly T[] {
50
+ if (this.buffer.length < this.size) {
43
51
  // Buffer is not filled yet, just return it as-is
44
- return this.items
52
+ return this.buffer
45
53
  }
46
54
 
47
55
  // Buffer was filled and started to "overwrite itself", will need to return 2 slices
48
- return [...this.items.slice(this.nextIndex), ...this.items.slice(0, this.nextIndex)]
56
+ return [...this.buffer.slice(this.nextIndex), ...this.buffer.slice(0, this.nextIndex)]
49
57
  }
50
58
  }
51
59
 
@@ -90,7 +98,7 @@ export class NumberStack extends Stack<number> {
90
98
  return this.items.length === 0 ? null : _percentile(this.items, pc)
91
99
  }
92
100
 
93
- percentiles(pcs: number[]): Record<number, number> {
101
+ percentiles(pcs: readonly number[]): Record<number, number> {
94
102
  return _percentiles(this.items, pcs)
95
103
  }
96
104
  }
@@ -9,7 +9,7 @@ export function _randomInt(minIncl: number, maxIncl: number): number {
9
9
  * Should be used on non-empty arrays! (otherwise will return undefined,
10
10
  * which is not reflected in the output type)
11
11
  */
12
- export function _randomArrayItem<T>(array: T[]): T {
12
+ export function _randomArrayItem<T>(array: readonly T[]): T {
13
13
  return array[_randomInt(0, array.length - 1)]!
14
14
  }
15
15
 
@@ -29,7 +29,7 @@ export class KeySortedMap<K, V> {
29
29
  private readonly map: Map<K, V>
30
30
  readonly #sortedKeys: K[]
31
31
 
32
- constructor(entries: [K, V][] = [], opt: KeySortedMapOptions<K> = {}) {
32
+ constructor(entries: readonly [K, V][] = [], opt: KeySortedMapOptions<K> = {}) {
33
33
  this.#comparator = opt.comparator
34
34
  this.map = new Map(entries)
35
35
  this.#sortedKeys = Array.from(this.map.keys())
@@ -25,7 +25,7 @@ export class LazyKeySortedMap<K, V> {
25
25
  private readonly maybeSortedKeys: K[]
26
26
  private keysAreSorted = false
27
27
 
28
- constructor(entries: [K, V][] = [], opt: LazyKeySortedMapOptions<K> = {}) {
28
+ constructor(entries: readonly [K, V][] = [], opt: LazyKeySortedMapOptions<K> = {}) {
29
29
  this.#comparator = opt.comparator
30
30
  this.map = new Map(entries)
31
31
  this.maybeSortedKeys = Array.from(this.map.keys())
@@ -101,7 +101,11 @@ export function _omitWithUndefined<T extends AnyObject, K extends keyof T>(
101
101
  * 'account.updated',
102
102
  * ])
103
103
  */
104
- export function _mask<T extends AnyObject>(obj: T, props: string[], opt: MutateOptions = {}): T {
104
+ export function _mask<T extends AnyObject>(
105
+ obj: T,
106
+ props: readonly string[],
107
+ opt: MutateOptions = {},
108
+ ): T {
105
109
  const r = opt.mutate ? obj : _deepCopy(obj)
106
110
  for (const k of props) {
107
111
  _unset(r, k)
@@ -5,7 +5,7 @@ import type { AnyObject } from '../types.js'
5
5
  * All keys that are not listed in `keyOrder` go last.
6
6
  * Does not mutate original object.
7
7
  */
8
- export function _sortObject<T extends AnyObject>(obj: T, keyOrder: (keyof T)[]): T {
8
+ export function _sortObject<T extends AnyObject>(obj: T, keyOrder: readonly (keyof T)[]): T {
9
9
  const r = {} as T
10
10
 
11
11
  // First, go over ordered keys
@@ -166,7 +166,7 @@ export async function pMap<IN, OUT>(
166
166
  pMap with serial (non-concurrent) execution.
167
167
  */
168
168
  async function pMap1<IN, OUT>(
169
- items: IN[],
169
+ items: readonly IN[],
170
170
  mapper: AbortableAsyncMapper<IN, OUT>,
171
171
  errorMode: ErrorMode,
172
172
  logger: CommonLogger | null,
@@ -202,7 +202,7 @@ async function pMap1<IN, OUT>(
202
202
  pMap with fully concurrent execution, like Promise.all
203
203
  */
204
204
  async function pMapAll<IN, OUT>(
205
- items: IN[],
205
+ items: readonly IN[],
206
206
  mapper: AbortableAsyncMapper<IN, OUT>,
207
207
  errorMode: ErrorMode,
208
208
  logger: CommonLogger | null,
@@ -47,7 +47,7 @@ export interface PTimeoutOptions {
47
47
  * If the Function rejects - passes this rejection further.
48
48
  */
49
49
  export function pTimeoutFn<T extends AnyAsyncFunction>(fn: T, opt: PTimeoutOptions): T {
50
- opt.name ||= fn.name
50
+ opt = { ...opt, name: opt.name || fn.name } // copy, to not mutate the input options
51
51
 
52
52
  if (!opt.timeout) {
53
53
  return fn
package/src/semver.ts CHANGED
@@ -88,7 +88,7 @@ class SemverFactory {
88
88
  /**
89
89
  * Returns the highest (max) Semver from the array, or undefined if the array is empty.
90
90
  */
91
- maxOrUndefined(items: SemverInputNullable[]): Semver | undefined {
91
+ maxOrUndefined(items: readonly SemverInputNullable[]): Semver | undefined {
92
92
  let max: Semver | undefined
93
93
  for (const item of items) {
94
94
  const input = this.fromInputOrUndefined(item)
@@ -103,7 +103,7 @@ class SemverFactory {
103
103
  * Returns the highest Semver from the array.
104
104
  * Throws if the array is empty.
105
105
  */
106
- max(items: SemverInputNullable[]): Semver {
106
+ max(items: readonly SemverInputNullable[]): Semver {
107
107
  const max = this.maxOrUndefined(items)
108
108
  _assert(max, 'semver.max called on empty array')
109
109
  return max
@@ -112,7 +112,7 @@ class SemverFactory {
112
112
  /**
113
113
  * Returns the lowest (min) Semver from the array, or undefined if the array is empty.
114
114
  */
115
- minOrUndefined(items: SemverInputNullable[]): Semver | undefined {
115
+ minOrUndefined(items: readonly SemverInputNullable[]): Semver | undefined {
116
116
  let min: Semver | undefined
117
117
  for (const item of items) {
118
118
  const input = this.fromInputOrUndefined(item)
@@ -127,7 +127,7 @@ class SemverFactory {
127
127
  * Returns the lowest Semver from the array.
128
128
  * Throws if the array is empty.
129
129
  */
130
- min(items: SemverInputNullable[]): Semver {
130
+ min(items: readonly SemverInputNullable[]): Semver {
131
131
  const min = this.minOrUndefined(items)
132
132
  _assert(min, 'semver.min called on empty array')
133
133
  return min
@@ -36,7 +36,11 @@ export interface PupaOptions {
36
36
  /**
37
37
  * API: https://github.com/sindresorhus/pupa
38
38
  */
39
- export function pupa(template: string, data: any[] | AnyObject, opt: PupaOptions = {}): string {
39
+ export function pupa(
40
+ template: string,
41
+ data: readonly any[] | AnyObject,
42
+ opt: PupaOptions = {},
43
+ ): string {
40
44
  if (typeof template !== 'string') {
41
45
  throw new TypeError(`Expected a \`string\` in the first argument, got \`${typeof template}\``)
42
46
  }
@@ -41,7 +41,7 @@ export interface ReadingTimeResult extends ReadingTimeStats {
41
41
 
42
42
  type WordBoundFunction = (char: string) => boolean
43
43
 
44
- function codeIsInRanges(num: number, arrayOfRanges: number[][]): boolean {
44
+ function codeIsInRanges(num: number, arrayOfRanges: readonly number[][]): boolean {
45
45
  return arrayOfRanges.some(([lowerBound, upperBound]) => lowerBound! <= num && num <= upperBound!)
46
46
  }
47
47
 
package/src/types.ts CHANGED
@@ -225,6 +225,18 @@ export interface BatchResult<RES = any, ERR = Error> {
225
225
  */
226
226
  export type ValuesOf<T extends readonly any[]> = T[number]
227
227
 
228
+ /**
229
+ * Array with at least one element.
230
+ * Index 0 is typed as `T` (not `T | undefined`) under `noUncheckedIndexedAccess`.
231
+ *
232
+ * Use `_isNonEmptyArray` to narrow a regular array to it.
233
+ *
234
+ * Caveats:
235
+ * - Mutating methods (pop, shift, splice, `length = 0`) can empty it without the type noticing.
236
+ * - Non-emptiness is lost after map, filter, slice, concat (they return `T[]`). Spread preserves it.
237
+ */
238
+ export type NonEmptyArray<T> = [T, ...T[]]
239
+
228
240
  /**
229
241
  * Based on: https://stackoverflow.com/a/49286056/4919972
230
242
  *
package/src/ua/ua.ts CHANGED
@@ -379,7 +379,7 @@ function mergeMapEntries(
379
379
  function rgxMapper(
380
380
  target: UABrowser | UACpu | UADevice | UAEngine | UAOS,
381
381
  ua: string,
382
- arrays: UARegexMapEntry[],
382
+ arrays: readonly UARegexMapEntry[],
383
383
  ): void {
384
384
  const t = target as unknown as Record<string, string | undefined>
385
385
  let matches: RegExpExecArray | null = null