Skip to content

Commit a7427ad

Browse files
committed
fix: trying to evict unused primitive refs
Using FinalizationRegistry to try to garbage collected unused primitive refs, to better memory usage of tuple function
1 parent eaa284e commit a7427ad

4 files changed

Lines changed: 58 additions & 40 deletions

File tree

libs/js-tuple/src/get-ref.ts

Lines changed: 0 additions & 35 deletions
This file was deleted.

libs/js-tuple/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export * from './get-ref';
21
export * from './nested-map';
32
export * from './nested-set';
43
export * from './tuple';
4+
export * from './types';

libs/js-tuple/src/tuple.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getRef, Reference } from './get-ref';
1+
import { RefInfo } from './types';
22

33
const objectMap = Symbol('objectMap');
44
const value = Symbol('value');
@@ -16,6 +16,52 @@ function createCacheNode(): CacheNode {
1616

1717
const EMPTY_TUPLE: unknown = Object.freeze([]);
1818
const tupleCache: CacheNode = createCacheNode();
19+
const primitives = new Map<unknown, RefInfo>();
20+
export type Reference = object & { [value]: Reference };
21+
22+
/**
23+
* Gets an object reference for any value, enabling it to be used as a WeakMap key.
24+
*
25+
* For objects (including arrays, functions, etc.), returns the object itself.
26+
* For primitives (string, number, boolean, null, undefined, symbol, bigint),
27+
* returns a cached wrapper object that represents that primitive value.
28+
*
29+
* This function ensures that the same primitive value always gets the same wrapper object,
30+
* enabling primitives to be used consistently in WeakMap-based caching scenarios.
31+
*
32+
* @param v - Any value to get a reference for
33+
* @returns An object that can be used as a WeakMap key
34+
*
35+
* @example
36+
* ```typescript
37+
* const ref1 = getRef(42);
38+
* const ref2 = getRef(42);
39+
* console.log(ref1 === ref2); // true - same wrapper for same primitive
40+
*
41+
* const obj = {};
42+
* const ref3 = getRef(obj);
43+
* console.log(ref3 === obj); // true - objects are returned as-is
44+
* ```
45+
*/
46+
export function getRef(v: unknown, basics?: RefInfo[]): Reference {
47+
if (typeof v === 'object' && v !== null) return v as Reference;
48+
let el = primitives.get(v);
49+
if (!el) primitives.set(v, (el = { ref: { [value]: v }, count: 0 }));
50+
basics?.push(el);
51+
return el.ref as Reference;
52+
}
53+
54+
const primitiveRegistry = new FinalizationRegistry((heldValue) => {
55+
const current = primitives.get(heldValue);
56+
if (!current || current.count <= 1) primitives.delete(heldValue);
57+
else current.count--;
58+
});
59+
function registerUsage(basics: RefInfo[], result: object) {
60+
basics.forEach((x) => {
61+
x.count++;
62+
primitiveRegistry.register(result, x.ref);
63+
});
64+
}
1965

2066
/**
2167
* Creates a cached, immutable tuple from an array of elements.
@@ -66,9 +112,10 @@ export function tuple<T extends Readonly<Array<unknown>>>(
66112
if (!length) return EMPTY_TUPLE as Readonly<T>;
67113

68114
let current: CacheNode = tupleCache;
115+
const basics: RefInfo[] = [];
69116

70117
for (let i = 0; i < length; ++i) {
71-
const el = getRef(elements[i]);
118+
const el = getRef(elements[i], basics);
72119

73120
const map = current[objectMap];
74121
let node = map.get(el);
@@ -81,11 +128,12 @@ export function tuple<T extends Readonly<Array<unknown>>>(
81128

82129
let ref = current[value];
83130
let result: Readonly<T> | undefined;
84-
if (ref) result = ref.deref() as Readonly<T>;
85-
else {
131+
if (ref) result = ref.deref() as Readonly<T> | undefined;
132+
if (!result) {
86133
current[value] = ref = new WeakRef(
87134
(result = Object.freeze(elements.slice()) as Readonly<T>),
88135
);
136+
registerUsage(basics, result);
89137
}
90138

91139
return result;

libs/js-tuple/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ export type IterationOptions<K> = {
2929
traverseMode?: TraverseMode;
3030
yieldMode?: YieldMode;
3131
};
32+
33+
export type RefInfo = {
34+
ref: object;
35+
count: number;
36+
};

0 commit comments

Comments
 (0)