tuple-ts 0.3.1 → 0.4.1
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 +259 -85
- package/dist/hash.d.ts +1 -1
- package/dist/hash.js +6 -6
- package/dist/index.d.ts +3 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -6
- package/dist/record.spec.js +15 -9
- package/dist/struct.spec.d.ts +2 -0
- package/dist/struct.spec.d.ts.map +1 -0
- package/dist/struct.spec.js +35 -0
- package/dist/tuple.spec.js +23 -5
- package/package.json +6 -13
package/README.md
CHANGED
|
@@ -1,156 +1,330 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tuple-ts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Immutable tuples and structs with value-based identity for JavaScript and TypeScript.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **Composite Keys for `Set` & `Map`**: Use tuples or records as keys without custom serialization or hashing workarounds.
|
|
7
|
-
- **Memory-Safe**: Uses `WeakRef` and `FinalizationRegistry` to automatically purge entries upon garbage collection.
|
|
8
|
-
- **Deeply Nestable & Immutable**: Tuples can contain other tuples; all created tuples and records are `Object.freeze`'d.
|
|
9
|
-
- **TypeScript First**: Full type inference and type-safe factories.
|
|
10
|
-
- **Zero Dependencies**: Lightweight implementation with fast MurmurHash3-based hashing.
|
|
5
|
+
JavaScript normally compares arrays and objects by reference:
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
```ts
|
|
8
|
+
[1, 2] === [1, 2] // false
|
|
9
|
+
|
|
10
|
+
{ x: 1 } === { x: 1 } // false
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`tuple-ts` gives you a different model:
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
```ts
|
|
16
|
+
tuple(1, 2) === tuple(1, 2) // true
|
|
17
|
+
|
|
18
|
+
struct({ x: 1 }) === struct({ x: 1 }) // true
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Equal values are **interned to the same object**.
|
|
22
|
+
|
|
23
|
+
That makes tuples and structs immutable, naturally usable as composite `Map` keys and `Set` values, and particularly pleasant to use in functional TypeScript.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
15
26
|
|
|
16
27
|
```bash
|
|
17
28
|
npm install tuple-ts
|
|
18
29
|
```
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
## Tuples
|
|
21
32
|
|
|
22
|
-
|
|
33
|
+
Create an immutable tuple with full TypeScript inference:
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
```ts
|
|
36
|
+
import { tuple } from "tuple-ts";
|
|
25
37
|
|
|
26
|
-
|
|
38
|
+
const point = tuple(10, 20);
|
|
39
|
+
// ^? readonly [10, 20]
|
|
40
|
+
|
|
41
|
+
console.log(point[0]); // 10
|
|
42
|
+
console.log(point[1]); // 20
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Calling `tuple` again with the same values gives you the **same object**:
|
|
27
46
|
|
|
28
47
|
```ts
|
|
29
|
-
|
|
48
|
+
const a = tuple(10, 20);
|
|
49
|
+
const b = tuple(10, 20);
|
|
50
|
+
|
|
51
|
+
a === b; // true
|
|
52
|
+
Object.is(a, b); // true
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Different values give you different objects:
|
|
30
56
|
|
|
31
|
-
|
|
32
|
-
|
|
57
|
+
```ts
|
|
58
|
+
tuple(10, 20) === tuple(10, 21); // false
|
|
59
|
+
tuple(10, 20) === tuple(20, 10); // false
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Tuples are ordinary frozen arrays, so there is no special API to learn:
|
|
33
63
|
|
|
34
|
-
|
|
64
|
+
```ts
|
|
65
|
+
const t = tuple("hello", 42, true);
|
|
35
66
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
67
|
+
t.length; // 3
|
|
68
|
+
t[0]; // "hello"
|
|
69
|
+
[...t]; // ["hello", 42, true]
|
|
70
|
+
Object.isFrozen(t); // true
|
|
40
71
|
```
|
|
41
72
|
|
|
42
|
-
|
|
73
|
+
## Why is this useful?
|
|
74
|
+
|
|
75
|
+
### Composite `Map` keys
|
|
43
76
|
|
|
44
|
-
|
|
77
|
+
JavaScript doesn't have value-based arrays, so this doesn't work:
|
|
45
78
|
|
|
46
79
|
```ts
|
|
47
|
-
|
|
80
|
+
const map = new Map();
|
|
48
81
|
|
|
49
|
-
|
|
50
|
-
const t2 = tuple("matrix", tuple(1, 0), tuple(0, 1));
|
|
51
|
-
const t3 = tuple("matrix", tuple(1, 1), tuple(0, 1));
|
|
82
|
+
map.set([10, 20], "visited");
|
|
52
83
|
|
|
53
|
-
|
|
54
|
-
console.log(t1 === t3); // false
|
|
84
|
+
map.get([10, 20]); // undefined
|
|
55
85
|
```
|
|
56
86
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
In standard JavaScript, arrays and objects compare by reference, preventing them from being used as composite keys in `Map` or values in `Set`. With `tuple`, structural equality makes composite keys work seamlessly:
|
|
87
|
+
With `tuple-ts`:
|
|
60
88
|
|
|
61
89
|
```ts
|
|
62
|
-
|
|
90
|
+
const map = new Map();
|
|
91
|
+
|
|
92
|
+
map.set(tuple(10, 20), "visited");
|
|
93
|
+
|
|
94
|
+
map.get(tuple(10, 20)); // "visited"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The tuple constructed during the lookup is the same canonical object as the tuple used as the key.
|
|
98
|
+
|
|
99
|
+
This is particularly useful for things like coordinates, memoization keys, graph nodes, dynamic-programming states, and other composite values.
|
|
63
100
|
|
|
64
|
-
|
|
101
|
+
### `Set` of values
|
|
102
|
+
|
|
103
|
+
The same applies to `Set`:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
65
106
|
const visited = new Set();
|
|
107
|
+
|
|
66
108
|
visited.add(tuple(10, 20));
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
|
|
110
|
+
visited.has(tuple(10, 20)); // true
|
|
111
|
+
visited.has(tuple(10, 21)); // false
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
No serialization, stringification, or custom hashing is required.
|
|
70
115
|
|
|
71
|
-
|
|
72
|
-
const grid = new Map();
|
|
73
|
-
grid.set(tuple(0, 0), "Origin");
|
|
74
|
-
grid.set(tuple(3, 4), "Target");
|
|
116
|
+
## Nested tuples
|
|
75
117
|
|
|
76
|
-
|
|
77
|
-
|
|
118
|
+
Tuples can be nested:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const a = tuple(
|
|
122
|
+
"matrix",
|
|
123
|
+
tuple(1, 0),
|
|
124
|
+
tuple(0, 1),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const b = tuple(
|
|
128
|
+
"matrix",
|
|
129
|
+
tuple(1, 0),
|
|
130
|
+
tuple(0, 1),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
a === b; // true
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Because nested tuples themselves have value-based identity, the resulting structures are canonicalised naturally.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const a = tuple("a", tuple("b", "c"));
|
|
140
|
+
const b = tuple("a", tuple("b", "c"));
|
|
141
|
+
const c = tuple("a", tuple("b", "d"));
|
|
142
|
+
|
|
143
|
+
a === b; // true
|
|
144
|
+
a === c; // false
|
|
78
145
|
```
|
|
79
146
|
|
|
80
|
-
|
|
147
|
+
## Typed tuples
|
|
81
148
|
|
|
82
|
-
You can create
|
|
149
|
+
You can create a reusable constructor for a particular tuple type:
|
|
83
150
|
|
|
84
151
|
```ts
|
|
85
|
-
|
|
152
|
+
type Point = [x: number, y: number];
|
|
86
153
|
|
|
87
|
-
|
|
88
|
-
const Point = defineTupleType<Point2D>();
|
|
154
|
+
const Point = tuple<Point>;
|
|
89
155
|
|
|
90
|
-
const
|
|
91
|
-
const
|
|
156
|
+
const a = Point(10, 20);
|
|
157
|
+
const b = Point(10, 20);
|
|
92
158
|
|
|
93
|
-
|
|
159
|
+
a === b; // true
|
|
94
160
|
```
|
|
95
161
|
|
|
96
|
-
|
|
162
|
+
The result retains the tuple's TypeScript type while gaining the same runtime identity semantics.
|
|
163
|
+
|
|
164
|
+
## Structs
|
|
97
165
|
|
|
98
|
-
|
|
166
|
+
`struct` provides the same semantics for objects:
|
|
99
167
|
|
|
100
168
|
```ts
|
|
101
|
-
import {
|
|
169
|
+
import { struct } from "tuple-ts";
|
|
170
|
+
|
|
171
|
+
const a = struct({
|
|
172
|
+
id: 1,
|
|
173
|
+
role: "admin",
|
|
174
|
+
});
|
|
102
175
|
|
|
103
|
-
const
|
|
104
|
-
|
|
176
|
+
const b = struct({
|
|
177
|
+
role: "admin",
|
|
178
|
+
id: 1,
|
|
179
|
+
});
|
|
105
180
|
|
|
106
|
-
|
|
107
|
-
console.log(userA.role); // "admin"
|
|
181
|
+
a === b; // true
|
|
108
182
|
```
|
|
109
183
|
|
|
110
|
-
|
|
184
|
+
Property order does not matter.
|
|
185
|
+
|
|
186
|
+
Structs are frozen:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
Object.isFrozen(a); // true
|
|
190
|
+
```
|
|
111
191
|
|
|
112
|
-
|
|
192
|
+
And they work naturally as `Map` keys:
|
|
113
193
|
|
|
114
194
|
```ts
|
|
115
|
-
|
|
195
|
+
const users = new Map();
|
|
196
|
+
|
|
197
|
+
users.set(
|
|
198
|
+
struct({ id: 42, role: "admin" }),
|
|
199
|
+
"Alice",
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
users.get(
|
|
203
|
+
struct({ role: "admin", id: 42 }),
|
|
204
|
+
); // "Alice"
|
|
205
|
+
```
|
|
116
206
|
|
|
117
|
-
|
|
207
|
+
### Typed structs
|
|
208
|
+
|
|
209
|
+
As with tuples, you can define a reusable typed constructor:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
interface User {
|
|
118
213
|
id: string;
|
|
119
214
|
name: string;
|
|
120
215
|
updatedAt: number;
|
|
121
216
|
}
|
|
122
217
|
|
|
123
|
-
const User =
|
|
218
|
+
const User = struct<User>;
|
|
124
219
|
|
|
125
|
-
const
|
|
126
|
-
|
|
220
|
+
const a = User({
|
|
221
|
+
id: "alice",
|
|
222
|
+
name: "Alice",
|
|
223
|
+
updatedAt: 1000,
|
|
224
|
+
});
|
|
127
225
|
|
|
128
|
-
|
|
129
|
-
|
|
226
|
+
const b = User({
|
|
227
|
+
name: "Alice",
|
|
228
|
+
id: "alice",
|
|
229
|
+
updatedAt: 1000,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
a === b; // true
|
|
130
233
|
```
|
|
131
234
|
|
|
132
|
-
|
|
235
|
+
## Equality semantics
|
|
133
236
|
|
|
134
|
-
|
|
237
|
+
Primitive values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
|
|
135
238
|
|
|
136
239
|
```ts
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
240
|
+
tuple(NaN) === tuple(NaN); // true
|
|
241
|
+
|
|
242
|
+
tuple(+0) === tuple(-0); // false
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Object values retain JavaScript's normal identity semantics.
|
|
246
|
+
|
|
247
|
+
The same object is the same value:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const value = { x: 1 };
|
|
251
|
+
|
|
252
|
+
tuple(value) === tuple(value); // true
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Two separately created objects are different values:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
tuple({ x: 1 }) === tuple({ x: 1 }); // false
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
This is **shallow equality**. `tuple-ts` does not recursively compare arbitrary JavaScript objects.
|
|
262
|
+
|
|
263
|
+
Nested tuples and structs work particularly well because they are themselves canonical values.
|
|
264
|
+
|
|
265
|
+
## Immutable by design
|
|
266
|
+
|
|
267
|
+
Tuples and structs returned by `tuple-ts` are frozen:
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
const point = tuple(10, 20);
|
|
271
|
+
|
|
272
|
+
Object.isFrozen(point); // true
|
|
142
273
|
```
|
|
143
274
|
|
|
144
|
-
|
|
275
|
+
This is important to the identity semantics: once a value has been interned, its contents cannot subsequently change while it is being used as a `Map` key or `Set` member.
|
|
276
|
+
|
|
277
|
+
## Garbage-collection friendly
|
|
278
|
+
|
|
279
|
+
Interning values creates a natural question: *doesn't the cache grow forever?*
|
|
280
|
+
|
|
281
|
+
`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.
|
|
282
|
+
|
|
283
|
+
In other words, you don't have to manually maintain a global cache of every tuple you've ever created.
|
|
284
|
+
|
|
285
|
+
## TypeScript first
|
|
286
|
+
|
|
287
|
+
`tuple` and `struct` are designed to preserve TypeScript's inference:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const value = tuple(
|
|
291
|
+
"hello",
|
|
292
|
+
42,
|
|
293
|
+
true,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// readonly ["hello", 42, true]
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The library is deliberately small: the runtime API consists of the two constructors you actually need:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
import { tuple, struct } from "tuple-ts";
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## When should I use it?
|
|
306
|
+
|
|
307
|
+
`tuple-ts` is particularly useful when you want **small immutable values with stable identity**:
|
|
308
|
+
|
|
309
|
+
* composite `Map` keys
|
|
310
|
+
* `Set` membership
|
|
311
|
+
* memoization keys
|
|
312
|
+
* graph/search states
|
|
313
|
+
* coordinates and other multi-dimensional values
|
|
314
|
+
* functional programming
|
|
315
|
+
* immutable application state
|
|
316
|
+
* canonical representations of frequently repeated values
|
|
317
|
+
|
|
318
|
+
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**.
|
|
319
|
+
|
|
320
|
+
## Design
|
|
321
|
+
|
|
322
|
+
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.
|
|
323
|
+
|
|
324
|
+
The intern cache uses weak references, so canonical values do not need to be kept alive solely because they have previously been created.
|
|
325
|
+
|
|
326
|
+
There are no runtime dependencies.
|
|
145
327
|
|
|
146
|
-
##
|
|
328
|
+
## License
|
|
147
329
|
|
|
148
|
-
|
|
149
|
-
- `NaN` is equivalent to `NaN` (`tuple(NaN) === tuple(NaN)`).
|
|
150
|
-
- `+0` and `-0` are treated as distinct values (`tuple(+0) !== tuple(-0)`).
|
|
151
|
-
- Object elements are interned by object identity:
|
|
152
|
-
```ts
|
|
153
|
-
const shared = { value: 1 };
|
|
154
|
-
tuple(shared) === tuple(shared); // true
|
|
155
|
-
tuple({}) === tuple({}); // false (different object references)
|
|
156
|
-
```
|
|
330
|
+
ISC
|
package/dist/hash.d.ts
CHANGED
package/dist/hash.js
CHANGED
|
@@ -27,7 +27,7 @@ function linearCongruentialGenerator(seed = Date.now()) {
|
|
|
27
27
|
}
|
|
28
28
|
const randomUint32 = linearCongruentialGenerator();
|
|
29
29
|
const tupleSeed = randomUint32();
|
|
30
|
-
const
|
|
30
|
+
const structSeed = randomUint32();
|
|
31
31
|
const nullHash = randomUint32();
|
|
32
32
|
const undefinedHash = randomUint32();
|
|
33
33
|
const falseHash = randomUint32();
|
|
@@ -102,12 +102,12 @@ function valueHashes(values) {
|
|
|
102
102
|
export function tupleHash(elements) {
|
|
103
103
|
return mixHashes(valueHashes(elements), tupleSeed);
|
|
104
104
|
}
|
|
105
|
-
function keyAndValueHashes(
|
|
106
|
-
return Object.keys(
|
|
105
|
+
function keyAndValueHashes(struct) {
|
|
106
|
+
return Object.keys(struct).sort().flatMap(key => [
|
|
107
107
|
valueHash(key),
|
|
108
|
-
valueHash(
|
|
108
|
+
valueHash(struct[key])
|
|
109
109
|
]);
|
|
110
110
|
}
|
|
111
|
-
export function
|
|
112
|
-
return mixHashes(keyAndValueHashes(
|
|
111
|
+
export function structHash(struct) {
|
|
112
|
+
return mixHashes(keyAndValueHashes(struct), structSeed);
|
|
113
113
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import InternCache from "./intern-cache";
|
|
2
2
|
export { InternCache };
|
|
3
|
-
export type TupleType<T extends readonly unknown[]> = (...t: T) => Readonly<T>;
|
|
4
|
-
export type RecordType<T extends Record<string, unknown>> = (r: T) => Readonly<T>;
|
|
5
3
|
export declare const _tupleCache: InternCache<number, readonly unknown[]>;
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const tuple: <const
|
|
8
|
-
export declare const
|
|
9
|
-
export declare const defineTupleType: <T extends readonly unknown[]>() => TupleType<T>;
|
|
10
|
-
export declare const defineRecordType: <T extends Record<string, unknown>>() => RecordType<T>;
|
|
4
|
+
export declare const _structCache: InternCache<number, Record<string, unknown>>;
|
|
5
|
+
export declare const tuple: <const T extends readonly unknown[]>(...t: T) => Readonly<T>;
|
|
6
|
+
export declare const struct: <const T extends Record<string, unknown>>({ ...r }: T) => Readonly<T>;
|
|
11
7
|
//# 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;AACzC,OAAO,EAAE,WAAW,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,CAAC;AA6BvB,eAAO,MAAM,WAAW,yCAGvB,CAAC;AAEF,eAAO,MAAM,YAAY,8CAGxB,CAAC;AAEF,eAAO,MAAM,KAAK,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,gBAC9B,CAAC;AAEpC,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,gBACvC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import InternCache from "./intern-cache";
|
|
2
2
|
export { InternCache };
|
|
3
3
|
import * as hash from "./hash";
|
|
4
|
-
export const _tupleCache = new InternCache(hash.tupleHash, arrayEquivalent);
|
|
5
|
-
export const _recordCache = new InternCache(hash.recordHash, recordEquivalent);
|
|
6
4
|
function arrayEquivalent(a1, a2) {
|
|
7
5
|
if (a2.length !== a1.length)
|
|
8
6
|
return false;
|
|
@@ -12,7 +10,7 @@ function arrayEquivalent(a1, a2) {
|
|
|
12
10
|
}
|
|
13
11
|
return true;
|
|
14
12
|
}
|
|
15
|
-
function
|
|
13
|
+
function structEquivalent(r1, r2) {
|
|
16
14
|
const r1Keys = Object.keys(r1);
|
|
17
15
|
const r2Keys = Object.keys(r2);
|
|
18
16
|
if (r2Keys.length !== r1Keys.length)
|
|
@@ -26,7 +24,7 @@ function recordEquivalent(r1, r2) {
|
|
|
26
24
|
}
|
|
27
25
|
return true;
|
|
28
26
|
}
|
|
27
|
+
export const _tupleCache = new InternCache(hash.tupleHash, arrayEquivalent);
|
|
28
|
+
export const _structCache = new InternCache(hash.structHash, structEquivalent);
|
|
29
29
|
export const tuple = (...t) => Object.freeze(_tupleCache.get(t));
|
|
30
|
-
export const
|
|
31
|
-
export const defineTupleType = () => (...t) => tuple(...t);
|
|
32
|
-
export const defineRecordType = () => (r) => record(r);
|
|
30
|
+
export const struct = ({ ...r }) => Object.freeze(_structCache.get(r));
|
package/dist/record.spec.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
describe("
|
|
1
|
+
import { struct } from ".";
|
|
2
|
+
describe("struct", () => {
|
|
3
3
|
it("preserves keys and values", () => {
|
|
4
|
-
const a =
|
|
4
|
+
const a = struct({ x: 1, y: 2 });
|
|
5
5
|
expect(a).toEqual({ x: 1, y: 2 });
|
|
6
6
|
});
|
|
7
7
|
it("returns the same object for shallow-equal input objects", () => {
|
|
8
|
-
const a =
|
|
9
|
-
const b =
|
|
8
|
+
const a = struct({ x: 1, y: 2 });
|
|
9
|
+
const b = struct({ x: 1, y: 2 });
|
|
10
10
|
expect(a).toBe(b);
|
|
11
11
|
});
|
|
12
12
|
it("returns different object if keys or values differ", () => {
|
|
13
|
-
const r =
|
|
14
|
-
expect(
|
|
15
|
-
expect(
|
|
16
|
-
expect(
|
|
13
|
+
const r = struct({ x: 1, y: 2 });
|
|
14
|
+
expect(struct({ x: 1, y: 3 })).not.toBe(r);
|
|
15
|
+
expect(struct({ x: 1, y: 3 })).not.toBe(r);
|
|
16
|
+
expect(struct({ x: 1, y: 2, z: 3 })).not.toBe(r);
|
|
17
|
+
});
|
|
18
|
+
it("can be customised", () => {
|
|
19
|
+
const geocode = (struct);
|
|
20
|
+
const a = geocode({ lat: 1, long: 2 });
|
|
21
|
+
const b = geocode({ lat: 1, long: 2 });
|
|
22
|
+
expect(a).toBe(b);
|
|
17
23
|
});
|
|
18
24
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"struct.spec.d.ts","sourceRoot":"","sources":["../src/struct.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { struct } from ".";
|
|
2
|
+
describe("struct", () => {
|
|
3
|
+
it("preserves keys and values", () => {
|
|
4
|
+
const obj = { x: 1, y: 2 };
|
|
5
|
+
const s = struct(obj);
|
|
6
|
+
expect(s).toEqual(obj);
|
|
7
|
+
});
|
|
8
|
+
it("returns the same object for shallow-equal input objects", () => {
|
|
9
|
+
const a = struct({ x: 1, y: 2 });
|
|
10
|
+
const b = struct({ x: 1, y: 2 });
|
|
11
|
+
expect(a).toBe(b);
|
|
12
|
+
expect(a === b).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
it("returns different object if keys or values differ", () => {
|
|
15
|
+
const s1 = struct({ x: 1, y: 2 });
|
|
16
|
+
const s2 = struct({ x: 1, y: 3 });
|
|
17
|
+
const s3 = struct({ x: 1, y: 2, z: 3 });
|
|
18
|
+
expect(s1).not.toBe(s2);
|
|
19
|
+
expect(s1).not.toBe(s3);
|
|
20
|
+
expect(s1 === s3).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
it("can be customised", () => {
|
|
23
|
+
const geocode = (struct);
|
|
24
|
+
const a = geocode({ lat: 1, long: 2 });
|
|
25
|
+
const b = geocode({ lat: 1, long: 2 });
|
|
26
|
+
expect(a).toBe(b);
|
|
27
|
+
});
|
|
28
|
+
it("returns frozen instances", () => {
|
|
29
|
+
const s = struct({ x: 1, y: 2 });
|
|
30
|
+
expect(Object.isFrozen(s)).toBe(true);
|
|
31
|
+
expect(() => {
|
|
32
|
+
s.x = 42;
|
|
33
|
+
}).toThrow();
|
|
34
|
+
});
|
|
35
|
+
});
|
package/dist/tuple.spec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tuple,
|
|
1
|
+
import { tuple, _tupleCache } from ".";
|
|
2
2
|
async function waitForGC() {
|
|
3
3
|
await new Promise((res) => setTimeout(res, 0));
|
|
4
4
|
global.gc(); // requires node --expose-gc
|
|
@@ -7,7 +7,8 @@ async function waitForGC() {
|
|
|
7
7
|
test("Empty tuples", () => {
|
|
8
8
|
const a = tuple();
|
|
9
9
|
const b = tuple();
|
|
10
|
-
expect(b).toBe(a);
|
|
10
|
+
expect(b).toBe(a); // identity
|
|
11
|
+
expect(a === b).toBe(true); // equality
|
|
11
12
|
});
|
|
12
13
|
test("Indexability", () => {
|
|
13
14
|
const a = {};
|
|
@@ -27,12 +28,14 @@ test("Basic interned equality (if same input, same object)", () => {
|
|
|
27
28
|
const f = () => { };
|
|
28
29
|
const t1 = tuple(a, f, 42, "hi", undefined, true, null, 7839278492n);
|
|
29
30
|
const t2 = tuple(a, f, 42, "hi", undefined, true, null, 7839278492n);
|
|
30
|
-
expect(t1).toBe(t2); //
|
|
31
|
+
expect(t1).toBe(t2); // identity
|
|
32
|
+
expect(t1 === t2).toBe(true); // equality
|
|
31
33
|
});
|
|
32
34
|
test("Referential inequality (same structure, different references)", () => {
|
|
33
35
|
const t1 = tuple({}, 42);
|
|
34
36
|
const t2 = tuple({}, 42);
|
|
35
|
-
expect(t1).not.toBe(t2); //
|
|
37
|
+
expect(t1).not.toBe(t2); // non-identity
|
|
38
|
+
expect(t1 === t2).toBe(false); // non-equality
|
|
36
39
|
});
|
|
37
40
|
test("Different-length tuples are different tuples", () => {
|
|
38
41
|
const t1 = tuple(1, 2);
|
|
@@ -46,10 +49,12 @@ test("Nestable", () => {
|
|
|
46
49
|
const t2 = tuple("a", tuple("b1", "b2"), "c");
|
|
47
50
|
const t3 = tuple("a", tuple("b1", "b3"), "c");
|
|
48
51
|
expect(t2).toBe(t1);
|
|
52
|
+
expect(t1 === t2).toBe(true);
|
|
49
53
|
expect(t3).not.toBe(t1);
|
|
50
54
|
});
|
|
51
55
|
test("NaN is equivalent to itself", () => {
|
|
52
56
|
expect(tuple(NaN)).toBe(tuple(NaN));
|
|
57
|
+
expect(tuple(NaN) === tuple(NaN)).toBe(true);
|
|
53
58
|
});
|
|
54
59
|
test("NaN values with different bit representations are interned as identical", () => {
|
|
55
60
|
// Construct NaN with payload 1
|
|
@@ -69,16 +74,29 @@ test("NaN values with different bit representations are interned as identical",
|
|
|
69
74
|
// If NaNs were not canonicalized in numberToUint32s, this would fail (.not.toBe)
|
|
70
75
|
// because nan1 and nan2 would hash to different buckets:
|
|
71
76
|
expect(tuple(nan1)).toBe(tuple(nan2));
|
|
77
|
+
expect(tuple(nan1) === tuple(nan2)).toBe(true);
|
|
72
78
|
});
|
|
73
79
|
test("Positive and negative zero are distinct", () => {
|
|
80
|
+
const t1 = tuple(+0);
|
|
81
|
+
const t2 = tuple(-0);
|
|
82
|
+
expect(t1).not.toBe(t2);
|
|
83
|
+
expect(t1 === t2).toBe(false);
|
|
74
84
|
expect(tuple(+0)).not.toBe(tuple(-0));
|
|
85
|
+
expect(tuple(+0) === tuple(-0)).toBe(false);
|
|
75
86
|
});
|
|
76
87
|
test("Tuple cache entry is cleared when tuple is garbage-collected", async () => {
|
|
77
88
|
_tupleCache.reset();
|
|
78
89
|
expect(_tupleCache.size).toBe(0);
|
|
79
|
-
const Point =
|
|
90
|
+
const Point = (tuple);
|
|
80
91
|
Point(15, 42);
|
|
81
92
|
expect(_tupleCache.size).toBe(1);
|
|
82
93
|
await waitForGC();
|
|
83
94
|
expect(_tupleCache.size).toBe(0);
|
|
84
95
|
});
|
|
96
|
+
test("Tuples are frozen", () => {
|
|
97
|
+
const t = tuple(1, 2, 3);
|
|
98
|
+
expect(Object.isFrozen(t)).toBe(true);
|
|
99
|
+
expect(() => {
|
|
100
|
+
t[0] = 42;
|
|
101
|
+
}).toThrow();
|
|
102
|
+
});
|
package/package.json
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "tuple-ts",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"description": "Tuples and structs for TypeScript and JavaScript with equality and identity semantics.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tuple",
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"hash-consing",
|
|
13
|
-
"value-equality",
|
|
14
|
-
"structural-equality",
|
|
15
|
-
"composite-key",
|
|
16
|
-
"immutable",
|
|
17
|
-
"immutability",
|
|
18
|
-
"weakref",
|
|
8
|
+
"struct",
|
|
9
|
+
"equality",
|
|
10
|
+
"identity",
|
|
11
|
+
"shallow-equality",
|
|
19
12
|
"typescript"
|
|
20
13
|
],
|
|
21
14
|
"main": "./dist/index.js",
|