node/lib/internal/source_map/source_map_cache_map.js
Chengzhong Wu 2d3b4a8d65
test: print stack immediately avoiding GC interleaving
`require(mod)` does not keep the mod in require cache if
mod throws synchronously. This fixes the tests to print
the stack immediately in case that source map cache could
be cleaned up when the CJS module is reclaimed by GC in
the next event loop tick.

PR-URL: https://github.com/nodejs/node/pull/61699
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
2026-02-22 23:16:20 +01:00

121 lines
3.8 KiB
JavaScript

'use strict';
const {
ArrayPrototypeForEach,
ObjectFreeze,
SafeFinalizationRegistry,
SafeMap,
SafeWeakRef,
SymbolIterator,
} = primordials;
const {
privateSymbols: {
source_map_data_private_symbol,
},
} = internalBinding('util');
let debug = require('internal/util/debuglog').debuglog('source_map', (fn) => {
debug = fn;
});
/**
* Specialized map of WeakRefs to module instances that caches source map
* entries by `filename` and `sourceURL`. Cached entries can be iterated with
* `for..of` syntax.
*
* The cache map maintains the cache entries by:
* - `weakModuleMap`(Map): a strong sourceURL -> WeakRef(Module),
* - WeakRef(Module[source_map_data_private_symbol]): source map data.
*
* Obsolete `weakModuleMap` entries are removed by the `finalizationRegistry`
* callback. This pattern decouples the strong url reference to the source map
* data and allow the cache to be reclaimed eagerly, without depending on an
* indeterministic callback of a finalization registry.
*/
class SourceMapCacheMap {
/**
* @type {Map<string, WeakRef<*>>}
* The cached module instance can be removed from the global module registry
* with approaches like mutating `require.cache`.
* The `weakModuleMap` exposes entries by `filename` and `sourceURL`.
* In the case of mutated module registry, obsolete entries are removed from
* the cache by the `finalizationRegistry`.
*/
#weakModuleMap = new SafeMap();
#cleanup = ({ keys }) => {
// Delete the entry if the weak target has been reclaimed.
// If the weak target is not reclaimed, the entry was overridden by a new
// weak target.
ArrayPrototypeForEach(keys, (key) => {
const ref = this.#weakModuleMap.get(key);
if (ref && ref.deref() === undefined) {
debug(`Cleanup obsolete source map cache entry with key: ${key}`);
this.#weakModuleMap.delete(key);
}
});
};
#finalizationRegistry = new SafeFinalizationRegistry(this.#cleanup);
/**
* Sets the value for the given key, associated with the given module
* instance.
* @param {string[]} keys array of urls to index the value entry.
* @param {*} sourceMapData the value entry.
* @param {object} moduleInstance an object that can be weakly referenced and
* invalidate the [key, value] entry after this object is reclaimed.
*/
set(keys, sourceMapData, moduleInstance) {
const weakRef = new SafeWeakRef(moduleInstance);
ArrayPrototypeForEach(keys, (key) => this.#weakModuleMap.set(key, weakRef));
moduleInstance[source_map_data_private_symbol] = sourceMapData;
this.#finalizationRegistry.register(moduleInstance, { keys });
}
/**
* Get an entry by the given key.
* @param {string} key a file url or source url
* @returns {object|undefined}
*/
get(key) {
const weakRef = this.#weakModuleMap.get(key);
const moduleInstance = weakRef?.deref();
if (moduleInstance === undefined) {
return;
}
return moduleInstance[source_map_data_private_symbol];
}
/**
* Estimate the size of the cache. The actual size may be smaller because
* some entries may be reclaimed with the module instance.
* @returns {number}
*/
get size() {
return this.#weakModuleMap.size;
}
[SymbolIterator]() {
const iterator = this.#weakModuleMap.entries();
const next = () => {
const result = iterator.next();
if (result.done) return result;
const { 0: key, 1: weakRef } = result.value;
const moduleInstance = weakRef.deref();
if (moduleInstance == null) return next();
const value = moduleInstance[source_map_data_private_symbol];
return { done: false, value: [key, value] };
};
return {
[SymbolIterator]() { return this; },
next,
};
}
}
ObjectFreeze(SourceMapCacheMap.prototype);
module.exports = {
SourceMapCacheMap,
};