v1.5.0
Adds three consumer-utility sub-packages that ride on top of the
assembled *http.Client and compose with the existing transport
stack: polling (range-over-func iterator over an HTTP endpoint on
an interval), search (envelope iterator for /search/* endpoints
that pages.As[T] cannot serve), and cond (visible 304 / change-
vs-unchanged signal building on the etag layer). Hardens retry's
Retry-After parser as part of the same release.
Fixed
retry:Retry-Afterparser saturates extremely large delta-seconds
values (abovemath.MaxInt64 / int64(time.Second)) instead of wrapping
int64 nanoseconds. Clamped values route through the existing
ErrRetryAfterExceedsMaxabort path.retry: whitespace aroundRetry-Aftervalues (" 5","5 ") is now
trimmed before parsing.
Added
polling.Pollandpolling.As[T]: Go 1.23 range-over-func iterator
that re-issues a request on a caller-tunable interval, reusing the
supplied*http.Clientso retry, etag, ratelimit, throttle, and
oauth2 apply per attempt. Options:WithDone(header/status
predicate),WithDoneT[T](typed predicate on the decoded value),
WithDecode[T](custom decoder),WithChangeOnly(skip yields on
cache hit),WithMaxAttempts,WithMaxWallClock,WithJitter
(deterministic mid-point),WithHonorRetryAfter,WithLogger,
plusWithSleepFunc/WithNowFunctest seams. Sentinels
ErrMaxAttemptsExceeded,ErrMaxWallClockExceeded(wraps
context.DeadlineExceeded),ErrInvalidInterval,
ErrInvalidOption,ErrPredicatePanic. Boundary yields surface
(lastResp, sentinel)so the caller can inspect what triggered
the limit.search.Issues,search.Code,search.Repos,search.Users:
typed iterators over GitHub's/search/*envelope endpoints.
YieldsResult[T]{Item, TotalCount, IncompleteResults}per item;
surfaces the post-page-10 1000-result hard cap as
ErrResultCapHit. Reusespages.Pagesfor Link-header walking.
Functional options (WithBaseURL,WithPerPage,WithSort,
WithOrder,WithHeaders) match the convention used by the rest
of ghkit;WithBaseURLoverrides the GitHub default for GHES or
test fixtures.cond.Status,cond.StatusOf,cond.Fetch[T]: surfaces the
change-vs-unchanged signal the etag layer already computes.
cond.HeaderCacheStatusis the canonical"X-Ghkit-Cache"header
the etag layer sets on synth-200 ("hit") and wire-200 store
("miss").StatusOfis nil-safe and maps absent toUpdated.
Sentinel errorscond.ErrNilClient,cond.ErrNilContext,
cond.ErrNilRequestfor invalidFetcharguments. Decode errors
wrap ascond: decode: %wfor parity with pages, polling, search.etag/transport.go: setscond.HeaderCacheStatuson synth-200
(after header merge) and wire-200 (aftercache.Addreturns) so
the cache does not ingest the key. Drift detector and
ComputeExpectedETagare unaffected (the header is response-only
and not in the hash domain).retry.RetryAfter(resp) (time.Duration, bool): exported wrapper
aroundparseRetryAfter. Returns(0, false)for absent,
unparseable, AND negative-numeric values. Used bypollingto
honor server hints without duplicating the parser.ghtest.ETagServer(t, body) (*httptest.Server, *int64): lifted
frometag/transport_test.go. Used by polling, cond, and any
future test that exercises etag composition.retry:retry_retry_after_unparseableslog event at Warn when
the header is present but matches neither delta-seconds nor
HTTP-date. Carries a length-bounded, ASCII-sanitisedraw
attribute (32 bytes max).retry:retry_sleepevent'ssourceattribute gains a
"malformed"value, distinct from"jitter"and"retry_after".retry:FuzzParseRetryAfterfuzz test pinning the parser as
total.
Documentation
- New
polling/doc.go,search/doc.go,cond/doc.godocumenting
contracts and sharp edges (retry compounding, throttle backpressure,
etag synth-200 sameness, gofri ctx-cancel observed on next
round-trip, header set ordering for the cond signal). retry/doc.godocuments the unparseable arm and the
source="malformed"label.pages/doc.go: corrected the stale chain-order note (predates the
v1.4.0 ratelimit/throttle inversion).README.md: Go Report Card badge added; coverage badge skipped
(peer cohort split: 3 of 4 transport-utility libraries do not
carry one).
Examples
- New
examples/poll-workflow-run/: waits for a workflow run to
reachstatus="completed"viapolling.As[*github.WorkflowRun]
withWithDoneT,WithMaxWallClock,WithJitter. Pairs polling
withWithETagCacheso unchanged ticks short-circuit at the etag
layer. - New
examples/search-issues/: walks/search/issueswith
search.Issues[*github.Issue], surfacesincomplete_resultsand
the 1000-result cap. - New
examples/conditional-fetch/: visible 304 via
cond.Fetch[*github.Repository]; the second call returns
cond.Unchangedso downstream work can be skipped.
Tooling
polling,search, andcondintroduce no new runtime
dependencies in the root module. Theghtest.ETagServerlift adds
no module surface;condis stdlib-only;pollingimportscond
andretry;searchimportspages. The import graph remains
acyclic (etag -> cond,polling -> cond, retry,search -> pages).