|
| 1 | +# SDK Roadmap |
| 2 | + |
| 3 | +This document tracks confirmed-but-deferred enhancements, open design decisions, and the |
| 4 | +code-generation effort. Each item links to its tracking issue and records a recommended |
| 5 | +direction and its dependencies, so the work can be sequenced rather than picked up ad hoc. |
| 6 | + |
| 7 | +Bug fixes, documentation, CI, and the smaller self-contained enhancements are handled directly |
| 8 | +as pull requests and are not listed here. The items below are larger features, decisions that |
| 9 | +should be made before the relevant code is written, or design work for a generator that does not |
| 10 | +yet exist. |
| 11 | + |
| 12 | +## Status legend |
| 13 | + |
| 14 | +- **Planned** — confirmed worth doing; deferred only for scope/sequencing. |
| 15 | +- **Decision needed** — a real fork with material trade-offs; pick a direction before writing code. |
| 16 | +- **Design** — forward-looking design for the (not-yet-started) code generator. |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## Track A — Async parity |
| 21 | + |
| 22 | +The synchronous pipeline ships retry, bearer auth, and pagination; the asynchronous side does |
| 23 | +not yet have equivalents. These bring the async path up to parity. |
| 24 | + |
| 25 | +### Async retry step at the RETRY stage — [#31] (Planned, large) |
| 26 | +No `AsyncHttpStep` occupies `Stage.RETRY`, so async pipelines get no retry. Add a public |
| 27 | +`AsyncRetryStep : AsyncHttpStep` with `final override val stage = Stage.RETRY` mirroring the sync |
| 28 | +`RetryStep`, plus a concrete `DefaultAsyncRetryStep` that reuses `HttpRetryOptions` and a |
| 29 | +`ScheduledExecutorService` for non-blocking backoff (no thread parked during the delay). Reuse the |
| 30 | +existing classification/`Retry-After` logic. Should land after the retry-defaults reconciliation |
| 31 | +(#38) so both async and sync share one backoff source of truth. |
| 32 | + |
| 33 | +### Async bearer-auth with background token refresh — [#32] (Planned, medium) |
| 34 | +Add an async bearer-auth `AsyncHttpStep` and a non-blocking refresh path on the token provider |
| 35 | +(`fetchAsync(...) : CompletableFuture<BearerToken>`, defaulting to wrapping the blocking `fetch`), |
| 36 | +so a valid-but-near-expiry token is returned immediately while the refresh runs off-thread. |
| 37 | +Coordinate with the bearer-token eviction work (#33) so the sync and async steps share eviction |
| 38 | +and refresh semantics. |
| 39 | + |
| 40 | +### Async pagination — [#34] (Planned, large) |
| 41 | +Both pagination surfaces are blocking today. After the pagination unification decision (#30), |
| 42 | +add an async sibling that drives an iterative `executeAsync` re-arm, closing each page on |
| 43 | +completion, with `Flow`/`Flux` bridges living in the coroutines/reactor adapter modules (not in |
| 44 | +`sdk-core`). Blocked on #30. |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## Track B — Core capabilities |
| 49 | + |
| 50 | +Confirmed features deferred for scope. None is a defect; each adds a genuinely missing capability. |
| 51 | + |
| 52 | +### Shared instrumentation emitter — [#26] (Planned, medium) |
| 53 | +The sync and async instrumentation steps duplicate the emit/redact/preview/metrics logic, with a |
| 54 | +standing extraction marker. The two copies have already diverged once. Extract the stateless logic |
| 55 | +into an internal `InstrumentationEmitters` constructed from the values both steps need |
| 56 | +(`HttpInstrumentationOptions`, `ClientLogger`, `Clock`, the lazy metric instruments). Pure internal |
| 57 | +refactor — high value for preventing future drift between the two steps. |
| 58 | + |
| 59 | +### Per-call options channel on the request context — [#27] (Planned, medium) |
| 60 | +`RequestContext` carries no per-call override channel. Add an immutable `RequestOptions` |
| 61 | +(per-phase timeout overlay, response-validation toggle, ad-hoc credential override) with |
| 62 | +`applyDefaults` merge semantics, keeping the transport SPIs single-method. Depends on the |
| 63 | +`Timeout` value type from #41. |
| 64 | + |
| 65 | +### Per-phase `Timeout` value type — [#41] (Planned, medium) |
| 66 | +No core timeout type exists; per-phase timeouts are configured ad hoc per transport. Add an |
| 67 | +immutable `Timeout` (connect / read / write / request `Duration`s, read/write defaulting to |
| 68 | +request) that adapters translate to native settings, kept distinct from the retry total-timeout. |
| 69 | +Prerequisite for #27. |
| 70 | + |
| 71 | +### AutoCloseable SSE stream — [#35] (Planned, medium) |
| 72 | +The SSE surface is a bare `Sequence`, so a partially-consumed stream can leak the response. Add an |
| 73 | +`SseStream : AutoCloseable, Iterable<ServerSentEvent>` that owns the `Response` and closes it on |
| 74 | +stream close or partial consumption — mirroring the close-on-partial-consume invariant pagination |
| 75 | +already enforces. |
| 76 | + |
| 77 | +### Multipart request body — [#61] (Planned, large) |
| 78 | +No multipart support today. Add a public `MultipartRequestBody : RequestBody` (immutable + Builder) |
| 79 | +with one shared frame-size function driving both `writeTo` and `contentLength`, file parts |
| 80 | +streaming zero-copy via `FileRequestBody` and non-file parts encoded through the `Serde` SPI (no |
| 81 | +Jackson in `sdk-core`). |
| 82 | + |
| 83 | +### Opt-in resource leak detector — [#45] (Planned, medium) |
| 84 | +No leak detection exists. Add an internal, opt-in, log-only `LeakDetector` that WARNs when a |
| 85 | +caller-owned closeable becomes phantom-reachable unclosed, using a reflectively-obtained |
| 86 | +`java.lang.ref.Cleaner` so Java-8 bytecode no-ops on JDK 8 and the whole thing is gated behind a |
| 87 | +system property. Never auto-closes. |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Track C — Decisions to make |
| 92 | + |
| 93 | +These need a recorded decision (and sometimes a small spike) before any code is written. |
| 94 | + |
| 95 | +### URL model: resolved `java.net.URL` vs deconstructed — [#29] (Decision needed) |
| 96 | +**Recommended:** keep a single opaque URL field on `Request` (do not explode into |
| 97 | +scheme/host/port/segments/query on the immutable model), but migrate the stored type from |
| 98 | +`java.net.URL` to `java.net.URI` — `URI` is parse-only, DNS-free, and already what the JDK |
| 99 | +transport needs. Preserve the existing textual, DNS-free equality. Record the decision in |
| 100 | +`docs/architecture.md`. Gates #28 and the typed-page generation in #56. |
| 101 | + |
| 102 | +### First-class `QueryParams` multimap — [#28] (Planned, medium; gated on #29) |
| 103 | +`QueryParam` is a `TODO()` stub and `RequestRebuilder` does query-string surgery by splitting on |
| 104 | +`&` with single-value semantics. Add a public `QueryParams` multimap modeled on `Headers` |
| 105 | +(insertion-ordered, multi-value, explicit encoding rules). Lands after the URL-model decision so it |
| 106 | +projects into the chosen type. |
| 107 | + |
| 108 | +### Unify the two pagination stacks — [#30] (Decision needed, large) |
| 109 | +Two parallel pagination surfaces exist and are both public API: `http.paging` |
| 110 | +(`PagedIterable`/`PagedResponse`, BYO fetcher) and `pagination` (`Paginator`/`Page` + strategies, |
| 111 | +self-driving). **Recommended:** make the strategy-driven `Paginator` + `Page` the canonical |
| 112 | +surface (it owns the client, matches peer SDKs, and is the model the typed-page generation in #56 |
| 113 | +builds on) and deprecate the other. A public-API redesign — decide before coding. Gates #34. |
| 114 | + |
| 115 | +### Deep-array value-equality utility — [#49] (Decision needed, small) |
| 116 | +A `contentEquals`/`contentHashCode` helper has no consumer in the current tree; it is tied to the |
| 117 | +future generated DTOs (with `ByteArray` fields). **Recommended:** defer until codegen needs it, to |
| 118 | +avoid adding public surface that immediately churns the API snapshot and the coverage floor with no |
| 119 | +caller. Fold into the codegen runtime when that lands. |
| 120 | + |
| 121 | +### VirtualThreads close-event log level — [#10] (Decision needed, trivial) |
| 122 | +The `executor.closed` event is emitted at DEBUG (not INFO). **Recommended:** keep DEBUG (it matches |
| 123 | +every other async-adapter event and keeps clean-shutdown noise off INFO) and close the issue, |
| 124 | +optionally noting the intent in the `close()` KDoc. No code change beyond the optional doc line. |
| 125 | + |
| 126 | +### Release automation — [#75] (Decision needed, gated on CI) |
| 127 | +Versioning, changelog, and publishing are manual, and the version string is duplicated across ~10 |
| 128 | +build scripts. **Recommended (once CI lands):** record a decision on adopting release-please; if |
| 129 | +adopted, collapse the duplicated version to a single anchor and add a release workflow + Sonatype |
| 130 | +publishing. Gated on CI (#70) and complements the publishing-convention-plugin work (#71). |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Track D — Code generation |
| 135 | + |
| 136 | +The SDK is deliberately a hand-written HTTP-client toolkit; **there is no generator in the tree |
| 137 | +yet.** Issues #50–#69 are design for a future KotlinPoet-based generator that would emit typed |
| 138 | +clients over this toolkit. None is a defect, and most cannot become code until the generator and |
| 139 | +its keystone runtime type (#50) exist. They are captured here (and several warrant short design-doc |
| 140 | +sections now) so the effort can start coherently. |
| 141 | + |
| 142 | +### Keystone: dependency-free four-state JSON field model — [#50] (Design, large) |
| 143 | +The foundation the rest of the model generation builds on: a dep-free sealed `JsonField<T>` |
| 144 | +(`Known` / `Missing` / `Null` / `Raw`) plus a `RawJson` tree in `sdk-core`, with all |
| 145 | +Jackson↔`RawJson` conversion confined to the `sdk-serde-jackson` adapter — mirroring how `Tristate` |
| 146 | +is split today. Almost every other codegen item depends on this. **Land a design doc first** |
| 147 | +(`docs/codegen-json-field.md`), then the runtime type, then the generator template. |
| 148 | + |
| 149 | +### Generated model shape (all depend on #50) |
| 150 | +- **Thin models over a hand-written runtime — [#51]** (Design): generated models stay <100 lines |
| 151 | + (fields + accessors) while the runtime owns the forward-compat machinery. |
| 152 | +- **`additionalProperties` pass-through — [#52]** (Design): capture unknown fields into an immutable |
| 153 | + `Map<String, RawJson>` so read-modify-write round-trips don't drop server-added fields. |
| 154 | +- **Unions as private-ctor + per-variant accessors + visitor — [#53]** (Design): Java-8-safe |
| 155 | + `oneOf` emission with a retained raw node and an `unknown(raw)` fallback. |
| 156 | +- **Forward-compatible enums — [#54]** (Design): open value + known/value pair so deserialization |
| 157 | + never throws on an unrecognized server value. |
| 158 | +- **Discriminator/const fields as defaulted raw values with dual accessors — [#65]** (Design): |
| 159 | + fold into the #50 design effort. |
| 160 | +- **Optional `validate()`/`isValid()`/`validity()` triad — [#64]** (Design): opt-in, memoized, |
| 161 | + off the deserialize path; used only as last-resort union disambiguation behind a discriminator. |
| 162 | + |
| 163 | +### Generated service/client shape |
| 164 | +- **Two-tier raw/cooked service methods — [#55]** (Design): a "cooked" method returning the parsed |
| 165 | + body and a "raw" method returning a lazy `ParsedResponse<T>`. Builds on the `ResponseHandler` |
| 166 | + seam (#36, now in review) and the operation-params SPI (#57). |
| 167 | +- **Minimal `OperationParams` SPI — [#57]** (Design): projects an operation's inputs into |
| 168 | + headers/query/path/body and feeds the context chain. Gated on the `QueryParams` multimap (#28). |
| 169 | +- **Curated operation overload set — [#58]** (Design): one canonical method per operation plus a |
| 170 | + small curated overload set leaning on Kotlin default arguments, rather than the full overload |
| 171 | + cross-product. |
| 172 | +- **Lazy sub-service accessor tree — [#59]** (Design): `by lazy` sub-service accessors on a |
| 173 | + generated root client, reusing a nested raw-response impl. |
| 174 | +- **`withOptions(Consumer<Builder>)` returning a new immutable client — [#60]** (Design): gated on |
| 175 | + first deciding whether to introduce a single cloneable client-config with `toBuilder()`. |
| 176 | +- **Typed page classes that rebuild typed params — [#56]** (Design, xlarge): `nextPage()` |
| 177 | + re-invokes the operation with a typed param object, not a spliced URL string. Gated on #57, #28, |
| 178 | + and the pagination unification (#30). |
| 179 | +- **Per-endpoint SSE adapter — [#62]** (Design): maps an `SseStream` to a lazily-decoded |
| 180 | + `Iterable<TModel>`. Gated on #35 and #36. |
| 181 | +- **Per-operation auth descriptors with a precedence ladder — [#63]** (Design): the generator emits |
| 182 | + an `AuthMetadata` per operation; `sdk-core`'s auth step consumes it scheme-agnostically. The |
| 183 | + scheme-agnostic primitives partly exist already. |
| 184 | + |
| 185 | +### Generator plumbing & outputs |
| 186 | +- **Strict structured-output JSON-schema encoding rules — [#66]** (Design, small): capture the |
| 187 | + encoding contract (all-required + `additionalProperties:false` + optional-as-nullable-union) as a |
| 188 | + design-doc section now; it is adapter-only, never `sdk-core`. |
| 189 | +- **Reusable fail-soft recursive validator skeleton — [#67]** (Design, small): a small generic |
| 190 | + recursion-guarded, path-prefixed validator idiom for the generator's own IR; build in codegen |
| 191 | + week 1–2, once the IR exists. |
| 192 | +- **Provenance file stamped into generated SDKs — [#68]** (Design, small): generator version + |
| 193 | + input-contract hash, emitted into generated output only, never into the hand-written toolkit. |
| 194 | +- **Spring Boot starter per generated API — [#69]** (Design, medium): an optional sibling |
| 195 | + `<api>-spring-boot-starter` with `@ConfigurationProperties`, a customizer `fun interface`, and an |
| 196 | + `@AutoConfiguration` assembling {IoProvider + transport + HttpPipeline}, keeping Spring out of |
| 197 | + `sdk-core` and the generated client. |
| 198 | + |
| 199 | +### Suggested codegen sequencing |
| 200 | + |
| 201 | +1. Design docs: #50 (keystone), #66, #67, #68 — these can be written now, before any generator code. |
| 202 | +2. Land #50's runtime type in `sdk-core`; decide #29 (URL model) and #28 (`QueryParams`). |
| 203 | +3. Stand up the generator IR + the fail-soft validator (#67), then model emission (#51–#54, #64, #65). |
| 204 | +4. Service/client emission (#55, #57, #58, #59, #60), then pagination/SSE/auth generation (#56, #62, #63). |
| 205 | +5. Packaging outputs (#68 provenance, #69 Spring starter). |
0 commit comments