Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
JettyEventStream.java
Go to the documentation of this file.
1package dev.rafex.ether.http.jetty12.exchange;
2
3/*-
4 * #%L
5 * ether-http-jetty12
6 * %%
7 * Copyright (C) 2025 - 2026 Raúl Eduardo González Argote
8 * %%
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 * #L%
27 */
28
29import java.nio.ByteBuffer;
30import java.nio.charset.StandardCharsets;
31import java.util.ArrayDeque;
32import java.util.Deque;
33
34import org.eclipse.jetty.server.Response;
35import org.eclipse.jetty.util.Callback;
36
37import dev.rafex.ether.http.core.HttpExchange.EventStream;
38
39/**
40 * Jetty 12 {@link EventStream} implementation.
41 *
42 * <p>Jetty forbids issuing a new {@link Response#write} before the {@link Callback} of the
43 * previous one completes, but {@link #send}/{@link #comment}/{@link #close} may be invoked
44 * concurrently (e.g. from a message-bus callback thread and a heartbeat scheduler thread), so
45 * every write — including the final empty/last one — goes through a single queue that is
46 * pumped one entry at a time.
47 */
48final class JettyEventStream implements EventStream {
49
50 private static final ByteBuffer EMPTY = ByteBuffer.allocate(0);
51
52 private final Response response;
53 private final Callback finalCallback;
54 private final Deque<byte[]> queue = new ArrayDeque<>();
55
56 private boolean writePending;
57 private boolean closing;
58 private boolean finished;
59 private Runnable onClose;
60
61 JettyEventStream(final Response response, final Callback finalCallback) {
62 this.response = response;
63 this.finalCallback = finalCallback;
64 }
65
66 @Override
67 public void send(final String event, final String data) {
68 final var sb = new StringBuilder();
69 if (event != null && !event.isBlank()) {
70 sb.append("event: ").append(event).append('\n');
71 }
72 final String safeData = data == null ? "" : data;
73 for (final String line : safeData.split("\n", -1)) {
74 sb.append("data: ").append(line).append('\n');
75 }
76 sb.append('\n');
77 enqueue(sb.toString().getBytes(StandardCharsets.UTF_8));
78 }
79
80 @Override
81 public void comment(final String text) {
82 final String safe = (text == null ? "" : text).replace("\n", " ");
83 enqueue((": " + safe + "\n\n").getBytes(StandardCharsets.UTF_8));
84 }
85
86 @Override
87 public synchronized void onClose(final Runnable callback) {
88 this.onClose = callback;
89 }
90
91 @Override
92 public void close() {
93 synchronized (this) {
94 if (closing) {
95 return;
96 }
97 closing = true;
98 }
99 pump();
100 }
101
102 private void enqueue(final byte[] bytes) {
103 synchronized (this) {
104 if (closing || finished) {
105 return;
106 }
107 queue.addLast(bytes);
108 }
109 pump();
110 }
111
112 /** Drains the queue one write at a time; issues the final empty write once drained and closing. */
113 private void pump() {
114 final byte[] next;
115 final boolean last;
116 synchronized (this) {
117 if (writePending || finished) {
118 return;
119 }
120 if (!queue.isEmpty()) {
121 next = queue.pollFirst();
122 last = false;
123 } else if (closing) {
124 next = null;
125 last = true;
126 } else {
127 return;
128 }
129 writePending = true;
130 }
131 response.write(last, next == null ? EMPTY : ByteBuffer.wrap(next), new Callback() {
132 @Override
133 public void succeeded() {
134 if (last) {
135 complete(null);
136 } else {
137 synchronized (JettyEventStream.this) {
138 writePending = false;
139 }
140 pump();
141 }
142 }
143
144 @Override
145 public void failed(final Throwable cause) {
146 complete(cause);
147 }
148 });
149 }
150
151 private void complete(final Throwable cause) {
152 final Runnable callback;
153 synchronized (this) {
154 if (finished) {
155 return;
156 }
157 finished = true;
158 closing = true;
159 queue.clear();
160 callback = onClose;
161 }
162 if (cause == null) {
163 finalCallback.succeeded();
164 } else {
165 finalCallback.failed(cause);
166 }
167 if (callback != null) {
168 callback.run();
169 }
170 }
171}
A long-lived, transport-agnostic handle for pushing Server-Sent Events.