ecluse:ecluse-core
Safe HaskellNone
LanguageGHC2021

Ecluse.Core.Registry.Npm.Metadata

Description

The npm realization of the serve-path read operations: fetch a package's full packument and project it into the domain manifest, reporting every failure as a typed MetadataError.

npm satisfies both serve-path needs from the same full-packument endpoint: the publish-age rules require the packument's time map, which npm exposes only in the full form, so even the single-version need fetches the full bytes. This module owns the npm side of both serve-path operations -- the fetch and the projection -- and the constructor (newNpmMetadataClient) that leads them into the serve layer's agnostic caching, metrics, and failure-log policy (Ecluse.Core.Server.Metadata), which is where the cross-cutting caching policy belongs:

  • fetchNpmManifest / projectNpmManifest back the full-manifest operation. The projection is the sequence the serve path has always applied to a fetched packument -- decode, bound the nesting depth, project and validate the self-reported name, bound the version count -- re-expressed as a total Either so the serve path maps each cause onto a response rather than catching a typed throw.
  • fetchNpmVersion / projectNpmVersion back the single-version operation. The full bytes are still fetched (npm carries time only in the full form), but they are parsed selectively (Ecluse.Core.Registry.Npm.SelectiveDecode): only the requested version's object and time entry are materialised, the others skipped unallocated, so a cold tarball gate no longer pays a whole-packument decode to consult one version. The selected version is projected through the same per-version code the full path runs, so its PackageDetails is identical to selecting it out of a full projection -- the optimization the stable boundary was always meant to admit.
Synopsis

Per-request read handle

newNpmMetadataClient :: TracingPort -> MetricsPort -> Upstream -> ManifestCaching -> (PackageName -> MetadataError -> IO ()) -> (PackageName -> [InvalidEntry] -> IO ()) -> (PackageName -> IO ()) -> NpmClientConfig -> MetadataClient Source #

Build a per-request read handle for the npm protocol over one origin's fetch configuration: the npm full-manifest and single-version fetches as the raw primitives, with the serve-path caching, metrics, and the failure and dropped-entry logs wired by newMetadataClient.

npm full-manifest fetch

fetchNpmManifest :: TracingPort -> NpmClientConfig -> PackageName -> IO (Either MetadataError Manifest) Source #

Fetch a package's full packument and project it into a Manifest (typed view, raw document, and the wire bytes' ContentDigest), or the typed MetadataError for why it could not.

The body is read bounded against the config's response budget (so an oversized upstream is refused fail-closed before it is buffered whole); any fetch fault -- a response-bound breach, an unformable upstream URL, or an unreachable upstream -- is threaded straight onto its MetadataError by the shared fetchFaultError fold. Total by type: this Either carries every outcome the serve path renders, the transport channel included.

The digest is computed here, over the strict body the bounded read already produced: the one place the wire bytes exist, so no later stage re-encodes the document just to fingerprint it.

npm single-version fetch

fetchNpmVersion :: TracingPort -> NpmClientConfig -> PackageName -> Version -> IO (Either MetadataError (Maybe PackageDetails)) Source #

Fetch a package's full packument and project only the requested version into its PackageDetails, or the typed MetadataError for why it could not -- the cheap counterpart to fetchNpmManifest for the single-version serve operation.

npm carries the time map only in the full document, so the __full bytes are still fetched__ (bounded against the config's budget, exactly as fetchNpmManifest); the win is that they are parsed selectively (projectNpmVersion), materialising the one requested version rather than every version. A Nothing is a version genuinely absent from a sound document (a forwarded miss); a MetadataError is metadata that could not be obtained at all, an unreachable upstream included, exactly as fetchNpmManifest.

Pure projection

projectNpmManifest :: Limits -> PackageName -> ByteString -> Either MetadataError (PackageInfo, Value) Source #

Project a fetched packument's bytes into (manifest, raw document), applying the serve path's response bounds and name validation. Pure and total.

The sequence -- decode to a Value, bound its nesting depth, project the typed PackageInfo and validate its self-reported name against the request, then bound the version count -- is the one the serve path has always run; the raw Value returned is the nesting-checked document the serve path edits in place, so the typed view and the served bytes describe the same parse. Each refusal maps to the constructor the serve path renders: a decode failure or an absent/undecodable name is MetadataUndecodable; a self-reported different name is MetadataNameMismatch; a nesting-depth or version-count breach is MetadataBoundExceeded.

projectNpmVersion :: Limits -> PackageName -> Version -> ByteString -> Either MetadataError (Maybe PackageDetails) Source #

Project a fetched packument's bytes into one version's PackageDetails (or the typed MetadataError), without decoding the other versions. Pure and total.

The outcome is the same the whole-document path would reach for that one version, computed selectively: selectVersionFromPackument walks the token stream -- depth-bounding every value (the maxNestingDepth ceiling projectNpmManifest applies through checkNestingDepth) and reporting malformed JSON as MetadataUndecodable -- and materialises only the document name, the requested version's object, and its time entry. Those are then validated and projected exactly as projectNpmManifest would:

  • the self-reported name is validated against the request -- an absent/undecodable name is MetadataUndecodable, a self-reported different name is MetadataNameMismatch (the anti-shadowing distinction);
  • the versions count is bounded against maxVersionCount (the raw entry count -- a fail-closed defence-in-depth backstop on this path, which evaluates only the one version regardless, so it never needs the projected count the full path bounds);
  • the requested version's object is projected through projectVersionEntry -- the same per-version projection the full path runs -- so a present version yields a PackageDetails identical to lookup-ing it out of a full projectNpmManifest, and an absent/unprojectable version yields Nothing.