tuple-ts 0.4.6 → 0.5.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
@@ -15,7 +15,7 @@ JavaScript normally compares arrays and objects by reference:
15
15
  ```ts
16
16
  tuple(1, 2) === tuple(1, 2) // true
17
17
 
18
- struct({ x: 1 }) === struct({ x: 1 }) // true
18
+ record({ x: 1 }) === record({ x: 1 }) // true
19
19
  ```
20
20
 
21
21
  Equal values are **interned to the same object**.
@@ -163,19 +163,19 @@ a === b; // true
163
163
 
164
164
  The result retains the tuple's TypeScript type while gaining the same runtime identity semantics.
165
165
 
166
- ## Structs
166
+ ## Records
167
167
 
168
- `struct` provides the same semantics for objects:
168
+ `record` provides the same value-based identity semantics for objects:
169
169
 
170
170
  ```ts
171
- import { struct } from "tuple-ts";
171
+ import { record } from "tuple-ts";
172
172
 
173
- const a = struct({
173
+ const a = record({
174
174
  id: 1,
175
175
  role: "admin",
176
176
  });
177
177
 
178
- const b = struct({
178
+ const b = record({
179
179
  role: "admin",
180
180
  id: 1,
181
181
  });
@@ -185,7 +185,7 @@ a === b; // true
185
185
 
186
186
  Property order does not matter.
187
187
 
188
- Structs are frozen:
188
+ Records are frozen:
189
189
 
190
190
  ```ts
191
191
  Object.isFrozen(a); // true
@@ -197,18 +197,18 @@ And they work naturally as `Map` keys:
197
197
  const users = new Map();
198
198
 
199
199
  users.set(
200
- struct({ id: 42, role: "admin" }),
200
+ record({ id: 42, role: "admin" }),
201
201
  "Alice",
202
202
  );
203
203
 
204
204
  users.get(
205
- struct({ role: "admin", id: 42 }),
205
+ record({ role: "admin", id: 42 }),
206
206
  ); // "Alice"
207
207
  ```
208
208
 
209
- ### Typed structs
209
+ ### Typed records
210
210
 
211
- As with tuples, you can define a reusable typed constructor:
211
+ As with tuples, you can define a reusable typed constructor by passing a type argument:
212
212
 
213
213
  ```ts
214
214
  interface User {
@@ -217,7 +217,7 @@ interface User {
217
217
  updatedAt: number;
218
218
  }
219
219
 
220
- const User = struct<User>;
220
+ const User = record<User>;
221
221
 
222
222
  const a = User({
223
223
  id: "alice",
@@ -234,6 +234,34 @@ const b = User({
234
234
  a === b; // true
235
235
  ```
236
236
 
237
+ ## Structs
238
+
239
+ `record` interns whatever shape you hand it — which means any difference in keys or values produces a different object. If you want something more locked-down, where a fixed declared set of keys defines identity, use `struct`.
240
+
241
+ `struct` takes key names as arguments and returns a factory. The factory accepts any object that has at least those keys, extracts exactly those keys, and interns the result.
242
+
243
+ ```ts
244
+ import { struct } from "tuple-ts";
245
+
246
+ const Point = struct("x", "y");
247
+
248
+ const a = Point({ x: 1, y: 2 });
249
+ const b = Point({ x: 1, y: 2 });
250
+
251
+ a === b; // true
252
+ ```
253
+
254
+ Because the schema is fixed at construction time, equality is always based on precisely those keys — no more, no less:
255
+
256
+ ```ts
257
+ const a = Point({ x: 1, y: 2 });
258
+ const b = Point({ x: 1, y: 3 });
259
+
260
+ a === b; // false — y differs
261
+ ```
262
+
263
+ All keys passed to `struct` must be distinct strings. Passing duplicate keys throws a `DuplicateKeyError`.
264
+
237
265
  ## Equality semantics
238
266
 
239
267
  Primitive values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
@@ -286,7 +314,7 @@ In other words, you don't have to manually maintain a global cache of every tupl
286
314
 
287
315
  ## TypeScript first
288
316
 
289
- `tuple` and `struct` are designed to preserve TypeScript's inference:
317
+ `tuple`, `record`, and `struct` are designed to preserve TypeScript's inference:
290
318
 
291
319
  ```ts
292
320
  const value = tuple(
@@ -298,10 +326,10 @@ const value = tuple(
298
326
  // readonly ["hello", 42, true]
299
327
  ```
300
328
 
301
- The library is deliberately small: the runtime API consists of the two constructors you actually need:
329
+ The library is deliberately small: the runtime API consists of three constructors:
302
330
 
303
331
  ```ts
304
- import { tuple, struct } from "tuple-ts";
332
+ import { tuple, record, struct } from "tuple-ts";
305
333
  ```
306
334
 
307
335
  ## When should I use it?
@@ -1 +1 @@
1
- {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AAgHA,eAAO,MAAM,UAAU,GAAI,QAAQ,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,MAAM,KAAG,MACtB,CAAC"}
1
+ {"version":3,"file":"hash.d.ts","sourceRoot":"","sources":["../src/hash.ts"],"names":[],"mappings":"AA4GA,eAAO,MAAM,UAAU,GAAI,QAAQ,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,MAAM,KAAG,MACtB,CAAC"}
package/dist/hash.js CHANGED
@@ -26,8 +26,6 @@ function linearCongruentialGenerator(seed = Date.now()) {
26
26
  };
27
27
  }
28
28
  const randomUint32 = linearCongruentialGenerator();
29
- // const tupleSeed = randomUint32();
30
- // const structSeed = randomUint32();
31
29
  const nullHash = randomUint32();
32
30
  const undefinedHash = randomUint32();
33
31
  const falseHash = randomUint32();
@@ -75,18 +73,18 @@ function bigintToUint32s(value) {
75
73
  function valueHash(value) {
76
74
  switch (typeof value) {
77
75
  case "string":
78
- return mixHashes(stringToUint32s(value), stringSeed);
76
+ return hashMixFn(stringToUint32s(value), stringSeed);
79
77
  case "number":
80
- return mixHashes(numberToUint32s(value), numberSeed);
78
+ return hashMixFn(numberToUint32s(value), numberSeed);
81
79
  case "bigint":
82
- return mixHashes(bigintToUint32s(value), bigIntSeed);
80
+ return hashMixFn(bigintToUint32s(value), bigIntSeed);
83
81
  case "boolean":
84
82
  return value ? trueHash : falseHash;
85
83
  case "symbol":
86
84
  const k = Symbol.keyFor(value);
87
85
  return k === undefined
88
86
  ? stableRandomHash(symbolHashMap, value)
89
- : mixHashes(stringToUint32s(k), registeredSymbolSeed);
87
+ : hashMixFn(stringToUint32s(k), registeredSymbolSeed);
90
88
  case "undefined":
91
89
  return undefinedHash;
92
90
  case "object":
@@ -95,23 +93,5 @@ function valueHash(value) {
95
93
  return stableRandomHash(objectHashMap, value);
96
94
  }
97
95
  }
98
- const mixHashes = murmurHash3Mix_32bit;
99
- export const valuesHash = (values, seed) => mixHashes(Array.from(values, valueHash), seed);
100
- // function valueHashes(values: Iterable<unknown>): Array<number> {
101
- // return Array.from(values, valueHash);
102
- // }
103
- // export function tupleHash(elements: readonly unknown[]): number {
104
- // return mixHashes(valueHashes(elements), tupleSeed);
105
- // }
106
- // function keyAndValueHashes(struct: Record<string, unknown>): Array<number> {
107
- // return Object.keys(struct).sort().flatMap(key => [
108
- // valueHash(key),
109
- // valueHash(struct[key])
110
- // ]);
111
- // }
112
- // export function structHash(struct: Record<string, unknown>): number {
113
- // return mixHashes(
114
- // keyAndValueHashes(struct),
115
- // structSeed
116
- // );
117
- // }
96
+ export const valuesHash = (values, seed) => hashMixFn(Array.from(values, valueHash), seed);
97
+ const hashMixFn = murmurHash3Mix_32bit;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import InternCache from "./intern-cache";
2
2
  export declare const _tupleCache: InternCache<number, readonly unknown[]>;
3
- export declare const _structCache: InternCache<number, Record<string, unknown>>;
4
- export declare const tuple: <const T extends readonly unknown[]>(...t: T) => Readonly<T>;
5
- export declare const struct: <const T extends Record<string, unknown>>({ ...r }: T) => Readonly<T>;
3
+ export declare const _recordCache: InternCache<number, Record<string, unknown>>;
4
+ export declare const tuple: <const T extends readonly unknown[]>(...t: T) => T;
5
+ export declare const record: <const T extends Record<string, unknown>>({ ...r }: T) => T;
6
+ export declare class DuplicateKeyError extends Error {
7
+ readonly name = "DuplicateKeyError";
8
+ constructor();
9
+ }
10
+ export declare const struct: <const K extends Array<string>>(...keys: K) => <const T extends Record<K[number], unknown>>(s: T) => Pick<T, K[number]>;
6
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;AAgCzC,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AA6BzC,eAAO,MAAM,WAAW,yCAIvB,CAAC;AAEF,eAAO,MAAM,YAAY,8CASxB,CAAC;AAEF,eAAO,MAAM,KAAK,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,MAC7C,CAAC;AAErB,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,MACtD,CAAC;AAEtB,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,SAAkB,IAAI,uBAAuB;;CAI9C;AAED,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,MAetD,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,uBAOzD,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import InternCache from "./intern-cache";
2
2
  import * as hash from "./hash";
3
- const TUPLE_SEED = 0x7A3F91C2;
4
- const STRUCT_SEED = 0xE84B26D7;
5
3
  function arrayEquivalent(a1, a2) {
6
4
  if (a2.length !== a1.length)
7
5
  return false;
@@ -11,7 +9,7 @@ function arrayEquivalent(a1, a2) {
11
9
  }
12
10
  return true;
13
11
  }
14
- function structEquivalent(r1, r2) {
12
+ function recordEquivalent(r1, r2) {
15
13
  const r1Keys = Object.keys(r1);
16
14
  const r2Keys = Object.keys(r2);
17
15
  if (r2Keys.length !== r1Keys.length)
@@ -25,7 +23,27 @@ function structEquivalent(r1, r2) {
25
23
  }
26
24
  return true;
27
25
  }
28
- export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent);
29
- export const _structCache = new InternCache((r) => hash.valuesHash(Object.entries(r).flat()), structEquivalent);
30
- export const tuple = (...t) => Object.freeze(_tupleCache.get(t));
31
- export const struct = ({ ...r }) => Object.freeze(_structCache.get(r));
26
+ export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent, (x) => Object.freeze(x));
27
+ export const _recordCache = new InternCache((r) => hash.valuesHash(Object.keys(r)
28
+ .sort()
29
+ .flatMap((k) => [k, r[k]])), recordEquivalent, (x) => Object.freeze(x));
30
+ export const tuple = (...t) => _tupleCache.get(t);
31
+ export const record = ({ ...r }) => _recordCache.get(r);
32
+ export class DuplicateKeyError extends Error {
33
+ constructor() {
34
+ super("All keys must be distinct.");
35
+ this.name = "DuplicateKeyError";
36
+ }
37
+ }
38
+ export const struct = (...keys) => {
39
+ keys.forEach((k) => {
40
+ if (typeof k !== "string") {
41
+ throw new TypeError("All keys must be strings.");
42
+ }
43
+ });
44
+ if (new Set(keys).size !== keys.length) {
45
+ throw new DuplicateKeyError();
46
+ }
47
+ const cache = new InternCache((s) => hash.valuesHash(keys.map((k) => s[k])), (s1, s2) => keys.every((k) => Object.is(s1[k], s2[k])), (r) => Object.fromEntries(keys.map((k) => [k, r[k]])));
48
+ return (s) => cache.get(Object.fromEntries(keys.map((k) => [k, s[k]])));
49
+ };
@@ -1,11 +1,12 @@
1
1
  export default class<H, T extends object> {
2
- private readonly map;
3
2
  private readonly hashFn;
4
- private readonly identityFn;
3
+ private readonly matchFn;
4
+ private readonly injestFn;
5
+ private readonly map;
5
6
  private cleanupRegistry;
6
- constructor(hashFn: (obj: T) => H, identityFn: (obj1: T, obj2: T) => boolean);
7
+ constructor(hashFn: (value: T) => H, matchFn: (key: T, target: T) => boolean, injestFn: <T_ extends T>(value: T_) => T_);
7
8
  private remove;
8
- get<U extends T>(obj: U): U;
9
+ get<T_ extends T>(key: T_): T_;
9
10
  get size(): number;
10
11
  reset(): void;
11
12
  }
@@ -1 +1 @@
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
+ {"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;IAOpC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAR3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,eAAe,CAErB;gBAGiB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EACvB,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,EACvC,QAAQ,EAAE,CAAC,EAAE,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE;IAG5D,OAAO,CAAC,MAAM;IAWd,GAAG,CAAC,EAAE,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE;IAsB9B,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,KAAK,IAAI,IAAI;CAId"}
@@ -1,9 +1,10 @@
1
1
  export default class {
2
- constructor(hashFn, identityFn) {
2
+ constructor(hashFn, matchFn, injestFn) {
3
+ this.hashFn = hashFn;
4
+ this.matchFn = matchFn;
5
+ this.injestFn = injestFn;
3
6
  this.map = new Map();
4
7
  this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
5
- this.hashFn = hashFn;
6
- this.identityFn = identityFn;
7
8
  }
8
9
  remove([hash, ref]) {
9
10
  const hashBucket = this.map.get(hash);
@@ -15,13 +16,13 @@ export default class {
15
16
  this.map.delete(hash);
16
17
  }
17
18
  }
18
- get(obj) {
19
- const hash = this.hashFn(obj);
19
+ get(key) {
20
+ const hash = this.hashFn(key);
20
21
  let hashBucket = this.map.get(hash);
21
22
  if (hashBucket !== undefined) {
22
23
  for (const ref of hashBucket) {
23
24
  const o = ref.deref();
24
- if (o && this.identityFn(o, obj))
25
+ if (o && this.matchFn(key, o))
25
26
  return o;
26
27
  }
27
28
  }
@@ -31,6 +32,7 @@ export default class {
31
32
  this.map.set(hash, hashBucket);
32
33
  }
33
34
  // Object is not in the cache. Add it and return it.
35
+ const obj = this.injestFn(key);
34
36
  const ref = new WeakRef(obj);
35
37
  hashBucket.add(ref);
36
38
  this.cleanupRegistry.register(obj, [hash, ref]);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "tuple-ts",
4
- "version": "0.4.6",
4
+ "version": "0.5.0",
5
5
  "description": "Tuples and structs for TypeScript and JavaScript with equality and identity semantics.",
6
6
  "keywords": [
7
7
  "tuple",