This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/docs by this push:
     new 44b087fd3b TODO-119/120 - Reactive-streams REST returns (JDK 
Flow.Publisher core + Reactor/RxJava bridge) as opt-in 
juneau-rest-server-reactive / juneau-rest-server-reactor modules
44b087fd3b is described below

commit 44b087fd3b24a42ca1e0eb6837f720d11c14e1d5
Author: James Bognar <[email protected]>
AuthorDate: Fri May 29 12:31:56 2026 -0400

    TODO-119/120 - Reactive-streams REST returns (JDK Flow.Publisher core + 
Reactor/RxJava bridge) as opt-in juneau-rest-server-reactive / 
juneau-rest-server-reactor modules
---
 pages/release-notes/9.5.0.md              |  97 ++++++++++++++
 pages/topics/10.20m.RestServerReactive.md | 212 ++++++++++++++++++++++++++++++
 sidebars.ts                               |   5 +
 3 files changed, 314 insertions(+)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 5786563f58..84d55f1acc 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -2685,6 +2685,103 @@ New API:
 - **`RestOpContext.getAsyncCompletionExecutor()`** — op-level resolved 
executor with resource-level fallback.
 - **`RestServerConstants.PROPERTY_asyncCompletionExecutor`** — annotation 
property key constant.
 
+#### Reactive-Streams return types — `Flow.Publisher` core + Reactor / RxJava 
bridge (TODO-119 + TODO-120)
+
+`@RestOp` handlers may now return reactive-streams values. **The feature is 
fully opt-in by adding a module to the classpath** — a bare 
`juneau-rest-server` has zero reactive behavior and `DefaultConfig` wires no 
reactive processor. The JDK-native `java.util.concurrent.Flow.Publisher<T>` is 
supported by the new dependency-free, opt-in `juneau-rest-server-reactive` 
module; Project Reactor (`Mono` / `Flux`), RxJava 3 (`Single` / `Maybe` / 
`Completable` / `Flowable` / `Observable`), and the [...]
+
+```java
+@Rest(path="/feed")
+public class FeedResource extends BasicRestServlet {
+
+    // Single-value reactive type — collapses onto the existing async 
(CompletableFuture) path.
+    @RestGet("/{id}")
+    public Mono<Article> get(@Path String id) {
+        return articleService.findById(id);                    // Reactor Mono 
→ CompletionStage
+    }
+
+    // Bounded stream, default shape — collected into a JSON array.
+    @RestGet("/recent")
+    public Flux<Article> recent() {
+        return articleService.recent(20);                      // buffered to 
a List, serialized as JSON
+    }
+
+    // Unbounded stream as Server-Sent Events — opt in via the response 
content type.
+    @RestGet("/live")
+    public Flux<SseEvent> live(RestResponse res) {
+        res.setContentType("text/event-stream");               // → SSE 
frames, one per element
+        return articleService.liveEvents();
+    }
+
+    // Same stream as newline-delimited JSON.
+    @RestGet("/ndjson")
+    public Flux<Article> ndjson(RestResponse res) {
+        res.setContentType("application/x-ndjson");            // → one JSON 
object per line
+        return articleService.recent(1000);
+    }
+}
+```
+
+Unified architecture (the two promoted plan items ship as one coherent 
feature):
+
+- **Opt-in by classpath presence.** `ReactiveResponseProcessor` is **not** 
wired into `DefaultConfig`. It ships in `juneau-rest-server-reactive` with a 
`META-INF/services/org.apache.juneau.rest.processor.ResponseProcessor` provider 
file; `RestContext` discovers module-contributed response processors via 
`ServiceLoader` and front-loads them ahead of `AsyncResponseProcessor`. On a 
bare `juneau-rest-server` the discovery finds nothing and the processor chain 
is byte-for-byte the pre-feature [...]
+- **`ReactiveResponseProcessor`** (`org.apache.juneau.rest.reactive`, in the 
opt-in `juneau-rest-server-reactive` module) is the single shared spine. Once 
on the classpath it is front-loaded ahead of `AsyncResponseProcessor` and 
natively understands `Flow.Publisher`. Synchronous returns (`String`, beans, 
`byte[]`, …) are untouched.
+- **Single-value reactive types** (`Mono`, `Single`, `Maybe`, `Completable`) 
are adapted to a `CompletionStage` and collapse onto the existing 
`AsyncResponseProcessor` path — inheriting its timeout, 
`@Rest(asyncCompletionExecutor)` (TODO-118), and SLF4J MDC bridge (TODO-117) 
behavior for free.
+- **Multi-value streams** are rendered in one of three shapes, selected by the 
negotiated response media type (handler `Content-Type`, then request `Accept`):
+  - **Buffer** (default) — all elements collected into a `List` and serialized 
through the normal chain (e.g. a JSON array). Wrapped in a `CompletableFuture` 
so a slow producer never blocks the request thread.
+  - **SSE** (`text/event-stream`) — each element emitted as a 
Server-Sent-Events frame; `SseEvent` elements are written verbatim, any other 
element type is JSON-encoded into the `data:` field.
+  - **NDJSON** (`application/x-ndjson`, `application/jsonl`) — each element 
JSON-encoded on its own line.
+- **Backpressure** — streaming subscribers request one element at a time 
(`request(1)` on subscribe and again after each frame is flushed). Because the 
servlet output stream blocks until the socket drains, the producer is paced by 
the client and the server-side buffer stays bounded.
+- **Third-party libraries are `provided`-scoped** — `reactive-streams`, 
`reactor-core`, and `rxjava` are all declared `provided` in 
`juneau-rest-server-reactor`. A `dependency:tree` on `juneau-rest-server` never 
surfaces them. Adapters are discovered lazily via `ServiceLoader` and skipped 
at runtime if their backing library is absent, so a consumer adds 
`juneau-rest-server-reactor` plus only the reactive library (and version) they 
actually use.
+- **Extensibility** — the `ReactiveStreamsAdapter` SPI + `Adaptation` value 
type let third parties bridge any other reactive library to the core 
`Flow.Publisher` / `CompletionStage` plumbing without modifying 
`juneau-rest-server`.
+
+New API (all in the opt-in `juneau-rest-server-reactive` module):
+
+- **`org.apache.juneau.rest.reactive.ReactiveResponseProcessor`** — the 
response processor, auto-registered via `ServiceLoader` when the 
`juneau-rest-server-reactive` module is on the classpath (no `DefaultConfig` 
wiring).
+- **`org.apache.juneau.rest.reactive.ReactiveStreamsAdapter`** — SPI for 
bridging third-party reactive types (`boolean canAdapt(Object)` + `Adaptation 
adapt(Object)`).
+- **`org.apache.juneau.rest.reactive.Adaptation`** — single-value 
(`CompletionStage`) or streaming (`Flow.Publisher`) adaptation result.
+
+`RestContext` now discovers module-contributed `ResponseProcessor`s via 
`ServiceLoader` (inert on a bare classpath) — this is the seam that 
auto-activates the reactive processor.
+
+See [Reactive-Streams Returns](/docs/topics/RestServerReactive) for the full 
topic.
+
+#### New module: `juneau-rest-server-reactive` (TODO-119/120 refactor)
+
+A new dependency-free, opt-in module that holds the JDK-native 
`Flow.Publisher` spine (`ReactiveResponseProcessor`), the 
`ReactiveStreamsAdapter` SPI, the `Adaptation` value type, and the 
`META-INF/services/...ResponseProcessor` auto-registration file. Adding it 
alone enables `Flow.Publisher<T>` returns with no third-party dependency. A 
bare `juneau-rest-server` without it has zero reactive behavior.
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-reactive</artifactId>
+    <version>9.5.0</version>
+</dependency>
+```
+
+#### New module: `juneau-rest-server-reactor` (TODO-119)
+
+A further opt-in bridge module (depends on `juneau-rest-server-reactive`) 
providing `ReactiveStreamsAdapter` implementations for the popular reactive 
libraries. Server-side only — there is no client-side counterpart. All three 
backing libraries are `provided`-scope.
+
+| Adapter | Handles | Adapts to |
+|---|---|---|
+| `ReactorReactiveAdapter` | `Mono<T>` / `Flux<T>` | `Mono` → 
`CompletionStage` (via `toFuture()`); `Flux` → `Flow.Publisher` (via 
`FlowAdapters`) |
+| `RxJavaReactiveAdapter` | `Single` / `Maybe` / `Completable` / `Flowable` / 
`Observable` | single types → `CompletionStage`; `Flowable` → `Flow.Publisher`; 
`Observable` → `Flowable(BackpressureStrategy.BUFFER)` → `Flow.Publisher` |
+| `ReactiveStreamsPublisherAdapter` | generic 
`org.reactivestreams.Publisher<T>` | `Flow.Publisher` (via `FlowAdapters`); 
registered last so `Mono`/`Flux` are matched as single/stream first |
+
+Maven coordinate:
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-reactor</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<!-- plus ONLY the reactive library you use, e.g.: -->
+<dependency>
+    <groupId>io.projectreactor</groupId>
+    <artifactId>reactor-core</artifactId>
+    <version>3.6.11</version>
+</dependency>
+```
+
 #### Health Probe SPI + Resource (TODO-65)
 
 `juneau-rest-server` now includes a built-in probe SPI and aggregation 
resource under
diff --git a/pages/topics/10.20m.RestServerReactive.md 
b/pages/topics/10.20m.RestServerReactive.md
new file mode 100644
index 0000000000..b5fd87afe7
--- /dev/null
+++ b/pages/topics/10.20m.RestServerReactive.md
@@ -0,0 +1,212 @@
+---
+title: "Reactive-Streams Returns"
+slug: RestServerReactive
+---
+
+`@RestOp` handler methods may return reactive-streams values. **The feature is 
fully opt-in by adding a module to the classpath** — a bare 
`juneau-rest-server` has zero reactive behavior and its `DefaultConfig` wires 
no reactive processor.
+
+Two layers stack on top of core:
+
+| Classpath | Enables |
+|---|---|
+| bare `juneau-rest-server` | *(no reactive behavior — a `Flow.Publisher` 
return is serialized as an ordinary object)* |
+| `+ juneau-rest-server-reactive` | JDK-native 
`java.util.concurrent.Flow.Publisher<T>` returns (dependency-free) |
+| `+ juneau-rest-server-reactor` | Project Reactor (`Mono` / `Flux`), RxJava 3 
(`Single` / `Maybe` / `Completable` / `Flowable` / `Observable`), and the 
generic `org.reactivestreams.Publisher<T>` |
+
+Both modules are server-side only. `juneau-rest-server-reactive` is 
dependency-free; `juneau-rest-server-reactor` declares Reactor / RxJava / 
Reactive-Streams in `provided` scope.
+
+This feature shares the [async-returns](/docs/topics/RestServerAsyncDispatch) 
plumbing: single-value reactive types collapse onto the existing 
`CompletableFuture` path, and multi-value streams are rendered as a buffered 
list, Server-Sent Events, or newline-delimited JSON.
+
+## Quick start
+
+```java
+@Rest(path="/feed")
+public class FeedResource extends BasicRestServlet {
+
+    @Inject ArticleService articles;
+
+    // Single value — uses the async (CompletableFuture) path under the hood.
+    @RestGet("/{id}")
+    public Mono<Article> get(@Path String id) {
+        return articles.findById(id);
+    }
+
+    // Bounded stream — collected into a JSON array (the default shape).
+    @RestGet("/recent")
+    public Flux<Article> recent() {
+        return articles.recent(20);
+    }
+
+    // Unbounded stream as Server-Sent Events.
+    @RestGet("/live")
+    public Flux<SseEvent> live(RestResponse res) {
+        res.setContentType("text/event-stream");
+        return articles.liveEvents();
+    }
+}
+```
+
+## Architecture
+
+A single response processor — `ReactiveResponseProcessor` 
(`org.apache.juneau.rest.reactive`, shipped in the opt-in 
`juneau-rest-server-reactive` module) — is the shared spine for all reactive 
return-type support.
+
+**Auto-registration (no manual wiring).** The processor is *not* listed in 
`DefaultConfig`. Instead, `juneau-rest-server-reactive` ships a 
`META-INF/services/org.apache.juneau.rest.processor.ResponseProcessor` provider 
file naming `ReactiveResponseProcessor`. `RestContext` discovers 
module-contributed response processors via `ServiceLoader` and front-loads them 
ahead of `AsyncResponseProcessor` in the chain. On a bare `juneau-rest-server` 
classpath this discovery finds nothing, so the ch [...]
+
+1. Natively recognizes `java.util.concurrent.Flow.Publisher<T>` (no external 
dependency).
+2. For any other return value, consults registered `ReactiveStreamsAdapter` 
providers (discovered via the same `ServiceLoader` mechanism). The 
`juneau-rest-server-reactor` module supplies adapters for Reactor, RxJava 3, 
and the generic Reactive-Streams `Publisher`.
+
+Each adapter converts its library's type into one of two JDK-native shapes, 
expressed by the `Adaptation` value type:
+
+- a **single-value** `java.util.concurrent.CompletionStage` (e.g. `Mono`, 
`Single`, `Maybe`, `Completable`), or
+- a **multi-value** `Flow.Publisher` (e.g. `Flux`, `Flowable`, `Observable`, 
`Publisher`).
+
+Single-value adaptations are handed back to the response chain as a 
`CompletionStage`, where the existing `AsyncResponseProcessor` picks them up — 
so they inherit its timeout, 
[`@Rest(asyncCompletionExecutor)`](/docs/topics/RestServerAsyncDispatch) 
routing (TODO-118), and [SLF4J MDC 
bridge](/docs/topics/RestServerAsyncDispatch) (TODO-117) for free.
+
+```mermaid
+flowchart TD
+    A["@RestOp returns reactive value"] --> B{ReactiveResponseProcessor}
+    B -->|Flow.Publisher| D[dispatch stream]
+    B -->|other type| C{ReactiveStreamsAdapter}
+    C -->|single| E[CompletionStage]
+    C -->|stream| D
+    E --> F[AsyncResponseProcessor: timeout, executor, MDC]
+    D --> G{shape?}
+    G -->|Buffer default| H[collect to List → JSON]
+    G -->|text/event-stream| I[SSE frames]
+    G -->|application/x-ndjson| J[NDJSON lines]
+```
+
+## Response shapes for multi-value streams
+
+A streaming publisher is rendered as one of three shapes, **selected by the 
negotiated response media type** — the handler's `Content-Type` (set via 
`res.setContentType(...)` or `@RestOp(produces=...)`), and failing that, the 
request `Accept` header:
+
+| Shape | Trigger media type(s) | Behavior |
+|---|---|---|
+| **Buffer** (default) | anything else / unspecified | All elements collected 
into a `List` and serialized through the normal serializer chain (e.g. a JSON 
array). The collection is wrapped in a `CompletableFuture` and handed to the 
async path, so a slow producer never blocks the request thread. |
+| **SSE** | `text/event-stream` | Each element emitted as a Server-Sent-Events 
frame. `SseEvent` elements are written verbatim; any other element type is 
JSON-encoded into the `data:` field. |
+| **NDJSON** | `application/x-ndjson`, `application/jsonl`, 
`application/json-seq` | Each element JSON-encoded on its own line. |
+
+The default-to-buffer choice is deliberate: a bare `Flux<Article>` with no 
content-type hint produces a normal JSON array, which is the least surprising 
result for the majority of callers. Streaming is an explicit opt-in via the 
content type.
+
+### Choosing the shape
+
+```java
+// Buffer (default) — JSON array.
+@RestGet("/page")
+public Flux<Row> page() { return repo.page(50); }
+
+// SSE — handler sets the content type.
+@RestGet("/events")
+public Flux<SseEvent> events(RestResponse res) {
+    res.setContentType("text/event-stream");
+    return bus.subscribe();
+}
+
+// NDJSON — via @RestOp(produces=...) instead of setting it imperatively.
+@RestGet(path="/export", produces="application/x-ndjson")
+public Flux<Row> export() { return repo.everything(); }
+
+// Shape can also be driven by the client's Accept header when the handler
+// leaves the content type unset.
+```
+
+## Backpressure
+
+Streaming subscribers request **one element at a time** — `request(1)` on 
subscribe, and `request(1)` again after each frame is written and flushed. 
Because writing to the servlet output stream blocks until the socket accepts 
the bytes, this provides natural backpressure: the producer is paced by the 
client's drain rate and the server-side buffer does not grow without bound.
+
+Buffer-shape subscribers request `Long.MAX_VALUE`, since the collection is 
bounded by the publisher's own completion. Apply your own bound (`.take(n)`, 
`.limitRequest(n)`, paging) before returning a publisher you intend to buffer — 
an unbounded `Flux` buffered to a `List` will accumulate in memory.
+
+`Observable` (which has no native backpressure) is converted to a `Flowable` 
with `BackpressureStrategy.BUFFER` before adaptation.
+
+## Threading, executors, and MDC
+
+- **Buffer-shape** responses route through `AsyncResponseProcessor` and 
therefore honor `@Rest(asyncCompletionExecutor)` and the SLF4J MDC bridge 
exactly as a `CompletableFuture` return would.
+- **Streaming-shape** frame writes happen on whichever thread the publisher 
emits on (the Reactor / RxJava scheduler). This processor does **not** impose a 
`subscribeOn(...)`, so it never fights the library's own scheduler model. When 
MDC propagation is enabled (`RestContext.isMdcAsyncPropagation()`), the 
request-thread MDC snapshot is reinstalled around each `onNext` / terminal 
callback so log statements emitted while writing a frame see the request's 
diagnostic context.
+
+## Synchronous fallback
+
+In environments where `HttpServletRequest.startAsync()` is unsupported — 
notably Juneau's `MockServletRequest` used by `MockRestClient` in unit tests — 
streaming subscribes synchronously and blocks the request thread until the 
publisher terminates (bounded by the configured async timeout), writing frames 
as they arrive. This keeps the unit-test surface working without a real servlet 
container.
+
+## The modules
+
+### `juneau-rest-server-reactive` (JDK-native core)
+
+Dependency-free. Carries `ReactiveResponseProcessor`, the 
`ReactiveStreamsAdapter` SPI, the `Adaptation` value type, and the 
`META-INF/services/...ResponseProcessor` auto-registration file. Add this alone 
to enable `Flow.Publisher<T>` returns with no third-party library:
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-reactive</artifactId>
+    <version>9.5.0</version>
+</dependency>
+```
+
+### `juneau-rest-server-reactor` (third-party bridge)
+
+The bridge module is **server-side only** and opt-in, and depends on 
`juneau-rest-server-reactive`. All three backing libraries are 
`provided`-scope: a `dependency:tree` on `juneau-rest-server` (or 
`juneau-rest-server-reactive`) never surfaces `reactor-core`, `rxjava`, or 
`reactive-streams`. Adapters are discovered lazily via `ServiceLoader` and 
skipped at runtime if their backing library is absent from the classpath, so 
you add the module plus **only the reactive library you actually us [...]
+
+```xml
+<dependency>
+    <groupId>org.apache.juneau</groupId>
+    <artifactId>juneau-rest-server-reactor</artifactId>
+    <version>9.5.0</version>
+</dependency>
+<dependency>
+    <groupId>io.projectreactor</groupId>
+    <artifactId>reactor-core</artifactId>
+    <version>3.6.11</version>
+</dependency>
+```
+
+| Adapter | Handles | Adapts to |
+|---|---|---|
+| `ReactorReactiveAdapter` | `Mono<T>` / `Flux<T>` | `Mono` → 
`CompletionStage` (`Mono.toFuture()`); `Flux` → `Flow.Publisher` 
(`FlowAdapters`) |
+| `RxJavaReactiveAdapter` | `Single` / `Maybe` / `Completable` / `Flowable` / 
`Observable` | single types → `CompletionStage`; `Flowable` → `Flow.Publisher`; 
`Observable` → `Flowable(BUFFER)` → `Flow.Publisher` |
+| `ReactiveStreamsPublisherAdapter` | generic 
`org.reactivestreams.Publisher<T>` | `Flow.Publisher` (`FlowAdapters`); 
registered last so `Mono`/`Flux` resolve as single/stream first |
+
+An empty `Mono` / `Maybe` / `Completable` completes the response with a `null` 
body.
+
+### Extending with your own adapter
+
+Bridge any other reactive library by implementing `ReactiveStreamsAdapter` and 
registering it via `META-INF/services`:
+
+```java
+public class MyLibAdapter implements ReactiveStreamsAdapter {
+    @Override public boolean canAdapt(Object v) { return v instanceof MyAsync 
|| v instanceof MyStream; }
+    @Override public Adaptation adapt(Object v) {
+        if (v instanceof MyAsync<?> a) return 
Adaptation.single(a.toCompletionStage());
+        return Adaptation.stream(FlowAdapters.toFlowPublisher(((MyStream<?>) 
v).toReactiveStreams()));
+    }
+}
+```
+
+```
+# META-INF/services/org.apache.juneau.rest.reactive.ReactiveStreamsAdapter
+com.example.MyLibAdapter
+```
+
+## Configuration summary
+
+| Setting | Where | Effect |
+|---|---|---|
+| return `Flow.Publisher<T>` | `@RestOp` method (with 
`juneau-rest-server-reactive`) | JDK-native streaming, no third-party 
dependency |
+| return `Mono` / `Single` / `Maybe` / `Completable` | `@RestOp` method (with 
`juneau-rest-server-reactor`) | single value via async path |
+| return `Flux` / `Flowable` / `Observable` / `Publisher` | `@RestOp` method 
(with `juneau-rest-server-reactor`) | stream — shape by content type |
+| `res.setContentType("text/event-stream")` | handler body | force SSE shape |
+| `res.setContentType("application/x-ndjson")` | handler body | force NDJSON 
shape |
+| (no content type) | — | buffer to list (JSON array) |
+
+## Acceptance verification
+
+The opt-in contract is verified from both sides:
+
+| Behavior | Test | Module |
+|---|---|---|
+| **Bare** `juneau-rest-server` does NOT process `Flow.Publisher` reactively | 
`ReactiveOptIn_BareServer_Test` | `juneau-rest-mock` (no reactive module on its 
classpath) |
+| `Flow.Publisher` buffered to JSON array (default) | 
`ReactiveResponseProcessor_Test` | `juneau-utest` |
+| `Flow.Publisher<SseEvent>` → SSE frames | `ReactiveResponseProcessor_Test` | 
`juneau-utest` |
+| POJO element stream → SSE / NDJSON | `ReactiveResponseProcessor_Test` | 
`juneau-utest` |
+| Publisher error propagation | `ReactiveResponseProcessor_Test` | 
`juneau-utest` |
+| Reactor `Mono` / `Flux` adaptation | `ReactiveBridge_Test` | `juneau-utest` |
+| RxJava `Single` / `Maybe` / `Completable` / `Flowable` / `Observable` 
adaptation | `ReactiveBridge_Test` | `juneau-utest` |
+| Generic `org.reactivestreams.Publisher` adaptation | `ReactiveBridge_Test` | 
`juneau-utest` |
diff --git a/sidebars.ts b/sidebars.ts
index e0110a361a..5a4c61afbd 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1571,6 +1571,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/10.20h.RestServerAsyncDispatch',
                                                        label: '10.20h. Async 
Returns + Virtual-Thread Dispatch',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/10.20m.RestServerReactive',
+                                                       label: '10.20m. 
Reactive-Streams Returns',
+                                               },
                                                {
                                                        type: 'doc',
                                                        id: 
'topics/10.21.BuiltInParameters',

Reply via email to