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 +115 -392
- package/dist/index.d.ts +27 -61
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -19
- package/dist/intern-cache.d.ts +5 -4
- package/dist/intern-cache.d.ts.map +1 -1
- package/dist/intern-cache.js +13 -12
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
# tuple-ts
|
|
2
2
|
|
|
3
|
-
**Immutable tuples
|
|
3
|
+
**Immutable interned tuples and named tuples for JavaScript and TypeScript.**
|
|
4
4
|
|
|
5
|
-
JavaScript normally compares arrays
|
|
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
|
|
11
|
+
`tuple-ts` gives tuples value-based identity through *interning*:
|
|
14
12
|
|
|
15
13
|
```ts
|
|
16
|
-
|
|
14
|
+
import { tuple } from "tuple-ts";
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
tuple(1, 2) === tuple(1, 2); // true
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
142
|
+
Tuples can contain other interned tuples:
|
|
121
143
|
|
|
122
144
|
```ts
|
|
123
|
-
const a = tuple(
|
|
124
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
+
const Point = struct(["x", "y"]).narrow<[number, number]>();
|
|
186
170
|
|
|
187
|
-
|
|
171
|
+
const p = Point(10, 20);
|
|
188
172
|
|
|
189
|
-
|
|
190
|
-
|
|
173
|
+
p[0]; // 10
|
|
174
|
+
p[1]; // 20
|
|
175
|
+
p.x; // 10
|
|
176
|
+
p.y; // 20
|
|
191
177
|
|
|
192
|
-
|
|
193
|
-
|
|
178
|
+
Array.isArray(p); // true
|
|
179
|
+
Object.isFrozen(p); // true
|
|
194
180
|
```
|
|
195
181
|
|
|
196
|
-
|
|
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
|
-
|
|
184
|
+
### Constructing from a named object with `.from()`
|
|
199
185
|
|
|
200
|
-
`
|
|
186
|
+
`.from()` accepts an object with the declared labels and creates the corresponding positional tuple:
|
|
201
187
|
|
|
202
188
|
```ts
|
|
203
|
-
|
|
189
|
+
const Point = struct(["x", "y"]).narrow<[number, number]>();
|
|
204
190
|
|
|
205
|
-
const a =
|
|
206
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
record({ y: 2, x: 1 }); // true
|
|
203
|
+
const p = Point.from({ x: 10, y: 20, altitude: 100 });
|
|
250
204
|
|
|
251
|
-
|
|
252
|
-
|
|
205
|
+
p.x; // 10
|
|
206
|
+
p.y; // 20
|
|
253
207
|
```
|
|
254
208
|
|
|
255
|
-
|
|
209
|
+
The returned value contains the tuple slots and the schema's named properties, not the unrelated input properties.
|
|
256
210
|
|
|
257
|
-
|
|
211
|
+
### Constraining a struct factory
|
|
258
212
|
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
id: "",
|
|
292
|
-
name: "",
|
|
293
|
-
updatedAt: 0,
|
|
294
|
-
});
|
|
218
|
+
Point(10, 20); // OK
|
|
219
|
+
Point(10, "20"); // TypeScript error
|
|
295
220
|
```
|
|
296
221
|
|
|
297
|
-
|
|
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
|
-
|
|
225
|
+
type Coordinates = { x: number; y: number };
|
|
305
226
|
|
|
306
|
-
const
|
|
307
|
-
const b = User({ id: "alice", role: "admin" });
|
|
227
|
+
const Point = struct(["x", "y"]).narrowFrom<Coordinates>();
|
|
308
228
|
|
|
309
|
-
|
|
229
|
+
Point(10, 20); // OK
|
|
230
|
+
Point(10, "20"); // TypeScript error
|
|
231
|
+
Point.from({ x: 10, y: 20 }); // OK
|
|
310
232
|
```
|
|
311
233
|
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
```
|
|
245
|
+
### Struct schemas and identity
|
|
341
246
|
|
|
342
|
-
|
|
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
|
-
|
|
250
|
+
const PointA = struct(["x", "y"]).narrow<[number, number]>();
|
|
251
|
+
const PointB = struct(["x", "y"]).narrow<[number, number]>();
|
|
346
252
|
|
|
347
|
-
|
|
253
|
+
PointA(1, 2) === PointA(1, 2); // true
|
|
254
|
+
PointA(1, 2) === PointB(1, 2); // false
|
|
348
255
|
```
|
|
349
256
|
|
|
350
|
-
|
|
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
|
-
|
|
353
|
-
const Position = struct("x", "y");
|
|
259
|
+
### Choosing labels
|
|
354
260
|
|
|
355
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
461
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
316
|
+
- Graph and search states
|
|
580
317
|
- Coordinates and other multi-dimensional values
|
|
581
318
|
- Functional programming
|
|
582
319
|
- Immutable application state
|
|
583
|
-
-
|
|
320
|
+
- Named, slot-based values such as points or pairs
|
|
584
321
|
|
|
585
|
-
|
|
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
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AAGzC,KAAK,
|
|
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
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
31
|
-
|
|
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
|
+
};
|
package/dist/intern-cache.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
export default class<H,
|
|
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: (
|
|
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<
|
|
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;
|
|
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"}
|
package/dist/intern-cache.js
CHANGED
|
@@ -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,
|
|
9
|
+
remove([hash, valueRef]) {
|
|
9
10
|
const hashBucket = this.map.get(hash);
|
|
10
11
|
if (!hashBucket) {
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
|
-
hashBucket.delete(
|
|
14
|
+
hashBucket.delete(valueRef);
|
|
14
15
|
if (hashBucket.size === 0) {
|
|
15
16
|
this.map.delete(hash);
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
get(
|
|
19
|
-
const hash = this.hashFn(
|
|
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(
|
|
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
|
-
|
|
35
|
-
const
|
|
36
|
-
hashBucket.add(
|
|
37
|
-
this.cleanupRegistry.register(
|
|
38
|
-
return
|
|
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.
|
|
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",
|