v0.4.7 (26 June 2026)
@HkjHttpClient: generated Effect-Path HTTP clients
hkj-spring was server-side only: the *PathReturnValueHandlers map an EitherPath/VTaskPath/… returned from a controller into an HTTP response, but when service A called service B, B's typed error collapsed into a raw status code at A's boundary, losing the typed error channel the library is built around. The new @HkjHttpClient closes that gap with the client-side inverse, preserving the typed error end-to-end across services. Two additive modules (hkj-spring/client runtime + hkj-spring/client-processor), no breaking changes.
- Runtime (
hkj-spring/client):HkjClientExchangefolds an HTTP exchange into an Effect Path:either(2xx→Right, 4xx/5xx→Left),eitherVTask(deferred on a virtual thread, so callers getwithRetry/withCircuitBreaker/timeout), andmaybe(404/empty→Nothing). A pluggableResponseErrorDecoderdecodes the server's{"success":false,"error":…}envelope into the declared error type via the shared Jackson mapper; auto-configuration contributes the default factory. - Codegen (
hkj-spring/client-processor): annotating a Path-typed@HttpExchangeinterface generates a native@HttpExchangeinterface (return types unwrapped toResponseEntity<T>, all mapping/parameter annotations copied through), a…Clientimplementation that dispatches by return type, and a…ClientConfigurationthat wires the client via Spring 7@ImportHttpServices; base URL/timeouts/versioning come fromspring.http.serviceclient.<group>.*. - A concrete error type decodes with no extra annotations; a sealed
DomainErrorhierarchy needs@JsonTypeInfo/@JsonSubTypes. The processor is wired intohkj-spring-boot-starter; thehkj-spring/client-examplemodule is a standalone client application that calls the server example over HTTP (with an end-to-endMockRestServiceServertest), and the Declarative HTTP Clients guide walks through it. - Additional capabilities:
@OnStatus(value, error)maps individual statuses to distinct error subtypes (404 →UserNotFoundError, …); generic@HkjHttpClientinterfaces are supported codegen-only;ClientErrorResponse.retryAfter()exposes the server'sRetry-Afterhint for back-off; andHkjClientExchange.vstream(...)consumes the server's SSE stream into aVStreamPath<T>(deferred, resource-safe). The runtime itself is written in the library's own idioms (Try/Either), and the client is documented across thehkj-springmodule docs plus a dedicatedHTTP_CLIENT.md.
Spring Boot 4.1.0 / Framework 7.0.8 upgrade
The hkj-spring modules move from Spring Boot 4.0.6 to 4.1.0 (Spring Framework 7.0.8), with the managed Jackson 3.x line advancing from 3.1.2 to 3.1.4 to match the jackson-bom shipped by Boot 4.1.0. Dependency-only, centralised in the version catalog; the modules compile and pass against 4.1.0 with no source changes and no new deprecations. No public API change (#575).
Writer.of(log, value) factory
New Writer.of(W log, @Nullable A value) static factory for the common custom-log-plus-value case, sitting between Writer.value(Monoid<W>, A) (empty log) and Writer.tell(W) (Unit value). The (log, value) order mirrors the record components and accessors, and returning the plain Writer<W, A> launders the @Nullable A nullness contract that the raw constructor's diamond leaks at the call site. Purely additive (#554).
Consistent recoverWith / recover null-handling across MonadError
recoverWith(ma, fallback) now rejects a null ma/fallback eagerly and identically on every MonadError instance (TryMonad, OptionalMonad, VTask, CompletableFuture, EitherT/MaybeT/OptionalT), replacing the previous state-dependent, mislabelled NullPointerException (EitherMonad/ValidatedMonad already guarded it). recover(ma, value) keeps its @Nullable value, so recover(failure, null) stays a valid Success(null)/Nothing/empty; ValidatedMonad keeps only recoverWith because its of rejects null. Behaviour-preserving except on null input (#553).
Internal: type-safety and soundness cleanups
A sweep across hkj-core removing avoidable unchecked casts and holder indirection; behaviour-preserving with no public API change unless noted:
- Turned on
-Xlint:unchecked,rawtypes -Werroracross all modules, so any new unchecked or raw-type use must carry an explicit suppression; generated@ComposeEffectsSupport classes carry one so downstream lint-enabled builds stay clean (#560). Maybe/Either/Validatedroots now extend theirKindinterfaces, making the fivewiden/widen2methods cast-free upcasts (#561).- Every remaining HKJ-owned type direct-implements its
Kind:widenis an allocation-free upcast, seventeen*Holderrecords are deleted, andnarrow(null)now uniformly raisesKindUnwrapException(Lazyand the JDK-wrapped types keep their holders) (#568). - Funnelled the covariant
flatMap/recoverWithreinterpretations through a privatecovaryhelper, and replaced the public API's last raw-Kindwrapper (IndexedTraversal.asIndexedFold()) with a typedIdBox(#562). - Consolidated
Free.foldMap's two stack-safe interpreters behind the singleNaturalpath (#563).
EachIndexed: type-safe replacement for Each.eachWithIndex()
Each.eachWithIndex() returned Optional<IndexedTraversal<I, S, A>> with a caller-chosen index type, so requesting the wrong index compiled and then failed at runtime with a ClassCastException. New EachIndexed<I, S, A> extends Each<S, A> carries the real index type at the type level and exposes indexedTraversal() directly (no Optional, no cast); the EachInstances factories now return it. Each.eachWithIndex() is deprecated for removal in 0.5.0 and still works as a bridge, so existing code compiles. Additive plus one deprecation, no behaviour change for existing callers (#564). See Each type class / Indexed Optics.
raw-kind checker rule
The HKJ compiler plugin gains a raw-kind rule: a raw Kind/Kind2 drops its witness type argument (the one route that lets a value tagged with one witness be narrowed through another, compiling silently and throwing KindUnwrapException at runtime), and javac accepts it, so the checker is the sole compile-time signal. Flags variable/parameter/field declarations and casts at warn by default (disable=raw-kind, severity:raw-kind=error); a properly parameterised Kind<W, A> is never flagged (#565). Documented in Compile-Time Checks.