tuple-ts 0.6.0 → 0.7.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/README.md CHANGED
@@ -1,26 +1,24 @@
1
1
  # tuple-ts
2
2
 
3
- **Immutable tuples, records and structs with value-based identity for JavaScript and TypeScript.**
3
+ **Immutable interned tuples and named tuples for JavaScript and TypeScript.**
4
4
 
5
- JavaScript normally compares arrays and objects by reference:
5
+ JavaScript normally compares arrays by reference:
6
6
 
7
7
  ```ts
8
- [1, 2] === [1, 2] // false
9
-
10
- ({ x: 1 }) === ({ x: 1 }) // false
8
+ [1, 2] === [1, 2]; // false
11
9
  ```
12
10
 
13
- `tuple-ts` gives you a different model:
11
+ `tuple-ts` gives tuples value-based identity through *interning*:
14
12
 
15
13
  ```ts
16
- tuple(1, 2) === tuple(1, 2) // true
14
+ import { tuple } from "tuple-ts";
17
15
 
18
- record({ x: 1 }) === record({ x: 1 }) // true
16
+ tuple(1, 2) === tuple(1, 2); // true
19
17
  ```
20
18
 
21
- Equal values are **interned to the same object**.
19
+ Equivalent tuples created through the same tuple cache are canonicalised to the same object. Values are frozen, so their contents cannot change after interning. This makes tuples useful as composite `Map` keys, `Set` members, memoization keys, and immutable values in functional TypeScript.
22
20
 
23
- That makes tuples, records and structs immutable, naturally usable as composite `Map` keys and `Set` values, and particularly pleasant to use in functional TypeScript.
21
+ `tuple-ts` also supports **named tuples** through `struct()`: values remain arrays with positional slots, while the declared labels are available as named properties.
24
22
 
25
23
  **Zero dependencies · Less than 1 KB gzipped**
26
24
 
@@ -32,7 +30,7 @@ npm install tuple-ts
32
30
 
33
31
  ## Tuples
34
32
 
35
- Create an immutable tuple with full TypeScript inference:
33
+ Create a tuple with narrow TypeScript inference:
36
34
 
37
35
  ```ts
38
36
  import { tuple } from "tuple-ts";
@@ -40,11 +38,12 @@ import { tuple } from "tuple-ts";
40
38
  const point = tuple(10, 20);
41
39
  // ^? readonly [10, 20]
42
40
 
43
- console.log(point[0]); // 10
44
- console.log(point[1]); // 20
41
+ point[0]; // 10
42
+ point[1]; // 20
43
+ Object.isFrozen(point); // true
45
44
  ```
46
45
 
47
- Calling `tuple` again with the same values gives you the **same object**:
46
+ Calling `tuple()` again with the same values returns the same canonical object:
48
47
 
49
48
  ```ts
50
49
  const a = tuple(10, 20);
@@ -54,14 +53,14 @@ a === b; // true
54
53
  Object.is(a, b); // true
55
54
  ```
56
55
 
57
- Different values give you different objects:
56
+ Different values or a different element order produce different tuples:
58
57
 
59
58
  ```ts
60
59
  tuple(10, 20) === tuple(10, 21); // false
61
60
  tuple(10, 20) === tuple(20, 10); // false
62
61
  ```
63
62
 
64
- Tuples are ordinary frozen arrays, so there is no special API to learn:
63
+ Tuples are ordinary frozen arrays:
65
64
 
66
65
  ```ts
67
66
  const t = tuple("hello", 42, true);
@@ -69,20 +68,48 @@ const t = tuple("hello", 42, true);
69
68
  t.length; // 3
70
69
  t[0]; // "hello"
71
70
  [...t]; // ["hello", 42, true]
71
+ Array.isArray(t); // true
72
72
  Object.isFrozen(t); // true
73
73
  ```
74
74
 
75
+ ### Constraining a tuple factory with `.narrow()`
76
+
77
+ Use `.narrow()` to make a reusable factory whose arguments must satisfy a tuple type.
78
+
79
+ Specify the type explicitly:
80
+
81
+ ```ts
82
+ const Point = tuple.narrow<[number, number]>();
83
+
84
+ const a = Point(10, 20);
85
+ const b = Point(10, 20);
86
+
87
+ a === b; // true
88
+ Point(10, "20"); // TypeScript error
89
+ ```
90
+
91
+ You can also refine an existing fixed-length tuple factory using an exemplar. The exemplar is used to infer the constraint, not as a default value:
92
+
93
+ ```ts
94
+ const Pair = tuple.narrow<[unknown, unknown]>();
95
+ const NumberPair = Pair.narrow([0, 0]);
96
+
97
+ NumberPair(10, 20); // OK
98
+ NumberPair(10, "20"); // TypeScript error
99
+ ```
100
+
101
+ In this example, the parent factory establishes the two-slot shape, and the exemplar narrows the element types to numbers. `.narrow()` is a TypeScript constraint; it does not create a new cache. Narrowed factories continue to use the shared tuple cache, so equal tuple values are interned together regardless of which narrowed factory constructs them.
102
+
75
103
  ## Why is this useful?
76
104
 
77
105
  ### Composite `Map` keys
78
106
 
79
- JavaScript doesn't have value-based arrays, so this doesn't work:
107
+ Without interning, separately constructed arrays don't work as value-based keys:
80
108
 
81
109
  ```ts
82
110
  const map = new Map();
83
111
 
84
112
  map.set([10, 20], "visited");
85
-
86
113
  map.get([10, 20]); // undefined
87
114
  ```
88
115
 
@@ -92,18 +119,13 @@ With `tuple-ts`:
92
119
  const map = new Map();
93
120
 
94
121
  map.set(tuple(10, 20), "visited");
95
-
96
122
  map.get(tuple(10, 20)); // "visited"
97
123
  ```
98
124
 
99
- The tuple constructed during the lookup is the same canonical object as the tuple used as the key.
100
-
101
- This is particularly useful for things like coordinates, memoization keys, graph nodes, dynamic-programming states, and other composite values.
125
+ The tuple used for lookup is the same canonical object as the tuple used as the key. This is useful for coordinates, memoization keys, graph nodes, dynamic-programming states, and other composite values.
102
126
 
103
127
  ### `Set` of values
104
128
 
105
- The same applies to `Set`:
106
-
107
129
  ```ts
108
130
  const visited = new Set();
109
131
 
@@ -117,25 +139,16 @@ No serialization, stringification, or custom hashing is required.
117
139
 
118
140
  ## Nested tuples
119
141
 
120
- Tuples can be nested:
142
+ Tuples can contain other interned tuples:
121
143
 
122
144
  ```ts
123
- const a = tuple(
124
- "matrix",
125
- tuple(1, 0),
126
- tuple(0, 1),
127
- );
128
-
129
- const b = tuple(
130
- "matrix",
131
- tuple(1, 0),
132
- tuple(0, 1),
133
- );
145
+ const a = tuple("matrix", tuple(1, 0), tuple(0, 1));
146
+ const b = tuple("matrix", tuple(1, 0), tuple(0, 1));
134
147
 
135
148
  a === b; // true
136
149
  ```
137
150
 
138
- Because nested tuples themselves have value-based identity, the resulting structures are canonicalised naturally.
151
+ Because the inner tuples are themselves canonicalised, the outer tuples can be interned as well.
139
152
 
140
153
  ```ts
141
154
  const a = tuple("a", tuple("b", "c"));
@@ -146,461 +159,171 @@ a === b; // true
146
159
  a === c; // false
147
160
  ```
148
161
 
149
- ## Typed tuples
150
-
151
- Use `tuple.like()` to create a reusable constructor for a particular tuple type.
152
-
153
- ### Inferring the type from an exemplar
154
-
155
- ```ts
156
- const Point = tuple.like(0, 0);
157
-
158
- const a = Point(10, 20);
159
- const b = Point(10, 20);
160
-
161
- a === b; // true
162
- ```
163
-
164
- The exemplar determines the TypeScript type expected by the resulting factory. Its values are not used as defaults.
165
-
166
- By default, TypeScript widens the exemplar's literal types, so `tuple.like(0, 0)` produces a factory accepting two numbers.
167
-
168
- ### Explicit type arguments
162
+ ## Named tuples with `struct()`
169
163
 
170
- Alternatively, specify the type directly:
164
+ Use `struct()` when you want positional tuple storage with meaningful labels. Pass a readonly label list to create a reusable factory:
171
165
 
172
166
  ```ts
173
- type Point = [x: number, y: number];
174
-
175
- const Point = tuple.like<Point>();
176
-
177
- const a = Point(10, 20);
178
- const b = Point(10, 20);
179
-
180
- a === b; // true
181
- ```
182
-
183
- No exemplar is required when the type argument is provided explicitly.
167
+ import { struct } from "tuple-ts";
184
168
 
185
- ### Preserving literal types
169
+ const Point = struct(["x", "y"]).narrow<[number, number]>();
186
170
 
187
- To preserve literal types rather than widen them, use `as const`:
171
+ const p = Point(10, 20);
188
172
 
189
- ```ts
190
- const Direction = tuple.like("north" as const, 1 as const);
173
+ p[0]; // 10
174
+ p[1]; // 20
175
+ p.x; // 10
176
+ p.y; // 20
191
177
 
192
- Direction("north", 1); // OK
193
- Direction("south", 1); // TypeScript error
178
+ Array.isArray(p); // true
179
+ Object.isFrozen(p); // true
194
180
  ```
195
181
 
196
- In all cases, the resulting tuples are frozen and interned using the same shared cache as ordinary tuples.
182
+ The label order corresponds to the tuple slot order: the first label names slot `0`, the second names slot `1`, and so on. The named properties are added to the array when it is interned. They are own, enumerable properties and are frozen along with the array.
197
183
 
198
- ## Records
184
+ ### Constructing from a named object with `.from()`
199
185
 
200
- `record` provides the same value-based identity semantics for objects:
186
+ `.from()` accepts an object with the declared labels and creates the corresponding positional tuple:
201
187
 
202
188
  ```ts
203
- import { record } from "tuple-ts";
189
+ const Point = struct(["x", "y"]).narrow<[number, number]>();
204
190
 
205
- const a = record({
206
- id: 1,
207
- role: "admin",
208
- });
209
-
210
- const b = record({
211
- role: "admin",
212
- id: 1,
213
- });
191
+ const a = Point.from({ x: 10, y: 20 });
192
+ const b = Point.from({ y: 20, x: 10 });
214
193
 
215
194
  a === b; // true
216
- ```
217
-
218
- Property order does not matter.
219
-
220
- Records are frozen:
221
-
222
- ```ts
223
- Object.isFrozen(a); // true
224
- ```
225
-
226
- And they work naturally as `Map` keys:
227
-
228
- ```ts
229
- const users = new Map();
230
195
 
231
- users.set(
232
- record({ id: 42, role: "admin" }),
233
- "Alice",
234
- );
235
-
236
- users.get(
237
- record({ role: "admin", id: 42 }),
238
- ); // "Alice"
196
+ a[0]; // 10
197
+ a.x; // 10
239
198
  ```
240
199
 
241
- ### Record identity
242
-
243
- A record's identity is determined by its own, enumerable, string-keyed properties and their values.
244
-
245
- Two records are equivalent if they contain exactly the same keys, with equivalent values, regardless of property order.
200
+ The values are read in the schema's label order, regardless of the input object's property order. Extra properties on the input object are ignored:
246
201
 
247
202
  ```ts
248
- record({ x: 1, y: 2 }) ===
249
- record({ y: 2, x: 1 }); // true
203
+ const p = Point.from({ x: 10, y: 20, altitude: 100 });
250
204
 
251
- record({ x: 1 }) ===
252
- record({ x: 1, y: 2 }); // false
205
+ p.x; // 10
206
+ p.y; // 20
253
207
  ```
254
208
 
255
- Symbol-keyed, inherited and non-enumerable properties are not copied into records.
209
+ The returned value contains the tuple slots and the schema's named properties, not the unrelated input properties.
256
210
 
257
- Numeric property keys are supported and follow JavaScript's normal conversion to string keys.
211
+ ### Constraining a struct factory
258
212
 
259
- ### Typed records
260
-
261
- Use `record.like()` to create a reusable typed record constructor:
213
+ Use `.narrow()` to specify the tuple's positional types:
262
214
 
263
215
  ```ts
264
- interface User {
265
- id: string;
266
- name: string;
267
- updatedAt: number;
268
- }
269
-
270
- const User = record.like<User>();
271
-
272
- const a = User({
273
- id: "alice",
274
- name: "Alice",
275
- updatedAt: 1000,
276
- });
277
-
278
- const b = User({
279
- name: "Alice",
280
- id: "alice",
281
- updatedAt: 1000,
282
- });
283
-
284
- a === b; // true
285
- ```
286
-
287
- Alternatively, infer the type from an exemplar:
216
+ const Point = struct(["x", "y"]).narrow<[number, number]>();
288
217
 
289
- ```ts
290
- const User = record.like({
291
- id: "",
292
- name: "",
293
- updatedAt: 0,
294
- });
218
+ Point(10, 20); // OK
219
+ Point(10, "20"); // TypeScript error
295
220
  ```
296
221
 
297
- As with `tuple.like()`, the exemplar defines the type; it does not supply default values.
298
-
299
- The resulting factory requires the properties established by `like()`, but may also accept additional properties.
300
-
301
- Unlike `struct`, `record` preserves those additional properties, and they contribute to identity.
222
+ Use `.narrowFrom()` when you want to express the value types by label instead:
302
223
 
303
224
  ```ts
304
- const User = record.like({ id: "" });
225
+ type Coordinates = { x: number; y: number };
305
226
 
306
- const a = User({ id: "alice" });
307
- const b = User({ id: "alice", role: "admin" });
227
+ const Point = struct(["x", "y"]).narrowFrom<Coordinates>();
308
228
 
309
- a === b; // false
229
+ Point(10, 20); // OK
230
+ Point(10, "20"); // TypeScript error
231
+ Point.from({ x: 10, y: 20 }); // OK
310
232
  ```
311
233
 
312
- ## Structs
313
-
314
- `record` interns whatever properties you give it. If you want a fixed set of keys to define identity, use `struct`.
315
-
316
- `struct` takes key names as arguments and returns a factory. The factory accepts any object with at least those keys, extracts **exactly the declared keys**, and interns the result.
234
+ You can also provide a typed exemplar to `.narrowFrom()`:
317
235
 
318
236
  ```ts
319
- import { struct } from "tuple-ts";
320
-
321
- const Point = struct("x", "y");
322
-
323
- const a = Point({ x: 1, y: 2 });
324
- const b = Point({ x: 1, y: 2 });
325
-
326
- a === b; // true
237
+ const coordinateExample: Coordinates = { x: 0, y: 0 };
238
+ const Point = struct(["x", "y"]).narrowFrom(coordinateExample);
327
239
  ```
328
240
 
329
- ### Additional properties are discarded
330
-
331
- Unlike `record`, a struct factory ignores properties that are not part of its declared structure:
332
-
333
- ```ts
334
- const Point = struct("x", "y");
241
+ The exemplar establishes types; its values are not used as defaults. If you pass an inline `const` exemplar whose properties are literal values, those literal types may be preserved. Give the exemplar a widened type, or use an explicit generic argument, when you want types such as `number` rather than the literal type `0`.
335
242
 
336
- const a = Point({ x: 1, y: 2 });
337
- const b = Point({ x: 1, y: 2, z: 3 });
243
+ Both `.narrow()` and `.narrowFrom()` preserve the cache associated with the original `struct()` schema. They refine the factory's TypeScript constraints rather than creating a new interning domain.
338
244
 
339
- a === b; // true
340
- ```
245
+ ### Struct schemas and identity
341
246
 
342
- The resulting objects contain only `x` and `y`:
247
+ Each call to `struct(labels)` creates a schema with its own intern cache. Factories narrowed from that schema share the cache, but separate calls to `struct()` create separate caches, even when given identical labels:
343
248
 
344
249
  ```ts
345
- Object.keys(b); // ["x", "y"]
250
+ const PointA = struct(["x", "y"]).narrow<[number, number]>();
251
+ const PointB = struct(["x", "y"]).narrow<[number, number]>();
346
252
 
347
- b.z; // TypeScript error
253
+ PointA(1, 2) === PointA(1, 2); // true
254
+ PointA(1, 2) === PointB(1, 2); // false
348
255
  ```
349
256
 
350
- This makes structs useful for projecting larger objects into canonical values with a precisely defined shape.
257
+ Struct identity is therefore scoped to the schema factory. Ordinary `tuple()` values use a separate, shared tuple cache and do not share identity with struct values, even when their indexed elements are equal.
351
258
 
352
- ```ts
353
- const Position = struct("x", "y");
259
+ ### Choosing labels
354
260
 
355
- const location = {
356
- x: 10,
357
- y: 20,
358
- altitude: 100,
359
- timestamp: Date.now(),
360
- };
361
-
362
- const position = Position(location);
363
- // { x: 10, y: 20 }
364
- ```
365
-
366
- The factory is deliberately permissive about additional input properties, but strict about which properties appear in its output.
367
-
368
- ### Typed structs
369
-
370
- Use `struct(...keys).like()` to constrain the types of the declared properties.
371
-
372
- The type can be inferred from an exemplar:
373
-
374
- ```ts
375
- const Point = struct("x", "y").like({
376
- x: 0,
377
- y: 0,
378
- });
379
-
380
- Point({ x: 10, y: 20 }); // OK
381
- Point({ x: "10", y: 20 }); // TypeScript error
382
- ```
383
-
384
- Or provided explicitly:
385
-
386
- ```ts
387
- type Coordinates = {
388
- x: number;
389
- y: number;
390
- };
391
-
392
- const Point = struct("x", "y").like<Coordinates>();
393
-
394
- Point({ x: 10, y: 20 }); // OK
395
- ```
396
-
397
- As with the other factories, `like()` defines types, not default values.
398
-
399
- ### Strict definitions, flexible construction
400
-
401
- There is an intentional distinction between defining a struct and constructing one.
402
-
403
- A `like()` exemplar must contain only the keys declared by `struct`:
404
-
405
- ```ts
406
- const Point = struct("x", "y");
407
-
408
- Point.like({
409
- x: 0,
410
- y: 0,
411
- z: 0, // TypeScript error: unexpected property
412
- });
413
- ```
414
-
415
- But the resulting factory accepts extra properties and discards them:
416
-
417
- ```ts
418
- const Point = struct("x", "y").like({
419
- x: 0,
420
- y: 0,
421
- });
422
-
423
- Point({ x: 10, y: 20, z: 30 }); // OK
424
- // { x: 10, y: 20 }
425
- ```
426
-
427
- This keeps struct definitions precise without making callers manually remove unrelated properties from their input objects.
261
+ Use distinct labels, and avoid labels that collide with array properties or indices. In particular, `"length"`, numeric-looking labels such as `"0"`, and method names such as `"map"` can conflict with normal array behaviour. The current implementation does not validate label names or duplicate labels, so callers should ensure labels are unique and suitable for named properties.
428
262
 
429
263
  ## Equality semantics
430
264
 
431
- Values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
265
+ Tuple elements are compared shallowly using `Object.is`:
432
266
 
433
267
  ```ts
434
268
  tuple(NaN) === tuple(NaN); // true
435
-
436
269
  tuple(+0) === tuple(-0); // false
437
270
  ```
438
271
 
439
- Object values retain JavaScript's normal identity semantics.
440
-
441
- The same object is the same value:
272
+ Object-valued elements retain JavaScript's normal reference identity:
442
273
 
443
274
  ```ts
444
275
  const value = { x: 1 };
445
-
446
276
  tuple(value) === tuple(value); // true
447
- ```
448
277
 
449
- Two separately created objects are different values:
450
-
451
- ```ts
452
278
  tuple({ x: 1 }) === tuple({ x: 1 }); // false
453
279
  ```
454
280
 
455
- This is **shallow equality**. `tuple-ts` does not recursively compare arbitrary JavaScript objects.
456
-
457
- Nested tuples, records and structs work particularly well because they are themselves canonical values:
281
+ `tuple-ts` does not recursively compare arbitrary JavaScript objects. Nested tuples work well because they are themselves interned:
458
282
 
459
283
  ```ts
460
- const a = record({
461
- position: tuple(10, 20),
462
- });
463
-
464
- const b = record({
465
- position: tuple(10, 20),
466
- });
284
+ const a = tuple(tuple(1, 2));
285
+ const b = tuple(tuple(1, 2));
467
286
 
468
287
  a === b; // true
469
288
  ```
470
289
 
471
- The outer records compare equal because the inner tuples have already been interned to the same object.
472
-
473
- All three constructors share this model of identity.
290
+ Struct caches use the same shallow slot-value comparison, but each struct schema has its own cache. Within a schema, the label properties are determined by the schema; the indexed slot values determine which canonical tuple is returned.
474
291
 
475
292
  ## Immutable by design
476
293
 
477
- Tuples, records and structs returned by `tuple-ts` are frozen:
478
-
479
- ```ts
480
- const point = tuple(10, 20);
481
- const user = record({ id: 1 });
482
- const position = struct("x", "y")({ x: 1, y: 2 });
483
-
484
- Object.isFrozen(point); // true
485
- Object.isFrozen(user); // true
486
- Object.isFrozen(position); // true
487
- ```
488
-
489
- This is important to the identity semantics: once a value has been interned, its own properties cannot subsequently change while it is being used as a `Map` key or `Set` member.
490
-
491
- Freezing is shallow. If an interned value contains a reference to an ordinary mutable object, that nested object is not automatically frozen.
492
-
493
- For example:
294
+ Tuples and structs returned by `tuple-ts` are frozen. Freezing is shallow, just like `Object.freeze()`:
494
295
 
495
296
  ```ts
496
297
  const details = { name: "Alice" };
497
- const user = record({ details });
298
+ const value = tuple(details);
498
299
 
499
- details.name = "Bob"; // Allowed
300
+ details.name = "Bob"; // Allowed: the nested object itself is not frozen
500
301
  ```
501
302
 
502
- The reference to `details` remains unchanged, so the interned record retains its identity.
503
-
504
- If you need immutable nested values, construct them using `tuple`, `record` or `struct` as well.
303
+ The tuple's reference to `details` is unchanged, so its interned identity remains the same. If you need immutable nested values, construct those values with `tuple()` or `struct()` too.
505
304
 
506
305
  ## Garbage-collection friendly
507
306
 
508
- Interning values creates a natural question: *doesn't the cache grow forever?*
509
-
510
- `tuple-ts` uses `WeakRef` and `FinalizationRegistry` internally. Cached values are weakly referenced, allowing the JavaScript garbage collector to reclaim values that are no longer referenced by the application.
511
-
512
- In other words, you don't have to manually maintain a global cache of every tuple, record or struct you've ever created.
513
-
514
- ## TypeScript first
515
-
516
- `tuple`, `record` and `struct` are designed to preserve TypeScript's inference.
517
-
518
- ### Literal inference
519
-
520
- By default, constructors preserve literal types:
521
-
522
- ```ts
523
- const t = tuple("hello", 42, true);
524
- // readonly ["hello", 42, true]
525
-
526
- const r = record({ x: 1, y: 2 });
527
- // readonly properties x: 1, y: 2
528
- ```
529
-
530
- ### Reusable typed factories
531
-
532
- Use `.like()` to define reusable constructors:
533
-
534
- ```ts
535
- const Pair = tuple.like("", 0);
536
-
537
- const User = record.like({
538
- id: "",
539
- name: "",
540
- });
541
-
542
- const Point = struct("x", "y").like({
543
- x: 0,
544
- y: 0,
545
- });
546
- ```
547
-
548
- Or specify types explicitly:
549
-
550
- ```ts
551
- const Pair = tuple.like<[string, number]>();
552
-
553
- const User = record.like<{
554
- id: string;
555
- name: string;
556
- }>();
557
-
558
- const Point = struct("x", "y").like<{
559
- x: number;
560
- y: number;
561
- }>();
562
- ```
563
-
564
- In either form, TypeScript checks arguments to the resulting factories.
565
-
566
- ### Readonly results
567
-
568
- All constructors return readonly types, reflecting the frozen runtime objects.
569
-
570
- The readonly typing is shallow, just like `Object.freeze()`.
307
+ Interning raises a natural question: *doesn't the cache grow forever?* The cache implementation uses weak references and finalization support so that values no longer referenced by the application can be reclaimed by the JavaScript garbage collector. Values are not intended to be kept alive forever simply because they were once constructed.
571
308
 
572
309
  ## When should I use it?
573
310
 
574
- `tuple-ts` is particularly useful when you want **small immutable values with stable identity**:
311
+ `tuple-ts` is useful when you want small immutable values with stable identity:
575
312
 
576
313
  - Composite `Map` keys
577
314
  - `Set` membership
578
315
  - Memoization keys
579
- - Graph/search states
316
+ - Graph and search states
580
317
  - Coordinates and other multi-dimensional values
581
318
  - Functional programming
582
319
  - Immutable application state
583
- - Canonical representations of frequently repeated values
320
+ - Named, slot-based values such as points or pairs
584
321
 
585
- Choose between the three constructors according to your data:
586
-
587
- | Constructor | Shape | Identity |
588
- |---|---|---|
589
- | `tuple` | Ordered sequence | Position and value of each element |
590
- | `record` | Arbitrary object properties | Complete set of keys and values |
591
- | `struct` | Fixed declared keys | Values of the declared keys only |
592
-
593
- If you just need an ordinary mutable array or object, use one. `tuple-ts` is for when you want the thing to behave more like a **value**.
322
+ Use `tuple()` for positional values and `struct()` when the same kind of value benefits from stable field labels.
594
323
 
595
324
  ## Design
596
325
 
597
- The implementation uses hashing to locate candidate values and then performs an exact shallow equality check before reusing an existing value. Hash collisions therefore do not determine equality.
598
-
599
- Property order is ignored for records and structs, but element order matters for tuples.
600
-
601
- The intern cache uses weak references, so canonical values do not need to be kept alive solely because they have previously been created.
602
-
603
- Constructed records and structs are copied before interning, so callers cannot alter an interned value by modifying the source object.
326
+ The intern cache uses hashing to find candidates, then checks exact shallow equality before reusing an existing value. Hash collisions therefore do not determine equality. Tuple element order matters. Struct label order defines the mapping between slots and names, and each schema has its own cache.
604
327
 
605
328
  There are no runtime dependencies.
606
329
 
package/dist/index.d.ts CHANGED
@@ -1,68 +1,33 @@
1
1
  import InternCache from "./intern-cache";
2
- type NoExtraKeys<T, K extends PropertyKey> = T & Record<Exclude<keyof T, K>, never>;
3
- type BaseKey = string | number;
4
2
  type BaseTuple = readonly unknown[];
5
- type BaseRecord<K extends BaseKey> = Record<K, unknown>;
6
- export declare const _tupleCache: InternCache<number, readonly unknown[]>;
7
- export declare const _recordCache: InternCache<number, Record<BaseKey, unknown>>;
8
- export declare const tuple: (<const T_ extends BaseTuple>(...t: T_) => Readonly<T_>) & {
9
- like: <T_ extends BaseTuple>(...exemplar: [] | T_) => (<const T__1 extends T_>(...t: T__1) => Readonly<T__1>) & {
10
- like: <T__1 extends T_>(...exemplar: [] | T__1) => (<const T__2 extends T__1>(...t: T__2) => Readonly<T__2>) & {
11
- like: <T__2 extends T__1>(...exemplar: [] | T__2) => (<const T__3 extends T__2>(...t: T__3) => Readonly<T__3>) & {
12
- like: <T__3 extends T__2>(...exemplar: [] | T__3) => (<const T__4 extends T__3>(...t: T__4) => Readonly<T__4>) & {
13
- like: <T__4 extends T__3>(...exemplar: [] | T__4) => (<const T__5 extends T__4>(...t: T__5) => Readonly<T__5>) & {
14
- like: <T__5 extends T__4>(...exemplar: [] | T__5) => (<const T__6 extends T__5>(...t: T__6) => Readonly<T__6>) & {
15
- like: <T__6 extends T__5>(...exemplar: [] | T__6) => (<const T__7 extends T__6>(...t: T__7) => Readonly<T__7>) & {
16
- like: <T__7 extends T__6>(...exemplar: [] | T__7) => (<const T__8 extends T__7>(...t: T__8) => Readonly<T__8>) & {
17
- like: <T__8 extends T__7>(...exemplar: [] | T__8) => (<const T__9 extends T__8>(...t: T__9) => Readonly<T__9>) & {
18
- like: <T__9 extends T__8>(...exemplar: [] | T__9) => (<const T__10 extends T__9>(...t: T__10) => Readonly<T__10>) & {
19
- like: <T__10 extends T__9>(...exemplar: [] | T__10) => (<const T__11 extends T__10>(...t: T__11) => Readonly<T__11>) & /*elided*/ any;
20
- };
21
- };
22
- };
23
- };
24
- };
25
- };
26
- };
27
- };
28
- };
29
- };
3
+ type BaseLabels = readonly string[];
4
+ type ObjectOf<T extends BaseTuple, L extends BaseLabels> = {
5
+ [I in Extract<keyof L, `${number}`> as L[I]]: T[I extends `${infer N extends number}` ? N : never];
30
6
  };
31
- export declare const record: (<const R_ extends BaseRecord<BaseKey>>(r: R_) => Readonly<Pick<R_, Extract<keyof R_, BaseKey>>>) & {
32
- like: <R_ extends BaseRecord<BaseKey>>(exemplar?: NoExtraKeys<R_, BaseKey> | undefined) => (<const R__1 extends R_>(r: R__1) => Readonly<Pick<R__1, Extract<keyof R__1, BaseKey>>>) & {
33
- like: <R__1 extends R_>(exemplar?: NoExtraKeys<R__1, BaseKey> | undefined) => (<const R__2 extends R__1>(r: R__2) => Readonly<Pick<R__2, Extract<keyof R__2, BaseKey>>>) & {
34
- like: <R__2 extends R__1>(exemplar?: NoExtraKeys<R__2, BaseKey> | undefined) => (<const R__3 extends R__2>(r: R__3) => Readonly<Pick<R__3, Extract<keyof R__3, BaseKey>>>) & {
35
- like: <R__3 extends R__2>(exemplar?: NoExtraKeys<R__3, BaseKey> | undefined) => (<const R__4 extends R__3>(r: R__4) => Readonly<Pick<R__4, Extract<keyof R__4, BaseKey>>>) & {
36
- like: <R__4 extends R__3>(exemplar?: NoExtraKeys<R__4, BaseKey> | undefined) => (<const R__5 extends R__4>(r: R__5) => Readonly<Pick<R__5, Extract<keyof R__5, BaseKey>>>) & {
37
- like: <R__5 extends R__4>(exemplar?: NoExtraKeys<R__5, BaseKey> | undefined) => (<const R__6 extends R__5>(r: R__6) => Readonly<Pick<R__6, Extract<keyof R__6, BaseKey>>>) & {
38
- like: <R__6 extends R__5>(exemplar?: NoExtraKeys<R__6, BaseKey> | undefined) => (<const R__7 extends R__6>(r: R__7) => Readonly<Pick<R__7, Extract<keyof R__7, BaseKey>>>) & {
39
- like: <R__7 extends R__6>(exemplar?: NoExtraKeys<R__7, BaseKey> | undefined) => (<const R__8 extends R__7>(r: R__8) => Readonly<Pick<R__8, Extract<keyof R__8, BaseKey>>>) & {
40
- like: <R__8 extends R__7>(exemplar?: NoExtraKeys<R__8, BaseKey> | undefined) => (<const R__9 extends R__8>(r: R__9) => Readonly<Pick<R__9, Extract<keyof R__9, BaseKey>>>) & {
41
- like: <R__9 extends R__8>(exemplar?: NoExtraKeys<R__9, BaseKey> | undefined) => (<const R__10 extends R__9>(r: R__10) => Readonly<Pick<R__10, Extract<keyof R__10, BaseKey>>>) & {
42
- like: <R__10 extends R__9>(exemplar?: NoExtraKeys<R__10, BaseKey> | undefined) => (<const R__11 extends R__10>(r: R__11) => Readonly<Pick<R__11, Extract<keyof R__11, BaseKey>>>) & /*elided*/ any;
43
- };
44
- };
45
- };
46
- };
47
- };
48
- };
49
- };
50
- };
51
- };
52
- };
7
+ type StructOf<T extends BaseTuple, L extends BaseLabels> = Readonly<T & ObjectOf<T, L>>;
8
+ type TupleFromObject<T extends BaseTuple, L extends BaseLabels, O extends ObjectOf<T, L>> = T & {
9
+ [I in keyof L]: O[L[I]];
10
+ };
11
+ type StructFactory<T extends BaseTuple, L extends BaseLabels> = (<const T_ extends T>(...args: T_) => StructOf<T_, L>) & {
12
+ from: <const O extends ObjectOf<T, L>>(arg: O) => Readonly<O & TupleFromObject<T, L, O>>;
13
+ narrow: <const T_ extends T>() => StructFactory<T_, L>;
14
+ narrowFrom: <const O extends ObjectOf<T, L>>(..._args: [exemplar: O] | []) => StructFactory<{
15
+ [I in keyof L]: O[L[I]];
16
+ }, L>;
53
17
  };
54
- export declare const struct: <const K extends string>(...keys: readonly K[]) => (<const R_ extends BaseRecord<K>>(r: R_) => Readonly<Pick<R_, Extract<keyof R_, K>>>) & {
55
- like: <R_ extends BaseRecord<K>>(exemplar?: NoExtraKeys<R_, K> | undefined) => (<const R__1 extends R_>(r: R__1) => Readonly<Pick<R__1, Extract<keyof R__1, K>>>) & {
56
- like: <R__1 extends R_>(exemplar?: NoExtraKeys<R__1, K> | undefined) => (<const R__2 extends R__1>(r: R__2) => Readonly<Pick<R__2, Extract<keyof R__2, K>>>) & {
57
- like: <R__2 extends R__1>(exemplar?: NoExtraKeys<R__2, K> | undefined) => (<const R__3 extends R__2>(r: R__3) => Readonly<Pick<R__3, Extract<keyof R__3, K>>>) & {
58
- like: <R__3 extends R__2>(exemplar?: NoExtraKeys<R__3, K> | undefined) => (<const R__4 extends R__3>(r: R__4) => Readonly<Pick<R__4, Extract<keyof R__4, K>>>) & {
59
- like: <R__4 extends R__3>(exemplar?: NoExtraKeys<R__4, K> | undefined) => (<const R__5 extends R__4>(r: R__5) => Readonly<Pick<R__5, Extract<keyof R__5, K>>>) & {
60
- like: <R__5 extends R__4>(exemplar?: NoExtraKeys<R__5, K> | undefined) => (<const R__6 extends R__5>(r: R__6) => Readonly<Pick<R__6, Extract<keyof R__6, K>>>) & {
61
- like: <R__6 extends R__5>(exemplar?: NoExtraKeys<R__6, K> | undefined) => (<const R__7 extends R__6>(r: R__7) => Readonly<Pick<R__7, Extract<keyof R__7, K>>>) & {
62
- like: <R__7 extends R__6>(exemplar?: NoExtraKeys<R__7, K> | undefined) => (<const R__8 extends R__7>(r: R__8) => Readonly<Pick<R__8, Extract<keyof R__8, K>>>) & {
63
- like: <R__8 extends R__7>(exemplar?: NoExtraKeys<R__8, K> | undefined) => (<const R__9 extends R__8>(r: R__9) => Readonly<Pick<R__9, Extract<keyof R__9, K>>>) & {
64
- like: <R__9 extends R__8>(exemplar?: NoExtraKeys<R__9, K> | undefined) => (<const R__10 extends R__9>(r: R__10) => Readonly<Pick<R__10, Extract<keyof R__10, K>>>) & {
65
- like: <R__10 extends R__9>(exemplar?: NoExtraKeys<R__10, K> | undefined) => (<const R__11 extends R__10>(r: R__11) => Readonly<Pick<R__11, Extract<keyof R__11, K>>>) & /*elided*/ any;
18
+ export declare const _tupleCache: InternCache<number, readonly unknown[], readonly unknown[]>;
19
+ export declare const tuple: (<const T_ extends BaseTuple>(...args: T_) => T_) & {
20
+ narrow: <T_ extends BaseTuple>(..._args: [] | [exemplar: T_]) => (<const T__1 extends T_>(...args: T__1) => T__1) & {
21
+ narrow: <T__1 extends T_>(..._args: [] | [exemplar: T__1]) => (<const T__2 extends T__1>(...args: T__2) => T__2) & {
22
+ narrow: <T__2 extends T__1>(..._args: [] | [exemplar: T__2]) => (<const T__3 extends T__2>(...args: T__3) => T__3) & {
23
+ narrow: <T__3 extends T__2>(..._args: [] | [exemplar: T__3]) => (<const T__4 extends T__3>(...args: T__4) => T__4) & {
24
+ narrow: <T__4 extends T__3>(..._args: [] | [exemplar: T__4]) => (<const T__5 extends T__4>(...args: T__5) => T__5) & {
25
+ narrow: <T__5 extends T__4>(..._args: [] | [exemplar: T__5]) => (<const T__6 extends T__5>(...args: T__6) => T__6) & {
26
+ narrow: <T__6 extends T__5>(..._args: [] | [exemplar: T__6]) => (<const T__7 extends T__6>(...args: T__7) => T__7) & {
27
+ narrow: <T__7 extends T__6>(..._args: [] | [exemplar: T__7]) => (<const T__8 extends T__7>(...args: T__8) => T__8) & {
28
+ narrow: <T__8 extends T__7>(..._args: [] | [exemplar: T__8]) => (<const T__9 extends T__8>(...args: T__9) => T__9) & {
29
+ narrow: <T__9 extends T__8>(..._args: [] | [exemplar: T__9]) => (<const T__10 extends T__9>(...args: T__10) => T__10) & {
30
+ narrow: <T__10 extends T__9>(..._args: [] | [exemplar: T__10]) => (<const T__11 extends T__10>(...args: T__11) => T__11) & /*elided*/ any;
66
31
  };
67
32
  };
68
33
  };
@@ -74,5 +39,6 @@ export declare const struct: <const K extends string>(...keys: readonly K[]) =>
74
39
  };
75
40
  };
76
41
  };
42
+ export declare const struct: <const L extends readonly string[]>(labels: L) => StructFactory<BaseTuple, L>;
77
43
  export {};
78
44
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AAGzC,KAAK,WAAW,CAAC,CAAC,EAAE,CAAC,SAAS,WAAW,IAAI,CAAC,GAC5C,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAErC,KAAK,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC/B,KAAK,SAAS,GAAG,SAAS,OAAO,EAAE,CAAC;AACpC,KAAK,UAAU,CAAC,CAAC,SAAS,OAAO,IAAI,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AA+CxD,eAAO,MAAM,WAAW,yCAAoD,CAAC;AAE7E,eAAO,MAAM,YAAY,+CAQxB,CAAC;AAEF,eAAO,MAAM,KAAK,UAhCP,EAAE;WAEA,EAAE;;;;;;;;;;;;;;;;;;;;;CA8BmB,CAAC;AAEnC,eAAO,MAAM,MAAM,UAxBR,EAAE;WAKA,EAAE;;;;;;;;;;;;;;;;;;;;;CAmBqB,CAAC;AAErC,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,EAAE,aA1BzD,EAAE;WAKA,EAAE;;;;;;;;;;;;;;;;;;;;;CAsBK,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AAGzC,KAAK,SAAS,GAAG,SAAS,OAAO,EAAE,CAAC;AACpC,KAAK,UAAU,GAAG,SAAS,MAAM,EAAE,CAAC;AAqBpC,KAAK,QAAQ,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,IAAI;KACxD,CAAC,IAAI,OAAO,CACX,MAAM,CAAC,EACP,GAAG,MAAM,EAAE,CACZ,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,MAAM,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC;CACjE,CAAC;AACF,KAAK,QAAQ,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,IAAI,QAAQ,CACjE,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CACnB,CAAC;AACF,KAAK,eAAe,CAClB,CAAC,SAAS,SAAS,EACnB,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,IACtB,CAAC,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEpC,KAAK,aAAa,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,IAAI,CAAC,CAC/D,KAAK,CAAC,EAAE,SAAS,CAAC,EAElB,GAAG,IAAI,EAAE,EAAE,KACR,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG;IACtB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EACnC,GAAG,EAAE,CAAC,KACH,QAAQ,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,OAAO,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACvD,UAAU,EAAE,CACV,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAG9B,GAAG,KAAK,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,KACzB,aAAa,CAAC;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,EAAE,CAAC,CAAC,CAAC;CACpD,CAAC;AA4CF,eAAO,MAAM,WAAW,6DAIvB,CAAC;AAEF,eAAO,MAAM,KAAK,UAvFS,EAAE;aAEX,EAAE;;;;;;;;;;;;;;;;;;;;;CAqFyB,CAAC;AAE9C,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,EAAE,QAAQ,CAAC,gCAelE,CAAC"}
package/dist/index.js CHANGED
@@ -9,23 +9,36 @@ function arrayEquivalent(a1, a2) {
9
9
  }
10
10
  return true;
11
11
  }
12
- function recordEquivalent(r1, r2) {
13
- const r1Keys = Object.keys(r1);
14
- const r2Keys = Object.keys(r2);
15
- if (r2Keys.length !== r1Keys.length)
16
- return false;
17
- return r1Keys.every(k => Object.hasOwn(r2, k) && Object.is(r1[k], r2[k]));
18
- }
19
- const defineTuple = () => Object.assign((...t) => _tupleCache.get(t), {
20
- like: (...exemplar) => defineTuple(),
21
- });
22
- const defineRecord = (keys) => Object.assign((r) => _recordCache.get(Object.fromEntries(keys ? keys.map((k) => [k, r[k]]) : Object.entries(r))), {
23
- like: (exemplar) => defineRecord(keys),
24
- });
25
- export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent);
26
- export const _recordCache = new InternCache((r) => hash.valuesHash(Object.keys(r)
27
- .sort()
28
- .flatMap((k) => [k, r[k]])), recordEquivalent);
12
+ const defineTuple = () => {
13
+ const construct = (...args) => _tupleCache.get(args);
14
+ const narrow = (..._args) => defineTuple();
15
+ return Object.assign(construct, { narrow });
16
+ };
17
+ const defineStruct = (cache, labels) => {
18
+ const construct = (...args) => {
19
+ return cache.get(args);
20
+ };
21
+ const fromTuple = (...args) => {
22
+ return construct(...args);
23
+ };
24
+ const fromObject = (arg) => {
25
+ const args = labels.map((label) => arg[label]);
26
+ return construct(...args);
27
+ };
28
+ const narrow = (..._args) => defineStruct(cache, labels);
29
+ const narrowFrom = (..._args) => defineStruct(cache, labels);
30
+ return Object.assign(fromTuple, { from: fromObject, narrow, narrowFrom });
31
+ };
32
+ export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent, Object.freeze);
29
33
  export const tuple = defineTuple();
30
- export const record = defineRecord();
31
- export const struct = (...keys) => defineRecord(keys);
34
+ export const struct = (labels) => {
35
+ const cacheInjestFn = (tuple) => {
36
+ const labeledTuple = tuple;
37
+ labels.forEach((label, index) => {
38
+ labeledTuple[label] = tuple[index];
39
+ });
40
+ return Object.freeze(labeledTuple);
41
+ };
42
+ const cache = new InternCache(hash.valuesHash, arrayEquivalent, cacheInjestFn);
43
+ return defineStruct(cache, labels);
44
+ };
@@ -1,11 +1,12 @@
1
- export default class<H, T extends object> {
1
+ export default class<H, K, V extends object> {
2
2
  private readonly hashFn;
3
3
  private readonly matchFn;
4
+ private readonly injestFn;
4
5
  private readonly map;
5
- private cleanupRegistry;
6
- constructor(hashFn: (value: T) => H, matchFn: (a: T, b: T) => boolean);
6
+ private readonly cleanupRegistry;
7
+ constructor(hashFn: (key: K) => H, matchFn: (key: K, value: V) => boolean, injestFn: (key: K) => V);
7
8
  private remove;
8
- get<T_ extends T>(obj: T_): Readonly<T_>;
9
+ get<K_ extends K, V_ extends V>(key: K_): V_;
9
10
  get size(): number;
10
11
  reset(): void;
11
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"intern-cache.d.ts","sourceRoot":"","sources":["../src/intern-cache.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM;IAOpC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAP1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,eAAe,CAErB;gBAGiB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EACvB,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO;IAGnD,OAAO,CAAC,MAAM;IAWd,GAAG,CAAC,EAAE,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,QAAQ,CAAC,EAAE,CAAC;IAsBxC,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,KAAK,IAAI,IAAI;CAId"}
1
+ {"version":3,"file":"intern-cache.d.ts","sourceRoot":"","sources":["../src/intern-cache.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM;IAOvC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAR3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAE9B;gBAGiB,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EACrB,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,EACtC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC;IAG1C,OAAO,CAAC,MAAM;IAWd,GAAG,CAAC,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE;IAsB5C,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,KAAK,IAAI,IAAI;CAId"}
@@ -1,27 +1,28 @@
1
1
  export default class {
2
- constructor(hashFn, matchFn) {
2
+ constructor(hashFn, matchFn, injestFn) {
3
3
  this.hashFn = hashFn;
4
4
  this.matchFn = matchFn;
5
+ this.injestFn = injestFn;
5
6
  this.map = new Map();
6
7
  this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
7
8
  }
8
- remove([hash, ref]) {
9
+ remove([hash, valueRef]) {
9
10
  const hashBucket = this.map.get(hash);
10
11
  if (!hashBucket) {
11
12
  return;
12
13
  }
13
- hashBucket.delete(ref);
14
+ hashBucket.delete(valueRef);
14
15
  if (hashBucket.size === 0) {
15
16
  this.map.delete(hash);
16
17
  }
17
18
  }
18
- get(obj) {
19
- const hash = this.hashFn(obj);
19
+ get(key) {
20
+ const hash = this.hashFn(key);
20
21
  let hashBucket = this.map.get(hash);
21
22
  if (hashBucket !== undefined) {
22
23
  for (const ref of hashBucket) {
23
24
  const o = ref.deref();
24
- if (o && this.matchFn(obj, o))
25
+ if (o && this.matchFn(key, o))
25
26
  return o;
26
27
  }
27
28
  }
@@ -31,17 +32,17 @@ export default class {
31
32
  this.map.set(hash, hashBucket);
32
33
  }
33
34
  // Object is not in the cache. Add it and return it.
34
- Object.freeze(obj);
35
- const ref = new WeakRef(obj);
36
- hashBucket.add(ref);
37
- this.cleanupRegistry.register(obj, [hash, ref]);
38
- return obj;
35
+ const value = this.injestFn(key);
36
+ const valueRef = new WeakRef(value);
37
+ hashBucket.add(valueRef);
38
+ this.cleanupRegistry.register(value, [hash, valueRef], this);
39
+ return value;
39
40
  }
40
41
  get size() {
41
42
  return Array.from(this.map.entries(), ([_, b]) => b.size).reduce((acc, x) => acc + x, 0);
42
43
  }
43
44
  reset() {
45
+ this.cleanupRegistry.unregister(this);
44
46
  this.map.clear();
45
- this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
46
47
  }
47
48
  }
package/package.json CHANGED
@@ -1,11 +1,18 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "tuple-ts",
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "description": "Tuples and structs for TypeScript and JavaScript with equality and identity semantics.",
6
6
  "keywords": [
7
7
  "tuple",
8
8
  "struct",
9
+ "labeled",
10
+ "labelled",
11
+ "labels",
12
+ "names",
13
+ "namedtuple",
14
+ "named-tuple",
15
+ "python",
9
16
  "equality",
10
17
  "identity",
11
18
  "shallow-equality",