tuple-ts 0.2.3 → 0.3.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 +14 -15
- package/dist/hash.d.ts.map +1 -1
- package/dist/hash.js +35 -29
- package/dist/index.d.ts +8 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -28
- package/dist/intern-cache.d.ts +1 -0
- package/dist/intern-cache.d.ts.map +1 -1
- package/dist/intern-cache.js +12 -9
- package/dist/record.spec.js +1 -22
- package/dist/tuple.spec.js +31 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TupleTS
|
|
2
2
|
|
|
3
|
-
TupleTS is an implementation of
|
|
3
|
+
TupleTS is an implementation of tuples and records intended to be memory-safe, performant, and tiny.
|
|
4
4
|
|
|
5
5
|
- **Referential Equality (`===`)**: Identical tuples and records return the same instance in memory.
|
|
6
6
|
- **Composite Keys for `Set` & `Map`**: Use tuples or records as keys without custom serialization or hashing workarounds.
|
|
@@ -77,15 +77,15 @@ console.log(grid.get(tuple(0, 0))); // "Origin"
|
|
|
77
77
|
console.log(grid.get(tuple(3, 4))); // "Target"
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
### 4. Custom Typed Tuples (`
|
|
80
|
+
### 4. Custom Typed Tuples (`defineTupleType`)
|
|
81
81
|
|
|
82
|
-
You can create
|
|
82
|
+
You can create strictly-typed tuple factories:
|
|
83
83
|
|
|
84
84
|
```ts
|
|
85
|
-
import {
|
|
85
|
+
import { defineTupleType } from "tuple-ts";
|
|
86
86
|
|
|
87
87
|
type Point2D = [x: number, y: number];
|
|
88
|
-
const Point =
|
|
88
|
+
const Point = defineTupleType<Point2D>();
|
|
89
89
|
|
|
90
90
|
const p1 = Point(12, 34);
|
|
91
91
|
const p2 = Point(12, 34);
|
|
@@ -107,12 +107,12 @@ console.log(userA === userB); // true
|
|
|
107
107
|
console.log(userA.role); // "admin"
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
### 6.
|
|
110
|
+
### 6. Custom Typed Records (`defineRecordType`)
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
As with tuples, you can also create strictly-typed record factories:
|
|
113
113
|
|
|
114
114
|
```ts
|
|
115
|
-
import {
|
|
115
|
+
import { defineRecordType } from "tuple-ts";
|
|
116
116
|
|
|
117
117
|
interface UserProfile {
|
|
118
118
|
id: string;
|
|
@@ -120,14 +120,13 @@ interface UserProfile {
|
|
|
120
120
|
updatedAt: number;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
const UserById = recordType<UserProfile>().withKeys("id");
|
|
123
|
+
const User = defineRecordType<UserProfile>();
|
|
125
124
|
|
|
126
|
-
const
|
|
127
|
-
const
|
|
125
|
+
const user1 = User({ id: "alice", name: "Alice", updatedAt: 1000 });
|
|
126
|
+
const user2 = User({ name: "Alice", id: "alice", updatedAt: 1000 });
|
|
128
127
|
|
|
129
|
-
console.log(
|
|
130
|
-
console.log(
|
|
128
|
+
console.log(user1 === user2); // true
|
|
129
|
+
console.log(user1["name"]); // "Alice"
|
|
131
130
|
```
|
|
132
131
|
|
|
133
132
|
### 7. Memory Safety & Garbage Collection
|
|
@@ -135,7 +134,7 @@ console.log(session1); // { id: "usr_1" }
|
|
|
135
134
|
TupleTS uses `WeakRef` and `FinalizationRegistry` under the hood. When a tuple or record is no longer referenced anywhere in your program, the JavaScript runtime garbage-collects it, and its entry in the intern cache is automatically pruned.
|
|
136
135
|
|
|
137
136
|
```ts
|
|
138
|
-
// No need to manually clear caches or worry about memory leaks
|
|
137
|
+
// No need to manually clear caches or worry about memory leaks.
|
|
139
138
|
{
|
|
140
139
|
const temp = tuple("transient", 123);
|
|
141
140
|
// once 'temp' leaves scope and GC runs, the intern cache entry is freed.
|
package/dist/hash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAoHA,wBAAgB,SAAS,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAE9D;AASD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAKlE"}
|
package/dist/hash.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
const BIGINT_UINT32_MASK = 0xffffffffn;
|
|
2
|
+
const float64Buffer = new Float64Array(1);
|
|
3
|
+
const uint32View = new Uint32Array(float64Buffer.buffer);
|
|
2
4
|
function murmurHash3Mix_32bit(keys, seed = 0) {
|
|
3
5
|
// The first (i.e., mixing) stage of the MurmurHash3 algorithm for 32-bit keys
|
|
4
6
|
let hash = seed;
|
|
@@ -10,12 +12,12 @@ function murmurHash3Mix_32bit(keys, seed = 0) {
|
|
|
10
12
|
k = Math.imul(k, 0x1b873593);
|
|
11
13
|
hash ^= k;
|
|
12
14
|
hash = (hash << 13) | (hash >>> 19); // ROTL32(h1, 13);
|
|
13
|
-
hash = Math.imul(hash, 5) + 0xe6546b64;
|
|
15
|
+
hash = (Math.imul(hash, 5) + 0xe6546b64) | 0; // Bitwise OR to force 32-bit
|
|
14
16
|
}
|
|
15
17
|
hash ^= keyCount * 4; // number of input bytes
|
|
16
18
|
return hash >>> 0; // unsigned
|
|
17
19
|
}
|
|
18
|
-
function
|
|
20
|
+
function linearCongruentialGenerator(seed = Date.now()) {
|
|
19
21
|
let state = seed >>> 0; // Force to unsigned 32-bit
|
|
20
22
|
return () => {
|
|
21
23
|
// LCG constants from Numerical Recipes (good default)
|
|
@@ -23,7 +25,7 @@ function linearConguentialGenerator(seed = Date.now() >>> 0) {
|
|
|
23
25
|
return state;
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
|
-
const randomUint32 =
|
|
28
|
+
const randomUint32 = linearCongruentialGenerator();
|
|
27
29
|
const tupleSeed = randomUint32();
|
|
28
30
|
const recordSeed = randomUint32();
|
|
29
31
|
const nullHash = randomUint32();
|
|
@@ -35,6 +37,7 @@ const bigIntSeed = randomUint32();
|
|
|
35
37
|
const stringSeed = randomUint32();
|
|
36
38
|
const registeredSymbolSeed = randomUint32();
|
|
37
39
|
const objectHashMap = new WeakMap();
|
|
40
|
+
const symbolHashMap = new WeakMap();
|
|
38
41
|
function stableRandomHash(map, value) {
|
|
39
42
|
if (value === null)
|
|
40
43
|
return nullHash;
|
|
@@ -45,24 +48,29 @@ function stableRandomHash(map, value) {
|
|
|
45
48
|
map.set(value, newHash);
|
|
46
49
|
return newHash;
|
|
47
50
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
function stringToUint32s(value) {
|
|
52
|
+
const result = Array(Math.ceil(value.length / 2) + 1);
|
|
53
|
+
result[0] = value.length;
|
|
54
|
+
for (let i = 0; i < value.length / 2; i += 1) {
|
|
55
|
+
const char1 = value.charCodeAt(2 * i);
|
|
56
|
+
const char2 = value.charCodeAt(2 * i + 1);
|
|
57
|
+
result[i + 1] = (char2 << 16) | char1;
|
|
53
58
|
}
|
|
59
|
+
return result;
|
|
54
60
|
}
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
view.setFloat64(0, value);
|
|
59
|
-
yield view.getUint32(0);
|
|
60
|
-
yield view.getUint32(4);
|
|
61
|
+
function numberToUint32s(value) {
|
|
62
|
+
float64Buffer[0] = Number.isNaN(value) ? NaN : value;
|
|
63
|
+
return [uint32View[0], uint32View[1]];
|
|
61
64
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
function bigintToUint32s(value) {
|
|
66
|
+
const isNegative = value < 0n;
|
|
67
|
+
let n = isNegative ? -value : value;
|
|
68
|
+
const result = [isNegative ? 1 : 0];
|
|
69
|
+
while (n !== 0n) {
|
|
70
|
+
result.push(Number(n & BIGINT_UINT32_MASK));
|
|
71
|
+
n >>= 32n;
|
|
65
72
|
}
|
|
73
|
+
return result;
|
|
66
74
|
}
|
|
67
75
|
function valueHash(value) {
|
|
68
76
|
switch (typeof value) {
|
|
@@ -71,13 +79,13 @@ function valueHash(value) {
|
|
|
71
79
|
case "number":
|
|
72
80
|
return mixHashes(numberToUint32s(value), numberSeed);
|
|
73
81
|
case "bigint":
|
|
74
|
-
return mixHashes(
|
|
82
|
+
return mixHashes(bigintToUint32s(value), bigIntSeed);
|
|
75
83
|
case "boolean":
|
|
76
84
|
return value ? trueHash : falseHash;
|
|
77
85
|
case "symbol":
|
|
78
86
|
const k = Symbol.keyFor(value);
|
|
79
87
|
return k === undefined
|
|
80
|
-
? stableRandomHash(
|
|
88
|
+
? stableRandomHash(symbolHashMap, value)
|
|
81
89
|
: mixHashes(stringToUint32s(k), registeredSymbolSeed);
|
|
82
90
|
case "undefined":
|
|
83
91
|
return undefinedHash;
|
|
@@ -88,19 +96,17 @@ function valueHash(value) {
|
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
const mixHashes = murmurHash3Mix_32bit;
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
yield valueHash(v);
|
|
94
|
-
}
|
|
99
|
+
function valueHashes(values) {
|
|
100
|
+
return Array.from(values, valueHash);
|
|
95
101
|
}
|
|
96
102
|
export function tupleHash(elements) {
|
|
97
103
|
return mixHashes(valueHashes(elements), tupleSeed);
|
|
98
104
|
}
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
function keyAndValueHashes(record) {
|
|
106
|
+
return Object.keys(record).sort().flatMap(key => [
|
|
107
|
+
valueHash(key),
|
|
108
|
+
valueHash(record[key])
|
|
109
|
+
]);
|
|
104
110
|
}
|
|
105
111
|
export function recordHash(record) {
|
|
106
112
|
return mixHashes(keyAndValueHashes(record), recordSeed);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import InternCache from "./intern-cache";
|
|
2
2
|
export { InternCache };
|
|
3
|
-
export type
|
|
4
|
-
export type
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
};
|
|
12
|
-
export type RecordType<R extends Record<string, unknown> = Record<string, unknown>> = KeyedRecordType<R> & {
|
|
13
|
-
withKeys: <K extends readonly (keyof R)[]>(...keys: K) => KeyedRecordType<Pick<R, K[number]>>;
|
|
14
|
-
};
|
|
15
|
-
export declare function tupleType<T extends Tuple>(): TupleType<T>;
|
|
16
|
-
export declare function recordType<T extends Record<string, unknown>>(): RecordType<T>;
|
|
17
|
-
export declare const tuple: TupleType<readonly unknown[]>;
|
|
18
|
-
export declare const record: RecordType<Record<string, unknown>>;
|
|
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
|
+
export declare const _tupleCache: InternCache<number, readonly unknown[]>;
|
|
6
|
+
export declare const _recordCache: InternCache<number, Record<string, unknown>>;
|
|
7
|
+
export declare const tuple: <const T_ extends readonly unknown[]>(...t: T_) => Readonly<T_>;
|
|
8
|
+
export declare const record: <const T_ extends Record<string, unknown>>({ ...r }: T_) => T_;
|
|
9
|
+
export declare const defineTupleType: <T extends readonly unknown[]>() => TupleType<T>;
|
|
10
|
+
export declare const defineRecordType: <T extends Record<string, unknown>>() => RecordType<T>;
|
|
19
11
|
//# 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;AAGvB,MAAM,MAAM,
|
|
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;AAGvB,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC/E,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAC1D,CAAC,EAAE,CAAC,KACD,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEjB,eAAO,MAAM,WAAW,yCAGvB,CAAC;AACF,eAAO,MAAM,YAAY,8CAGxB,CAAC;AA4BF,eAAO,MAAM,KAAK,GAAI,KAAK,CAAC,EAAE,SAAS,SAAS,OAAO,EAAE,EAAE,GAAG,GAAG,EAAE,iBAChC,CAAC;AAEpC,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAE9D,EAAE,KAAG,EAAwC,CAAC;AAEjD,eAAO,MAAM,eAAe,GACzB,CAAC,SAAS,SAAS,OAAO,EAAE,OAAK,SAAS,CAAC,CAAC,CAEjB,CAAC;AAE/B,eAAO,MAAM,gBAAgB,GAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,OAAK,UAAU,CAAC,CAAC,CAExC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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);
|
|
4
6
|
function arrayEquivalent(a1, a2) {
|
|
5
7
|
if (a2.length !== a1.length)
|
|
6
8
|
return false;
|
|
@@ -24,31 +26,7 @@ function recordEquivalent(r1, r2) {
|
|
|
24
26
|
}
|
|
25
27
|
return true;
|
|
26
28
|
}
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
export function recordType() {
|
|
34
|
-
const _cache = new InternCache(hash.recordHash, recordEquivalent);
|
|
35
|
-
return Object.assign(({ ...r }) => Object.freeze(_cache.get(r)), {
|
|
36
|
-
_cache,
|
|
37
|
-
withKeys: (...keys) => recordTypeToKeyedRecordType(...keys),
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
export const tuple = tupleType();
|
|
41
|
-
export const record = recordType();
|
|
42
|
-
function recordToTuple(record, keys) {
|
|
43
|
-
return keys.map((k) => record[k]);
|
|
44
|
-
}
|
|
45
|
-
function recordTypeToKeyedRecordType(...keys) {
|
|
46
|
-
const _cache = new InternCache((r) => hash.tupleHash(recordToTuple(r, keys)), (r1, r2) => arrayEquivalent(recordToTuple(r1, keys), recordToTuple(r2, keys)));
|
|
47
|
-
return Object.assign((r) => {
|
|
48
|
-
const r_ = {};
|
|
49
|
-
for (const k of keys) {
|
|
50
|
-
r_[k] = r[k];
|
|
51
|
-
}
|
|
52
|
-
return Object.freeze(_cache.get(r_));
|
|
53
|
-
}, { _cache });
|
|
54
|
-
}
|
|
29
|
+
export const tuple = (...t) => Object.freeze(_tupleCache.get(t));
|
|
30
|
+
export const record = ({ ...r }) => Object.freeze(_recordCache.get(r));
|
|
31
|
+
export const defineTupleType = () => (...t) => tuple(...t);
|
|
32
|
+
export const defineRecordType = () => (r) => record(r);
|
package/dist/intern-cache.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"intern-cache.d.ts","sourceRoot":"","sources":["../src/intern-cache.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,
|
|
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;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgC;IAC3D,OAAO,CAAC,eAAe,CAErB;gBAGA,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EACrB,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO;IAM3C,OAAO,CAAC,MAAM;IAWd,GAAG,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC;IAqB3B,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,KAAK,IAAI,IAAI;CAId"}
|
package/dist/intern-cache.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
export default class {
|
|
2
2
|
constructor(hashFn, identityFn) {
|
|
3
3
|
this.map = new Map();
|
|
4
|
-
this.cleanupRegistry = new FinalizationRegistry(this.remove
|
|
4
|
+
this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
|
|
5
5
|
this.hashFn = hashFn;
|
|
6
6
|
this.identityFn = identityFn;
|
|
7
7
|
}
|
|
8
8
|
remove([hash, ref]) {
|
|
9
9
|
const hashBucket = this.map.get(hash);
|
|
10
|
-
if (hashBucket
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (hashBucket.size === 0)
|
|
10
|
+
if (!hashBucket) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
hashBucket.delete(ref);
|
|
14
|
+
if (hashBucket.size === 0) {
|
|
15
15
|
this.map.delete(hash);
|
|
16
|
+
}
|
|
16
17
|
}
|
|
17
18
|
get(obj) {
|
|
18
19
|
const hash = this.hashFn(obj);
|
|
@@ -36,8 +37,10 @@ export default class {
|
|
|
36
37
|
return obj;
|
|
37
38
|
}
|
|
38
39
|
get size() {
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
return Array.from(this.map.entries(), ([_, b]) => b.size).reduce((acc, x) => acc + x, 0);
|
|
41
|
+
}
|
|
42
|
+
reset() {
|
|
43
|
+
this.map.clear();
|
|
44
|
+
this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
|
|
42
45
|
}
|
|
43
46
|
}
|
package/dist/record.spec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { record
|
|
1
|
+
import { record } from ".";
|
|
2
2
|
describe("record", () => {
|
|
3
3
|
it("preserves keys and values", () => {
|
|
4
4
|
const a = record({ x: 1, y: 2 });
|
|
@@ -16,24 +16,3 @@ describe("record", () => {
|
|
|
16
16
|
expect(record({ x: 1, y: 2, z: 3 })).not.toBe(r);
|
|
17
17
|
});
|
|
18
18
|
});
|
|
19
|
-
describe("Keyed record type", () => {
|
|
20
|
-
const Person = recordType().withKeys("name", "age");
|
|
21
|
-
it("interns on only specified keys and values", () => {
|
|
22
|
-
const a = { name: "Bob", age: 42, sex: "male" };
|
|
23
|
-
const b = { name: "Bob", age: 42, occupation: "teacher" };
|
|
24
|
-
expect(Person(a)).toBe(Person(b));
|
|
25
|
-
expect(Person(a)).toEqual({ name: "Bob", age: 42 });
|
|
26
|
-
});
|
|
27
|
-
it("returns different object if keys or values differ", () => {
|
|
28
|
-
const r = Person({ name: "Tom", age: 42 });
|
|
29
|
-
expect({ x: 1, y: 3 }).not.toBe(r);
|
|
30
|
-
expect({ x: 1, y: 3 }).not.toBe(r);
|
|
31
|
-
expect({ x: 1, y: 2, z: 3 }).not.toBe(r);
|
|
32
|
-
});
|
|
33
|
-
it("respects inherited attributes", () => {
|
|
34
|
-
const proto = { name: "Bob" };
|
|
35
|
-
const a = Object.create(proto);
|
|
36
|
-
a.age = 42;
|
|
37
|
-
expect(Person(a)).toBe(Person({ name: "Bob", age: 42 }));
|
|
38
|
-
});
|
|
39
|
-
});
|
package/dist/tuple.spec.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import { tuple,
|
|
1
|
+
import { tuple, defineTupleType, _tupleCache } from ".";
|
|
2
|
+
async function waitForGC() {
|
|
3
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
4
|
+
global.gc(); // requires node --expose-gc
|
|
5
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
6
|
+
}
|
|
2
7
|
test("Empty tuples", () => {
|
|
3
8
|
const a = tuple();
|
|
4
9
|
const b = tuple();
|
|
@@ -46,16 +51,34 @@ test("Nestable", () => {
|
|
|
46
51
|
test("NaN is equivalent to itself", () => {
|
|
47
52
|
expect(tuple(NaN)).toBe(tuple(NaN));
|
|
48
53
|
});
|
|
54
|
+
test("NaN values with different bit representations are interned as identical", () => {
|
|
55
|
+
// Construct NaN with payload 1
|
|
56
|
+
const view1 = new DataView(new ArrayBuffer(8));
|
|
57
|
+
view1.setUint32(0, 0x7ff80000);
|
|
58
|
+
view1.setUint32(4, 0x00000001);
|
|
59
|
+
const nan1 = view1.getFloat64(0);
|
|
60
|
+
// Construct NaN with payload 2
|
|
61
|
+
const view2 = new DataView(new ArrayBuffer(8));
|
|
62
|
+
view2.setUint32(0, 0x7ff80000);
|
|
63
|
+
view2.setUint32(4, 0x00000002);
|
|
64
|
+
const nan2 = view2.getFloat64(0);
|
|
65
|
+
// Verify they are both recognized as NaNs and Object.is equivalent:
|
|
66
|
+
expect(Number.isNaN(nan1)).toBe(true);
|
|
67
|
+
expect(Number.isNaN(nan2)).toBe(true);
|
|
68
|
+
expect(Object.is(nan1, nan2)).toBe(true);
|
|
69
|
+
// If NaNs were not canonicalized in numberToUint32s, this would fail (.not.toBe)
|
|
70
|
+
// because nan1 and nan2 would hash to different buckets:
|
|
71
|
+
expect(tuple(nan1)).toBe(tuple(nan2));
|
|
72
|
+
});
|
|
49
73
|
test("Positive and negative zero are distinct", () => {
|
|
50
74
|
expect(tuple(+0)).not.toBe(tuple(-0));
|
|
51
75
|
});
|
|
52
76
|
test("Tuple cache entry is cleared when tuple is garbage-collected", async () => {
|
|
53
|
-
|
|
77
|
+
_tupleCache.reset();
|
|
78
|
+
expect(_tupleCache.size).toBe(0);
|
|
79
|
+
const Point = defineTupleType();
|
|
54
80
|
Point(15, 42);
|
|
55
|
-
expect(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
global.gc();
|
|
59
|
-
await new Promise((res) => setTimeout(res, 100));
|
|
60
|
-
expect(Point._cache.size).toBe(0);
|
|
81
|
+
expect(_tupleCache.size).toBe(1);
|
|
82
|
+
await waitForGC();
|
|
83
|
+
expect(_tupleCache.size).toBe(0);
|
|
61
84
|
});
|