tuple-ts 0.3.0 → 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 CHANGED
@@ -1,13 +1,14 @@
1
1
  # TupleTS
2
2
 
3
- TupleTS is an implementation of interned tuples and records intended to be memory-safe, performant, and tiny.
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
- - **Referential Equality (`===`)**: Identical tuples and records return the same instance in memory.
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
+ - **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 & Referential Equality
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]); // 1
38
- console.log(a.length); // 3
39
- console.log([...a]); // [1, "hello", true]
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 (`defineTupleType`)
83
+ ### 4. Custom Typed Tuples
81
84
 
82
- You can create strictly-typed tuple factories:
85
+ You can easily create custom tuple sub-types:
83
86
 
84
87
  ```ts
85
- import { defineTupleType } from "tuple-ts";
88
+ import { tuple } from "tuple-ts";
86
89
 
87
90
  type Point2D = [x: number, y: number];
88
- const Point = defineTupleType<Point2D>();
91
+ const Point2D = tuple<Point2D>;
89
92
 
90
- const p1 = Point(12, 34);
91
- const p2 = Point(12, 34);
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. Interned Records (`record`)
100
+ ### 5. Structs
97
101
 
98
- Just like tuples, `record` interns shallow objects. Property key insertion order does not affect equality:
102
+ Structs with the same keys and values evaluate to the exact same frozen object reference:
99
103
 
100
104
  ```ts
101
- import { record } from "tuple-ts";
105
+ import { struct } from "tuple-ts";
102
106
 
103
- const userA = record({ id: 1, role: "admin" });
104
- const userB = record({ role: "admin", id: 1 }); // different key order
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.role); // "admin"
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 Records (`defineRecordType`)
116
+ ### 6. Custom Typed Structs
111
117
 
112
- As with tuples, you can also create strictly-typed record factories:
118
+ As with tuples, you can easily create custom struct sub-types:
113
119
 
114
120
  ```ts
115
- import { defineRecordType } from "tuple-ts";
121
+ import { struct } from "tuple-ts";
116
122
 
117
- interface UserProfile {
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["name"]); // "Alice"
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 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.
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 interned by object identity:
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({}) === tuple({}); // false (different object references)
160
+ tuple({}) !== tuple({}); // true (different object instances)
156
161
  ```
package/dist/hash.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export declare function tupleHash(elements: readonly unknown[]): number;
2
- export declare function recordHash(record: Record<string, unknown>): number;
2
+ export declare function structHash(struct: Record<string, unknown>): number;
3
3
  //# sourceMappingURL=hash.d.ts.map
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 recordSeed = randomUint32();
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(record) {
106
- return Object.keys(record).sort().flatMap(key => [
105
+ function keyAndValueHashes(struct) {
106
+ return Object.keys(struct).sort().flatMap(key => [
107
107
  valueHash(key),
108
- valueHash(record[key])
108
+ valueHash(struct[key])
109
109
  ]);
110
110
  }
111
- export function recordHash(record) {
112
- return mixHashes(keyAndValueHashes(record), recordSeed);
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 _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>;
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
@@ -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,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"}
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 _recordCache = new InternCache(hash.recordHash, recordEquivalent);
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 recordEquivalent(r1, r2) {
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 record = ({ ...r }) => Object.freeze(_recordCache.get(r));
31
- export const defineTupleType = () => (...t) => tuple(...t);
32
- export const defineRecordType = () => (r) => record(r);
30
+ export const struct = ({ ...r }) => Object.freeze(_structCache.get(r));
@@ -1,18 +1,24 @@
1
- import { record } from ".";
2
- describe("record", () => {
1
+ import { struct } from ".";
2
+ describe("struct", () => {
3
3
  it("preserves keys and values", () => {
4
- const a = record({ x: 1, y: 2 });
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 = record({ x: 1, y: 2 });
9
- const b = record({ x: 1, y: 2 });
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 = record({ x: 1, y: 2 });
14
- expect(record({ x: 1, y: 3 })).not.toBe(r);
15
- expect(record({ x: 1, y: 3 })).not.toBe(r);
16
- expect(record({ x: 1, y: 2, z: 3 })).not.toBe(r);
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=struct.spec.d.ts.map
@@ -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
+ });
@@ -1,4 +1,4 @@
1
- import { tuple, defineTupleType, _tupleCache } from ".";
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); // same object reference
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); // different objects
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 = defineTupleType();
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.3.0",
5
- "description": "Python-like tuples for JavaScript/TypeScript",
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
- "tuples",
9
- "record",
10
- "records",
11
- "interning",
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",