-
Notifications
You must be signed in to change notification settings - Fork 25.6k
[ML] Refactor SSE Parsing #125959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ML] Refactor SSE Parsing #125959
Changes from 1 commit
5459929
e42377d
f35ae98
50dc134
039e583
30e35d8
c69a28f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
| import java.nio.charset.StandardCharsets; | ||
| import java.util.ArrayDeque; | ||
| import java.util.Deque; | ||
| import java.util.Locale; | ||
| import java.util.Optional; | ||
| import java.util.regex.Pattern; | ||
|
|
||
|
|
@@ -20,11 +21,15 @@ | |
| * If the line starts with a colon, we discard this event. | ||
| * If the line contains a colon, we process it into {@link ServerSentEvent} with a non-empty value. | ||
| * If the line does not contain a colon, we process it into {@link ServerSentEvent}with an empty string value. | ||
| * If the line's field is not one of {@link ServerSentEventField}, we discard this event. | ||
| * If the line's field is not one of (data, event), we discard this event. `id` and `retry` are not implemented, because we do not use them | ||
| * and have no plans to use them. | ||
| */ | ||
| public class ServerSentEventParser { | ||
| private static final Pattern END_OF_LINE_REGEX = Pattern.compile("\\n|\\r|\\r\\n"); | ||
| private static final Pattern END_OF_LINE_REGEX = Pattern.compile("\\r\\n|\\n|\\r"); | ||
| private static final String BOM = "\uFEFF"; | ||
| private static final String TYPE_FIELD = "event"; | ||
| private static final String DATA_FIELD = "data"; | ||
| private final EventBuffer eventBuffer = new EventBuffer(); | ||
| private volatile String previousTokens = ""; | ||
|
|
||
| public Deque<ServerSentEvent> parse(byte[] bytes) { | ||
|
|
@@ -39,11 +44,13 @@ public Deque<ServerSentEvent> parse(byte[] bytes) { | |
| for (var i = 0; i < lines.length - 1; i++) { | ||
| var line = lines[i].replace(BOM, ""); | ||
|
|
||
| if (line.isBlank() == false && line.startsWith(":") == false) { | ||
| if (line.isBlank()) { | ||
| eventBuffer.dispatch().ifPresent(collector::offer); | ||
| } else if (line.startsWith(":") == false) { | ||
| if (line.contains(":")) { | ||
| fieldValueEvent(line).ifPresent(collector::offer); | ||
| } else { | ||
| ServerSentEventField.oneOf(line).map(ServerSentEvent::new).ifPresent(collector::offer); | ||
| fieldValueEvent(line); | ||
| } else if (DATA_FIELD.equals(line.toLowerCase(Locale.ROOT))) { | ||
| eventBuffer.data(""); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -55,21 +62,64 @@ public Deque<ServerSentEvent> parse(byte[] bytes) { | |
| return collector; | ||
| } | ||
|
|
||
| private Optional<ServerSentEvent> fieldValueEvent(String lineWithColon) { | ||
| private void fieldValueEvent(String lineWithColon) { | ||
| var firstColon = lineWithColon.indexOf(":"); | ||
| var fieldStr = lineWithColon.substring(0, firstColon); | ||
| var serverSentField = ServerSentEventField.oneOf(fieldStr); | ||
|
|
||
| if ((firstColon + 1) != lineWithColon.length()) { | ||
| var value = lineWithColon.substring(firstColon + 1); | ||
| if (value.equals(" ") == false) { | ||
| var trimmedValue = value.charAt(0) == ' ' ? value.substring(1) : value; | ||
| return serverSentField.map(field -> new ServerSentEvent(field, trimmedValue)); | ||
| var fieldStr = lineWithColon.substring(0, firstColon).toLowerCase(Locale.ROOT); | ||
|
|
||
| var value = lineWithColon.substring(firstColon + 1); | ||
| var trimmedValue = value.length() > 0 && value.charAt(0) == ' ' ? value.substring(1) : value; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah it's literally remove the first space char only:
https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation Or at least I'm interpreting that as "if there are two or more spaces, only remove one space" |
||
|
|
||
| if (DATA_FIELD.equals(fieldStr)) { | ||
| eventBuffer.data(trimmedValue); | ||
| } else if (TYPE_FIELD.equals(fieldStr)) { | ||
| eventBuffer.type(trimmedValue); | ||
| } | ||
| } | ||
|
|
||
| private static class EventBuffer { | ||
| private static final char LINE_FEED = '\n'; | ||
| private static final String MESSAGE = "message"; | ||
| private StringBuilder type = new StringBuilder(); | ||
| private StringBuilder data = new StringBuilder(); | ||
| private boolean appendLineFeed = false; | ||
|
|
||
| private void type(String type) { | ||
| this.type.append(type); | ||
| } | ||
|
|
||
| private void data(String data) { | ||
| if (appendLineFeed) { | ||
| this.data.append(LINE_FEED); | ||
| } else { | ||
| // the next time we add data, append line feed | ||
| appendLineFeed = true; | ||
| } | ||
| this.data.append(data); | ||
| } | ||
|
|
||
| // if we have "data:" or "data: ", treat it like a no-value line | ||
| return serverSentField.map(ServerSentEvent::new); | ||
| private Optional<ServerSentEvent> dispatch() { | ||
| var dataValue = data.toString(); | ||
|
|
||
| // if the data buffer is empty, reset without dispatching | ||
| if (dataValue.isEmpty()) { | ||
| reset(); | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| // if the type buffer is not empty, set that as the type, else default to message | ||
| var typeValue = type.toString(); | ||
| typeValue = typeValue.isBlank() ? MESSAGE : typeValue; | ||
|
|
||
| reset(); | ||
|
|
||
| return Optional.of(new ServerSentEvent(typeValue, dataValue)); | ||
| } | ||
|
|
||
| private void reset() { | ||
| type = new StringBuilder(); | ||
| data = new StringBuilder(); | ||
| appendLineFeed = false; | ||
| } | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't looked at the String class docs for a while and was pleased to find there is actually a method for splitting a string like this
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/String.html#lines()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I didn't know that existed. Let's use that, it does change the logic a bit though