@naturalcycles/js-lib 15.93.3 → 15.94.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.
@@ -19,10 +19,11 @@ export interface PromiseDecoratorCfg<RES = any, PARAMS = any> {
19
19
  * which will most likely be "unhandledRejection" (unless there's another higher-level @Decorator that will catch it).
20
20
  * If `catchFn` is present - it's responsible for handling or re-throwing the error.
21
21
  * Whatever `catchFn` returns - passed to the original output.
22
+ * If Promise is returned - it will be awaited.
22
23
  */
23
24
  catchFn?: (r: PromiseDecoratorResp<PARAMS> & {
24
25
  err: any;
25
- }) => RES;
26
+ }) => RES | Promise<RES>;
26
27
  /**
27
28
  * Fires AFTER thenFn / catchFn, like a usual Promise.finally().
28
29
  * Doesn't have access to neither res nor err (same as Promise.finally).
@@ -63,17 +63,18 @@ export function _createPromiseDecorator(cfg, decoratorParams = {}) {
63
63
  decoratorName,
64
64
  started,
65
65
  };
66
- let handled = false;
67
- if (cfg.catchFn) {
68
- cfg.catchFn({
66
+ if (!cfg.catchFn) {
67
+ cfg.finallyFn?.(resp);
68
+ throw err; // rethrow
69
+ }
70
+ try {
71
+ return await cfg.catchFn({
69
72
  ...resp,
70
73
  err,
71
74
  });
72
- handled = true;
73
75
  }
74
- cfg.finallyFn?.(resp);
75
- if (!handled) {
76
- throw err; // rethrow
76
+ finally {
77
+ cfg.finallyFn?.(resp);
77
78
  }
78
79
  }
79
80
  };
@@ -12,6 +12,19 @@ import type { IANATimezone } from '../types.js';
12
12
  * Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
13
13
  */
14
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;
15
28
  /**
16
29
  * Returns the IANA timezone e.g `Europe/Stockholm`.
17
30
  * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
package/dist/intl/intl.js CHANGED
@@ -14,6 +14,19 @@ import { _Memo } from '../decorators/memo.decorator.js';
14
14
  * Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
15
15
  */
16
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;
17
30
  /**
18
31
  * Returns the IANA timezone e.g `Europe/Stockholm`.
19
32
  * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
@@ -78,7 +78,7 @@ export declare class KeySortedMap<K, V> {
78
78
  */
79
79
  toMap(): Map<K, V>;
80
80
  /**
81
- * lowerBound: first index i s.t. keys[i] >= target
81
+ * lowerBound: first index i s.t. keys[i] >= target, in comparator order
82
82
  */
83
83
  private lowerBound;
84
84
  private sortKeys;
@@ -32,12 +32,17 @@ export class KeySortedMap {
32
32
  * Allows to set multiple key-value pairs at once.
33
33
  */
34
34
  setMany(obj) {
35
+ let hasNewKeys = false;
35
36
  for (const [k, v] of Object.entries(obj)) {
37
+ if (!this.map.has(k)) {
38
+ this.#sortedKeys.push(k);
39
+ hasNewKeys = true;
40
+ }
36
41
  this.map.set(k, v);
37
- this.#sortedKeys.push(k);
38
42
  }
39
43
  // Resort all at once
40
- this.sortKeys();
44
+ if (hasNewKeys)
45
+ this.sortKeys();
41
46
  return this;
42
47
  }
43
48
  /**
@@ -101,8 +106,7 @@ export class KeySortedMap {
101
106
  return this.entries();
102
107
  }
103
108
  toString() {
104
- console.log('toString called !!!!!!!!!!!!!!!!!!!!!');
105
- return 'abc';
109
+ return `KeySortedMap(${this.size}) ${JSON.stringify(this.toObject())}`;
106
110
  }
107
111
  [Symbol.toStringTag] = 'KeySortedMap';
108
112
  /**
@@ -179,15 +183,17 @@ export class KeySortedMap {
179
183
  return new Map(this.entries());
180
184
  }
181
185
  /**
182
- * lowerBound: first index i s.t. keys[i] >= target
186
+ * lowerBound: first index i s.t. keys[i] >= target, in comparator order
183
187
  */
184
188
  lowerBound(target) {
189
+ const comparator = this.#comparator;
185
190
  let lo = 0;
186
191
  let hi = this.#sortedKeys.length;
187
192
  while (lo < hi) {
188
193
  // oxlint-disable-next-line no-bitwise
189
194
  const mid = (lo + hi) >>> 1;
190
- if (this.#sortedKeys[mid] < target) {
195
+ const key = this.#sortedKeys[mid];
196
+ if (comparator ? comparator(key, target) < 0 : key < target) {
191
197
  lo = mid + 1;
192
198
  }
193
199
  else {
@@ -50,7 +50,7 @@ export declare class LazyKeySortedMap<K, V> {
50
50
  values(): MapIterator<V>;
51
51
  entries(): MapIterator<[K, V]>;
52
52
  [Symbol.iterator](): MapIterator<[K, V]>;
53
- readonly [Symbol.toStringTag] = "KeySortedMap";
53
+ readonly [Symbol.toStringTag] = "LazyKeySortedMap";
54
54
  /**
55
55
  * Zero-allocation callbacks over sorted data (faster than spreading to arrays).
56
56
  */
@@ -34,10 +34,12 @@ export class LazyKeySortedMap {
34
34
  */
35
35
  setMany(obj) {
36
36
  for (const [k, v] of Object.entries(obj)) {
37
+ if (!this.map.has(k)) {
38
+ this.maybeSortedKeys.push(k);
39
+ this.keysAreSorted = false;
40
+ }
37
41
  this.map.set(k, v);
38
- this.maybeSortedKeys.push(k);
39
42
  }
40
- this.keysAreSorted = false;
41
43
  return this;
42
44
  }
43
45
  /**
@@ -86,7 +88,7 @@ export class LazyKeySortedMap {
86
88
  [Symbol.iterator]() {
87
89
  return this.entries();
88
90
  }
89
- [Symbol.toStringTag] = 'KeySortedMap';
91
+ [Symbol.toStringTag] = 'LazyKeySortedMap';
90
92
  /**
91
93
  * Zero-allocation callbacks over sorted data (faster than spreading to arrays).
92
94
  */
@@ -110,9 +112,9 @@ export class LazyKeySortedMap {
110
112
  return keys[keys.length - 1];
111
113
  }
112
114
  lastKey() {
113
- const k = this.lastKeyOrUndefined();
114
- _assert(k, 'Map.lastKey called on empty map');
115
- return k;
115
+ _assert(this.maybeSortedKeys.length, 'Map.lastKey called on empty map');
116
+ const keys = this.getSortedKeys();
117
+ return keys[keys.length - 1];
116
118
  }
117
119
  firstValueOrUndefined() {
118
120
  if (!this.maybeSortedKeys.length)
@@ -120,9 +122,8 @@ export class LazyKeySortedMap {
120
122
  return this.map.get(this.getSortedKeys()[0]);
121
123
  }
122
124
  firstValue() {
123
- const v = this.firstValueOrUndefined();
124
- _assert(v, 'Map.firstValue called on empty map');
125
- return v;
125
+ _assert(this.maybeSortedKeys.length, 'Map.firstValue called on empty map');
126
+ return this.map.get(this.getSortedKeys()[0]);
126
127
  }
127
128
  lastValueOrUndefined() {
128
129
  if (!this.maybeSortedKeys.length)
@@ -131,9 +132,9 @@ export class LazyKeySortedMap {
131
132
  return this.map.get(keys[keys.length - 1]);
132
133
  }
133
134
  lastValue() {
134
- const v = this.lastValueOrUndefined();
135
- _assert(v, 'Map.lastValue called on empty map');
136
- return v;
135
+ _assert(this.maybeSortedKeys.length, 'Map.lastValue called on empty map');
136
+ const keys = this.getSortedKeys();
137
+ return this.map.get(keys[keys.length - 1]);
137
138
  }
138
139
  firstEntryOrUndefined() {
139
140
  if (!this.maybeSortedKeys.length)
@@ -154,7 +155,7 @@ export class LazyKeySortedMap {
154
155
  return [k, this.map.get(k)];
155
156
  }
156
157
  lastEntry() {
157
- const e = this.firstEntryOrUndefined();
158
+ const e = this.lastEntryOrUndefined();
158
159
  _assert(e, 'Map.lastEntry called on empty map');
159
160
  return e;
160
161
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.93.3",
4
+ "version": "15.94.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.66.0",
18
18
  "@types/node": "^26",
19
19
  "@types/semver": "^7",
20
20
  "typescript": "^7",
@@ -63,7 +63,7 @@
63
63
  "types": "dist/index.d.ts",
64
64
  "sideEffects": false,
65
65
  "engines": {
66
- "node": ">=24.11.0"
66
+ "node": ">=26.12.1"
67
67
  },
68
68
  "publishConfig": {
69
69
  "provenance": true,
@@ -22,8 +22,9 @@ export interface PromiseDecoratorCfg<RES = any, PARAMS = any> {
22
22
  * which will most likely be "unhandledRejection" (unless there's another higher-level @Decorator that will catch it).
23
23
  * If `catchFn` is present - it's responsible for handling or re-throwing the error.
24
24
  * Whatever `catchFn` returns - passed to the original output.
25
+ * If Promise is returned - it will be awaited.
25
26
  */
26
- catchFn?: (r: PromiseDecoratorResp<PARAMS> & { err: any }) => RES
27
+ catchFn?: (r: PromiseDecoratorResp<PARAMS> & { err: any }) => RES | Promise<RES>
27
28
 
28
29
  /**
29
30
  * Fires AFTER thenFn / catchFn, like a usual Promise.finally().
@@ -122,20 +123,18 @@ export function _createPromiseDecorator<RES = any, PARAMS = any>(
122
123
  started,
123
124
  }
124
125
 
125
- let handled = false
126
+ if (!cfg.catchFn) {
127
+ cfg.finallyFn?.(resp)
128
+ throw err // rethrow
129
+ }
126
130
 
127
- if (cfg.catchFn) {
128
- cfg.catchFn({
131
+ try {
132
+ return await cfg.catchFn({
129
133
  ...resp,
130
134
  err,
131
135
  })
132
- handled = true
133
- }
134
-
135
- cfg.finallyFn?.(resp)
136
-
137
- if (!handled) {
138
- throw err // rethrow
136
+ } finally {
137
+ cfg.finallyFn?.(resp)
139
138
  }
140
139
  }
141
140
  }
package/src/intl/intl.ts CHANGED
@@ -16,6 +16,20 @@ import type { AnyObject, IANATimezone } from '../types.js'
16
16
  * Don't pass unbounded dynamic values (e.g arbitrary user input) as locales or options.
17
17
  */
18
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
+
19
33
  /**
20
34
  * Returns the IANA timezone e.g `Europe/Stockholm`.
21
35
  * https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
@@ -66,12 +66,16 @@ export class KeySortedMap<K, V> {
66
66
  * Allows to set multiple key-value pairs at once.
67
67
  */
68
68
  setMany(obj: Record<any, V>): this {
69
+ let hasNewKeys = false
69
70
  for (const [k, v] of Object.entries(obj)) {
71
+ if (!this.map.has(k as K)) {
72
+ this.#sortedKeys.push(k as K)
73
+ hasNewKeys = true
74
+ }
70
75
  this.map.set(k as K, v)
71
- this.#sortedKeys.push(k as K)
72
76
  }
73
77
  // Resort all at once
74
- this.sortKeys()
78
+ if (hasNewKeys) this.sortKeys()
75
79
  return this
76
80
  }
77
81
 
@@ -139,8 +143,7 @@ export class KeySortedMap<K, V> {
139
143
  }
140
144
 
141
145
  toString(): string {
142
- console.log('toString called !!!!!!!!!!!!!!!!!!!!!')
143
- return 'abc'
146
+ return `KeySortedMap(${this.size}) ${JSON.stringify(this.toObject())}`
144
147
  }
145
148
 
146
149
  readonly [Symbol.toStringTag] = 'KeySortedMap'
@@ -233,15 +236,17 @@ export class KeySortedMap<K, V> {
233
236
  }
234
237
 
235
238
  /**
236
- * lowerBound: first index i s.t. keys[i] >= target
239
+ * lowerBound: first index i s.t. keys[i] >= target, in comparator order
237
240
  */
238
241
  private lowerBound(target: K): number {
242
+ const comparator = this.#comparator
239
243
  let lo = 0
240
244
  let hi = this.#sortedKeys.length
241
245
  while (lo < hi) {
242
246
  // oxlint-disable-next-line no-bitwise
243
247
  const mid = (lo + hi) >>> 1
244
- if (this.#sortedKeys[mid]! < target) {
248
+ const key = this.#sortedKeys[mid]!
249
+ if (comparator ? comparator(key, target) < 0 : key < target) {
245
250
  lo = mid + 1
246
251
  } else {
247
252
  hi = mid
@@ -63,10 +63,12 @@ export class LazyKeySortedMap<K, V> {
63
63
  */
64
64
  setMany(obj: Record<any, V>): this {
65
65
  for (const [k, v] of Object.entries(obj)) {
66
+ if (!this.map.has(k as K)) {
67
+ this.maybeSortedKeys.push(k as K)
68
+ this.keysAreSorted = false
69
+ }
66
70
  this.map.set(k as K, v)
67
- this.maybeSortedKeys.push(k as K)
68
71
  }
69
- this.keysAreSorted = false
70
72
  return this
71
73
  }
72
74
 
@@ -120,7 +122,7 @@ export class LazyKeySortedMap<K, V> {
120
122
  return this.entries()
121
123
  }
122
124
 
123
- readonly [Symbol.toStringTag] = 'KeySortedMap'
125
+ readonly [Symbol.toStringTag] = 'LazyKeySortedMap'
124
126
 
125
127
  /**
126
128
  * Zero-allocation callbacks over sorted data (faster than spreading to arrays).
@@ -148,9 +150,9 @@ export class LazyKeySortedMap<K, V> {
148
150
  }
149
151
 
150
152
  lastKey(): K {
151
- const k = this.lastKeyOrUndefined()
152
- _assert(k, 'Map.lastKey called on empty map')
153
- return k
153
+ _assert(this.maybeSortedKeys.length, 'Map.lastKey called on empty map')
154
+ const keys = this.getSortedKeys()
155
+ return keys[keys.length - 1]!
154
156
  }
155
157
 
156
158
  firstValueOrUndefined(): V | undefined {
@@ -159,9 +161,8 @@ export class LazyKeySortedMap<K, V> {
159
161
  }
160
162
 
161
163
  firstValue(): V {
162
- const v = this.firstValueOrUndefined()
163
- _assert(v, 'Map.firstValue called on empty map')
164
- return v
164
+ _assert(this.maybeSortedKeys.length, 'Map.firstValue called on empty map')
165
+ return this.map.get(this.getSortedKeys()[0]!)!
165
166
  }
166
167
 
167
168
  lastValueOrUndefined(): V | undefined {
@@ -171,9 +172,9 @@ export class LazyKeySortedMap<K, V> {
171
172
  }
172
173
 
173
174
  lastValue(): V {
174
- const v = this.lastValueOrUndefined()
175
- _assert(v, 'Map.lastValue called on empty map')
176
- return v
175
+ _assert(this.maybeSortedKeys.length, 'Map.lastValue called on empty map')
176
+ const keys = this.getSortedKeys()
177
+ return this.map.get(keys[keys.length - 1]!)!
177
178
  }
178
179
 
179
180
  firstEntryOrUndefined(): [K, V] | undefined {
@@ -196,7 +197,7 @@ export class LazyKeySortedMap<K, V> {
196
197
  }
197
198
 
198
199
  lastEntry(): [K, V] {
199
- const e = this.firstEntryOrUndefined()
200
+ const e = this.lastEntryOrUndefined()
200
201
  _assert(e, 'Map.lastEntry called on empty map')
201
202
  return e
202
203
  }