Skip to content

Resolution: a shared package's children context is decided by an async race (nondeterministic lockfiles) #12358

Description

@zkochan

Summary

The context under which a shared package's children are resolved (and its missing-peer report computed) is decided by whichever occurrence completes resolution first — an async race shaped by network/cache timing. This is the last source of resolution nondeterminism surfaced by the pacquet lockfile-parity work (#12266) and the common root of several user-visible flakes.

Mechanism

  • ctx.resolvedPkgsById[pkgId] is claimed by the first occurrence to finish (resolveDependencies.ts#L1631-L1635); only that occurrence runs resolveChildren, under its per-level preferredVersions overlay and alias chain.
  • ctx.missingPeersOfChildrenByPkgId[pkgId] records the subtree's missing-peer report once, computed under the winner's chain (#L1703-L1725); revisits await the shared promise.
  • Which occurrence wins depends on packument-fetch completion order. The race is structurally biased toward the shallowest occurrence (its request starts RTTs earlier), but near-ties flip with cache state.

Observable symptoms

Why it isn't a quick fix

A correct deterministic winner needs claim takeover: when a deterministically-better occurrence arrives after a worse one already claimed, the children must be re-resolved under the better context. Two structural blockers:

  1. The claim's outputs are structure-shared across all occurrences — the missingPeersOfChildren promise may already be consumed by importer-level hoist logic by the time the better occurrence arrives, and revisit occurrences realize their tree structure from the first walk's children.
  2. The code already notes the constraint: "There might be a better way to hoist peer dependencies during resolution but it would probably require a big rewrite of the resolution algorithm" (#L1705-L1707).

Proposed direction

Define the winner as the smallest (depth, importer order, parent path) occurrence — matching the de-facto shallowest-wins outcome so lockfile churn from the change is minimal — with redo machinery for the rare out-of-order arrival (a deep occurrence completing before a shallow one). pacquet would adopt the identical rule (its sequential rounds make the redo simpler); pnpm's half is the substantial part: re-running resolveChildren under the new context, replacing the per-pkgId record, and keeping already-realized occurrences consistent.

Until then, pacquet's claimer (first importer in sorted order, then walk order — deterministic) and pnpm's (race) can disagree on context-sensitive picks; on the pnpm monorepo that is currently a single lockfile line.

Surfaced while working #12266; the deterministic barrier alignment for hoist rounds landed in #12357.


Written by an agent (Claude Code, claude-fable-5).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions