Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 43 additions & 35 deletions lib/Resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -758,52 +758,60 @@ class Resolver {
doResolve(hook, request, message, resolveContext, callback) {
const stackEntry = Resolver.createStackEntry(hook, request);

/** @type {Set<string> | undefined} */
let newStack;
if (resolveContext.stack) {
newStack = new Set(resolveContext.stack);
if (resolveContext.stack.has(stackEntry)) {
/**
* Prevent recursion
* @type {Error & { recursion?: boolean }}
*/
const recursionError = new Error(
`Recursion in resolving\nStack:\n ${[...newStack].join("\n ")}`,
);
recursionError.recursion = true;
if (resolveContext.log) {
resolveContext.log("abort resolving because of recursion");
}
return callback(recursionError);
// Recursion guard: each top-level `resolve()` call shares one Set across
// the whole pipeline. We add the current frame on the way down and
// remove it on the way up, so the Set only ever contains the live path
// from the root resolve down to the current hook. Avoids the
// O(depth^2) Set cloning that the previous "snapshot per call"
// implementation incurred on every doResolve.
let { stack } = resolveContext;
if (stack === undefined) {
stack = new Set();
resolveContext.stack = stack;
} else if (stack.has(stackEntry)) {
/**
* Prevent recursion
* @type {Error & { recursion?: boolean }}
*/
const recursionError = new Error(
`Recursion in resolving\nStack:\n ${[...stack, stackEntry].join("\n ")}`,
);
recursionError.recursion = true;
if (resolveContext.log) {
resolveContext.log("abort resolving because of recursion");
}
newStack.add(stackEntry);
} else {
// creating a set with new Set([item])
// allocates a new array that has to be garbage collected
// this is an EXTREMELY hot path, so let's avoid it
newStack = new Set();
newStack.add(stackEntry);
return callback(recursionError);
}
stack.add(stackEntry);
this.hooks.resolveStep.call(hook, request);

if (hook.isUsed()) {
const innerContext = createInnerContext(
{
log: resolveContext.log,
yield: resolveContext.yield,
fileDependencies: resolveContext.fileDependencies,
contextDependencies: resolveContext.contextDependencies,
missingDependencies: resolveContext.missingDependencies,
stack: newStack,
},
message,
);
// Hot path: when there is no log, the inner context is identical to
// the parent context (stack is shared via mutation), so we forward
// it directly and avoid two object allocations + a closure per
// doResolve call. Only the logging path needs createInnerContext to
// install the deferred-header wrapper.
const innerContext = resolveContext.log
? createInnerContext(
{
log: resolveContext.log,
yield: resolveContext.yield,
fileDependencies: resolveContext.fileDependencies,
contextDependencies: resolveContext.contextDependencies,
missingDependencies: resolveContext.missingDependencies,
stack,
},
message,
)
: resolveContext;
return hook.callAsync(request, innerContext, (err, result) => {
stack.delete(stackEntry);
if (err) return callback(err);
if (result) return callback(null, result);
callback();
});
}
stack.delete(stackEntry);
callback();
}

Expand Down
Loading