Commits on Source 4

  • Matt Cockayne's avatar
    feat: offer the connection-injection rungs through the registry · da859ecc
    Matt Cockayne authored
    Adds forge.WithHTTPTransport and forge.WithHTTPClient, so a consumer that
    has already assembled an HTTP stack can hand it to a provider built
    through the blank-import path. Additive: ProviderFactory's signature is
    unchanged, because Option was already varargs on it.
    
    Two rungs rather than one, because they differ in who owns the redirect
    policy — a security question rather than a matter of taste.
    
    WithHTTPTransport shares the expensive part. The connection pool and TLS
    session cache both live in the transport, so several providers stop
    opening their own connections to a host the consumer is already talking
    to, while each still builds its own client and keeps its own redirect and
    sensitive-header policy. A provider that attaches a credential by hand
    depends on that policy to stop the credential following a redirect off
    the host it pinned.
    
    WithHTTPClient hands over the whole client and that policy with it. It is
    documented as transferring the obligation rather than merely the object,
    and the docs say to prefer the transport.
    
    This package builds neither, and cannot: depfootprint_test.go forbids
    go/httpclient here so the contract stays cheap for anyone merely
    authoring a provider. The options carry standard-library types and each
    provider composes its own policy over what arrives. The root package's
    dependency graph is unchanged.
    
    A nil is ignored rather than treated as an instruction to unset, and
    WithHTTPTransport ignores a TYPED nil too. That is not defensive tidying:
    http.RoundTripper is an interface, so a nil *http.Transport inside one is
    not equal to nil, would sail past the check a provider would naturally
    write, and would panic at the first request far from the call that
    introduced it. staticcheck rejecting that comparison as "never true" is
    the evidence, and the test says so rather than asserting it at runtime.
    
    Implements forge spec 0008 D4.
    da859ecc
  • Matt Cockayne's avatar
    feat: reuse one provider per endpoint with forge/pool · f90a5693
    Matt Cockayne authored
    Adds forge/pool, so several components asking for the same forge get one
    provider — and one connection pool, and one credential resolution —
    between them rather than one each.
    
    The state machine is NOT written here. org 0003 P-4 places it once, in
    go/clientlifecycle, and this package uses its invalidatable strategy and
    owns only the keying: a map from Endpoint to one resolver each. Writing
    another copy of that concurrency is the thing that module exists to
    prevent.
    
    A pool is a value a consumer constructs, holds and passes, never package
    state, so sharing is visible in the wiring and a component handed nothing
    shares nothing. The configuration is fixed at New rather than passed per
    call: a pool answers from a memo, so a per-call configuration would be
    read on the first call and silently ignored on every later one — a
    parameter that sometimes does nothing, written into the signature.
    
    Source returns an accessor bound to one endpoint, and that is what a
    component should be given. Handing over a resolved provider would let the
    wiring code choose when resolution happens, and therefore when it fails,
    for every component at once. Handing over the pool would give a component
    reach over endpoints that are not its business. Consumers declare the
    one-method interface themselves, so the zero-conf fallback — a closure
    over forge.Lookup — satisfies the same shape and needs no second code
    path through the component.
    
    Resolve exists so a command can fail at its own boundary rather than one
    operation into work that appeared to start cleanly.
    
    Documented, and load-bearing: a pool is scoped to a UNIT OF WORK, not a
    process. Every adapter here captures its credential at construction and
    holds the string; nothing refreshes it and nothing can. Reuse therefore
    extends a captured credential's life from one component's work to the
    pool's. The transport is what carries connection reuse across cycles, and
    a transport holds no credential, so a daemon builds the transport once and
    the pool per cycle.
    
    Invalidate is documented as gated on asserted credential-invalidity, and
    the documentation says plainly that this module does not yet surface such
    errors — invalidating on a rate limit fans concurrent resolutions at an
    API already throttling, which is how a throttle becomes a lockout.
    
    Cost is one module, clientlifecycle, which has none of its own.
    TestPoolAddsExactlyOneModule measures that on the package rather than the
    module, because ./pool is what a consumer inherits and ./... would fold
    every sibling into one graph and prove nothing about the split.
    
    Implements forge spec 0008 D6 and D12.
    f90a5693
  • Matt Cockayne's avatar
    feat: name the refusal a forge returned · dfe237a1
    Matt Cockayne authored
    Adds ErrUnauthorized, ErrForbidden and ErrRateLimited, so a caller can
    tell which refusal it received and respond differently to each. Answering
    all of them the same way is wrong in three of four cases: a missing
    release is often tolerable, a lapsed credential wants re-authentication, a
    permission failure wants neither, and a rate limit wants a wait.
    
    Each is added to the error chain rather than replacing what the platform
    said, so errors.Is answers "which refusal" while errors.As still reaches
    the platform's own error.
    
    ErrUnauthorized is the only one that authorises discarding a cached
    connection, and its documentation says so while naming the other two as
    the cases that must not. Invalidating on a permission failure loops
    silently; invalidating on a rate limit is worse, because a consumer
    reusing providers resolves concurrently when invalidated — so it aims a
    burst of resolutions at an API already refusing, which is how a throttle
    becomes a lockout.
    
    A rate limit says WHEN it resets, twice: as a hint for the human reading a
    terminal, and as a duration through RetryAfter for the caller that has to
    decide. forge still does not retry — Provider.ListReleases already states
    that pagination loops here do not sleep on a caller's behalf, and that
    stance is right for a library without the program's deadline. What was
    awkward is that the same documentation told a caller to inspect the
    response for a Retry-After while giving it no supported way to do so
    except reaching into a platform's error type. Now it asks forge.
    
    The delay is SERVER-CONTROLLED input, so parsing it lives in one place
    rather than in five adapters, and it is bounded. A broken instance sending
    Retry-After: 999999999, or a hostile one sending it deliberately, would
    otherwise stall whoever honours it for eleven days. Three dialects are read
    by one helper — delta-seconds, HTTP-date, and X-RateLimit-Reset as an
    epoch — and anything absent, unparseable, negative or already past reports
    unknown rather than zero, so a caller cannot mistake silence for
    permission to retry now.
    
    ErrReleaseNotFound's caveat is updated to say what is true today rather
    than what was true when it was written: gitea does honour it, and the
    alignment of the rest is this spec's remaining work.
    
    Implements forge spec 0009 D1, D2 and D7.
    dfe237a1
  • Matt Cockayne's avatar
    chore(main): release v0.13.0 · b7d22496
    Matt Cockayne authored
    b7d22496
Loading
Loading