@actualwave/weak-storage
v0.1.1
Published
WeakStorage and WeakRefMap are based on WeakRef objects allowing to store values as weak references.
Readme
@actualwave/weak-storage
A Map-like collection that holds its values as weak references, letting the garbage collector reclaim them when nothing else holds a reference. Built on the WeakRef and FinalizationRegistry APIs.
Two classes are provided:
| Class | Description |
|---|---|
| WeakValueMap<K, V> | Map with weak values. Any key type; iteration skips collected entries. |
| WeakStorage<K, V> | Extends WeakValueMap with a reverse index — look up the key given a value. |
Requirements
| Environment | Minimum version | |---|---| | Node.js | 14.6+ | | Chrome / Edge | 84+ | | Firefox | 79+ | | Safari | 14.1+ |
Both WeakRef and FinalizationRegistry must be available. Check MDN compatibility for the latest data.
Installation
npm install @actualwave/weak-storageQuick Start
import { WeakStorage } from '@actualwave/weak-storage';
const store = new WeakStorage();
const user = { id: 1, name: 'Alice' };
store.set('user:1', user);
store.get('user:1'); // { id: 1, name: 'Alice' }
store.getKey(user); // 'user:1'
store.has('user:1'); // true
// After the variable goes out of scope and GC runs,
// get() returns undefined and iteration skips the entry.WeakValueMap<K, V>
A drop-in replacement for Map where values are weakly referenced. Keys can be anything — strings, numbers, symbols, or objects.
Constructor
new WeakValueMap(autoCleanup?: boolean)| Option | Default | Description |
|---|---|---|
| autoCleanup | true | Registers a FinalizationRegistry that removes stale map entries when a value is collected. Disable for environments without FinalizationRegistry support or for manual cleanup via verify(). |
import { WeakValueMap } from '@actualwave/weak-storage';
// With auto-cleanup (default) — stale keys are removed automatically
const map = new WeakValueMap();
// Without auto-cleanup — use verify() to prune manually
const manual = new WeakValueMap(false);set(key, value): this
Stores a value under key as a weak reference. Returns this for chaining.
map.set('a', { x: 1 })
.set('b', { x: 2 })
.set('c', { x: 3 });get(key): V | undefined
Returns the value, or undefined if the key is missing or the value was collected.
const value = map.get('a'); // { x: 1 } or undefinedhas(key): boolean
Returns true only if the key exists and its value is still alive.
map.has('a'); // true
map.has('missing'); // falsedelete(key): boolean
Removes the entry. Returns true if the key existed.
map.delete('a'); // true
map.delete('a'); // false — already goneclear(): void
Removes all entries.
map.clear();
map.approximateSize; // 0forEach(callback): void
Iterates over live entries only — collected values are silently skipped.
map.forEach((value, key, map) => {
console.log(key, value);
});keys(), values(), entries()
Return IterableIterator instances that skip any entries whose values have been collected.
for (const key of map.keys()) { /* ... */ }
for (const value of map.values()) { /* ... */ }
for (const [key, value] of map.entries()) { /* ... */ }
// Spread also works
const liveKeys = [...map.keys()];approximateSize: number
Returns the number of internal map entries. This may include stale entries that have been collected but whose FinalizationRegistry callback has not yet fired.
map.set('x', { n: 1 });
// After GC but before the finalizer runs:
map.approximateSize; // still 1
map.has('x'); // false — correctly reports the value is goneIf you need an accurate count, call verify() first:
map.verify();
map.approximateSize; // reflects only live entriesverify(): void
Rebuilds the internal map, discarding any entries whose values have been collected. Useful when:
autoCleanupis disabled and you want to reclaim memory explicitly.- You need
approximateSizeto reflect the true live count.
const map = new WeakValueMap(false); // no auto-cleanup
// ... time passes, some values get collected ...
map.verify(); // prune dead entries
console.log(map.approximateSize); // accurate live countWeakStorage<K, V>
Extends WeakValueMap with a reverse index backed by a WeakMap, giving O(1) key lookup from a value. Values must be objects (required by WeakMap).
Inherits all WeakValueMap methods. Additionally:
getKey(value): K | undefined
Returns the key associated with a value, or undefined if the value was never stored or the entry was deleted.
import { WeakStorage } from '@actualwave/weak-storage';
const store = new WeakStorage();
const request = { url: '/api/users' };
store.set('req-1', request);
store.getKey(request); // 'req-1'delete() and clear() both clean up the reverse index:
store.delete('req-1');
store.getKey(request); // undefined
store.clear();
store.getKey(request); // undefinedTypeScript
Both classes are fully generic. The value type V is constrained to object because WeakRef cannot hold primitives.
import { WeakValueMap, WeakStorage } from '@actualwave/weak-storage';
interface Session {
userId: string;
token: string;
}
// Explicit generics
const sessions = new WeakValueMap<string, Session>();
sessions.set('sess-abc', { userId: '1', token: 'xyz' });
const session = sessions.get('sess-abc'); // Session | undefined
// WeakStorage with bidirectional lookup
const store = new WeakStorage<string, Session>();
store.set('sess-abc', { userId: '1', token: 'xyz' });
store.getKey({ userId: '1', token: 'xyz' }); // string | undefinedThe exported interfaces let you substitute custom WeakRef / FinalizationRegistry implementations — useful for testing or non-standard environments:
import type {
IWeakRef,
IWeakRefConstructor,
IFinalizationRegistry,
IFinalizationRegistryConstructor,
} from '@actualwave/weak-storage';How weak references work
A WeakRef wraps an object without keeping it alive. When the JavaScript engine determines that no strong references remain, it may collect the object. Calling ref.deref() afterwards returns undefined.
set('k', obj)
└─ map.set('k', new WeakRef(obj))
└─ finalizer.register(obj, 'k')
get('k')
└─ map.get('k').deref() → obj if alive, undefined if collected
[GC runs, obj is collected]
└─ FinalizationRegistry fires → map.delete('k') (if autoCleanup=true)Key reuse guard
If a key is reused before the finalizer fires for the old value, the cleanup callback checks that the current ref is also dead before deleting. A live replacement value is never evicted:
map.set('k', oldValue);
// oldValue gets collected, but before the finalizer fires:
map.set('k', freshValue);
// finalizer fires for 'k' — but freshValue is alive, so the entry is kept
map.get('k'); // freshValue ✓Caveats
- GC timing is non-deterministic. A collected value may not disappear from
get()immediately after all strong references are dropped — the engine decides when to collect. Never rely on finalizers for correctness-critical logic. approximateSizecan lag. It reflects the raw internal map size, which can include stale entries between GC and finalizer execution. Useverify()+approximateSizewhen an exact count matters.WeakStorage.getKey()does not survive GC. The reverse index is aWeakMap, so when the value is collected, its reverse entry is also automatically removed.- Primitives as values are not supported.
WeakRefcan only wrap objects. Passing a primitive toset()will throw at runtime.
