recursive-set 5.0.2 → 6.0.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.
package/dist/esm/index.js CHANGED
@@ -1,30 +1,64 @@
1
1
  /**
2
2
  * @module recursive-set
3
3
  * High-Performance Recursive Set with "Freeze-on-Hash" semantics.
4
+ * Version: 6.0.0
4
5
  */
5
- // === HASHING ENGINE (FNV-1a) ===
6
+ // ============================================================================
7
+ // HASHING ENGINE (FNV-1a with DataView & Safe Integer Split)
8
+ // ============================================================================
6
9
  const FNV_PRIME = 16777619;
7
10
  const FNV_OFFSET = 2166136261;
11
+ // Shared buffer to avoid allocation overhead (reused for all number hashing)
12
+ const floatBuffer = new ArrayBuffer(8);
13
+ const view = new DataView(floatBuffer);
14
+ /**
15
+ * Hashes a number using FNV-1a.
16
+ * Handles both safe integers (via high/low split) and floats (via IEEE 754 bits).
17
+ * Ensures platform consistency by enforcing Little Endian byte order.
18
+ */
19
+ function hashNumber(val) {
20
+ // Integer Path: Handle Safe Integers correctly (up to 2^53)
21
+ if (Number.isSafeInteger(val)) {
22
+ let h = FNV_OFFSET;
23
+ const lowU = val >>> 0;
24
+ const high = ((val - lowU) / 4294967296) | 0;
25
+ h ^= lowU;
26
+ h = Math.imul(h, FNV_PRIME);
27
+ h ^= high;
28
+ h = Math.imul(h, FNV_PRIME);
29
+ return h >>> 0;
30
+ }
31
+ // Float Path: IEEE 754 Bit Pattern (Little Endian Enforced)
32
+ view.setFloat64(0, val, true);
33
+ let h = FNV_OFFSET;
34
+ const low = view.getInt32(0, true);
35
+ const high = view.getInt32(4, true);
36
+ h ^= low;
37
+ h = Math.imul(h, FNV_PRIME);
38
+ h ^= high;
39
+ h = Math.imul(h, FNV_PRIME);
40
+ return h >>> 0;
41
+ }
8
42
  function hashString(str) {
9
- let hash = FNV_OFFSET;
43
+ let h = FNV_OFFSET;
10
44
  const len = str.length;
11
45
  for (let i = 0; i < len; i++) {
12
- hash ^= str.charCodeAt(i);
13
- hash = Math.imul(hash, FNV_PRIME);
46
+ h ^= str.charCodeAt(i);
47
+ h = Math.imul(h, FNV_PRIME);
14
48
  }
15
- return hash >>> 0;
49
+ return h >>> 0;
16
50
  }
51
+ /**
52
+ * Computes a 32-bit hash code for a supported value.
53
+ * @param val The value to hash (number, string, Tuple, RecursiveSet, or Array)
54
+ */
17
55
  function hashValue(val) {
18
56
  if (typeof val === 'string')
19
57
  return hashString(val);
20
- if (typeof val === 'number') {
21
- let hash = FNV_OFFSET;
22
- hash ^= (val | 0);
23
- hash = Math.imul(hash, FNV_PRIME);
24
- return hash >>> 0;
25
- }
26
- // Fast Path: Objects with cached hash
27
- if (val && typeof val === 'object' && 'hashCode' in val) {
58
+ if (typeof val === 'number')
59
+ return hashNumber(val);
60
+ // Strict Fast-Path: Only trust our own Primitives
61
+ if (val instanceof RecursiveSet || val instanceof Tuple) {
28
62
  return val.hashCode;
29
63
  }
30
64
  if (Array.isArray(val)) {
@@ -38,92 +72,167 @@ function hashValue(val) {
38
72
  }
39
73
  return 0;
40
74
  }
41
- // === COMPARATOR ===
75
+ // ============================================================================
76
+ // STRICT COMPARATOR (Total Order)
77
+ // ============================================================================
78
+ /**
79
+ * Determines the sort priority of a type.
80
+ * Order: number (1) < string (2) < Sequence (3) < Set (4)
81
+ */
82
+ function getTypeScore(a) {
83
+ if (typeof a === 'number')
84
+ return 1;
85
+ if (typeof a === 'string')
86
+ return 2;
87
+ if (Array.isArray(a) || a instanceof Tuple)
88
+ return 3;
89
+ if (a instanceof RecursiveSet)
90
+ return 4;
91
+ return 0;
92
+ }
93
+ /**
94
+ * Compares two values for sorting.
95
+ * Implements a strict total order:
96
+ * 1. Semantic Type Score (Number < String < Seq < Set)
97
+ * 2. Hash Code (Short-Circuit)
98
+ * 3. Deep Structural Comparison
99
+ */
42
100
  function compare(a, b) {
43
101
  if (a === b)
44
102
  return 0;
45
- // 1. Hash Short-Circuit (Optimization)
46
- const aH = a?.hashCode;
47
- const bH = b?.hashCode;
48
- const ha = (aH !== undefined) ? aH : hashValue(a);
49
- const hb = (bH !== undefined) ? bH : hashValue(b);
50
- if (ha !== hb)
51
- return ha < hb ? -1 : 1;
52
- // 2. Structural Type Check
53
- const typeA = typeof a;
54
- const typeB = typeof b;
55
- if (typeA === 'string' && typeB === 'string')
103
+ // 1. Semantic Grouping
104
+ const scoreA = getTypeScore(a);
105
+ const scoreB = getTypeScore(b);
106
+ if (scoreA !== scoreB) {
107
+ return scoreA < scoreB ? -1 : 1;
108
+ }
109
+ // 2. PRIMITIVES: Value Sort (Human readable & Fast)
110
+ if (scoreA === 1)
56
111
  return a < b ? -1 : 1;
57
- if (typeA === 'number' && typeB === 'number')
112
+ if (scoreA === 2)
58
113
  return a < b ? -1 : 1;
59
- const isSetA = a instanceof RecursiveSet;
60
- const isSetB = b instanceof RecursiveSet;
61
- if (isSetA && isSetB) {
114
+ // 3. OBJECTS: Hash Optimization (Performance Protection)
115
+ const ha = hashValue(a);
116
+ const hb = hashValue(b);
117
+ if (ha !== hb)
118
+ return ha < hb ? -1 : 1;
119
+ // 3. Fallback / Structural Comparison
120
+ if (scoreA === 4) {
62
121
  return a.compare(b);
63
122
  }
64
- const isSeqA = Array.isArray(a) || a instanceof Tuple;
65
- const isSeqB = Array.isArray(b) || b instanceof Tuple;
66
- // Sort Order: Primitives (0) < Sequences (1) < Sets (2)
67
- if (isSetA !== isSetB || isSeqA !== isSeqB) {
68
- const scoreA = isSetA ? 2 : isSeqA ? 1 : 0;
69
- const scoreB = isSetB ? 2 : isSeqB ? 1 : 0;
70
- return scoreA - scoreB;
123
+ // Sequence Comparison (Arrays & Tuples)
124
+ const valA = (a instanceof Tuple) ? a.values : a;
125
+ const valB = (b instanceof Tuple) ? b.values : b;
126
+ const len = valA.length;
127
+ if (len !== valB.length)
128
+ return len - valB.length;
129
+ for (let i = 0; i < len; i++) {
130
+ const diff = compare(valA[i], valB[i]);
131
+ if (diff !== 0)
132
+ return diff;
71
133
  }
72
- // 3. Sequence Comparison
73
- if (isSeqA && isSeqB) {
74
- const valA = (a instanceof Tuple) ? a.values : a;
75
- const valB = (b instanceof Tuple) ? b.values : b;
76
- const len = valA.length;
77
- if (len !== valB.length)
78
- return len - valB.length;
79
- for (let i = 0; i < len; i++) {
80
- const diff = compare(valA[i], valB[i]);
81
- if (diff !== 0)
82
- return diff;
134
+ return 0;
135
+ }
136
+ // ============================================================================
137
+ // VALIDATION
138
+ // ============================================================================
139
+ /**
140
+ * Validates that an element is of a supported immutable-compatible type.
141
+ * Throws if the type is mutable (plain object) or unsupported (function, symbol).
142
+ * @throws {Error} if element type is invalid.
143
+ */
144
+ function validateType(element) {
145
+ if (typeof element === 'number') {
146
+ if (Number.isNaN(element))
147
+ throw new Error("NaN is not supported");
148
+ return;
149
+ }
150
+ if (typeof element === 'string')
151
+ return;
152
+ if (Array.isArray(element)) {
153
+ for (const item of element)
154
+ validateType(item);
155
+ return;
156
+ }
157
+ if (typeof element === 'object' && element !== null) {
158
+ if (element instanceof RecursiveSet || element instanceof Tuple) {
159
+ return;
83
160
  }
84
- return 0;
85
161
  }
86
- return a < b ? -1 : 1;
162
+ throw new Error("Unsupported Type: Use number, string, Tuple, Array or RecursiveSet.");
87
163
  }
88
- // === CLASSES ===
164
+ // ============================================================================
165
+ // CLASSES
166
+ // ============================================================================
167
+ /**
168
+ * Immutable Tuple container.
169
+ * * Guarantees structural immutability by freezing the internal array storage.
170
+ * Note: Immutability is shallow. If you store mutable Arrays inside a Tuple,
171
+ * the Tuple logic remains correct, but the content inside the Array might change.
172
+ * For strict value semantics, use Tuple<Primitive | RecursiveSet | Tuple>.
173
+ * * @template T The tuple type.
174
+ */
89
175
  export class Tuple {
90
- values;
176
+ #values;
91
177
  hashCode;
178
+ /**
179
+ * Creates a new Tuple.
180
+ * @param values Elements of the tuple.
181
+ */
92
182
  constructor(...values) {
93
- this.values = values;
94
- this.hashCode = hashValue(values);
183
+ for (const v of values)
184
+ validateType(v);
185
+ this.#values = [...values];
186
+ Object.freeze(this.#values);
187
+ this.hashCode = hashValue(this.#values);
95
188
  }
96
- get length() { return this.values.length; }
97
- get(i) { return this.values[i]; }
98
- *[Symbol.iterator]() { yield* this.values; }
99
- toString() { return `(${this.values.join(', ')})`; }
189
+ /** Returns the read-only backing array. */
190
+ get values() { return this.#values; }
191
+ get length() { return this.#values.length; }
192
+ get(i) { return this.#values[i]; }
193
+ *[Symbol.iterator]() { yield* this.#values; }
194
+ toString() { return `(${this.#values.join(', ')})`; }
100
195
  [Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
101
196
  }
197
+ /**
198
+ * A mathematical Set with Value Semantics.
199
+ * * Features:
200
+ * - **Strict Typing:** Supports number, string, Tuple, Array, RecursiveSet.
201
+ * - **Sorted Storage:** Elements are internally sorted for O(1) equality checks via hash.
202
+ * - **Freeze-on-Hash:** Once the hash code is accessed (e.g. when added to another Set),
203
+ * this Set becomes immutable to prevent hash corruption.
204
+ * * @template T The type of elements in the set.
205
+ */
102
206
  export class RecursiveSet {
103
- _elements;
104
- _hashCode = null;
105
- _isFrozen = false;
207
+ #elements = [];
208
+ #hashCode = null;
209
+ #isFrozen = false;
210
+ /**
211
+ * Exposes the static compare function used internally.
212
+ */
106
213
  static compare(a, b) { return compare(a, b); }
214
+ /**
215
+ * Creates a new RecursiveSet.
216
+ * @param elements Initial elements (will be sorted and deduplicated).
217
+ */
107
218
  constructor(...elements) {
108
- if (elements.length === 0) {
109
- this._elements = [];
110
- this._hashCode = 0;
111
- }
112
- else {
113
- this._elements = elements;
114
- this._elements.sort(compare);
115
- this._unique();
219
+ if (elements.length > 0) {
220
+ for (const el of elements)
221
+ validateType(el);
222
+ this.#elements = elements;
223
+ this.#elements.sort(compare);
224
+ this.#unique();
116
225
  }
117
226
  }
118
- _checkFrozen(op) {
119
- if (this._isFrozen) {
227
+ #checkFrozen(op) {
228
+ if (this.#isFrozen) {
120
229
  throw new Error(`InvalidOperation: Cannot ${op} a frozen RecursiveSet.\n` +
121
230
  `This Set has been hashed or used in a collection (Value Semantics).\n` +
122
231
  `Use .mutableCopy() to create a modifiable copy.`);
123
232
  }
124
233
  }
125
- _unique() {
126
- const arr = this._elements;
234
+ #unique() {
235
+ const arr = this.#elements;
127
236
  const len = arr.length;
128
237
  if (len < 2)
129
238
  return;
@@ -136,22 +245,28 @@ export class RecursiveSet {
136
245
  arr.length = write;
137
246
  }
138
247
  /**
139
- * Calculates/Caches hash code and FREEZES the set.
248
+ * Calculates and caches the hash code.
249
+ * Freezes the set to ensure the hash remains valid.
140
250
  */
141
251
  get hashCode() {
142
- if (this._hashCode !== null)
143
- return this._hashCode;
252
+ if (this.#hashCode !== null)
253
+ return this.#hashCode;
144
254
  let h = 0;
145
- const arr = this._elements;
255
+ const arr = this.#elements;
146
256
  const len = arr.length;
147
257
  for (let i = 0; i < len; i++) {
148
- h = Math.imul(31, h) + hashValue(arr[i]);
258
+ // Wrap to 32-bit at each step for consistency
259
+ h = (Math.imul(31, h) + hashValue(arr[i])) | 0;
149
260
  }
150
- this._hashCode = h | 0;
151
- this._isFrozen = true;
152
- return this._hashCode;
261
+ this.#hashCode = h | 0;
262
+ this.#isFrozen = true;
263
+ return this.#hashCode;
153
264
  }
154
- get isFrozen() { return this._isFrozen; }
265
+ /** Returns true if the set is frozen (hashed). */
266
+ get isFrozen() { return this.#isFrozen; }
267
+ /**
268
+ * Compares this set with another for sorting.
269
+ */
155
270
  compare(other) {
156
271
  if (this === other)
157
272
  return 0;
@@ -159,8 +274,8 @@ export class RecursiveSet {
159
274
  const h2 = other.hashCode;
160
275
  if (h1 !== h2)
161
276
  return h1 < h2 ? -1 : 1;
162
- const arrA = this._elements;
163
- const arrB = other._elements;
277
+ const arrA = this.#elements;
278
+ const arrB = other.#elements;
164
279
  const len = arrA.length;
165
280
  if (len !== arrB.length)
166
281
  return len - arrB.length;
@@ -171,12 +286,15 @@ export class RecursiveSet {
171
286
  }
172
287
  return 0;
173
288
  }
174
- get size() { return this._elements.length; }
175
- isEmpty() { return this._elements.length === 0; }
289
+ get size() { return this.#elements.length; }
290
+ isEmpty() { return this.#elements.length === 0; }
291
+ /**
292
+ * Checks if the set contains the given element.
293
+ * Uses binary search for sets > 16 elements.
294
+ */
176
295
  has(element) {
177
- const arr = this._elements;
296
+ const arr = this.#elements;
178
297
  const len = arr.length;
179
- // Linear Scan (Prefetch-friendly for small sets)
180
298
  if (len < 16) {
181
299
  for (let i = 0; i < len; i++) {
182
300
  if (compare(arr[i], element) === 0)
@@ -184,7 +302,6 @@ export class RecursiveSet {
184
302
  }
185
303
  return false;
186
304
  }
187
- // Binary Search
188
305
  let low = 0, high = len - 1;
189
306
  while (low <= high) {
190
307
  const mid = (low + high) >>> 1;
@@ -198,25 +315,21 @@ export class RecursiveSet {
198
315
  }
199
316
  return false;
200
317
  }
318
+ /**
319
+ * Adds an element to the set.
320
+ * Throws if the set is frozen.
321
+ */
201
322
  add(element) {
202
- this._checkFrozen('add() to');
203
- // Validation (Inlined for Performance)
204
- if (typeof element === 'object' && element !== null) {
205
- if (!(element instanceof RecursiveSet || element instanceof Tuple || Array.isArray(element))) {
206
- throw new Error("Unsupported Type: Use Tuple, Array or RecursiveSet.");
207
- }
208
- }
209
- else if (Number.isNaN(element)) {
210
- throw new Error("NaN is not supported");
211
- }
212
- const arr = this._elements;
323
+ this.#checkFrozen('add() to');
324
+ validateType(element);
325
+ const arr = this.#elements;
213
326
  const len = arr.length;
214
- // Optimization: Append to end (common in construction)
327
+ // Optimization: Append to end (common pattern)
215
328
  if (len > 0) {
216
329
  const lastCmp = compare(arr[len - 1], element);
217
330
  if (lastCmp < 0) {
218
331
  arr.push(element);
219
- this._hashCode = null;
332
+ this.#hashCode = null;
220
333
  return this;
221
334
  }
222
335
  if (lastCmp === 0)
@@ -224,10 +337,10 @@ export class RecursiveSet {
224
337
  }
225
338
  else {
226
339
  arr.push(element);
227
- this._hashCode = null;
340
+ this.#hashCode = null;
228
341
  return this;
229
342
  }
230
- // Small Array Strategy
343
+ // Small Array: Linear Insert
231
344
  if (len < 16) {
232
345
  for (let i = 0; i < len; i++) {
233
346
  const cmp = compare(arr[i], element);
@@ -235,14 +348,14 @@ export class RecursiveSet {
235
348
  return this;
236
349
  if (cmp > 0) {
237
350
  arr.splice(i, 0, element);
238
- this._hashCode = null;
351
+ this.#hashCode = null;
239
352
  return this;
240
353
  }
241
354
  }
242
- arr.push(element); // Should be unreachable given append check, but safe fallback
355
+ arr.push(element);
243
356
  return this;
244
357
  }
245
- // Large Array Strategy
358
+ // Large Array: Binary Search Insert
246
359
  let low = 0, high = len - 1, idx = 0;
247
360
  while (low <= high) {
248
361
  const mid = (low + high) >>> 1;
@@ -259,18 +372,22 @@ export class RecursiveSet {
259
372
  }
260
373
  }
261
374
  arr.splice(idx, 0, element);
262
- this._hashCode = null;
375
+ this.#hashCode = null;
263
376
  return this;
264
377
  }
378
+ /**
379
+ * Removes an element from the set.
380
+ * Throws if the set is frozen.
381
+ */
265
382
  remove(element) {
266
- this._checkFrozen('remove() from');
267
- const arr = this._elements;
383
+ this.#checkFrozen('remove() from');
384
+ const arr = this.#elements;
268
385
  const len = arr.length;
269
386
  if (len < 16) {
270
387
  for (let i = 0; i < len; i++) {
271
388
  if (compare(arr[i], element) === 0) {
272
389
  arr.splice(i, 1);
273
- this._hashCode = null;
390
+ this.#hashCode = null;
274
391
  return this;
275
392
  }
276
393
  }
@@ -282,7 +399,7 @@ export class RecursiveSet {
282
399
  const cmp = compare(arr[mid], element);
283
400
  if (cmp === 0) {
284
401
  arr.splice(mid, 1);
285
- this._hashCode = null;
402
+ this.#hashCode = null;
286
403
  return this;
287
404
  }
288
405
  if (cmp < 0)
@@ -293,21 +410,25 @@ export class RecursiveSet {
293
410
  return this;
294
411
  }
295
412
  clear() {
296
- this._checkFrozen('clear()');
297
- this._elements = [];
298
- this._hashCode = 0;
413
+ this.#checkFrozen('clear()');
414
+ this.#elements = [];
415
+ this.#hashCode = 0;
299
416
  return this;
300
417
  }
418
+ /**
419
+ * Creates a mutable shallow copy of this set.
420
+ * Useful for modifying a set after it has been frozen.
421
+ */
301
422
  mutableCopy() {
302
423
  const s = new RecursiveSet();
303
- s._elements = this._elements.slice();
424
+ s.#elements = this.#elements.slice();
304
425
  return s;
305
426
  }
306
427
  clone() { return this.mutableCopy(); }
307
428
  union(other) {
308
429
  const s = new RecursiveSet();
309
- const arrA = this._elements;
310
- const arrB = other._elements;
430
+ const arrA = this.#elements;
431
+ const arrB = other.#elements;
311
432
  if (arrA.length === 0)
312
433
  return other.clone();
313
434
  if (arrB.length === 0)
@@ -330,13 +451,13 @@ export class RecursiveSet {
330
451
  res.push(arrA[i++]);
331
452
  while (j < lenB)
332
453
  res.push(arrB[j++]);
333
- s._elements = res;
454
+ s.#elements = res;
334
455
  return s;
335
456
  }
336
457
  intersection(other) {
337
458
  const s = new RecursiveSet();
338
- const arrA = this._elements;
339
- const arrB = other._elements;
459
+ const arrA = this.#elements;
460
+ const arrB = other.#elements;
340
461
  const res = [];
341
462
  let i = 0, j = 0;
342
463
  const lenA = arrA.length, lenB = arrB.length;
@@ -351,13 +472,13 @@ export class RecursiveSet {
351
472
  j++;
352
473
  }
353
474
  }
354
- s._elements = res;
475
+ s.#elements = res;
355
476
  return s;
356
477
  }
357
478
  difference(other) {
358
479
  const s = new RecursiveSet();
359
- const arrA = this._elements;
360
- const arrB = other._elements;
480
+ const arrA = this.#elements;
481
+ const arrB = other.#elements;
361
482
  const res = [];
362
483
  let i = 0, j = 0;
363
484
  const lenA = arrA.length, lenB = arrB.length;
@@ -374,13 +495,13 @@ export class RecursiveSet {
374
495
  }
375
496
  while (i < lenA)
376
497
  res.push(arrA[i++]);
377
- s._elements = res;
498
+ s.#elements = res;
378
499
  return s;
379
500
  }
380
501
  symmetricDifference(other) {
381
502
  const s = new RecursiveSet();
382
- const arrA = this._elements;
383
- const arrB = other._elements;
503
+ const arrA = this.#elements;
504
+ const arrB = other.#elements;
384
505
  const res = [];
385
506
  let i = 0, j = 0;
386
507
  const lenA = arrA.length, lenB = arrB.length;
@@ -399,7 +520,7 @@ export class RecursiveSet {
399
520
  res.push(arrA[i++]);
400
521
  while (j < lenB)
401
522
  res.push(arrB[j++]);
402
- s._elements = res;
523
+ s.#elements = res;
403
524
  return s;
404
525
  }
405
526
  powerset() {
@@ -412,7 +533,7 @@ export class RecursiveSet {
412
533
  const subset = new RecursiveSet();
413
534
  for (let j = 0; j < n; j++) {
414
535
  if (i & (1 << j))
415
- subset._elements.push(this._elements[j]);
536
+ subset.#elements.push(this.#elements[j]);
416
537
  }
417
538
  subsets.push(subset);
418
539
  }
@@ -420,20 +541,23 @@ export class RecursiveSet {
420
541
  }
421
542
  cartesianProduct(other) {
422
543
  const result = new RecursiveSet();
423
- const arrA = this._elements;
424
- const arrB = other._elements;
544
+ const arrA = this.#elements;
545
+ const arrB = other.#elements;
425
546
  for (const x of arrA) {
426
547
  for (const y of arrB) {
427
- result._elements.push(new Tuple(x, y));
548
+ result.#elements.push(new Tuple(x, y));
428
549
  }
429
550
  }
551
+ // Hash ordering is not monotonic, so we must resort
552
+ result.#elements.sort(compare);
553
+ result.#unique();
430
554
  return result;
431
555
  }
432
556
  isSubset(other) {
433
557
  if (this.size > other.size)
434
558
  return false;
435
559
  let i = 0, j = 0;
436
- const arrA = this._elements, arrB = other._elements;
560
+ const arrA = this.#elements, arrB = other.#elements;
437
561
  while (i < arrA.length && j < arrB.length) {
438
562
  const cmp = compare(arrA[i], arrB[j]);
439
563
  if (cmp < 0)
@@ -450,12 +574,12 @@ export class RecursiveSet {
450
574
  isSuperset(other) { return other.isSubset(this); }
451
575
  isProperSubset(other) { return this.isSubset(other) && this.size < other.size; }
452
576
  equals(other) { return this.compare(other) === 0; }
453
- toSet() { return new Set(this._elements); }
454
- *[Symbol.iterator]() { yield* this._elements; }
577
+ toSet() { return new Set(this.#elements); }
578
+ *[Symbol.iterator]() { yield* this.#elements; }
455
579
  toString() {
456
580
  if (this.isEmpty())
457
581
  return "∅";
458
- const elementsStr = this._elements.map(el => {
582
+ const elementsStr = this.#elements.map(el => {
459
583
  if (Array.isArray(el))
460
584
  return `[${el.join(', ')}]`;
461
585
  return String(el);
@@ -464,6 +588,9 @@ export class RecursiveSet {
464
588
  }
465
589
  [Symbol.for('nodejs.util.inspect.custom')]() { return this.toString(); }
466
590
  }
591
+ // ============================================================================
592
+ // EXPORTS
593
+ // ============================================================================
467
594
  export function emptySet() { return new RecursiveSet(); }
468
595
  export function singleton(element) { return new RecursiveSet(element); }
469
596
  export function fromIterable(iterable) { return new RecursiveSet(...iterable); }