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 CHANGED
@@ -1,6 +1,6 @@
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 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 (`tupleType`)
80
+ ### 4. Custom Typed Tuples (`defineTupleType`)
81
81
 
82
- You can create isolated, strictly-typed tuple factories with their own independent intern caches:
82
+ You can create strictly-typed tuple factories:
83
83
 
84
84
  ```ts
85
- import { tupleType } from "tuple-ts";
85
+ import { defineTupleType } from "tuple-ts";
86
86
 
87
87
  type Point2D = [x: number, y: number];
88
- const Point = tupleType<Point2D>();
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. Keyed Records (`recordType().withKeys(...)`)
110
+ ### 6. Custom Typed Records (`defineRecordType`)
111
111
 
112
- Define records interned and deduplicated on a specific subset of primary keys:
112
+ As with tuples, you can also create strictly-typed record factories:
113
113
 
114
114
  ```ts
115
- import { recordType } from "tuple-ts";
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
- // Only 'id' determines record identity
124
- const UserById = recordType<UserProfile>().withKeys("id");
123
+ const User = defineRecordType<UserProfile>();
125
124
 
126
- const session1 = UserById({ id: "usr_1", name: "Alice", updatedAt: 1000 });
127
- const session2 = UserById({ id: "usr_1", name: "Alice B.", updatedAt: 2000 });
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(session1 === session2); // true
130
- console.log(session1); // { id: "usr_1" }
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.
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AA6GA,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"}
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 UINT32_BASE = 2n ** 32n;
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 linearConguentialGenerator(seed = Date.now() >>> 0) {
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 = linearConguentialGenerator();
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* stringToUint32s(value) {
49
- for (let i = 0; i < value.length; i += 2) {
50
- const char1 = value.charCodeAt(i);
51
- const char2 = value.charCodeAt(i + 1);
52
- yield (char2 << 16) | char1;
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* numberToUint32s(value) {
56
- const buffer = new ArrayBuffer(8); // 8 bytes = 64 bits
57
- const view = new DataView(buffer);
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* bigIntToUint32s(value) {
63
- for (let n = value; n !== 0n; n /= UINT32_BASE) {
64
- yield Number(((n % UINT32_BASE) + UINT32_BASE) % UINT32_BASE); // Safely non-negative
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(bigIntToUint32s(value), bigIntSeed);
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(objectHashMap, value)
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* valueHashes(values) {
92
- for (const v of values) {
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* keyAndValueHashes(record) {
100
- for (const key of Object.keys(record).sort()) {
101
- yield valueHash(key);
102
- yield valueHash(record[key]);
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 Tuple<T extends readonly unknown[] = readonly unknown[]> = T;
4
- export type TupleType<T extends Tuple = Tuple> = {
5
- (...t: T): T;
6
- _cache: InternCache<number, T>;
7
- };
8
- export type KeyedRecordType<R extends Record<string, unknown> = Record<string, unknown>> = {
9
- (r: R): R;
10
- _cache: InternCache<number, R>;
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
@@ -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,KAAK,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,GAAG,SAAS,OAAO,EAAE,IAAI,CAAC,CAAC;AAEzE,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,IAAI;IAC/C,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACb,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,eAAe,CACzB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACzD;IAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,UAAU,CACpB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACzD,eAAe,CAAC,CAAC,CAAC,GAAG;IACvB,QAAQ,EAAE,CAAC,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,EACvC,GAAG,IAAI,EAAE,CAAC,KACP,eAAe,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;CAC1C,CAAC;AA4BF,wBAAgB,SAAS,CAAC,CAAC,SAAS,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,CAKzD;AAED,wBAAgB,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,UAAU,CAAC,CAAC,CAAC,CAO7E;AAED,eAAO,MAAM,KAAK,+BAAc,CAAC;AACjC,eAAO,MAAM,MAAM,qCAAe,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,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 function tupleType() {
28
- const _cache = new InternCache(hash.tupleHash, arrayEquivalent);
29
- return Object.assign((...t) => Object.freeze(_cache.get(t)), {
30
- _cache,
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);
@@ -7,5 +7,6 @@ export default class<H, T extends object> {
7
7
  private remove;
8
8
  get<U extends T>(obj: U): U;
9
9
  get size(): number;
10
+ reset(): void;
10
11
  }
11
12
  //# sourceMappingURL=intern-cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"intern-cache.d.ts","sourceRoot":"","sources":["../src/intern-cache.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAQ,CAAC,EAAE,CAAC,SAAS,MAAM;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgC;IAC3D,OAAO,CAAC,eAAe,CAAoD;gBAE/D,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO;IAK5E,OAAO,CAAC,MAAM;IAQd,GAAG,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC;IAqB3B,IAAI,IAAI,IAAI,MAAM,CAIjB;CACF"}
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"}
@@ -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.bind(this));
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 === undefined)
11
- throw new Error("Could not get tuple set");
12
- if (!hashBucket.delete(ref))
13
- throw new Error("Could not delete tuple WeakRef");
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 [...this.map.entries()]
40
- .map(([_, b]) => b.size)
41
- .reduce((a, b) => a + b, 0);
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
  }
@@ -1,4 +1,4 @@
1
- import { record, recordType } from ".";
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
- });
@@ -1,4 +1,9 @@
1
- import { tuple, tupleType } from ".";
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
- const Point = tupleType();
77
+ _tupleCache.reset();
78
+ expect(_tupleCache.size).toBe(0);
79
+ const Point = defineTupleType();
54
80
  Point(15, 42);
55
- expect(Point._cache.size).toBe(1);
56
- // Trigger garbage collection (requires node --expose-gc)
57
- await new Promise((res) => setTimeout(res, 100));
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
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "tuple-ts",
4
- "version": "0.2.3",
4
+ "version": "0.3.1",
5
5
  "description": "Python-like tuples for JavaScript/TypeScript",
6
6
  "keywords": [
7
7
  "tuple",