@naturalcycles/js-lib 15.45.0 → 15.46.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.
@@ -57,58 +57,29 @@ export function _stringify(obj, opt = {}) {
57
57
  return _stringify(obj.error, opt);
58
58
  }
59
59
  if (obj instanceof Error || _isErrorLike(obj)) {
60
- const { includeErrorCause = true } = opt;
61
- //
62
- // Error or ErrorLike
63
- //
64
- // Omit "default" error name as non-informative
65
- // UPD: no, it's still important to understand that we're dealing with Error and not just some string
66
- // if (obj?.name === 'Error') {
67
- // s = obj.message
68
- // }
69
- // if (_isErrorObject(obj) && _isHttpErrorObject(obj)) {
70
- // // Printing (0) to avoid ambiguity
71
- // s = `${obj.name}(${obj.data.backendResponseStatusCode}): ${obj.message}`
72
- // }
73
- s = [obj.name, obj.message].filter(Boolean).join(': ');
74
- if (typeof obj.code === 'string') {
75
- // Error that has no `data`, but has `code` property
76
- s += `\ncode: ${obj.code}`;
77
- }
78
- if (opt.includeErrorData && _isErrorObject(obj) && Object.keys(obj.data).length) {
79
- s += '\n' + _stringify(obj.data, opt);
80
- }
81
- if (opt.includeErrorStack && obj.stack) {
82
- // Here we're using the previously-generated "title line" (e.g "Error: some_message"),
83
- // concatenating it with the Stack (but without the title line of the Stack)
84
- // This is to fix the rare error (happened with Got) where `err.message` was changed,
85
- // but err.stack had "old" err.message
86
- // This should "fix" that
87
- const sLines = s.split('\n').length;
88
- s = [s, ...obj.stack.split('\n').slice(sLines)].join('\n');
89
- }
90
- if (supportsAggregateError && obj instanceof AggregateError && obj.errors.length) {
91
- s = [
92
- s,
93
- `${obj.errors.length} error(s):`,
94
- ...obj.errors.map((err, i) => `${i + 1}. ${_stringify(err, opt)}`),
95
- ].join('\n');
96
- }
97
- if (obj.cause && includeErrorCause) {
98
- s = s + '\nCaused by: ' + _stringify(obj.cause, opt);
99
- }
60
+ s = stringifyErrorLike(obj, opt);
100
61
  }
101
62
  else if (typeof obj === 'string') {
102
- //
103
- // String
104
- //
105
63
  s = obj.trim() || 'empty_string';
64
+ // todo: think about it more
65
+ // Stringifying it like a JSON would.
66
+ // To highlight that it's a String (and not a Number) - using double-quotes, JSON-like.
67
+ // s = `"${obj}"`
68
+ }
69
+ else if (typeof obj === 'number') {
70
+ s = String(obj);
71
+ // todo: support RegExp and Date, when split between Browser and Node stringification is implemented
72
+ // } else if (obj instanceof RegExp) {
73
+ // s = String(obj)
74
+ // } else if (obj instanceof Date) {
75
+ // s = `Date (${obj.toISOString()})`
106
76
  }
107
77
  else {
108
78
  //
109
79
  // Other
110
80
  //
111
81
  if (obj instanceof Map) {
82
+ // todo: double-check it, maybe Node's inspect has good built-in stringification
112
83
  obj = Object.fromEntries(obj);
113
84
  }
114
85
  else if (obj instanceof Set) {
@@ -132,3 +103,34 @@ export function _stringify(obj, opt = {}) {
132
103
  }
133
104
  return s;
134
105
  }
106
+ function stringifyErrorLike(obj, opt) {
107
+ const { includeErrorCause = true } = opt;
108
+ let s = [obj.name, obj.message].filter(Boolean).join(': ');
109
+ if (typeof obj.code === 'string') {
110
+ // Error that has no `data`, but has `code` property
111
+ s += `\ncode: ${obj.code}`;
112
+ }
113
+ if (opt.includeErrorData && _isErrorObject(obj) && Object.keys(obj.data).length) {
114
+ s += '\n' + _stringify(obj.data, opt);
115
+ }
116
+ if (opt.includeErrorStack && obj.stack) {
117
+ // Here we're using the previously-generated "title line" (e.g "Error: some_message"),
118
+ // concatenating it with the Stack (but without the title line of the Stack)
119
+ // This is to fix the rare error (happened with Got) where `err.message` was changed,
120
+ // but err.stack had "old" err.message
121
+ // This should "fix" that
122
+ const sLines = s.split('\n').length;
123
+ s = [s, ...obj.stack.split('\n').slice(sLines)].join('\n');
124
+ }
125
+ if (supportsAggregateError && obj instanceof AggregateError && obj.errors.length) {
126
+ s = [
127
+ s,
128
+ `${obj.errors.length} error(s):`,
129
+ ...obj.errors.map((err, i) => `${i + 1}. ${_stringify(err, opt)}`),
130
+ ].join('\n');
131
+ }
132
+ if (obj.cause && includeErrorCause) {
133
+ s = s + '\nCaused by: ' + _stringify(obj.cause, opt);
134
+ }
135
+ return s;
136
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.45.0",
4
+ "version": "15.46.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2",
7
7
  "undici": "^7",
@@ -13,7 +13,7 @@
13
13
  "@types/semver": "^7",
14
14
  "crypto-js": "^4",
15
15
  "dayjs": "^1",
16
- "@naturalcycles/dev-lib": "18.4.2"
16
+ "@naturalcycles/dev-lib": "20.8.0"
17
17
  },
18
18
  "exports": {
19
19
  ".": "./dist/index.js",
package/src/env.ts CHANGED
@@ -18,3 +18,12 @@ export function isClientSide(): boolean {
18
18
  // oxlint-disable-next-line unicorn/prefer-global-this
19
19
  return typeof window !== 'undefined' && !!window?.document
20
20
  }
21
+
22
+ /**
23
+ * Almost the same as isServerSide()
24
+ * (isServerSide should return true for Node),
25
+ * but detects Node specifically (not Deno, not Bun, etc).
26
+ */
27
+ export function isNode(): boolean {
28
+ return typeof process !== 'undefined' && process?.release?.name === 'node'
29
+ }
@@ -1,3 +1,4 @@
1
+ import { _assert } from '../error/index.js'
1
2
  import type { Comparator } from '../types.js'
2
3
 
3
4
  export interface KeySortedMapOptions<K> {
@@ -23,17 +24,17 @@ export interface KeySortedMapOptions<K> {
23
24
  */
24
25
  export class KeySortedMap<K, V> implements Map<K, V> {
25
26
  private readonly map: Map<K, V>
26
- private readonly sortedKeys: K[]
27
+ readonly #sortedKeys: K[]
27
28
 
28
- constructor(
29
- entries: [K, V][] = [],
30
- public opt: KeySortedMapOptions<K> = {},
31
- ) {
29
+ constructor(entries: [K, V][] = [], opt: KeySortedMapOptions<K> = {}) {
30
+ this.#comparator = opt.comparator
32
31
  this.map = new Map(entries)
33
- this.sortedKeys = [...this.map.keys()]
32
+ this.#sortedKeys = [...this.map.keys()]
34
33
  this.sortKeys()
35
34
  }
36
35
 
36
+ readonly #comparator: Comparator<K> | undefined
37
+
37
38
  /**
38
39
  * Convenience way to create KeySortedMap from object.
39
40
  */
@@ -47,7 +48,7 @@ export class KeySortedMap<K, V> implements Map<K, V> {
47
48
 
48
49
  clear(): void {
49
50
  this.map.clear()
50
- this.sortedKeys.length = 0
51
+ this.#sortedKeys.length = 0
51
52
  }
52
53
 
53
54
  has(key: K): boolean {
@@ -64,7 +65,7 @@ export class KeySortedMap<K, V> implements Map<K, V> {
64
65
  setMany(obj: Record<any, V>): this {
65
66
  for (const [k, v] of Object.entries(obj)) {
66
67
  this.map.set(k as K, v)
67
- this.sortedKeys.push(k as K)
68
+ this.#sortedKeys.push(k as K)
68
69
  }
69
70
  // Resort all at once
70
71
  this.sortKeys()
@@ -84,7 +85,7 @@ export class KeySortedMap<K, V> implements Map<K, V> {
84
85
  // Find insertion index (lower_bound).
85
86
  const i = this.lowerBound(key)
86
87
  // Only insert into keys when actually new.
87
- this.sortedKeys.splice(i, 0, key)
88
+ this.#sortedKeys.splice(i, 0, key)
88
89
  this.map.set(key, value)
89
90
  return this
90
91
  }
@@ -98,13 +99,13 @@ export class KeySortedMap<K, V> implements Map<K, V> {
98
99
  // Remove from keys using binary search to avoid O(n) find.
99
100
  const i = this.lowerBound(key)
100
101
  // Because key existed, it must be at i.
101
- if (i < this.sortedKeys.length && this.sortedKeys[i] === key) {
102
- this.sortedKeys.splice(i, 1)
102
+ if (i < this.#sortedKeys.length && this.#sortedKeys[i] === key) {
103
+ this.#sortedKeys.splice(i, 1)
103
104
  } else {
104
105
  // Extremely unlikely if external mutation happened; safe guard.
105
106
  // Fall back to linear search (shouldn't happen).
106
- const j = this.sortedKeys.indexOf(key)
107
- if (j !== -1) this.sortedKeys.splice(j, 1)
107
+ const j = this.#sortedKeys.indexOf(key)
108
+ if (j !== -1) this.#sortedKeys.splice(j, 1)
108
109
  }
109
110
  return true
110
111
  }
@@ -113,20 +114,19 @@ export class KeySortedMap<K, V> implements Map<K, V> {
113
114
  * Iterables (Map-compatible), all in sorted order.
114
115
  */
115
116
  *keys(): MapIterator<K> {
116
- for (let i = 0; i < this.sortedKeys.length; i++) {
117
- yield this.sortedKeys[i]!
117
+ for (const key of this.#sortedKeys) {
118
+ yield key
118
119
  }
119
120
  }
120
121
 
121
122
  *values(): MapIterator<V> {
122
- for (let i = 0; i < this.sortedKeys.length; i++) {
123
- yield this.map.get(this.sortedKeys[i]!)!
123
+ for (const key of this.#sortedKeys) {
124
+ yield this.map.get(key)!
124
125
  }
125
126
  }
126
127
 
127
128
  *entries(): MapIterator<[K, V]> {
128
- for (let i = 0; i < this.sortedKeys.length; i++) {
129
- const k = this.sortedKeys[i]!
129
+ for (const k of this.#sortedKeys) {
130
130
  yield [k, this.map.get(k)!]
131
131
  }
132
132
  }
@@ -135,64 +135,82 @@ export class KeySortedMap<K, V> implements Map<K, V> {
135
135
  return this.entries()
136
136
  }
137
137
 
138
+ toString(): string {
139
+ console.log('toString called !!!!!!!!!!!!!!!!!!!!!')
140
+ return 'abc'
141
+ }
142
+
138
143
  [Symbol.toStringTag] = 'KeySortedMap'
139
144
 
140
145
  /**
141
146
  * Zero-allocation callbacks over sorted data (faster than spreading to arrays).
142
147
  */
143
148
  forEach(cb: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
144
- const m = this.map
145
- for (let i = 0; i < this.sortedKeys.length; i++) {
146
- const k = this.sortedKeys[i]!
147
- cb.call(thisArg, m.get(k)!, k, this)
149
+ const { map } = this
150
+ for (const k of this.#sortedKeys) {
151
+ cb.call(thisArg, map.get(k)!, k, this)
148
152
  }
149
153
  }
150
154
 
151
- /**
152
- * Convenience methods that MATERIALIZE arrays (if you really want arrays).
153
- * These allocate; use iterators/forEach for maximum performance.
154
- */
155
- keysArray(): K[] {
156
- return this.sortedKeys.slice()
155
+ firstKeyOrUndefined(): K | undefined {
156
+ return this.#sortedKeys[0]
157
157
  }
158
158
 
159
- valuesArray(): V[] {
160
- // oxlint-disable-next-line unicorn/no-new-array
161
- const a = Array<V>(this.sortedKeys.length)
162
- for (let i = 0; i < this.sortedKeys.length; i++) {
163
- a[i] = this.map.get(this.sortedKeys[i]!)!
164
- }
165
- return a
159
+ firstKey(): K {
160
+ _assert(this.#sortedKeys.length, 'Map.firstKey called on empty map')
161
+ return this.#sortedKeys[0]!
166
162
  }
167
163
 
168
- entriesArray(): [K, V][] {
169
- // oxlint-disable-next-line unicorn/no-new-array
170
- const out = Array<[K, V]>(this.sortedKeys.length)
171
- for (let i = 0; i < this.sortedKeys.length; i++) {
172
- const k = this.sortedKeys[i]!
173
- out[i] = [k, this.map.get(k)!]
174
- }
175
- return out
164
+ lastKeyOrUndefined(): K | undefined {
165
+ return this.#sortedKeys.length ? this.#sortedKeys[this.#sortedKeys.length - 1] : undefined
166
+ }
167
+
168
+ lastKey(): K {
169
+ _assert(this.#sortedKeys.length, 'Map.lastKey called on empty map')
170
+ return this.#sortedKeys[this.#sortedKeys.length - 1]!
176
171
  }
177
172
 
178
- /** Fast helpers */
179
- firstKey(): K | undefined {
180
- return this.sortedKeys[0]
173
+ firstValueOrUndefined(): V | undefined {
174
+ return this.map.get(this.#sortedKeys[0]!)
181
175
  }
182
176
 
183
- lastKey(): K | undefined {
184
- return this.sortedKeys.length ? this.sortedKeys[this.sortedKeys.length - 1] : undefined
177
+ firstValue(): V {
178
+ _assert(this.#sortedKeys.length, 'Map.firstValue called on empty map')
179
+ return this.map.get(this.#sortedKeys[0]!)!
185
180
  }
186
181
 
187
- firstEntry(): [K, V] | undefined {
188
- if (!this.sortedKeys.length) return
189
- const k = this.sortedKeys[0]!
182
+ lastValueOrUndefined(): V | undefined {
183
+ return this.#sortedKeys.length
184
+ ? this.map.get(this.#sortedKeys[this.#sortedKeys.length - 1]!)
185
+ : undefined
186
+ }
187
+
188
+ lastValue(): V {
189
+ _assert(this.#sortedKeys.length, 'Map.lastValue called on empty map')
190
+ return this.map.get(this.#sortedKeys[this.#sortedKeys.length - 1]!)!
191
+ }
192
+
193
+ firstEntryOrUndefined(): [K, V] | undefined {
194
+ if (!this.#sortedKeys.length) return
195
+ const k = this.#sortedKeys[0]!
196
+ return [k, this.map.get(k)!]
197
+ }
198
+
199
+ firstEntry(): [K, V] {
200
+ _assert(this.#sortedKeys.length, 'Map.firstEntry called on empty map')
201
+ const k = this.#sortedKeys[0]!
190
202
  return [k, this.map.get(k)!]
191
203
  }
192
204
 
193
- lastEntry(): [K, V] | undefined {
194
- if (!this.sortedKeys.length) return
195
- const k = this.sortedKeys[this.sortedKeys.length - 1]!
205
+ lastEntryOrUndefined(): [K, V] | undefined {
206
+ if (!this.#sortedKeys.length) return
207
+ const k = this.#sortedKeys[this.#sortedKeys.length - 1]!
208
+ return [k, this.map.get(k)!]
209
+ }
210
+
211
+ lastEntry(): [K, V] {
212
+ _assert(this.#sortedKeys.length, 'Map.lastEntry called on empty map')
213
+ const k = this.#sortedKeys[this.#sortedKeys.length - 1]!
196
214
  return [k, this.map.get(k)!]
197
215
  }
198
216
 
@@ -201,7 +219,14 @@ export class KeySortedMap<K, V> implements Map<K, V> {
201
219
  }
202
220
 
203
221
  toObject(): Record<string, V> {
204
- return Object.fromEntries(this.map)
222
+ return Object.fromEntries(this.entries())
223
+ }
224
+
225
+ /**
226
+ * Clones the KeySortedMap into ordinary Map.
227
+ */
228
+ toMap(): Map<K, V> {
229
+ return new Map(this.entries())
205
230
  }
206
231
 
207
232
  /**
@@ -209,11 +234,11 @@ export class KeySortedMap<K, V> implements Map<K, V> {
209
234
  */
210
235
  private lowerBound(target: K): number {
211
236
  let lo = 0
212
- let hi = this.sortedKeys.length
237
+ let hi = this.#sortedKeys.length
213
238
  while (lo < hi) {
214
239
  // oxlint-disable-next-line no-bitwise
215
240
  const mid = (lo + hi) >>> 1
216
- if (this.sortedKeys[mid]! < target) {
241
+ if (this.#sortedKeys[mid]! < target) {
217
242
  lo = mid + 1
218
243
  } else {
219
244
  hi = mid
@@ -223,6 +248,6 @@ export class KeySortedMap<K, V> implements Map<K, V> {
223
248
  }
224
249
 
225
250
  private sortKeys(): void {
226
- this.sortedKeys.sort(this.opt.comparator)
251
+ this.#sortedKeys.sort(this.#comparator)
227
252
  }
228
253
  }
@@ -0,0 +1,221 @@
1
+ import { _assert } from '../error/index.js'
2
+ import type { Comparator } from '../types.js'
3
+
4
+ export interface KeySortedMapOptions<K> {
5
+ /**
6
+ * Defaults to undefined.
7
+ * Undefined (default comparator) works well for String keys.
8
+ * For Number keys - use comparators.numericAsc (or desc),
9
+ * otherwise sorting will be wrong (lexicographic).
10
+ */
11
+ comparator?: Comparator<K>
12
+ }
13
+
14
+ /**
15
+ * Maintains sorted array of keys.
16
+ * Sorts **data access**, not on insertion.
17
+ *
18
+ * @experimental
19
+ */
20
+ export class KeySortedMap2<K, V> implements Map<K, V> {
21
+ private readonly map: Map<K, V>
22
+ private readonly maybeSortedKeys: K[]
23
+ private keysAreSorted = false
24
+
25
+ constructor(entries: [K, V][] = [], opt: KeySortedMapOptions<K> = {}) {
26
+ this.#comparator = opt.comparator
27
+ this.map = new Map(entries)
28
+ this.maybeSortedKeys = [...this.map.keys()]
29
+ }
30
+
31
+ readonly #comparator: Comparator<K> | undefined
32
+
33
+ /**
34
+ * Convenience way to create KeySortedMap from object.
35
+ */
36
+ static of<V>(obj: Record<any, V>): KeySortedMap2<string, V> {
37
+ return new KeySortedMap2(Object.entries(obj))
38
+ }
39
+
40
+ get size(): number {
41
+ return this.map.size
42
+ }
43
+
44
+ clear(): void {
45
+ this.map.clear()
46
+ this.maybeSortedKeys.length = 0
47
+ this.keysAreSorted = true
48
+ }
49
+
50
+ has(key: K): boolean {
51
+ return this.map.has(key)
52
+ }
53
+
54
+ get(key: K): V | undefined {
55
+ return this.map.get(key)
56
+ }
57
+
58
+ /**
59
+ * Allows to set multiple key-value pairs at once.
60
+ */
61
+ setMany(obj: Record<any, V>): this {
62
+ for (const [k, v] of Object.entries(obj)) {
63
+ this.map.set(k as K, v)
64
+ this.maybeSortedKeys.push(k as K)
65
+ }
66
+ this.keysAreSorted = false
67
+ return this
68
+ }
69
+
70
+ /**
71
+ * Insert or update. Keeps keys array sorted at all times.
72
+ * Returns this (Map-like).
73
+ */
74
+ set(key: K, value: V): this {
75
+ if (!this.map.has(key)) {
76
+ this.maybeSortedKeys.push(key)
77
+ this.keysAreSorted = false
78
+ }
79
+ this.map.set(key, value)
80
+ return this
81
+ }
82
+
83
+ /**
84
+ * Delete by key. Returns boolean like Map.delete.
85
+ */
86
+ delete(key: K): boolean {
87
+ if (!this.map.has(key)) return false
88
+ this.map.delete(key)
89
+ // Delete operation keeps the array **as-is**, it may have been sorted or not.
90
+ const j = this.maybeSortedKeys.indexOf(key)
91
+ if (j !== -1) this.maybeSortedKeys.splice(j, 1)
92
+ return true
93
+ }
94
+
95
+ /**
96
+ * Iterables (Map-compatible), all in sorted order.
97
+ */
98
+ *keys(): MapIterator<K> {
99
+ for (const key of this.getSortedKeys()) {
100
+ yield key
101
+ }
102
+ }
103
+
104
+ *values(): MapIterator<V> {
105
+ for (const key of this.getSortedKeys()) {
106
+ yield this.map.get(key)!
107
+ }
108
+ }
109
+
110
+ *entries(): MapIterator<[K, V]> {
111
+ for (const k of this.getSortedKeys()) {
112
+ yield [k, this.map.get(k)!]
113
+ }
114
+ }
115
+
116
+ [Symbol.iterator](): MapIterator<[K, V]> {
117
+ return this.entries()
118
+ }
119
+
120
+ [Symbol.toStringTag] = 'KeySortedMap'
121
+
122
+ /**
123
+ * Zero-allocation callbacks over sorted data (faster than spreading to arrays).
124
+ */
125
+ forEach(cb: (value: V, key: K, map: Map<K, V>) => void, thisArg?: any): void {
126
+ const { map } = this
127
+ for (const k of this.getSortedKeys()) {
128
+ cb.call(thisArg, map.get(k)!, k, this)
129
+ }
130
+ }
131
+
132
+ firstKeyOrUndefined(): K | undefined {
133
+ return this.getSortedKeys()[0]
134
+ }
135
+
136
+ firstKey(): K {
137
+ _assert(this.maybeSortedKeys.length, 'Map.firstKey called on empty map')
138
+ return this.getSortedKeys()[0]!
139
+ }
140
+
141
+ lastKeyOrUndefined(): K | undefined {
142
+ if (!this.maybeSortedKeys.length) return
143
+ const keys = this.getSortedKeys()
144
+ return keys[keys.length - 1]
145
+ }
146
+
147
+ lastKey(): K {
148
+ const k = this.lastKeyOrUndefined()
149
+ _assert(k, 'Map.lastKey called on empty map')
150
+ return k
151
+ }
152
+
153
+ firstValueOrUndefined(): V | undefined {
154
+ if (!this.maybeSortedKeys.length) return
155
+ return this.map.get(this.getSortedKeys()[0]!)
156
+ }
157
+
158
+ firstValue(): V {
159
+ const v = this.firstValueOrUndefined()
160
+ _assert(v, 'Map.firstValue called on empty map')
161
+ return v
162
+ }
163
+
164
+ lastValueOrUndefined(): V | undefined {
165
+ if (!this.maybeSortedKeys.length) return
166
+ const keys = this.getSortedKeys()
167
+ return this.map.get(keys[keys.length - 1]!)
168
+ }
169
+
170
+ lastValue(): V {
171
+ const v = this.lastValueOrUndefined()
172
+ _assert(v, 'Map.lastValue called on empty map')
173
+ return v
174
+ }
175
+
176
+ firstEntryOrUndefined(): [K, V] | undefined {
177
+ if (!this.maybeSortedKeys.length) return
178
+ const k = this.getSortedKeys()[0]!
179
+ return [k, this.map.get(k)!]
180
+ }
181
+
182
+ firstEntry(): [K, V] {
183
+ const e = this.firstEntryOrUndefined()
184
+ _assert(e, 'Map.firstEntry called on empty map')
185
+ return e
186
+ }
187
+
188
+ lastEntryOrUndefined(): [K, V] | undefined {
189
+ if (!this.maybeSortedKeys.length) return
190
+ const keys = this.getSortedKeys()
191
+ const k = keys[keys.length - 1]!
192
+ return [k, this.map.get(k)!]
193
+ }
194
+
195
+ lastEntry(): [K, V] {
196
+ const e = this.firstEntryOrUndefined()
197
+ _assert(e, 'Map.lastEntry called on empty map')
198
+ return e
199
+ }
200
+
201
+ toJSON(): Record<string, V> {
202
+ return this.toObject()
203
+ }
204
+
205
+ toObject(): Record<string, V> {
206
+ return Object.fromEntries(this.entries())
207
+ }
208
+
209
+ private getSortedKeys(): K[] {
210
+ if (!this.keysAreSorted) {
211
+ return this.sortKeys()
212
+ }
213
+ return this.maybeSortedKeys
214
+ }
215
+
216
+ private sortKeys(): K[] {
217
+ this.maybeSortedKeys.sort(this.#comparator)
218
+ this.keysAreSorted = true
219
+ return this.maybeSortedKeys
220
+ }
221
+ }
@@ -31,5 +31,9 @@ export class Map2<K = any, V = any> extends Map<K, V> {
31
31
  return Object.fromEntries(this)
32
32
  }
33
33
 
34
+ override toString(): string {
35
+ return `Map2(${this.size}) ${JSON.stringify(Object.fromEntries(this))}`
36
+ }
37
+
34
38
  // consider more helpful .toString() ?
35
39
  }
@@ -41,8 +41,8 @@ export class Set2<T = any> extends Set<T> {
41
41
  return [...this]
42
42
  }
43
43
 
44
- override get [Symbol.toStringTag](): string {
45
- return 'Set'
44
+ override toString(): string {
45
+ return `Set2(${this.size}) ${JSON.stringify([...this])}`
46
46
  }
47
47
 
48
48
  // todo: consider more helpful .toString() ?