tuple-ts 0.4.7 → 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 +43 -15
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -5
- package/dist/intern-cache.d.ts +5 -4
- package/dist/intern-cache.d.ts.map +1 -1
- package/dist/intern-cache.js +8 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
-
##
|
|
166
|
+
## Records
|
|
167
167
|
|
|
168
|
-
`
|
|
168
|
+
`record` provides the same value-based identity semantics for objects:
|
|
169
169
|
|
|
170
170
|
```ts
|
|
171
|
-
import {
|
|
171
|
+
import { record } from "tuple-ts";
|
|
172
172
|
|
|
173
|
-
const a =
|
|
173
|
+
const a = record({
|
|
174
174
|
id: 1,
|
|
175
175
|
role: "admin",
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
const b =
|
|
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
|
-
|
|
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
|
-
|
|
200
|
+
record({ id: 42, role: "admin" }),
|
|
201
201
|
"Alice",
|
|
202
202
|
);
|
|
203
203
|
|
|
204
204
|
users.get(
|
|
205
|
-
|
|
205
|
+
record({ role: "admin", id: 42 }),
|
|
206
206
|
); // "Alice"
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
### Typed
|
|
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 =
|
|
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
|
|
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?
|
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
|
|
4
|
-
export declare const tuple: <const T extends readonly unknown[]>(...t: T) =>
|
|
5
|
-
export declare const
|
|
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
|
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;AA6BzC,eAAO,MAAM,WAAW,
|
|
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
|
@@ -9,7 +9,7 @@ function arrayEquivalent(a1, a2) {
|
|
|
9
9
|
}
|
|
10
10
|
return true;
|
|
11
11
|
}
|
|
12
|
-
function
|
|
12
|
+
function recordEquivalent(r1, r2) {
|
|
13
13
|
const r1Keys = Object.keys(r1);
|
|
14
14
|
const r2Keys = Object.keys(r2);
|
|
15
15
|
if (r2Keys.length !== r1Keys.length)
|
|
@@ -23,7 +23,27 @@ function structEquivalent(r1, r2) {
|
|
|
23
23
|
}
|
|
24
24
|
return true;
|
|
25
25
|
}
|
|
26
|
-
export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent);
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
};
|
package/dist/intern-cache.d.ts
CHANGED
|
@@ -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
|
|
3
|
+
private readonly matchFn;
|
|
4
|
+
private readonly injestFn;
|
|
5
|
+
private readonly map;
|
|
5
6
|
private cleanupRegistry;
|
|
6
|
-
constructor(hashFn: (
|
|
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<
|
|
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;
|
|
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"}
|
package/dist/intern-cache.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export default class {
|
|
2
|
-
constructor(hashFn,
|
|
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(
|
|
19
|
-
const hash = this.hashFn(
|
|
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.
|
|
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]);
|