tuple-ts 0.1.0 → 0.1.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/dist/hash.d.ts +3 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +107 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/intern-cache.d.ts +11 -0
- package/dist/intern-cache.d.ts.map +1 -0
- package/dist/intern-cache.js +43 -0
- package/dist/record.spec.d.ts +2 -0
- package/dist/record.spec.d.ts.map +1 -0
- package/dist/record.spec.js +39 -0
- package/dist/tuple.spec.d.ts +2 -0
- package/dist/tuple.spec.d.ts.map +1 -0
- package/dist/tuple.spec.js +61 -0
- package/package.json +3 -2
- package/.nvmrc +0 -1
- package/jest.config.js +0 -5
- package/src/hash.ts +0 -126
- package/src/index.ts +0 -105
- package/src/intern-cache.ts +0 -46
- package/src/record.spec.ts +0 -50
- package/src/tuple.spec.ts +0 -74
- package/tsconfig.json +0 -18
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +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"}
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const UINT32_BASE = 2n ** 32n;
|
|
2
|
+
function murmurHash3Mix_32bit(keys, seed = 0) {
|
|
3
|
+
// The first (i.e., mixing) stage of the MurmurHash3 algorithm for 32-bit keys
|
|
4
|
+
let hash = seed;
|
|
5
|
+
let keyCount = 0;
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
keyCount++;
|
|
8
|
+
let k = Math.imul(key, 0xcc9e2d51);
|
|
9
|
+
k = (k << 15) | (k >>> 17); // ROTL32(k1, 15)
|
|
10
|
+
k = Math.imul(k, 0x1b873593);
|
|
11
|
+
hash ^= k;
|
|
12
|
+
hash = (hash << 13) | (hash >>> 19); // ROTL32(h1, 13);
|
|
13
|
+
hash = Math.imul(hash, 5) + 0xe6546b64;
|
|
14
|
+
}
|
|
15
|
+
hash ^= keyCount * 4; // number of input bytes
|
|
16
|
+
return hash >>> 0; // unsigned
|
|
17
|
+
}
|
|
18
|
+
function linearConguentialGenerator(seed = Date.now() >>> 0) {
|
|
19
|
+
let state = seed >>> 0; // Force to unsigned 32-bit
|
|
20
|
+
return () => {
|
|
21
|
+
// LCG constants from Numerical Recipes (good default)
|
|
22
|
+
state = (state * 1664525 + 1013904223) >>> 0;
|
|
23
|
+
return state;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const randomUint32 = linearConguentialGenerator();
|
|
27
|
+
const tupleSeed = randomUint32();
|
|
28
|
+
const recordSeed = randomUint32();
|
|
29
|
+
const nullHash = randomUint32();
|
|
30
|
+
const undefinedHash = randomUint32();
|
|
31
|
+
const falseHash = randomUint32();
|
|
32
|
+
const trueHash = randomUint32();
|
|
33
|
+
const numberSeed = randomUint32();
|
|
34
|
+
const bigIntSeed = randomUint32();
|
|
35
|
+
const stringSeed = randomUint32();
|
|
36
|
+
const registeredSymbolSeed = randomUint32();
|
|
37
|
+
const objectHashMap = new WeakMap();
|
|
38
|
+
function stableRandomHash(map, value) {
|
|
39
|
+
if (value === null)
|
|
40
|
+
return nullHash;
|
|
41
|
+
const hash = map.get(value);
|
|
42
|
+
if (hash !== undefined)
|
|
43
|
+
return hash;
|
|
44
|
+
const newHash = randomUint32();
|
|
45
|
+
map.set(value, newHash);
|
|
46
|
+
return newHash;
|
|
47
|
+
}
|
|
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;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function valueHash(value) {
|
|
68
|
+
switch (typeof value) {
|
|
69
|
+
case "string":
|
|
70
|
+
return mixHashes(stringToUint32s(value), stringSeed);
|
|
71
|
+
case "number":
|
|
72
|
+
return mixHashes(numberToUint32s(value), numberSeed);
|
|
73
|
+
case "bigint":
|
|
74
|
+
return mixHashes(bigIntToUint32s(value), bigIntSeed);
|
|
75
|
+
case "boolean":
|
|
76
|
+
return value ? trueHash : falseHash;
|
|
77
|
+
case "symbol":
|
|
78
|
+
const k = Symbol.keyFor(value);
|
|
79
|
+
return k === undefined
|
|
80
|
+
? stableRandomHash(objectHashMap, value)
|
|
81
|
+
: mixHashes(stringToUint32s(k), registeredSymbolSeed);
|
|
82
|
+
case "undefined":
|
|
83
|
+
return undefinedHash;
|
|
84
|
+
case "object":
|
|
85
|
+
return value === null ? nullHash : stableRandomHash(objectHashMap, value);
|
|
86
|
+
case "function":
|
|
87
|
+
return stableRandomHash(objectHashMap, value);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const mixHashes = murmurHash3Mix_32bit;
|
|
91
|
+
function* valueHashes(values) {
|
|
92
|
+
for (const v of values) {
|
|
93
|
+
yield valueHash(v);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function tupleHash(elements) {
|
|
97
|
+
return mixHashes(valueHashes(elements), tupleSeed);
|
|
98
|
+
}
|
|
99
|
+
function* keyAndValueHashes(record) {
|
|
100
|
+
for (const key of Object.keys(record).sort()) {
|
|
101
|
+
yield valueHash(key);
|
|
102
|
+
yield valueHash(record[key]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function recordHash(record) {
|
|
106
|
+
return mixHashes(keyAndValueHashes(record), recordSeed);
|
|
107
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import InternCache from "./intern-cache";
|
|
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>>;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import InternCache from "./intern-cache";
|
|
2
|
+
export { InternCache };
|
|
3
|
+
import * as hash from "./hash";
|
|
4
|
+
function arrayEquivalent(a1, a2) {
|
|
5
|
+
if (a2.length !== a1.length)
|
|
6
|
+
return false;
|
|
7
|
+
for (let i = 0; i < a1.length; i++) {
|
|
8
|
+
if (!Object.is(a2[i], a1[i]))
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
function recordEquivalent(r1, r2) {
|
|
14
|
+
const r1Keys = Object.keys(r1);
|
|
15
|
+
const r2Keys = Object.keys(r2);
|
|
16
|
+
if (r2Keys.length !== r1Keys.length)
|
|
17
|
+
return false;
|
|
18
|
+
for (let i = 0; i < r1Keys.length; i++) {
|
|
19
|
+
const k = r1Keys[i];
|
|
20
|
+
if (!(k in r2))
|
|
21
|
+
return false;
|
|
22
|
+
if (!Object.is(r2[k], r1[k]))
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default class<H, T extends object> {
|
|
2
|
+
private readonly map;
|
|
3
|
+
private readonly hashFn;
|
|
4
|
+
private readonly identityFn;
|
|
5
|
+
private cleanupRegistry;
|
|
6
|
+
constructor(hashFn: (obj: T) => H, identityFn: (obj1: T, obj2: T) => boolean);
|
|
7
|
+
private remove;
|
|
8
|
+
get<U extends T>(obj: U): U;
|
|
9
|
+
get size(): number;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=intern-cache.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export default class {
|
|
2
|
+
constructor(hashFn, identityFn) {
|
|
3
|
+
this.map = new Map();
|
|
4
|
+
this.cleanupRegistry = new FinalizationRegistry(this.remove.bind(this));
|
|
5
|
+
this.hashFn = hashFn;
|
|
6
|
+
this.identityFn = identityFn;
|
|
7
|
+
}
|
|
8
|
+
remove([hash, ref]) {
|
|
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)
|
|
15
|
+
this.map.delete(hash);
|
|
16
|
+
}
|
|
17
|
+
get(obj) {
|
|
18
|
+
const hash = this.hashFn(obj);
|
|
19
|
+
let hashBucket = this.map.get(hash);
|
|
20
|
+
if (hashBucket !== undefined) {
|
|
21
|
+
for (const ref of hashBucket) {
|
|
22
|
+
const o = ref.deref();
|
|
23
|
+
if (o && this.identityFn(o, obj))
|
|
24
|
+
return o;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// This hash has no bucket. Create one.
|
|
29
|
+
hashBucket = new Set();
|
|
30
|
+
this.map.set(hash, hashBucket);
|
|
31
|
+
}
|
|
32
|
+
// Object is not in the cache. Add it and return it.
|
|
33
|
+
const ref = new WeakRef(obj);
|
|
34
|
+
hashBucket.add(ref);
|
|
35
|
+
this.cleanupRegistry.register(obj, [hash, ref]);
|
|
36
|
+
return obj;
|
|
37
|
+
}
|
|
38
|
+
get size() {
|
|
39
|
+
return [...this.map.entries()]
|
|
40
|
+
.map(([_, b]) => b.size)
|
|
41
|
+
.reduce((a, b) => a + b, 0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"record.spec.d.ts","sourceRoot":"","sources":["../src/record.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { record, recordType } from ".";
|
|
2
|
+
describe("record", () => {
|
|
3
|
+
it("preserves keys and values", () => {
|
|
4
|
+
const a = record({ x: 1, y: 2 });
|
|
5
|
+
expect(a).toEqual({ x: 1, y: 2 });
|
|
6
|
+
});
|
|
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 });
|
|
10
|
+
expect(a).toBe(b);
|
|
11
|
+
});
|
|
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);
|
|
17
|
+
});
|
|
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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tuple.spec.d.ts","sourceRoot":"","sources":["../src/tuple.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { tuple, tupleType } from ".";
|
|
2
|
+
test("Empty tuples", () => {
|
|
3
|
+
const a = tuple();
|
|
4
|
+
const b = tuple();
|
|
5
|
+
expect(b).toBe(a);
|
|
6
|
+
});
|
|
7
|
+
test("Indexability", () => {
|
|
8
|
+
const a = {};
|
|
9
|
+
const t = tuple(42, "blah", a);
|
|
10
|
+
expect(t[0]).toBe(42);
|
|
11
|
+
expect(t[1]).toBe("blah");
|
|
12
|
+
expect(t[2]).toBe(a);
|
|
13
|
+
});
|
|
14
|
+
test("Iterability", () => {
|
|
15
|
+
const a = {};
|
|
16
|
+
const t = tuple(42, "blah", a);
|
|
17
|
+
expect(typeof t[Symbol.iterator]).toBe("function");
|
|
18
|
+
expect([...t]).toEqual([42, "blah", a]);
|
|
19
|
+
});
|
|
20
|
+
test("Basic interned equality (if same input, same object)", () => {
|
|
21
|
+
const a = {};
|
|
22
|
+
const f = () => { };
|
|
23
|
+
const t1 = tuple(a, f, 42, "hi", undefined, true, null, 7839278492n);
|
|
24
|
+
const t2 = tuple(a, f, 42, "hi", undefined, true, null, 7839278492n);
|
|
25
|
+
expect(t1).toBe(t2); // same object reference
|
|
26
|
+
});
|
|
27
|
+
test("Referential inequality (same structure, different references)", () => {
|
|
28
|
+
const t1 = tuple({}, 42);
|
|
29
|
+
const t2 = tuple({}, 42);
|
|
30
|
+
expect(t1).not.toBe(t2); // different objects
|
|
31
|
+
});
|
|
32
|
+
test("Different-length tuples are different tuples", () => {
|
|
33
|
+
const t1 = tuple(1, 2);
|
|
34
|
+
const t2 = tuple(1, 2, 3);
|
|
35
|
+
const t3 = tuple(1, 2, undefined);
|
|
36
|
+
expect(t1).not.toBe(t2);
|
|
37
|
+
expect(t1).not.toBe(t3);
|
|
38
|
+
});
|
|
39
|
+
test("Nestable", () => {
|
|
40
|
+
const t1 = tuple("a", tuple("b1", "b2"), "c");
|
|
41
|
+
const t2 = tuple("a", tuple("b1", "b2"), "c");
|
|
42
|
+
const t3 = tuple("a", tuple("b1", "b3"), "c");
|
|
43
|
+
expect(t2).toBe(t1);
|
|
44
|
+
expect(t3).not.toBe(t1);
|
|
45
|
+
});
|
|
46
|
+
test("NaN is equivalent to itself", () => {
|
|
47
|
+
expect(tuple(NaN)).toBe(tuple(NaN));
|
|
48
|
+
});
|
|
49
|
+
test("Positive and negative zero are distinct", () => {
|
|
50
|
+
expect(tuple(+0)).not.toBe(tuple(-0));
|
|
51
|
+
});
|
|
52
|
+
test("Tuple cache entry is cleared when tuple is garbage-collected", async () => {
|
|
53
|
+
const Point = tupleType();
|
|
54
|
+
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);
|
|
61
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "tuple-ts",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"description": "Python-like tuples for JavaScript/TypeScript",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -16,5 +16,6 @@
|
|
|
16
16
|
"jest": "^30.0.0",
|
|
17
17
|
"ts-jest": "^29.4.0",
|
|
18
18
|
"typescript": "^5.8.3"
|
|
19
|
-
}
|
|
19
|
+
},
|
|
20
|
+
"files": ["./dist"]
|
|
20
21
|
}
|
package/.nvmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
22
|
package/jest.config.js
DELETED
package/src/hash.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
const UINT32_BASE = 2n ** 32n;
|
|
2
|
-
|
|
3
|
-
function murmurHash3Mix_32bit(keys: Iterable<number>, seed = 0): number {
|
|
4
|
-
// The first (i.e., mixing) stage of the MurmurHash3 algorithm for 32-bit keys
|
|
5
|
-
let hash = seed;
|
|
6
|
-
let keyCount = 0;
|
|
7
|
-
for (const key of keys) {
|
|
8
|
-
keyCount++;
|
|
9
|
-
let k = Math.imul(key, 0xcc9e2d51);
|
|
10
|
-
k = (k << 15) | (k >>> 17); // ROTL32(k1, 15)
|
|
11
|
-
k = Math.imul(k, 0x1b873593);
|
|
12
|
-
hash ^= k;
|
|
13
|
-
hash = (hash << 13) | (hash >>> 19); // ROTL32(h1, 13);
|
|
14
|
-
hash = Math.imul(hash, 5) + 0xe6546b64;
|
|
15
|
-
}
|
|
16
|
-
hash ^= keyCount * 4; // number of input bytes
|
|
17
|
-
return hash >>> 0; // unsigned
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function linearConguentialGenerator(seed: number = Date.now() >>> 0) {
|
|
21
|
-
let state = seed >>> 0; // Force to unsigned 32-bit
|
|
22
|
-
return () => {
|
|
23
|
-
// LCG constants from Numerical Recipes (good default)
|
|
24
|
-
state = (state * 1664525 + 1013904223) >>> 0;
|
|
25
|
-
return state;
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const randomUint32 = linearConguentialGenerator();
|
|
30
|
-
|
|
31
|
-
const tupleSeed = randomUint32();
|
|
32
|
-
const recordSeed = randomUint32();
|
|
33
|
-
const nullHash = randomUint32();
|
|
34
|
-
const undefinedHash = randomUint32();
|
|
35
|
-
const falseHash = randomUint32();
|
|
36
|
-
const trueHash = randomUint32();
|
|
37
|
-
const numberSeed = randomUint32();
|
|
38
|
-
const bigIntSeed = randomUint32();
|
|
39
|
-
const stringSeed = randomUint32();
|
|
40
|
-
const registeredSymbolSeed = randomUint32();
|
|
41
|
-
|
|
42
|
-
const objectHashMap = new WeakMap<object, number>();
|
|
43
|
-
|
|
44
|
-
function stableRandomHash(
|
|
45
|
-
map: WeakMap<object | symbol, number>,
|
|
46
|
-
value: object | symbol
|
|
47
|
-
): number {
|
|
48
|
-
if (value === null) return nullHash;
|
|
49
|
-
const hash = map.get(value);
|
|
50
|
-
if (hash !== undefined) return hash;
|
|
51
|
-
const newHash = randomUint32();
|
|
52
|
-
map.set(value, newHash);
|
|
53
|
-
return newHash;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function* stringToUint32s(value: string): Generator<number> {
|
|
57
|
-
for (let i = 0; i < value.length; i += 2) {
|
|
58
|
-
const char1 = value.charCodeAt(i);
|
|
59
|
-
const char2 = value.charCodeAt(i + 1);
|
|
60
|
-
yield (char2 << 16) | char1;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function* numberToUint32s(value: number): Generator<number> {
|
|
65
|
-
const buffer = new ArrayBuffer(8); // 8 bytes = 64 bits
|
|
66
|
-
const view = new DataView(buffer);
|
|
67
|
-
view.setFloat64(0, value);
|
|
68
|
-
yield view.getUint32(0);
|
|
69
|
-
yield view.getUint32(4);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function* bigIntToUint32s(value: bigint): Generator<number> {
|
|
73
|
-
for (let n = value; n !== 0n; n /= UINT32_BASE) {
|
|
74
|
-
yield Number(((n % UINT32_BASE) + UINT32_BASE) % UINT32_BASE); // Safely non-negative
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function valueHash(value: unknown): number {
|
|
79
|
-
switch (typeof value) {
|
|
80
|
-
case "string":
|
|
81
|
-
return mixHashes(stringToUint32s(value), stringSeed);
|
|
82
|
-
case "number":
|
|
83
|
-
return mixHashes(numberToUint32s(value), numberSeed);
|
|
84
|
-
case "bigint":
|
|
85
|
-
return mixHashes(bigIntToUint32s(value), bigIntSeed);
|
|
86
|
-
case "boolean":
|
|
87
|
-
return value ? trueHash : falseHash;
|
|
88
|
-
case "symbol":
|
|
89
|
-
const k = Symbol.keyFor(value);
|
|
90
|
-
return k === undefined
|
|
91
|
-
? stableRandomHash(objectHashMap, value)
|
|
92
|
-
: mixHashes(stringToUint32s(k), registeredSymbolSeed);
|
|
93
|
-
case "undefined":
|
|
94
|
-
return undefinedHash;
|
|
95
|
-
case "object":
|
|
96
|
-
return value === null ? nullHash : stableRandomHash(objectHashMap, value);
|
|
97
|
-
case "function":
|
|
98
|
-
return stableRandomHash(objectHashMap, value);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const mixHashes = murmurHash3Mix_32bit;
|
|
103
|
-
|
|
104
|
-
function* valueHashes(values: Iterable<unknown>): Generator<number> {
|
|
105
|
-
for (const v of values) {
|
|
106
|
-
yield valueHash(v);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function tupleHash(elements: readonly unknown[]): number {
|
|
111
|
-
return mixHashes(valueHashes(elements), tupleSeed);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function* keyAndValueHashes(record: Record<string, unknown>): Generator<number> {
|
|
115
|
-
for (const key of Object.keys(record).sort()) {
|
|
116
|
-
yield valueHash(key);
|
|
117
|
-
yield valueHash(record[key]);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function recordHash(record: Record<string, unknown>): number {
|
|
122
|
-
return mixHashes(
|
|
123
|
-
keyAndValueHashes(record),
|
|
124
|
-
recordSeed
|
|
125
|
-
);
|
|
126
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import InternCache from "./intern-cache";
|
|
2
|
-
export { InternCache };
|
|
3
|
-
import * as hash from "./hash";
|
|
4
|
-
|
|
5
|
-
export type Tuple<T extends readonly unknown[] = readonly unknown[]> = T;
|
|
6
|
-
|
|
7
|
-
export type TupleType<T extends Tuple = Tuple> = {
|
|
8
|
-
(...t: T): T;
|
|
9
|
-
_cache: InternCache<number, T>;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type KeyedRecordType<
|
|
13
|
-
R extends Record<string, unknown> = Record<string, unknown>
|
|
14
|
-
> = { (r: R): R; _cache: InternCache<number, R> };
|
|
15
|
-
|
|
16
|
-
export type RecordType<
|
|
17
|
-
R extends Record<string, unknown> = Record<string, unknown>
|
|
18
|
-
> = KeyedRecordType<R> & {
|
|
19
|
-
withKeys: <K extends readonly (keyof R)[]>(
|
|
20
|
-
...keys: K
|
|
21
|
-
) => KeyedRecordType<Pick<R, K[number]>>;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function arrayEquivalent(
|
|
25
|
-
a1: readonly unknown[],
|
|
26
|
-
a2: readonly unknown[]
|
|
27
|
-
): boolean {
|
|
28
|
-
if (a2.length !== a1.length) return false;
|
|
29
|
-
for (let i = 0; i < a1.length; i++) {
|
|
30
|
-
if (!Object.is(a2[i], a1[i])) return false;
|
|
31
|
-
}
|
|
32
|
-
return true;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function recordEquivalent(
|
|
36
|
-
r1: Record<string, unknown>,
|
|
37
|
-
r2: Record<string, unknown>
|
|
38
|
-
): boolean {
|
|
39
|
-
const r1Keys = Object.keys(r1);
|
|
40
|
-
const r2Keys = Object.keys(r2);
|
|
41
|
-
if (r2Keys.length !== r1Keys.length) return false;
|
|
42
|
-
for (let i = 0; i < r1Keys.length; i++) {
|
|
43
|
-
const k = r1Keys[i];
|
|
44
|
-
if (!(k in r2)) return false;
|
|
45
|
-
if (!Object.is(r2[k], r1[k])) return false;
|
|
46
|
-
}
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function tupleType<T extends Tuple>(): TupleType<T> {
|
|
51
|
-
const _cache = new InternCache<number, T>(hash.tupleHash, arrayEquivalent);
|
|
52
|
-
return Object.assign((...t: T) => Object.freeze(_cache.get(t)), {
|
|
53
|
-
_cache,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function recordType<T extends Record<string, unknown>>(): RecordType<T> {
|
|
58
|
-
const _cache = new InternCache<number, T>(hash.recordHash, recordEquivalent);
|
|
59
|
-
return Object.assign(({ ...r }: T): T => Object.freeze(_cache.get(r)), {
|
|
60
|
-
_cache,
|
|
61
|
-
withKeys: <K extends readonly (keyof T)[]>(...keys: K) =>
|
|
62
|
-
recordTypeToKeyedRecordType<T, K>(...keys),
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const tuple = tupleType();
|
|
67
|
-
export const record = recordType();
|
|
68
|
-
|
|
69
|
-
type RecordToTuple<
|
|
70
|
-
R extends Record<string, unknown>,
|
|
71
|
-
K extends readonly (keyof R)[]
|
|
72
|
-
> = K extends readonly [
|
|
73
|
-
infer Head extends keyof R,
|
|
74
|
-
...infer Tail extends readonly (keyof R)[]
|
|
75
|
-
]
|
|
76
|
-
? readonly [R[Head], ...RecordToTuple<R, Tail>]
|
|
77
|
-
: [];
|
|
78
|
-
|
|
79
|
-
function recordToTuple<
|
|
80
|
-
R extends Record<string, unknown>,
|
|
81
|
-
K extends readonly (keyof R)[]
|
|
82
|
-
>(record: R, keys: K): RecordToTuple<R, K> {
|
|
83
|
-
return keys.map((k) => record[k]) as RecordToTuple<R, K>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function recordTypeToKeyedRecordType<
|
|
87
|
-
R extends Record<string, unknown>,
|
|
88
|
-
K extends readonly (keyof R)[]
|
|
89
|
-
>(...keys: K): KeyedRecordType<Pick<R, K[number]>> {
|
|
90
|
-
const _cache = new InternCache<number, Pick<R, K[number]>>(
|
|
91
|
-
(r) => hash.tupleHash(recordToTuple(r, keys)),
|
|
92
|
-
(r1, r2) =>
|
|
93
|
-
arrayEquivalent(recordToTuple(r1, keys), recordToTuple(r2, keys))
|
|
94
|
-
);
|
|
95
|
-
return Object.assign(
|
|
96
|
-
(r: R): R => {
|
|
97
|
-
const r_: Partial<R> = {};
|
|
98
|
-
for (const k of keys) {
|
|
99
|
-
r_[k] = r[k];
|
|
100
|
-
}
|
|
101
|
-
return Object.freeze(_cache.get(r_ as R));
|
|
102
|
-
},
|
|
103
|
-
{ _cache }
|
|
104
|
-
);
|
|
105
|
-
}
|
package/src/intern-cache.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export default class <H, T extends object> {
|
|
2
|
-
private readonly map: Map<H, Set<WeakRef<T>>> = new Map();
|
|
3
|
-
private readonly hashFn: (obj: T) => H;
|
|
4
|
-
private readonly identityFn: (obj1: T, obj2: T) => boolean;
|
|
5
|
-
private cleanupRegistry = new FinalizationRegistry(this.remove.bind(this));
|
|
6
|
-
|
|
7
|
-
constructor(hashFn: (obj: T) => H, identityFn: (obj1: T, obj2: T) => boolean) {
|
|
8
|
-
this.hashFn = hashFn;
|
|
9
|
-
this.identityFn = identityFn;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
private remove([hash, ref]: [H, WeakRef<T>]): void {
|
|
13
|
-
const hashBucket = this.map.get(hash);
|
|
14
|
-
if (hashBucket === undefined) throw new Error("Could not get tuple set");
|
|
15
|
-
if (!hashBucket.delete(ref))
|
|
16
|
-
throw new Error("Could not delete tuple WeakRef");
|
|
17
|
-
if (hashBucket.size === 0) this.map.delete(hash);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
get<U extends T>(obj: U): U {
|
|
21
|
-
const hash = this.hashFn(obj);
|
|
22
|
-
let hashBucket = this.map.get(hash);
|
|
23
|
-
if (hashBucket !== undefined) {
|
|
24
|
-
for (const ref of hashBucket) {
|
|
25
|
-
const o = ref.deref();
|
|
26
|
-
if (o && this.identityFn(o, obj)) return o as U;
|
|
27
|
-
}
|
|
28
|
-
} else {
|
|
29
|
-
// This hash has no bucket. Create one.
|
|
30
|
-
hashBucket = new Set();
|
|
31
|
-
this.map.set(hash, hashBucket);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Object is not in the cache. Add it and return it.
|
|
35
|
-
const ref = new WeakRef<T>(obj);
|
|
36
|
-
hashBucket.add(ref);
|
|
37
|
-
this.cleanupRegistry.register(obj, [hash, ref]);
|
|
38
|
-
return obj;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
get size(): number {
|
|
42
|
-
return [...this.map.entries()]
|
|
43
|
-
.map(([_, b]) => b.size)
|
|
44
|
-
.reduce((a, b) => a + b, 0);
|
|
45
|
-
}
|
|
46
|
-
}
|
package/src/record.spec.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { record, recordType } from ".";
|
|
2
|
-
|
|
3
|
-
describe("record", () => {
|
|
4
|
-
it("preserves keys and values", () => {
|
|
5
|
-
const a = record({ x: 1, y: 2 });
|
|
6
|
-
expect(a).toEqual({ x: 1, y: 2 });
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it("returns the same object for shallow-equal input objects", () => {
|
|
10
|
-
const a = record({ x: 1, y: 2 });
|
|
11
|
-
const b = record({ x: 1, y: 2 });
|
|
12
|
-
expect(a).toBe(b);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("returns different object if keys or values differ", () => {
|
|
16
|
-
const r = record({ x: 1, y: 2 });
|
|
17
|
-
expect(record({ x: 1, y: 3 })).not.toBe(r);
|
|
18
|
-
expect(record({ x: 1, y: 3 })).not.toBe(r);
|
|
19
|
-
expect(record({ x: 1, y: 2, z: 3 })).not.toBe(r);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("Keyed record type", () => {
|
|
24
|
-
const Person = recordType<{
|
|
25
|
-
name: string;
|
|
26
|
-
age: number;
|
|
27
|
-
height: number;
|
|
28
|
-
}>().withKeys("name", "age");
|
|
29
|
-
|
|
30
|
-
it("interns on only specified keys and values", () => {
|
|
31
|
-
const a = { name: "Bob", age: 42, sex: "male" };
|
|
32
|
-
const b = { name: "Bob", age: 42, occupation: "teacher" };
|
|
33
|
-
expect(Person(a)).toBe(Person(b));
|
|
34
|
-
expect(Person(a)).toEqual({ name: "Bob", age: 42 });
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("returns different object if keys or values differ", () => {
|
|
38
|
-
const r = Person({ name: "Tom", age: 42 });
|
|
39
|
-
expect({ x: 1, y: 3 }).not.toBe(r);
|
|
40
|
-
expect({ x: 1, y: 3 }).not.toBe(r);
|
|
41
|
-
expect({ x: 1, y: 2, z: 3 }).not.toBe(r);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("respects inherited attributes", () => {
|
|
45
|
-
const proto = {name: "Bob"};
|
|
46
|
-
const a = Object.create(proto);
|
|
47
|
-
a.age = 42;
|
|
48
|
-
expect(Person(a)).toBe(Person({name: "Bob", age: 42}));
|
|
49
|
-
});
|
|
50
|
-
});
|
package/src/tuple.spec.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { tuple, tupleType } from ".";
|
|
2
|
-
|
|
3
|
-
test("Empty tuples", () => {
|
|
4
|
-
const a = tuple();
|
|
5
|
-
const b = tuple();
|
|
6
|
-
expect(b).toBe(a);
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
test("Indexability", () => {
|
|
10
|
-
const a = {};
|
|
11
|
-
const t = tuple(42, "blah", a);
|
|
12
|
-
expect(t[0]).toBe(42);
|
|
13
|
-
expect(t[1]).toBe("blah");
|
|
14
|
-
expect(t[2]).toBe(a);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("Iterability", () => {
|
|
18
|
-
const a = {};
|
|
19
|
-
const t = tuple(42, "blah", a);
|
|
20
|
-
expect(typeof t[Symbol.iterator]).toBe("function");
|
|
21
|
-
expect([...t]).toEqual([42, "blah", a]);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("Basic interned equality (if same input, same object)", () => {
|
|
25
|
-
const a = {};
|
|
26
|
-
const f = () => {};
|
|
27
|
-
const t1 = tuple(a, f, 42, "hi", undefined, true, null, 7839278492n);
|
|
28
|
-
const t2 = tuple(a, f, 42, "hi", undefined, true, null, 7839278492n);
|
|
29
|
-
expect(t1).toBe(t2); // same object reference
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("Referential inequality (same structure, different references)", () => {
|
|
33
|
-
const t1 = tuple({}, 42);
|
|
34
|
-
const t2 = tuple({}, 42);
|
|
35
|
-
expect(t1).not.toBe(t2); // different objects
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("Different-length tuples are different tuples", () => {
|
|
39
|
-
const t1 = tuple(1, 2);
|
|
40
|
-
const t2 = tuple(1, 2, 3);
|
|
41
|
-
const t3 = tuple(1, 2, undefined);
|
|
42
|
-
expect(t1).not.toBe(t2);
|
|
43
|
-
expect(t1).not.toBe(t3);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("Nestable", () => {
|
|
47
|
-
const t1 = tuple("a", tuple("b1", "b2"), "c");
|
|
48
|
-
const t2 = tuple("a", tuple("b1", "b2"), "c");
|
|
49
|
-
const t3 = tuple("a", tuple("b1", "b3"), "c");
|
|
50
|
-
expect(t2).toBe(t1);
|
|
51
|
-
expect(t3).not.toBe(t1);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("NaN is equivalent to itself", () => {
|
|
55
|
-
expect(tuple(NaN)).toBe(tuple(NaN));
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("Positive and negative zero are distinct", () => {
|
|
59
|
-
expect(tuple(+0)).not.toBe(tuple(-0));
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("Tuple cache entry is cleared when tuple is garbage-collected", async () => {
|
|
63
|
-
const Point = tupleType<[number, number]>();
|
|
64
|
-
|
|
65
|
-
Point(15, 42);
|
|
66
|
-
expect(Point._cache.size).toBe(1);
|
|
67
|
-
|
|
68
|
-
// Trigger garbage collection (requires node --expose-gc)
|
|
69
|
-
await new Promise((res) => setTimeout(res, 100));
|
|
70
|
-
(global as any).gc();
|
|
71
|
-
await new Promise((res) => setTimeout(res, 100));
|
|
72
|
-
|
|
73
|
-
expect(Point._cache.size).toBe(0);
|
|
74
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020", // supports WeakRef, FinalizationRegistry natively
|
|
4
|
-
"module": "esnext", // or "CommonJS" if you want CommonJS output
|
|
5
|
-
"declaration": true, // generate .d.ts files
|
|
6
|
-
"declarationMap": true,
|
|
7
|
-
"strict": true, // enable strict type checks
|
|
8
|
-
"noFallthroughCasesInSwitch": true,
|
|
9
|
-
"esModuleInterop": true, // allow default imports from CommonJS modules
|
|
10
|
-
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
"skipLibCheck": true, // speed up build by skipping lib .d.ts checks
|
|
12
|
-
"moduleResolution": "node", // resolve modules like Node.js
|
|
13
|
-
"outDir": "./dist", // output directory for compiled files
|
|
14
|
-
"lib": ["ES2023", "ESNext.Iterator"]
|
|
15
|
-
},
|
|
16
|
-
"files": ["src/index.ts"],
|
|
17
|
-
"include": ["src/*.test.ts", "src/*.spec.ts"],
|
|
18
|
-
}
|