@naturalcycles/js-lib 15.93.2 → 15.93.4

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.
@@ -868,8 +868,7 @@ class LocalTimeFactory {
868
868
  isTimezoneValid(tz) {
869
869
  if (tz === 'UTC')
870
870
  return true; // we deliberately consider UTC a valid timezone, while it's mostly used in testing
871
- // oxlint-disable-next-line no-restricted-globals
872
- return Intl.supportedValuesOf('timeZone').includes(tz);
871
+ return Intl2.getSupportedTimezones().includes(tz);
873
872
  }
874
873
  sort(items, dir = 'asc', opt = {}) {
875
874
  const cmp = comparators.by((lt) => lt.$date.valueOf(), { dir });
@@ -7,13 +7,41 @@ import type { IANATimezone } from '../types.js';
7
7
  *
8
8
  * Methods accept non-optional arguments consciously,
9
9
  * to be able to cache them better. Just pass {} for options.
10
+ *
11
+ * The cache is never evicted, it grows with each unique combination of arguments.
12
+ * Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
10
13
  */
11
14
  declare class MemoizedIntl {
15
+ /**
16
+ * Escape hatch: for when you want to use native Intl, but linter screams at you - then you can use
17
+ * Intl2.Intl.*
18
+ *
19
+ * Prefer the memoized methods of Intl2 for creating formatters; use this for static functions
20
+ * (e.g `getCanonicalLocales`), `instanceof` checks, and cheap objects like Locale.
21
+ *
22
+ * Intl.Locale is not wrapped consciously: it was benchmarked and does NOT benefit from memoization.
23
+ * It's cheap to create (~1.3µs raw vs ~0.2µs cache hit, so ~1µs saved), while its tag often comes
24
+ * from runtime data (e.g request language), which would grow the never-evicted cache unboundedly.
25
+ * Use `new Intl2.Intl.Locale(tag)` instead.
26
+ */
27
+ readonly Intl: typeof Intl;
12
28
  /**
13
29
  * Returns the IANA timezone e.g `Europe/Stockholm`.
14
30
  * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
31
+ *
32
+ * Memoized, so it returns the timezone at the time of the first call.
33
+ * Changing `process.env.TZ` at runtime after that is not reflected.
15
34
  */
16
35
  getTimezone(): IANATimezone;
36
+ /**
37
+ * Returns the list of IANA timezones supported by the runtime (~420 entries).
38
+ * Memoized, because V8 rebuilds the list on every call (~70µs).
39
+ *
40
+ * The list contains canonical CLDR ids only, so valid aliases are missing from it,
41
+ * e.g `UTC` and `Europe/Kyiv` (listed as `Europe/Kiev`).
42
+ * Don't use it to validate user input as is.
43
+ */
44
+ getSupportedTimezones(): readonly IANATimezone[];
17
45
  DateTimeFormat(locales: Intl.LocalesArgument, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat;
18
46
  RelativeTimeFormat(locales: Intl.LocalesArgument, options: Intl.RelativeTimeFormatOptions): Intl.RelativeTimeFormat;
19
47
  DurationFormat(locales: Intl.LocalesArgument, options: Intl.DurationFormatOptions): Intl.DurationFormat;
package/dist/intl/intl.js CHANGED
@@ -9,15 +9,45 @@ import { _Memo } from '../decorators/memo.decorator.js';
9
9
  *
10
10
  * Methods accept non-optional arguments consciously,
11
11
  * to be able to cache them better. Just pass {} for options.
12
+ *
13
+ * The cache is never evicted, it grows with each unique combination of arguments.
14
+ * Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
12
15
  */
13
16
  class MemoizedIntl {
17
+ /**
18
+ * Escape hatch: for when you want to use native Intl, but linter screams at you - then you can use
19
+ * Intl2.Intl.*
20
+ *
21
+ * Prefer the memoized methods of Intl2 for creating formatters; use this for static functions
22
+ * (e.g `getCanonicalLocales`), `instanceof` checks, and cheap objects like Locale.
23
+ *
24
+ * Intl.Locale is not wrapped consciously: it was benchmarked and does NOT benefit from memoization.
25
+ * It's cheap to create (~1.3µs raw vs ~0.2µs cache hit, so ~1µs saved), while its tag often comes
26
+ * from runtime data (e.g request language), which would grow the never-evicted cache unboundedly.
27
+ * Use `new Intl2.Intl.Locale(tag)` instead.
28
+ */
29
+ Intl = Intl;
14
30
  /**
15
31
  * Returns the IANA timezone e.g `Europe/Stockholm`.
16
32
  * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
33
+ *
34
+ * Memoized, so it returns the timezone at the time of the first call.
35
+ * Changing `process.env.TZ` at runtime after that is not reflected.
17
36
  */
18
37
  getTimezone() {
19
38
  return Intl.DateTimeFormat().resolvedOptions().timeZone;
20
39
  }
40
+ /**
41
+ * Returns the list of IANA timezones supported by the runtime (~420 entries).
42
+ * Memoized, because V8 rebuilds the list on every call (~70µs).
43
+ *
44
+ * The list contains canonical CLDR ids only, so valid aliases are missing from it,
45
+ * e.g `UTC` and `Europe/Kyiv` (listed as `Europe/Kiev`).
46
+ * Don't use it to validate user input as is.
47
+ */
48
+ getSupportedTimezones() {
49
+ return Intl.supportedValuesOf('timeZone');
50
+ }
21
51
  DateTimeFormat(locales, options) {
22
52
  return new Intl.DateTimeFormat(locales, options);
23
53
  }
@@ -51,29 +81,49 @@ __decorate([
51
81
  ], MemoizedIntl.prototype, "getTimezone", null);
52
82
  __decorate([
53
83
  _Memo()
84
+ ], MemoizedIntl.prototype, "getSupportedTimezones", null);
85
+ __decorate([
86
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
54
87
  ], MemoizedIntl.prototype, "DateTimeFormat", null);
55
88
  __decorate([
56
- _Memo()
89
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
57
90
  ], MemoizedIntl.prototype, "RelativeTimeFormat", null);
58
91
  __decorate([
59
- _Memo()
92
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
60
93
  ], MemoizedIntl.prototype, "DurationFormat", null);
61
94
  __decorate([
62
- _Memo()
95
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
63
96
  ], MemoizedIntl.prototype, "NumberFormat", null);
64
97
  __decorate([
65
- _Memo()
98
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
66
99
  ], MemoizedIntl.prototype, "Collator", null);
67
100
  __decorate([
68
- _Memo()
101
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
69
102
  ], MemoizedIntl.prototype, "PluralRules", null);
70
103
  __decorate([
71
- _Memo()
104
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
72
105
  ], MemoizedIntl.prototype, "ListFormat", null);
73
106
  __decorate([
74
- _Memo()
107
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
75
108
  ], MemoizedIntl.prototype, "DisplayNames", null);
76
109
  __decorate([
77
- _Memo()
110
+ _Memo({ cacheKeyFn: intlCacheKeyFn })
78
111
  ], MemoizedIntl.prototype, "Segmenter", null);
79
112
  export const Intl2 = new MemoizedIntl();
113
+ /**
114
+ * Default `jsonMemoSerializer` would serialize `Intl.Locale` instances as `{}`,
115
+ * making all Locale arguments share one cache key. Convert them to their string tags instead.
116
+ *
117
+ * Options keys are serialized in sorted order, so that `{ a, b }` and `{ b, a }` share one cache key.
118
+ * Passing a keys array as replacer works, because Intl options are flat objects.
119
+ */
120
+ function intlCacheKeyFn([locales, options]) {
121
+ return JSON.stringify(localesKey(locales)) + JSON.stringify(options, Object.keys(options).sort());
122
+ }
123
+ function localesKey(locales) {
124
+ if (locales === undefined)
125
+ return;
126
+ if (Array.isArray(locales))
127
+ return locales.map(String);
128
+ return String(locales);
129
+ }
package/dist/semver.d.ts CHANGED
@@ -16,6 +16,8 @@ export type SemverTokens = [major: number, minor: number, patch: number];
16
16
  *
17
17
  * _semver('1').toString() === '1.0.0'
18
18
  *
19
+ * 4-part strings are treated as equal to 3-part strings, e.g.: '1.2.3.4' === '1.2.3'
20
+ *
19
21
  * @experimental
20
22
  */
21
23
  export declare class Semver {
package/dist/semver.js CHANGED
@@ -14,6 +14,8 @@ import { _assert } from './error/assert.js';
14
14
  *
15
15
  * _semver('1').toString() === '1.0.0'
16
16
  *
17
+ * 4-part strings are treated as equal to 3-part strings, e.g.: '1.2.3.4' === '1.2.3'
18
+ *
17
19
  * @experimental
18
20
  */
19
21
  export class Semver {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.93.2",
4
+ "version": "15.93.4",
5
5
  "dependencies": {
6
6
  "tslib": "^2"
7
7
  },
@@ -1016,8 +1016,7 @@ class LocalTimeFactory {
1016
1016
  */
1017
1017
  isTimezoneValid(tz: string): boolean {
1018
1018
  if (tz === 'UTC') return true // we deliberately consider UTC a valid timezone, while it's mostly used in testing
1019
- // oxlint-disable-next-line no-restricted-globals
1020
- return Intl.supportedValuesOf('timeZone').includes(tz)
1019
+ return Intl2.getSupportedTimezones().includes(tz as IANATimezone)
1021
1020
  }
1022
1021
 
1023
1022
  sort(items: LocalTime[], dir: SortDirection = 'asc', opt: MutateOptions = {}): LocalTime[] {
package/src/intl/intl.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { _Memo } from '../decorators/memo.decorator.js'
2
- import type { IANATimezone } from '../types.js'
2
+ import type { AnyObject, IANATimezone } from '../types.js'
3
3
 
4
4
  // oxlint-disable no-restricted-globals
5
5
 
@@ -11,18 +11,51 @@ import type { IANATimezone } from '../types.js'
11
11
  *
12
12
  * Methods accept non-optional arguments consciously,
13
13
  * to be able to cache them better. Just pass {} for options.
14
+ *
15
+ * The cache is never evicted, it grows with each unique combination of arguments.
16
+ * Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
14
17
  */
15
18
  class MemoizedIntl {
19
+ /**
20
+ * Escape hatch: for when you want to use native Intl, but linter screams at you - then you can use
21
+ * Intl2.Intl.*
22
+ *
23
+ * Prefer the memoized methods of Intl2 for creating formatters; use this for static functions
24
+ * (e.g `getCanonicalLocales`), `instanceof` checks, and cheap objects like Locale.
25
+ *
26
+ * Intl.Locale is not wrapped consciously: it was benchmarked and does NOT benefit from memoization.
27
+ * It's cheap to create (~1.3µs raw vs ~0.2µs cache hit, so ~1µs saved), while its tag often comes
28
+ * from runtime data (e.g request language), which would grow the never-evicted cache unboundedly.
29
+ * Use `new Intl2.Intl.Locale(tag)` instead.
30
+ */
31
+ readonly Intl = Intl
32
+
16
33
  /**
17
34
  * Returns the IANA timezone e.g `Europe/Stockholm`.
18
35
  * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
36
+ *
37
+ * Memoized, so it returns the timezone at the time of the first call.
38
+ * Changing `process.env.TZ` at runtime after that is not reflected.
19
39
  */
20
40
  @_Memo()
21
41
  getTimezone(): IANATimezone {
22
42
  return Intl.DateTimeFormat().resolvedOptions().timeZone as IANATimezone
23
43
  }
24
44
 
45
+ /**
46
+ * Returns the list of IANA timezones supported by the runtime (~420 entries).
47
+ * Memoized, because V8 rebuilds the list on every call (~70µs).
48
+ *
49
+ * The list contains canonical CLDR ids only, so valid aliases are missing from it,
50
+ * e.g `UTC` and `Europe/Kyiv` (listed as `Europe/Kiev`).
51
+ * Don't use it to validate user input as is.
52
+ */
25
53
  @_Memo()
54
+ getSupportedTimezones(): readonly IANATimezone[] {
55
+ return Intl.supportedValuesOf('timeZone') as IANATimezone[]
56
+ }
57
+
58
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
26
59
  DateTimeFormat(
27
60
  locales: Intl.LocalesArgument,
28
61
  options: Intl.DateTimeFormatOptions,
@@ -30,7 +63,7 @@ class MemoizedIntl {
30
63
  return new Intl.DateTimeFormat(locales, options)
31
64
  }
32
65
 
33
- @_Memo()
66
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
34
67
  RelativeTimeFormat(
35
68
  locales: Intl.LocalesArgument,
36
69
  options: Intl.RelativeTimeFormatOptions,
@@ -38,7 +71,7 @@ class MemoizedIntl {
38
71
  return new Intl.RelativeTimeFormat(locales, options)
39
72
  }
40
73
 
41
- @_Memo()
74
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
42
75
  DurationFormat(
43
76
  locales: Intl.LocalesArgument,
44
77
  options: Intl.DurationFormatOptions,
@@ -46,7 +79,7 @@ class MemoizedIntl {
46
79
  return new Intl.DurationFormat(locales, options)
47
80
  }
48
81
 
49
- @_Memo()
82
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
50
83
  NumberFormat(
51
84
  locales: Intl.LocalesArgument,
52
85
  options: Intl.NumberFormatOptions,
@@ -54,22 +87,22 @@ class MemoizedIntl {
54
87
  return new Intl.NumberFormat(locales, options)
55
88
  }
56
89
 
57
- @_Memo()
90
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
58
91
  Collator(locales: Intl.LocalesArgument, options: Intl.CollatorOptions): Intl.Collator {
59
92
  return new Intl.Collator(locales, options)
60
93
  }
61
94
 
62
- @_Memo()
95
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
63
96
  PluralRules(locales: Intl.LocalesArgument, options: Intl.PluralRulesOptions): Intl.PluralRules {
64
97
  return new Intl.PluralRules(locales, options)
65
98
  }
66
99
 
67
- @_Memo()
100
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
68
101
  ListFormat(locales: Intl.LocalesArgument, options: Intl.ListFormatOptions): Intl.ListFormat {
69
102
  return new Intl.ListFormat(locales, options)
70
103
  }
71
104
 
72
- @_Memo()
105
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
73
106
  DisplayNames(
74
107
  locales: Intl.LocalesArgument,
75
108
  options: Intl.DisplayNamesOptions,
@@ -77,10 +110,27 @@ class MemoizedIntl {
77
110
  return new Intl.DisplayNames(locales, options)
78
111
  }
79
112
 
80
- @_Memo()
113
+ @_Memo({ cacheKeyFn: intlCacheKeyFn })
81
114
  Segmenter(locales: Intl.LocalesArgument, options: Intl.SegmenterOptions): Intl.Segmenter {
82
115
  return new Intl.Segmenter(locales, options)
83
116
  }
84
117
  }
85
118
 
86
119
  export const Intl2 = new MemoizedIntl()
120
+
121
+ /**
122
+ * Default `jsonMemoSerializer` would serialize `Intl.Locale` instances as `{}`,
123
+ * making all Locale arguments share one cache key. Convert them to their string tags instead.
124
+ *
125
+ * Options keys are serialized in sorted order, so that `{ a, b }` and `{ b, a }` share one cache key.
126
+ * Passing a keys array as replacer works, because Intl options are flat objects.
127
+ */
128
+ function intlCacheKeyFn([locales, options]: [Intl.LocalesArgument, AnyObject]): string {
129
+ return JSON.stringify(localesKey(locales)) + JSON.stringify(options, Object.keys(options).sort())
130
+ }
131
+
132
+ function localesKey(locales: Intl.LocalesArgument): string | string[] | undefined {
133
+ if (locales === undefined) return
134
+ if (Array.isArray(locales)) return locales.map(String)
135
+ return String(locales)
136
+ }
package/src/semver.ts CHANGED
@@ -20,6 +20,8 @@ export type SemverTokens = [major: number, minor: number, patch: number]
20
20
  *
21
21
  * _semver('1').toString() === '1.0.0'
22
22
  *
23
+ * 4-part strings are treated as equal to 3-part strings, e.g.: '1.2.3.4' === '1.2.3'
24
+ *
23
25
  * @experimental
24
26
  */
25
27
  export class Semver {