Skip to content

Future-enhancement (deferred): async-iterator helpers for paginated CSAPI list methods — out-of-scope until upstream broadens scope #170

@Sam-Bolling

Description

@Sam-Bolling

⏸️ Status: DEFERRED — Filed-but-not-actionable

This issue is intentionally non-actionable at the time of filing. It documents a known future-enhancement opportunity surfaced during Phase 8 triage of #167 so that the design context, supporting research, and decision criteria are preserved if/when the time comes to implement.

Do not pick up this issue under current governance. Implementation is gated on upstream camptocamp/ogc-client adopting (or signalling intent to adopt) auto-pagination patterns. Until then, adding this to our fork would be a scope-broadening contribution that diverges from upstream's convention — exactly the class of change our governance model says to defer.

When this issue does become actionable, the body below contains everything needed to start: motivation, audit results, prior art, design sketches, API options, scope boundaries, acceptance criteria, and a links-to-context section. No re-research should be required.


Why this issue exists

While triaging #167 ("List methods do not document the pagination contract"), it became clear that the genuine ergonomic solution to the underlying user-experience pain point — consumers reading partial results because they don't follow next HATEOAS links — is not better documentation but a first-class auto-pagination helper that walks next links on the consumer's behalf.

#167 intentionally restricts its scope to JSDoc-only changes that surface the pagination contract to consumers reading our API. That's the right Phase 8 fix because it's:

  • Spec-conformant (OGC 23-001 §7.6 delegates default page size to the server).
  • Aligned with upstream's existing convention (no auto-pagination helpers anywhere in src/).
  • Zero behavior change, low review-risk for PR #136.

This issue captures the next-step enhancement that would close the loop on the user-experience pain point — but only when it becomes appropriate to introduce it.

Upstream audit — confirming this is genuinely scope-broadening

Audit performed 2026-04-28 against src/ on branch phase-7 (which contains all upstream code we've inherited):

Searched src/**/*.ts for: async\s*\*, AsyncIterable, AsyncIterator, iterate, paginate, nextLink, followLink, fetchNext, allPages, getAllItems, fetchAll

Result: Zero matches in any upstream module — src/ogc-api/endpoint.ts, src/stac/endpoint.ts, src/wfs/, src/wms/, src/wmts/, src/tms/. Upstream's OgcApiEndpoint.getCollectionItems() returns a single Promise<OgcApiCollectionItem[]> representing one page; pagination link handling is documented in JSDoc but left to the consumer. STAC endpoint mentions rel="next" in JSDoc for one method but provides no walking helper. WFS/WMS/WMTS/TMS have no pagination-iterator patterns at all.

Conclusion: Adding async iterators to CSAPIQueryBuilder (or anywhere else) would establish a new pattern category in this codebase. That's the kind of architectural choice that should be made — or at minimum endorsed — by upstream maintainers, not unilaterally introduced by a contributor adding a new module.

Why we should NOT implement this now

  1. No upstream precedent. See audit above. Introducing a pattern category that doesn't exist anywhere else in the codebase risks reviewer pushback (similar to #122-class concerns about opinionated CSAPI surface).
  2. Spec-conformant alternative exists. Documentation alone (per #167) is sufficient for a careful consumer to implement correct pagination themselves. The library does not strictly need this helper to be usable.
  3. Adds public API surface to a draft PR. PR #136 is in active review with the maintainer trimming opinionated surface area. Adding 30+ public iterator methods (one per list endpoint) is the wrong direction for that conversation.
  4. Design space is large; choosing wrong is costly. Async iterators, AsyncIterable wrappers, page-callback callbacks, "fetch all" Promise<T[]>, and combinations thereof all have trade-offs (memory, abort-ability, error-handling semantics, type ergonomics with TypeScript generics). A choice made unilaterally and merged into upstream is one we live with for years.
  5. Per our governance model (docs/governance/AI_OPERATIONAL_CONSTRAINTS.md): scope expansion requires a deliberate decision; "we noticed an ergonomic gap during testing" is not by itself sufficient warrant.

When this issue becomes actionable

Any one of the following triggers should re-open the conversation:

  • ✅ Upstream camptocamp/ogc-client adds an auto-pagination helper to any existing module (OGC-API, STAC, etc.) — establishes the precedent we'd be aligning to.
  • ✅ Upstream maintainers explicitly endorse this direction in PR #136 review or a related conversation.
  • ✅ A user-facing consumer (e.g., ogc-csapi-explorer, OSHConnect-Python via WASM, or a future TypeScript-first CSAPI consumer) demonstrates that the documentation-only fix from #167 is materially insufficient for real production use, and the surfaced pain justifies a fork-specific extension.
  • ✅ A future Phase decides to broaden CSAPI surface beyond strict OGC-API parity (for example, to add SensorML-aware convenience methods or domain-specific aggregations) — at which point auto-pagination becomes a sibling concern.

If none of these trigger within a reasonable horizon (e.g., 18-24 months), this issue should be closed as wontfix with a one-line rationale.


Design sketch — preserved for future implementation

The remainder of this issue captures research-grade design exploration so that whoever picks this up later does not have to re-derive it. Nothing below this line is binding. It is a starting point for a real design conversation, not a finished spec.

Motivating user story

A consumer integrating with a CSAPI server wants to retrieve every system whose name matches a filter:

// Today (after #167 docs land), the consumer must hand-walk the contract:
let url = builder.getSystems({ q: 'temperature' });
const all: System[] = [];
while (url) {
  const res = await fetch(url).then((r) => r.json());
  all.push(...parseSystems(res));
  const next = res.links?.find((l) => l.rel === 'next');
  url = next?.href ?? null;
}

// With this enhancement, the consumer writes:
for await (const system of builder.iterateSystems({ q: 'temperature' })) {
  // ... process each system as it streams in
}

Affected method families

All 39 public list methods on CSAPIQueryBuilder would gain an iterate* sibling. Examples:

URL-builder method Proposed iterator sibling
getSystems(opts) → string iterateSystems(opts) → AsyncIterable
getDataStreams(opts) → string iterateDataStreams(opts) → AsyncIterable
getObservations(opts) → string iterateObservations(opts) → AsyncIterable
getDeployments(opts) → string iterateDeployments(opts) → AsyncIterable
getProcedures(opts), getProperties(opts), getControlStreams(opts), getCommands(opts), all get*History(), get*Subsystems(), get*Observations() corresponding iterate*

Architectural question — where do iterators live?

CSAPIQueryBuilder today returns URL strings only. It does not perform HTTP requests, does not parse responses, and does not depend on fetch semantics. Adding iterate* methods to it would fundamentally change its responsibility model — from "URL builder" to "URL builder + HTTP client."

Three architecture options to evaluate when this issue activates:

Option α — Methods on CSAPIQueryBuilder directly.

  • Pros: Discoverable; one-stop API surface.
  • Cons: Conflates URL building with HTTP I/O; complicates testing (URL builder tests today are sync and pure); breaks symmetry with upstream OgcApiEndpoint whose URL builders are also pure.

Option β — A separate CSAPIClient (or CSAPIPager) wrapper class that takes a builder + a fetcher and exposes the iterator methods.

  • Pros: Separation of concerns preserved; testable with mocked fetchers; lets builders stay pure; matches the layered pattern in upstream where high-level convenience composes lower-level URL builders.
  • Cons: Slightly more verbose for consumers (new CSAPIClient(builder).iterateSystems(...)); potentially more code.

Option γ — Free functions that take a builder + endpoint URL + options.

  • Pros: Smallest surface; tree-shakable; no class hierarchy to maintain.
  • Cons: Less discoverable; harder to hang shared state on (e.g., per-session HTTP defaults, AbortSignal).

Recommendation when activated: start with Option β unless upstream signals otherwise.

API shape options

Shape 1 — Async generator (AsyncIterable<T>):

async function* iterateSystems(
  builder: CSAPIQueryBuilder,
  options?: QueryOptions,
  signal?: AbortSignal
): AsyncIterable<System> {
  let url = builder.getSystems(options);
  while (url) {
    const page = await fetchAndParse(url, signal);
    for (const system of page.items) yield system;
    url = page.links.find((l) => l.rel === 'next')?.href ?? null;
  }
}

Idiomatic for-await-of consumption; small runtime overhead; AbortSignal-friendly.

Shape 2 — Page-by-page iterator (AsyncIterable<Page<T>>):

Yields whole pages instead of items. Useful when consumer wants explicit per-page control (e.g., progress callbacks, batched DB writes).

Shape 3 — Eager fetchAll() returning Promise<T[]>:

Easiest mental model but accumulates everything in memory. Should NOT be the only option (footgun on large datasets) but might be a thin convenience layer over Shape 1.

Recommendation when activated: provide both Shape 1 (primary) and Shape 3 (convenience), document that Shape 3 has no memory safety net.

Cross-cutting concerns to design through

  • AbortSignal: Iterators must be cancellable. Plumb AbortSignal from constructor or per-call.
  • Error semantics: Mid-iteration HTTP failure → throw from the iterator? Resume from last good page? Document explicitly.
  • Maximum-page guard: Pathological servers can return circular next links. Iterators should support a maxPages safety cap (default off; documented).
  • Rate limiting / backoff: Out of scope initially; consumers wrap the iterator.
  • TypeScript generics: Today our list methods are 39 distinct named methods, each returning a different type. Iterators must preserve that type information. Probably feasible without generics gymnastics if we keep one method per resource (rather than a single generic method).
  • Link-resolution helper #110: When #110 (DEFERRED — @link resolution utilities) eventually lands, it will likely share infrastructure with this enhancement (both walk OGC HATEOAS links). Coordinate the two if both reactivate.
  • Coordination with @link fallback work (#166): Iterators consume parsed objects — those objects must already correctly extract IDs from both @id and @link forms. #166 must land first.

Files this would likely touch (if implemented)

File Action Est. Lines
src/ogc-api/csapi/iterator.ts (new) Implement iterator helpers ~200-400
src/ogc-api/csapi/iterator.spec.ts (new) Unit tests with mocked fetchers ~300-500
src/ogc-api/csapi/integration/iteration.spec.ts (new) Integration tests against fixture servers ~200
src/ogc-api/csapi/index.ts Export new helpers ~5
src/ogc-api/csapi/model.ts Add iterator-related types if any ~10-30
Module-level docs Update pagination-contract section (#167) to point at iterator ~10

Scope — What to NOT touch when this activates

  • ❌ Do not modify CSAPIQueryBuilder.buildQueryString() behavior. Iterators consume URLs from the builder; they do not modify it.
  • ❌ Do not modify validateLimit() or any other helper.
  • ❌ Do not change behavior of any existing get*() URL-building method.
  • ❌ Do not modify the upstream-owned README.md.
  • ❌ Do not introduce iterators on upstream modules (OGC-API, STAC, WFS, etc.) unless upstream explicitly sanctions it. This issue is scoped to CSAPI only.
  • ❌ Do not implement rate-limiting, retry, or backoff logic — that's the consumer's concern.

Acceptance criteria (when this activates)

  • One trigger from the "When this issue becomes actionable" section has fired and is documented in the PR description.
  • Architecture option (α / β / γ) is chosen with documented rationale.
  • API shape (1 / 2 / 3 or combination) is chosen with documented rationale.
  • One iterator helper exists for every public list method on CSAPIQueryBuilder (current count: 39).
  • Each iterator supports AbortSignal cancellation.
  • Each iterator has unit tests (mocked fetcher) and at least one integration test (fixture server).
  • next-link circular-reference safety mechanism is in place (e.g., maxPages guard or visited-URL tracking).
  • JSDoc on each iterator clearly states pagination semantics, error behavior, and abort behavior.
  • #167 JSDoc updated to point consumers at the iterator as the recommended pattern (was: "follow next link manually").
  • No regression in existing CSAPI tests; no behavior change to existing methods.
  • All modified files pass npx prettier --check. No lint or typecheck regressions.

Dependencies

Blocked by (when activating):

  • A trigger from "When this issue becomes actionable" must have fired.
  • #166 (@link fallback in Part 2 parsers) should land first — iterators yield parsed objects whose IDs must be correctly extracted.
  • #167 (pagination JSDoc) should land first — establishes the documentation contract this enhancement implements.

Blocks: Nothing.

Related:

  • #110 (DEFERRED — @link resolution utilities; shares HATEOAS-link-walking infrastructure)
  • #122 (calibration on opinionated CSAPI surface concerns)
  • #166, #167 (companion findings from the same Go-server interop testing effort)

Operational Constraints

⚠️ MANDATORY when activating: Before starting work, review docs/governance/AI_OPERATIONAL_CONSTRAINTS.md.

Key constraints:

  • Precedence: OGC specifications → AI Collaboration Agreement → This issue description → Existing code → Conversational context.
  • Scope expansion gate: This issue itself documents a scope-broadening enhancement. Re-confirm an activation trigger has fired before any implementation work begins. Do not pre-emptively activate.
  • Minimal viable surface: When implementing, prefer the smallest API surface that satisfies the user story. Defer follow-on features (rate limiting, server-side filter caching, etc.) to separate issues.
  • Upstream alignment: Once activated, audit upstream again at that time — patterns may have shifted since this issue was filed.
  • No unilateral decisions on architecture: The α/β/γ and 1/2/3 choices in the design sketch above must be re-deliberated at activation time; they are not pre-decided.

Context preservation — what we knew at filing time

# Source What it provides
1 #167 Companion finding that motivated this enhancement; established the documentation-only Phase 8 fix this enhancement would later supplement.
2 #166 Companion finding from same Go-server interop effort; must land first so iterators yield correctly-parsed objects.
3 #110 Sibling DEFERRED enhancement on @link resolution; potential infrastructure overlap.
4 src/ogc-api/csapi/url_builder.ts Current CSAPIQueryBuilder — pure URL-building class with no HTTP I/O. Architectural baseline iterators would extend or wrap.
5 src/ogc-api/csapi/integration/observation.spec.ts L253-267 Hand-written next-link walking pattern in our own test suite; reference implementation for the iterator's loop body.
6 src/ogc-api/csapi/integration/navigation.spec.ts L360-394 Multi-page navigation with end-of-pagination handling (nextLink === undefined); reference for termination logic.
7 src/ogc-api/endpoint.ts L501-525 (upstream) Upstream getCollectionItems() — confirms upstream returns single-page promises; no auto-pagination.
8 src/stac/endpoint.ts L404 (upstream) Upstream STAC mentions rel="next" in JSDoc but provides no walking helper.
9 OGC 23-001 §7.6 limit is optional with server-defined default; consumer-side next-link walking is the spec-defined pagination contract.
10 docs/governance/AI_OPERATIONAL_CONSTRAINTS.md Governance model that requires deferring scope-broadening contributions absent upstream signal.
11 PR #136 Active upstream review where opinionated CSAPI surface is being trimmed — adds risk to introducing iterator surface unilaterally.
12 Audit (2026-04-28) Searched src/**/*.ts for async\s*\*, AsyncIterable, iterate, getAllItems, fetchAll, paginate — zero matches in upstream modules. Recorded in #167 institutional-learning comment.

Filing rationale: Filing this issue now, while the context is fresh, costs ~5 minutes; reconstructing this context from scratch in 18 months when the enhancement might activate would cost much more. The issue is harmless while it sits — it's labelled deferred, the status banner makes the non-actionability obvious to any future contributor, and our governance model forbids picking it up without a documented trigger. This is a "preservation of institutional memory" filing, not a work request.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions