tuple-ts 0.3.1 → 0.4.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 +39 -34
- 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 +3 -5
- 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 +28 -0
- package/dist/tuple.spec.js +16 -5
- package/package.json +6 -13
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# TupleTS
|
|
2
2
|
|
|
3
|
-
TupleTS
|
|
3
|
+
TupleTS implements tuples and structs with shallow equality and identity semantics in TypeScript/JavaScript. It is designed to be tiny, fast, and memory-safe.
|
|
4
4
|
|
|
5
|
-
- **
|
|
6
|
-
- **Composite Keys for `Set`
|
|
7
|
-
- **Memory-Safe**:
|
|
8
|
-
- **Deeply Nestable & Immutable**: Tuples can contain other tuples
|
|
9
|
-
- **TypeScript First**: Full type inference
|
|
10
|
-
- **
|
|
5
|
+
- **Equality and Identity Semantics (`===` & `Object.is()`)**: Identical tuples and structs map to the same instance in memory.
|
|
6
|
+
- **Composite Keys for `Set` and `Map`**: Use tuples or structs as keys without serialization or hashing workarounds.
|
|
7
|
+
- **Memory-Safe**: No memory leaks ever due to advanced integration with garbage collection.
|
|
8
|
+
- **Deeply Nestable & Immutable**: Tuples and structs can contain other tuples and structs.
|
|
9
|
+
- **TypeScript First**: Full type inference.
|
|
10
|
+
- **Customisable Sub-types**: Easily create your own custom tuple and struct sub-types.
|
|
11
|
+
- **Zero Dependencies**: Lightweight implementation with fast hashing and no dependencies.
|
|
11
12
|
|
|
12
13
|
---
|
|
13
14
|
|
|
@@ -21,9 +22,9 @@ npm install tuple-ts
|
|
|
21
22
|
|
|
22
23
|
## Usage Examples
|
|
23
24
|
|
|
24
|
-
### 1. Basic Tuples
|
|
25
|
+
### 1. Basic Tuples, Identity and Equality
|
|
25
26
|
|
|
26
|
-
Tuples with the same elements evaluate to the exact same object reference
|
|
27
|
+
Tuples with the same elements evaluate to the exact same object reference:
|
|
27
28
|
|
|
28
29
|
```ts
|
|
29
30
|
import { tuple } from "tuple-ts";
|
|
@@ -32,11 +33,13 @@ const a = tuple(1, "hello", true);
|
|
|
32
33
|
const b = tuple(1, "hello", true);
|
|
33
34
|
|
|
34
35
|
console.log(a === b); // true
|
|
36
|
+
console.log(Object.is(a, b)); // true
|
|
35
37
|
|
|
36
38
|
// Tuples are standard frozen arrays:
|
|
37
|
-
console.log(a[0]);
|
|
38
|
-
console.log(a.length);
|
|
39
|
-
console.log([...a]);
|
|
39
|
+
console.log(a[0]); // 1
|
|
40
|
+
console.log(a.length); // 3
|
|
41
|
+
console.log([...a]); // [1, "hello", true]
|
|
42
|
+
console.log(Object.isFrozen(a)); // true
|
|
40
43
|
```
|
|
41
44
|
|
|
42
45
|
### 2. Nested Tuples
|
|
@@ -77,61 +80,63 @@ console.log(grid.get(tuple(0, 0))); // "Origin"
|
|
|
77
80
|
console.log(grid.get(tuple(3, 4))); // "Target"
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
### 4. Custom Typed Tuples
|
|
83
|
+
### 4. Custom Typed Tuples
|
|
81
84
|
|
|
82
|
-
You can create
|
|
85
|
+
You can easily create custom tuple sub-types:
|
|
83
86
|
|
|
84
87
|
```ts
|
|
85
|
-
import {
|
|
88
|
+
import { tuple } from "tuple-ts";
|
|
86
89
|
|
|
87
90
|
type Point2D = [x: number, y: number];
|
|
88
|
-
const
|
|
91
|
+
const Point2D = tuple<Point2D>;
|
|
89
92
|
|
|
90
|
-
const p1 =
|
|
91
|
-
const p2 =
|
|
93
|
+
const p1 = Point2D(12, 34);
|
|
94
|
+
const p2 = Point2D(12, 34);
|
|
92
95
|
|
|
93
96
|
console.log(p1 === p2); // true
|
|
97
|
+
console.log(Object.is(p1, p2)); // true
|
|
94
98
|
```
|
|
95
99
|
|
|
96
|
-
### 5.
|
|
100
|
+
### 5. Structs
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
Structs with the same keys and values evaluate to the exact same frozen object reference:
|
|
99
103
|
|
|
100
104
|
```ts
|
|
101
|
-
import {
|
|
105
|
+
import { struct } from "tuple-ts";
|
|
102
106
|
|
|
103
|
-
const userA =
|
|
104
|
-
const userB =
|
|
107
|
+
const userA = struct({ id: 1, role: "admin" });
|
|
108
|
+
const userB = struct({ role: "admin", id: 1 }); // different key order
|
|
105
109
|
|
|
106
110
|
console.log(userA === userB); // true
|
|
107
|
-
console.log(userA
|
|
111
|
+
console.log(Object.is(userA, userB)); // true
|
|
112
|
+
console.log(userA.role); // "admin"
|
|
113
|
+
console.log(Object.isFrozen(userA)); // true
|
|
108
114
|
```
|
|
109
115
|
|
|
110
|
-
### 6. Custom Typed
|
|
116
|
+
### 6. Custom Typed Structs
|
|
111
117
|
|
|
112
|
-
As with tuples, you can
|
|
118
|
+
As with tuples, you can easily create custom struct sub-types:
|
|
113
119
|
|
|
114
120
|
```ts
|
|
115
|
-
import {
|
|
121
|
+
import { struct } from "tuple-ts";
|
|
116
122
|
|
|
117
|
-
interface
|
|
123
|
+
interface User {
|
|
118
124
|
id: string;
|
|
119
125
|
name: string;
|
|
120
126
|
updatedAt: number;
|
|
121
127
|
}
|
|
122
|
-
|
|
123
|
-
const User = defineRecordType<UserProfile>();
|
|
128
|
+
const User = struct<User>;
|
|
124
129
|
|
|
125
130
|
const user1 = User({ id: "alice", name: "Alice", updatedAt: 1000 });
|
|
126
131
|
const user2 = User({ name: "Alice", id: "alice", updatedAt: 1000 });
|
|
127
132
|
|
|
128
133
|
console.log(user1 === user2); // true
|
|
129
|
-
console.log(user1
|
|
134
|
+
console.log(user1.name); // "Alice"
|
|
130
135
|
```
|
|
131
136
|
|
|
132
137
|
### 7. Memory Safety & Garbage Collection
|
|
133
138
|
|
|
134
|
-
TupleTS uses `WeakRef` and `FinalizationRegistry` under the hood. When a tuple or
|
|
139
|
+
TupleTS uses `WeakRef` and `FinalizationRegistry` under the hood. When a tuple or struct is no longer referenced anywhere in your program, the JavaScript runtime garbage-collects it, and its entry in the intern cache is automatically pruned.
|
|
135
140
|
|
|
136
141
|
```ts
|
|
137
142
|
// No need to manually clear caches or worry about memory leaks.
|
|
@@ -148,9 +153,9 @@ TupleTS uses `WeakRef` and `FinalizationRegistry` under the hood. When a tuple o
|
|
|
148
153
|
TupleTS compares primitives via `Object.is`:
|
|
149
154
|
- `NaN` is equivalent to `NaN` (`tuple(NaN) === tuple(NaN)`).
|
|
150
155
|
- `+0` and `-0` are treated as distinct values (`tuple(+0) !== tuple(-0)`).
|
|
151
|
-
- Object elements are
|
|
156
|
+
- Object elements are compared by object instance identity:
|
|
152
157
|
```ts
|
|
153
158
|
const shared = { value: 1 };
|
|
154
159
|
tuple(shared) === tuple(shared); // true
|
|
155
|
-
tuple({})
|
|
160
|
+
tuple({}) !== tuple({}); // true (different object instances)
|
|
156
161
|
```
|
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;AAGvB,
|
|
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,eAAO,MAAM,WAAW,yCAGvB,CAAC;AACF,eAAO,MAAM,YAAY,8CAGxB,CAAC;AA4BF,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,UAE7D,CAAC,gBAAuC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import InternCache from "./intern-cache";
|
|
|
2
2
|
export { InternCache };
|
|
3
3
|
import * as hash from "./hash";
|
|
4
4
|
export const _tupleCache = new InternCache(hash.tupleHash, arrayEquivalent);
|
|
5
|
-
export const
|
|
5
|
+
export const _structCache = new InternCache(hash.structHash, structEquivalent);
|
|
6
6
|
function arrayEquivalent(a1, a2) {
|
|
7
7
|
if (a2.length !== a1.length)
|
|
8
8
|
return false;
|
|
@@ -12,7 +12,7 @@ function arrayEquivalent(a1, a2) {
|
|
|
12
12
|
}
|
|
13
13
|
return true;
|
|
14
14
|
}
|
|
15
|
-
function
|
|
15
|
+
function structEquivalent(r1, r2) {
|
|
16
16
|
const r1Keys = Object.keys(r1);
|
|
17
17
|
const r2Keys = Object.keys(r2);
|
|
18
18
|
if (r2Keys.length !== r1Keys.length)
|
|
@@ -27,6 +27,4 @@ function recordEquivalent(r1, r2) {
|
|
|
27
27
|
return true;
|
|
28
28
|
}
|
|
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,28 @@
|
|
|
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
|
+
});
|
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,14 +74,20 @@ 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();
|
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.0",
|
|
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",
|