diff --git a/README.md b/README.md index c84fbb0f4..bca21c796 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,10 @@ Familiarize yourself with the available cmdlets using the module's comprehensive Get-Command -Module GitHub ``` +## Design documentation + +[Pipeline prefetch](https://github.com/PSModule/GitHub/blob/main/docs/pipeline-prefetch/index.md) defines the reusable contract, design, and GitHub implementation profile for paginated fetch-ahead. These documents describe intended behavior, not a released feature. + ## References ### Official GitHub Resources diff --git a/docs/pipeline-prefetch/design.md b/docs/pipeline-prefetch/design.md new file mode 100644 index 000000000..434e54a76 --- /dev/null +++ b/docs/pipeline-prefetch/design.md @@ -0,0 +1,158 @@ +--- +ms.title: Pipeline prefetch - Design +description: A bounded background page producer feeds an ordinary foreground pipeline through service-specific adapters. +--- + +# Pipeline prefetch - Design + +A module-private coordinator runs page retrieval in an in-process background runspace and emits results on the foreground pipeline thread. Service adapters interpret pagination; the module's shared transport retains ownership of request policy. + +## Specification + +[Pipeline prefetch specification](spec.md) defines the required behavior. The [GitHub implementation profile](implementation.md) supplies concrete settings and integration points. + +## Approach + +The synchronous path and the prefetched path use the same single-page transport and pagination adapter. Disabling prefetch changes scheduling, not request interpretation or output conversion. + +One producer follows continuation tokens sequentially. Its work overlaps downstream processing, not other requests in the same continuation chain. This avoids trying to request a cursor that is not yet known. + +The reusable unit is the coordinator and its adapter contract. It contains no service names, endpoint rules, or resource-specific output types. Implementations can remain module-private or use an existing owned library without imposing a new public runtime dependency. + +## Alternatives considered + +| Option | Trade-off | Role | +| --- | --- | --- | +| Ordinary synchronous pagination | Simple and preserves non-prefetched timing; cannot overlap retrieval with downstream work | Opt-out path | +| Collect every result before emitting | Delays first output and retains the complete collection | Rejected | +| Parallelize downstream commands | Changes consumer behavior and side-effect ordering rather than solving producer scheduling | Outside scope | +| Separate-process background job | Adds serialization and authentication/bootstrap costs; can change object types | Not the baseline | +| In-process producer with bounded handoff | Overlaps retrieval while retaining foreground pipeline behavior | Baseline | + +## Architecture + +```mermaid +flowchart LR + Command["Public list command"] --> Emitter["Foreground emitter"] + Command --> Producer["Background page producer"] + Producer --> Adapter["Pagination adapter"] + Adapter --> Transport["Shared single-request transport"] + Producer --> Queue["Bounded page handoff"] + Queue --> Emitter + Emitter --> Consumer["Ordinary downstream commands"] + Emitter -. cancellation .-> Producer +``` + +| Component | Responsibility | +| --- | --- | +| Public command | Resolve caller settings and select the eligible operation; preserve the opt-out | +| Coordinator | Own background lifetime, page capacity, completion, and cancellation | +| Pagination adapter | Decode one response, classify it, and determine the next continuation | +| Shared transport | Authenticate, refresh, throttle, retry, time out, and perform one logical page request | +| Foreground emitter | Apply stream preferences and convert accepted items to the established public output | + +Only one layer owns pagination for a logical enumeration. Nested helpers use the same owner or call the single-request primitive; they do not create additional default-on producers. + +### Capacity accounting + +The handoff contains complete page envelopes rather than individual resource nodes. This works for both item-returning commands and low-level commands that return one response envelope per page. + +The producer reserves a future-page slot before starting a request. That slot remains occupied while the request is in flight and while its page waits for the emitter. The emitter releases it when it takes ownership of the page. + +Consequently, C slots cover queued, producer-held, and in-flight pages together, plus one foreground page. A bounded queue alone is insufficient: fetching before waiting for queue capacity would retain an extra response beyond that contract. + +`BlockingCollection` can provide the bounded handoff, with a cancellation-aware capacity reservation such as `SemaphoreSlim`. Completion and failure have a separate control path so they cannot be trapped behind a full data queue. + +## Data and contracts + +### Adapter boundary + +These are logical contracts, not required public function names. + +| Contract | Input | Output and ownership | +| --- | --- | --- | +| Initialize | Explicit operation and context selection | Worker-owned request state and read-only eligibility | +| Request one page | Request state, continuation, cancellation | One response through the shared transport; retries stay inside that transport | +| Decode page | Response and operation-specific selectors | One validated page envelope | +| Convert item | Accepted raw item | Established public output object, created in the foreground | + +A page envelope contains: + +| Field | Meaning | +| --- | --- | +| Items | Materialized results from this page only, with declared item-enumeration semantics | +| HasMore | Whether another page exists | +| Continuation | Opaque next-page state interpreted only by the adapter | +| Diagnostics | Ordered, classified stream records associated with this response | +| Metadata | Non-secret response context required for the public contract or diagnostics | + +A page-returning API puts its existing response envelope in `Items` as one output item. An object-returning API puts the page's resource records in `Items`. The coordinator does not flatten arbitrary nested collections or reinterpret provider response shapes. + +Page data is immutable after publication. Request filters and variables are invocation-owned copies; shallow copying a table is insufficient when it contains mutable nested values. + +### Completion and failure + +Completion is separate from "no page is available right now." Terminal state records success, cancellation, or failure independently of the data queue. + +A permanent request or decoding error closes publication and records the failure. The foreground drains accepted pages and then reports that error at the failed-page boundary. It does not retry the whole enumeration or replay emitted pages. + +Warnings and partial-result diagnostics are delivered through the foreground command runtime before the associated page's items. Caller preferences apply there. A warning promoted to an error cancels production rather than continuing to emit that page. + +Worker startup errors, script parse failures, and invocation failures are observed through the actual PowerShell invocation state and error streams, not only through a queue populated by the worker script. Otherwise, failure before the worker's own error handling starts could leave the consumer waiting forever. + +### Lifecycle + +1. Validate eligibility, settings, and output policy before side effects. Establish the cleanup scope before allocating worker resources. +2. Initialize a worker using an explicit module path/version and explicit invocation state. Load the adapter in that worker's module scope. +3. Reserve capacity, request and classify a page, publish it, and advance continuation. All capacity and retry waits accept cancellation. +4. Read pages while also observing cancellation and terminal worker state. Emit items immediately; retain neither completed pages nor a second result list. +5. On normal completion, observe invocation completion exactly once and dispose owned resources. +6. On early stop or failure, signal cancellation first, request background stop, observe completion within the declared bound, and dispose after the producer stops. + +Cleanup is idempotent and covers normal completion, partial initialization, worker failure, and downstream early termination. PowerShell `try`/`finally` and, where supported and needed, the advanced function's `clean` block cover the entire lifetime. + +The foreground does not block forever on queue enumeration or a synchronous stop call. Stop and completion waits are bounded. Only the producer completes the data queue during normal operation; a competing foreground completion must not race with publication. + +An intentional downstream stop discards unconsumed speculative results and their pending speculative failures. A downstream error remains primary if cleanup also encounters a failure; cleanup details are retained as secondary diagnostics rather than silently swallowed or substituted. + +### Runspace and stream boundaries + +Worker code executes in the worker's session state. An arbitrary caller-captured scriptblock is not treated as an isolated adapter merely because it is passed to another runspace. + +The worker imports the same module build and dependencies explicitly. Its adapter is resolved inside that module's scope so private helpers remain available. Mutable global caches and static state are not assumed to become isolated through runspace creation. + +The worker emits no data through the background invocation's ordinary success collection. Classified diagnostics travel through the bounded page path; terminal faults use the control path. PowerShell stream collections are observed and drained so they do not become an unbounded secondary log. + +## Security + +Authentication and refresh stay in the shared request policy, with worker-owned context and synchronization for any shared credential store. Initialization resolves interactive authentication before background retrieval. A background request never opens an unexpected login prompt; inability to refresh noninteractively is an explicit authentication failure. + +Credentials and sensitive request bodies are excluded from coordination diagnostics. Plaintext credential lifetime is limited to transport use, without claiming that managed memory can be reliably zeroed. + +Read-only eligibility is declared by a trusted adapter, not inferred from a command name or HTTP method alone. A GraphQL read may use POST; a generic GraphQL request may instead contain a mutation. Arbitrary caller-provided mutations are never repeated speculatively. + +Continuation URLs are validated under the same endpoint and credential-forwarding policy as ordinary requests. Prefetch does not create a second, more permissive HTTP client path. + +## Testing strategy + +The adopting module implements the specification scenarios in its existing runner. A controlled page source provides release gates, request counters, fixture responses, cancellation observation, and injected faults. + +Overlap is demonstrated by holding downstream processing and observing a later request before releasing it. Bounds are demonstrated by withholding capacity and observing that no additional request starts. These checks use synchronization rather than timing-sensitive sleeps. + +Transport seams execute inside the actual worker. A mock installed only in the caller's runspace is not assumed to intercept worker requests. Bootstrap, module import, and class conversion also run through the built module. + +Coverage includes empty/single/multiple results, empty intermediate pages, invalid continuation, partial responses, exhausted retries, token refresh, worker bootstrap failure, cancellation while waiting on either side, and immediate downstream termination. Adapter fixtures exercise both page-returning and item-returning contracts. + +Non-destructive integration coverage uses stable read-only resources. Mutation-sensitive pagination is simulated with fixtures; repository deletion is not required to prove concurrency. + +Benchmarks record time to first item, total enumeration time, requests issued, peak retained pages, and shutdown latency for both scheduling modes. They distinguish potential overlap from a guaranteed speedup. + +## Rollout and operability + +Every eligible paginated result producer routes through the shared coordinator and propagates the opt-out. An adapter becomes eligible only when it meets the same contract in both modes; unsupported cases are explicitly documented rather than silently falling back after a worker failure. + +Activation covers all eligible call paths, including private helpers behind public wrappers. A prototype covering one endpoint is not treated as module-wide adoption. + +Diagnostics distinguish consumer backpressure, source wait, transport backoff, completion, and cancellation. They remain on their intended PowerShell streams and never contaminate object output. + +The capacity setting's unit is always stated. A setting formerly measured in nodes cannot silently become a page-count setting. The explicit opt-out provides a predictable escape hatch without changing result shape. diff --git a/docs/pipeline-prefetch/implementation.md b/docs/pipeline-prefetch/implementation.md new file mode 100644 index 000000000..e8329cbb5 --- /dev/null +++ b/docs/pipeline-prefetch/implementation.md @@ -0,0 +1,114 @@ +--- +ms.title: GitHub pipeline prefetch - Implementation +description: Settings and integration contracts for adopting the reusable prefetch pattern in the GitHub PowerShell module. +--- + +# GitHub pipeline prefetch - Implementation + +This profile maps the reusable pattern to the GitHub module. Other modules replace this profile while retaining the [specification](spec.md). + +## Design + +[Pipeline prefetch design](design.md) owns the execution model, adapter contract, and lifecycle. + +## Settings + +| Setting | Value | Applies to | Rationale | +| --- | --- | --- | --- | +| Prefetch default | Enabled for eligible paginated list operations | Public list commands and their helpers | Consistent module-wide behavior | +| `NoPrefetch` | Public switch; absent means prefetch is enabled | Eligible public producers | Explicit opt-out propagated through every wrapper | +| `PrefetchPageCapacity` | Private setting; default 1; positive integer | One logical enumeration | One future page overlaps retrieval without the draft's larger node queue | +| `PerPage` | Existing endpoint-specific setting and limits | Requests | Prefetch does not change GitHub page size | +| Request concurrency | One logical page request per enumeration | Producer | Cursor-dependent retrieval remains ordered | +| Platform support | GitHub's declared PowerShell LTS support on Windows, macOS, and Linux | Complete contract | No platform-specific degradation | + +With capacity one, the enumeration retains at most one foreground page and one future page, including an in-flight response. This is not a fixed byte limit. + +The node-count `QueueCapacity` explored in [PSModule/GitHub#645](https://github.com/PSModule/GitHub/pull/645) is not an alias for `PrefetchPageCapacity`. Any externally consumed setting retains its existing meaning or receives an explicit compatibility migration. + +## Public behavior + +These examples express the target API: + +```powershell +Get-GitHubRepository -Owner 'octocat' | + Select-Object -First 10 + +Get-GitHubRepository -Owner 'octocat' -NoPrefetch | + Select-Object -First 10 +``` + +The first invocation can retrieve unused repositories within the lookahead bound. The second does not fetch ahead. Both emit `GitHubRepository` objects in source order. + +Mutation commands, such as `Remove-GitHubRepository`, do not gain a background mutation worker. Their `ShouldProcess`, `WhatIf`, and confirmation behavior remain on the normal downstream path. + +## Names and identifiers + +| Element | Contract | Owner | +| --- | --- | --- | +| `Get-GitHubRepository` and other eligible public list commands | Resolve and propagate `NoPrefetch`; retain public parameter sets and output types | Public command | +| `Get-GitHubMyRepositories` | Select the `viewer.repositories` connection and create `GitHubRepository` objects in the foreground | Repository adapter | +| `Get-GitHubRepositoryListByOwner` | Select the `repositoryOwner.repositories` connection and preserve filters | Repository adapter | +| Shared page coordinator | Own scheduling and lifetime without GitHub-specific response logic | Private reusable pattern | +| Shared single-request primitive | Perform one logical request using common API policy; do not auto-follow pagination | Private transport extracted behind existing API entry points | +| `Invoke-GitHubAPI` | Preserve its response-envelope contract and use the REST adapter for eligible pagination | REST API facade | +| `Invoke-GitHubGraphQLQuery` | Preserve raw-query behavior and GraphQL response classification | GraphQL API facade | + +The single-request primitive is shared by both synchronous and prefetched pagination. Simply invoking an already-auto-paginating `Invoke-GitHubAPI` from the worker is insufficient: it gives two layers ownership of pagination. + +## Adapter mappings + +| Concern | REST adapter | GraphQL connection adapter | +| --- | --- | --- | +| Eligibility | Known read-only list operation | Module-owned read-only query with one explicitly selected connection | +| Source continuation | Validated next relation from the response | `pageInfo.hasNextPage` and `pageInfo.endCursor` | +| Resource items | Endpoint-specific array or wrapper property | Selected connection's `nodes` | +| Empty page | Continue if a next relation exists | Continue if `hasNextPage` is true and the cursor advances | +| Invalid continuation | Surface an invalid or non-advancing next relation | Surface missing connection, missing required page information, or missing/non-advancing cursor | +| Raw API output | One existing API response envelope per page | Existing raw query returns `data`; connection-item mode is a distinct explicit contract | + +REST response envelopes retain `Request`, `Response`, `Headers`, `StatusCode`, and `StatusDescription` where those fields are part of the existing contract. High-level list commands continue to unwrap and construct their established resource types. + +GraphQL response classification is shared, not reimplemented inside a producer script. A response with both `data` and `errors` preserves the module's partial-data warnings; an error-only response remains terminating. Invalid selected-connection data is classified before any of that page's nodes are emitted. + +Arbitrary calls to `Invoke-GitHubGraphQLQuery` do not become automatic connection enumerations. The helper may execute mutations or queries with multiple unrelated connections. Known list wrappers select a trusted read-only connection adapter; a generic `ConnectionPath` alone does not establish read-only eligibility. + +## Request policy and authentication + +The transport reuses the configured API endpoint, API version, user agent, HTTP behavior, retry count, retry interval, response metadata, and error classification. It preserves rate-limit and server-directed retry behavior rather than adding a second retry loop. + +The worker initializes the same module build and resolves its private adapter in module scope. It obtains worker-owned request state for the selected context rather than sharing a mutable foreground context object. + +Token refresh uses the existing auth-type-specific policy and synchronization. For user access tokens, callers of `Update-GitHubUserAccessToken` adopt the returned context when required; a refreshed context can replace an earlier object. A long enumeration does not rely on one startup-only plaintext token snapshot. + +Interactive authentication is resolved before background work. If reauthentication is required during enumeration and cannot complete noninteractively, the invocation surfaces an actionable authentication failure. + +## Requirement crosswalk + +| Requirement | Satisfied by | +| --- | --- | +| FR1 | Default-on settings and `NoPrefetch` forwarding on every eligible public/private path | +| FR2, FR3 | Page adapters, unchanged raw envelopes, and foreground resource construction | +| FR4, NFR1 | Single producer and reserved future-page capacity | +| FR5 | Shared one-request transport and shared GraphQL response classification | +| FR6 | Independent terminal state plus ordered foreground stream handling | +| FR7, NFR2 | Cancellation-aware request/backoff/capacity waits and complete worker teardown | +| FR8 | Invocation-owned filters/context and coordinated shared credential state | +| FR9 | Endpoint-specific live-pagination documentation and no speculative mutations | +| NFR3 | Redacted transport diagnostics and no credential-bearing coordination records | +| NFR4 | Contract scenarios on the module's declared platform matrix | + +## Integration boundaries + +Adoption includes all eligible REST and GraphQL list paths, not only repository connections. A command returning one result, a consumer accepting pipeline input, or a mutating operation is not automatically eligible. + +Stable-source fixtures establish parity and complete pagination independently of prefetch. Known defects in an existing sequential paginator are corrected and identified as correctness changes, not preserved as expected output. + +Listing repositories while deleting them is not a snapshot operation. Cursor pagination can still be sensitive to remote changes; neither fetch-ahead nor opt-out promises complete mutation-safe enumeration. + +## Source context + +- [PSModule/GitHub#644](https://github.com/PSModule/GitHub/issues/644) records the cross-module motivation and shared-transport direction. +- [PSModule/GitHub#645](https://github.com/PSModule/GitHub/pull/645) provides the repository-connection prototype. +- [API transport source](https://github.com/PSModule/GitHub/blob/31206e3dac0d3d84bf3eb5f75a8b5123d7e7730e/src/functions/public/API/Invoke-GitHubAPI.ps1) shows response envelopes and automatic REST pagination. +- [GraphQL facade source](https://github.com/PSModule/GitHub/blob/31206e3dac0d3d84bf3eb5f75a8b5123d7e7730e/src/functions/public/API/Invoke-GitHubGraphQLQuery.ps1) shows raw-query and partial-response behavior. diff --git a/docs/pipeline-prefetch/index.md b/docs/pipeline-prefetch/index.md new file mode 100644 index 000000000..9f632005f --- /dev/null +++ b/docs/pipeline-prefetch/index.md @@ -0,0 +1,34 @@ +--- +ms.title: Pipeline prefetch +description: A reusable contract for fetching paginated results while downstream pipeline commands process earlier results. +--- + +# Pipeline prefetch + +Eligible paginated result producers fetch ahead by default, retain normal pipeline output, and expose an explicit opt-out. The pattern overlaps data retrieval with downstream work; it does not make downstream commands parallel. + +These documents describe intended behavior and target APIs, not a claim that the capability is already implemented in a released module. + +| Document | Purpose | Reuse | +| --- | --- | --- | +| [Specification](spec.md) | Required behavior, limits, and acceptance scenarios | Shared across modules without service-specific changes | +| [Design](design.md) | Producer, foreground emitter, pagination adapter, and shared transport | Reusable execution pattern | +| [GitHub implementation profile](implementation.md) | Parameter contract, defaults, and GitHub integration points | Replace with the adopting module's profile | + +## Applicability + +Prefetch belongs in commands that produce paginated results, whether invoked alone or as part of a pipeline. Accepting pipeline input is not itself a reason to prefetch. Mutation commands and downstream consumers keep their existing execution behavior. + +## Adoption + +1. Keep the specification as the shared contract, linking to an authoritative copy or recording the revision of a vendored copy. +2. Supply a module profile defining eligible operations, output shapes, continuation rules, transport policy, and cancellation support. +3. Reuse the design through a module-private implementation or an existing owned library; no additional public runtime dependency is required. +4. Implement the specification's scenarios in the module's existing test framework, including execution in the actual background context. + +The GitHub profile specializes the pattern. It is not a dependency of another module's adoption. + +## Motivation + +- [PSModule/GitHub#644](https://github.com/PSModule/GitHub/issues/644) describes pagination pauses during downstream processing. +- [PSModule/GitHub#645](https://github.com/PSModule/GitHub/pull/645) explores a GraphQL repository-list producer. diff --git a/docs/pipeline-prefetch/spec.md b/docs/pipeline-prefetch/spec.md new file mode 100644 index 000000000..765d73932 --- /dev/null +++ b/docs/pipeline-prefetch/spec.md @@ -0,0 +1,286 @@ +--- +ms.title: Pipeline prefetch - Spec +description: Eligible paginated result producers overlap retrieval and consumption without changing their output contract. +--- + +# Pipeline prefetch - Spec + +Eligible paginated result producers fetch ahead by default while downstream commands consume earlier results. Callers retain ordered streaming output, bounded resource use, established request policies, and an explicit opt-out. The contract applies across modules and services. + +## Problem + +An ordinary pipeline streams objects but processes its stages synchronously. A paginated producer can therefore wait for downstream processing of one page's objects before requesting the next page. Source latency and downstream latency accumulate even when their work could overlap. + +Collecting all results first replaces that delay with startup latency and potentially unbounded memory use. Neither behavior provides bounded fetch-ahead. + +## Outcomes and impact + +- **Outcome:** Paginated retrieval overlaps downstream work without requiring caller-managed concurrency. +- **DORA:** Shared acceptance criteria reduce duplicated pagination defects and lead time when adopting the pattern in another module. +- **Domain signal:** Time waiting for a next page decreases in workloads containing both source latency and downstream work; time to first item and total request count remain visible. + +## Users and jobs + +| User | Job | +| --- | --- | +| Pipeline caller | Process results as they arrive without managing background execution | +| Module author | Apply one behavior consistently across paginated list commands | +| Operator | Bound speculative requests and stop work when results are no longer needed | + +## Scope + +**In scope** + +- Read-only paginated result producers, including object-returning and page-returning interfaces. +- Default-on fetch-ahead, a consistent opt-out, and propagation through command wrappers. +- Ordering, request policy, cancellation, diagnostics, and resource bounds. + +**Out of scope** + +- Parallel downstream processing, speculative mutations, and automatic concurrency for every command accepting pipeline input. +- Source-specific snapshot guarantees, durable caching, and resumable background jobs. +- Changes to the PowerShell pipeline engine. + +## Non-goals + +- Prefetch does not promise a speedup for every workload; it creates an opportunity to overlap independent work. +- Prefetch does not make a changing remote collection into a stable snapshot. +- Prefetch does not require a shared public module or prescribe how implementations are packaged. + +## Functional requirements + +### FR1 - Apply the default consistently and honor opt-out {#fr1} + +Every eligible paginated result producer MUST enable prefetch by default and expose the same documented opt-out within its module. Wrappers MUST preserve that choice. With prefetch disabled, the invocation MUST NOT perform speculative next-page requests. + +Eligibility requires read-only page retrieval, valid continuation handling, bounded responses, and support for the cancellation contract. Exceptions MUST be documented by operation; worker failures MUST NOT silently change execution mode. + +#### Behavioral scenarios + +```gherkin +Scenario: A wrapper preserves opt-out + Given a list command delegates pagination to shared helpers + When the caller disables prefetch + Then every helper preserves that choice + And the next page is not requested while earlier results block downstream +``` + +### FR2 - Preserve streaming and output contracts {#fr2} + +The producer MUST emit available results without waiting for the complete collection. Output types, property shapes, item enumeration, and documented metadata MUST remain compatible with non-prefetched execution. Internal coordination records MUST NOT appear on the success stream. + +#### Behavioral scenarios + +```gherkin +Scenario: The first page is usable independently + Given the first page is available and a later page is blocked + When the caller consumes results + Then first-page items arrive before the later page completes + And each item has the established public output type and shape +``` + +### FR3 - Preserve ordered and complete enumeration {#fr3} + +For a stable source, prefetch MUST return the same ordered results as non-prefetched execution. It MUST NOT introduce loss or duplication when retrying a page. An empty page with a valid continuation MUST NOT end enumeration. A missing required continuation or an unchanged next continuation MUST produce a pagination error rather than silent truncation or repeated requests. + +#### Behavioral scenarios + +```gherkin +Scenario: An empty page has a successor + Given a page has no items and has a valid next continuation + When enumeration continues + Then the next page is requested + And its items retain source order +``` + +### FR4 - Overlap retrieval with consumption {#fr4} + +With prefetch enabled, spare capacity, a known continuation, and no transport-policy delay, the producer MUST be able to request the next page while downstream processing is blocked on an earlier item. It MUST NOT require the caller to make downstream commands concurrent. + +#### Behavioral scenarios + +```gherkin +Scenario: Downstream work does not prevent next-page retrieval + Given the first page is available and another page exists + And downstream processing pauses on the first item + When prefetch capacity is available + Then retrieval of the next page starts before downstream processing resumes +``` + +### FR5 - Preserve request and response policies {#fr5} + +Prefetched and non-prefetched retrieval MUST use the same authentication, refresh, retry, throttling, endpoint, timeout, and response-classification policies. Prefetch MUST NOT multiply an existing retry budget or reinterpret partial success as complete failure. A page MUST be classified before its results become eligible for output. + +#### Behavioral scenarios + +```gherkin +Scenario: A retryable response does not duplicate output + Given the transport permits a bounded retry for a failed page request + When that request succeeds on retry + Then the existing retry and delay policy is honored + And that page's results are emitted once +``` + +### FR6 - Surface failures at the correct stream boundary {#fr6} + +An unrecoverable retrieval or pagination failure MUST surface after previously accepted pages and before any results from the failed page, unless the caller stops earlier. Previously emitted results are not rolled back. Established warning and partial-result behavior MUST remain intact. + +Initialization and background-execution failures MUST also surface; an empty queue MUST NOT be mistaken for successful completion. Intentional caller cancellation MUST NOT become a fabricated retrieval error. + +#### Behavioral scenarios + +```gherkin +Scenario: Retrieval fails after accepted results + Given two pages are accepted and the next page fails permanently + When the caller consumes the enumeration to completion + Then accepted results are emitted in order + And the failure is reported once at the failed page boundary + And the invocation does not report successful complete enumeration +``` + +### FR7 - Stop when results are no longer wanted {#fr7} + +Early downstream termination, downstream failure, explicit cancellation, and producer-owned result limits MUST stop further retrieval. After stop is observed, no new page request or retry may begin. In-progress work MUST be canceled and invocation-owned resources released within the cancellation contract. + +Prefetch MAY retrieve results that are never consumed, within its declared bound. It MUST NOT intentionally retrieve pages known to be unnecessary for a producer-owned limit. + +#### Behavioral scenarios + +```gherkin +Scenario: The caller only needs a prefix + Given more pages exist than the caller needs + When downstream stops after the required prefix + Then no further page request starts after stop is observed + And cancellation of speculative work does not replace the caller's stop reason +``` + +### FR8 - Isolate invocation state {#fr8} + +Concurrent enumerations MUST retain their own filters, continuation, endpoint selection, and authentication identity. Fetch-ahead MUST NOT mutate caller-owned request inputs or expose ordinary downstream state to concurrent modification. Shared transport or credential coordination MUST remain safe across invocations. + +#### Behavioral scenarios + +```gherkin +Scenario: Two contexts enumerate concurrently + Given two invocations use different identities and filters + When both retrieve multiple pages + Then each request uses its invocation's identity and filters + And neither invocation changes the other's continuation or caller-owned inputs +``` + +### FR9 - State live-collection limitations {#fr9} + +The module MUST document whether each source provides snapshot or live enumeration. Prefetch MUST NOT claim stronger consistency than the source provides or introduce speculative writes. Disabling prefetch MUST remain available for callers that require non-prefetched request timing, but MUST NOT be described as a cure for mutation-sensitive pagination. + +#### Behavioral scenarios + +```gherkin +Scenario: Results are mutated while enumeration continues + Given the source provides live rather than snapshot pagination + When downstream changes the collection being listed + Then the module makes no guarantee against source-induced omissions or duplicates + And prefetch itself performs no mutation +``` + +## Non-functional requirements + +### NFR1 - Bound lookahead and retained data {#nfr1} + +The module MUST declare a finite positive page capacity, C. No more than C future pages may be queued, held for publication, or in flight in total, in addition to one page being consumed. At most one logical page retrieval may be in flight per enumeration. + +The implementation MUST NOT retain a second unbounded result or diagnostic history. A page-count bound is not a byte bound; the module MUST document source page-size limits and any additional payload limit it enforces. + +#### Behavioral scenarios + +```gherkin +Scenario: A blocked consumer applies backpressure + Given capacity is one future page + And downstream is blocked on the active page + When the future page has been retrieved + Then no third page request starts until future-page capacity becomes available + And no more than two pages are retained by that enumeration +``` + +### NFR2 - Make cancellation bounded and observable {#nfr2} + +With a controllable, cancellation-aware source, all invocation-owned background work and pending reads MUST finish within five seconds of stop. Production adapters MUST document finite request, retry, and shutdown bounds; they MUST NOT depend on an uninterruptible operation to satisfy this contract. + +#### Behavioral scenarios + +```gherkin +Scenario: Cancellation interrupts a blocked request + Given the source is waiting indefinitely until released or canceled + When the caller stops the enumeration + Then the request observes cancellation + And no invocation-owned background work remains after five seconds +``` + +### NFR3 - Keep credentials out of diagnostics {#nfr3} + +Prefetch MUST introduce zero credential values into emitted diagnostics, exception messages, or persisted coordination data. Logs MAY identify a request, page, or invocation, but MUST NOT expose credentials or unredacted sensitive request payloads. + +#### Behavioral scenarios + +```gherkin +Scenario: A request fails with diagnostics enabled + Given authentication contains a unique synthetic secret marker + When a request fails with verbose and debug diagnostics enabled + Then the marker appears in no diagnostic, exception message, or persisted artifact +``` + +### NFR4 - Preserve the supported platform matrix {#nfr4} + +Every adopting module MUST satisfy the contract on 100 percent of its declared supported runtime and operating-system combinations. Prefetch MUST NOT silently become unavailable on one supported platform. + +#### Behavioral scenarios + +```gherkin +Scenario: An adopter supports multiple platforms + Given the adopter declares its supported runtime and operating-system matrix + When the shared contract scenarios run on each combination + Then output, ordering, opt-out, bounds, and cancellation meet the same requirements +``` + +## Acceptance criteria + +```gherkin +# AC1 - Verifies: FR2, FR3, FR4, NFR1 +Scenario: A slow consumer receives a complete ordered stream + Given a stable multi-page source and capacity of one future page + When downstream pauses during each page + Then retrieval overlaps those pauses + And capacity remains bounded + And the final ordered results match non-prefetched enumeration + +# AC2 - Verifies: FR5, FR6, FR7, NFR2 +Scenario: Downstream fails while retrieval is backing off + Given a next-page request is waiting under the shared retry policy + When downstream raises an error + Then retry waiting and background retrieval are canceled + And cleanup completes within the cancellation contract + And the downstream error remains the primary failure + +# AC3 - Verifies: FR1, FR2, FR3, FR8 +Scenario: A second module adopts the same contract + Given two modules have different output types and continuation formats + When both enumerate stable fixtures with and without prefetch + Then both preserve their public result contracts + And both honor the same opt-out behavior through their wrappers +``` + +## Constraints and assumptions + +- **Constraint:** Source request policies and read-only eligibility take precedence over fetch-ahead. +- **Constraint:** Prefetch preserves ordinary synchronous downstream execution. +- **Assumption:** A source declares a bounded page response and meaningful continuation semantics. +- **Assumption:** Stable-source equivalence does not imply consistency while remote data changes. + +## Dependencies + +- An adopter-owned definition of eligible operations, response classification, and cancellation behavior. +- Source-specific fixtures and the adopting module's existing test framework. + +## Where this connects + +- [Design](design.md) describes how the contract is delivered. +- [GitHub implementation profile](implementation.md) maps it to one adopting module.