Skip to content

Commit 02ec43b

Browse files
feat: Add explicit SSE event names for local v3 streaming
1 parent fa15b36 commit 02ec43b

File tree

4 files changed

+65
-34
lines changed

4 files changed

+65
-34
lines changed

.stats.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
configured_endpoints: 8
2-
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-975ca868b31b1e45fb00b31a53d9df1ceec8663f6c2851bf40fdaa84171afadc.yml
3-
openapi_spec_hash: 37891379e0f47e5c69769fbaa1064dab
4-
config_hash: 0209737a4ab2a71afececb0ff7459998
2+
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-a4a5ea048bb50d06460d81d6828b53b12b19e9224121ee6338dcd1f0781e22a1.yml
3+
openapi_spec_hash: 9b81c0ae04576318d13d7a80d4ab7b5a
4+
config_hash: d6c6f623d03971bdba921650e5eb7e5f

stagehand-java-core/src/main/kotlin/com/browserbase/api/core/handlers/SseHandler.kt

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
1818
internal fun sseHandler(jsonMapper: JsonMapper): Handler<StreamResponse<SseMessage>> =
1919
streamHandler { response, lines ->
2020
val state = SseState(jsonMapper)
21-
var done = false
2221
for (line in lines) {
23-
// Stop emitting messages, but iterate through the full stream.
24-
if (done) {
25-
continue
26-
}
27-
2822
val message = state.decode(line) ?: continue
2923

30-
when {
31-
message.data.startsWith("{\"data\":{\"status\":\"finished\"") -> {
32-
// In this case we don't break because we still want to iterate through the full
33-
// stream.
34-
done = true
35-
continue
36-
}
37-
message.data.startsWith("error") -> {
24+
when (message.event) {
25+
"error" -> {
3826
throw SseException.builder()
3927
.statusCode(response.statusCode())
4028
.headers(response.headers())
@@ -47,10 +35,10 @@ internal fun sseHandler(jsonMapper: JsonMapper): Handler<StreamResponse<SseMessa
4735
)
4836
.build()
4937
}
50-
}
51-
52-
if (message.event == null) {
53-
yield(message)
38+
"starting",
39+
"connected",
40+
"running",
41+
"finished" -> yield(message)
5442
}
5543
}
5644
}

stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/StreamEvent.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ import java.util.Optional
3030
import kotlin.jvm.optionals.getOrNull
3131

3232
/**
33-
* Server-Sent Event emitted during streaming responses. Events are sent as `data: <JSON>\n\n`. Key
34-
* order: data (with status first), type, id.
33+
* Server-Sent Event emitted during streaming responses. Events are sent as `event: <status>\ndata:
34+
* <JSON>\n\n`, where the JSON payload has the shape `{ data, type, id }`.
3535
*/
3636
class StreamEvent
3737
@JsonCreator(mode = JsonCreator.Mode.DISABLED)

stagehand-java-core/src/test/kotlin/com/browserbase/api/core/handlers/SseHandlerTest.kt

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
package com.browserbase.api.core.handlers
44

5+
import com.browserbase.api.core.JsonValue
56
import com.browserbase.api.core.http.Headers
67
import com.browserbase.api.core.http.HttpResponse
78
import com.browserbase.api.core.http.SseMessage
89
import com.browserbase.api.core.jsonMapper
10+
import com.browserbase.api.errors.SseException
911
import java.io.InputStream
1012
import java.util.stream.Collectors.toList
1113
import org.assertj.core.api.Assertions.assertThat
@@ -21,64 +23,105 @@ internal class SseHandlerTest {
2123
internal val expectedMessages: List<SseMessage>? = null,
2224
internal val expectedException: Exception? = null,
2325
) {
24-
DATA_MISSING_EVENT(
26+
EVENT_AND_DATA(
2527
buildString {
28+
append("event: starting\n")
2629
append("data: {\"foo\":true}\n")
2730
append("\n")
2831
},
29-
listOf(sseMessageBuilder().data("{\"foo\":true}").build()),
32+
listOf(sseMessageBuilder().event("starting").data("{\"foo\":true}").build()),
3033
),
31-
MULTIPLE_DATA_MISSING_EVENT(
34+
EVENT_MISSING_DATA(
3235
buildString {
36+
append("event: starting\n")
37+
append("\n")
38+
},
39+
listOf(sseMessageBuilder().event("starting").build()),
40+
),
41+
MULTIPLE_EVENTS_AND_DATA(
42+
buildString {
43+
append("event: starting\n")
3344
append("data: {\"foo\":true}\n")
3445
append("\n")
46+
append("event: connected\n")
3547
append("data: {\"bar\":false}\n")
3648
append("\n")
3749
},
3850
listOf(
39-
sseMessageBuilder().data("{\"foo\":true}").build(),
40-
sseMessageBuilder().data("{\"bar\":false}").build(),
51+
sseMessageBuilder().event("starting").data("{\"foo\":true}").build(),
52+
sseMessageBuilder().event("connected").data("{\"bar\":false}").build(),
53+
),
54+
),
55+
MULTIPLE_EVENTS_MISSING_DATA(
56+
buildString {
57+
append("event: starting\n")
58+
append("\n")
59+
append("event: connected\n")
60+
append("\n")
61+
},
62+
listOf(
63+
sseMessageBuilder().event("starting").build(),
64+
sseMessageBuilder().event("connected").build(),
4165
),
4266
),
4367
DATA_JSON_ESCAPED_DOUBLE_NEW_LINE(
4468
buildString {
69+
append("event: starting\n")
4570
append("data: {\n")
4671
append("data: \"foo\":\n")
4772
append("data: true}\n")
4873
append("\n\n")
4974
},
50-
listOf(sseMessageBuilder().data("{\n\"foo\":\ntrue}").build()),
75+
listOf(sseMessageBuilder().event("starting").data("{\n\"foo\":\ntrue}").build()),
5176
),
5277
MULTIPLE_DATA_LINES(
5378
buildString {
79+
append("event: starting\n")
5480
append("data: {\n")
5581
append("data: \"foo\":\n")
5682
append("data: true}\n")
5783
append("\n\n")
5884
},
59-
listOf(sseMessageBuilder().data("{\n\"foo\":\ntrue}").build()),
85+
listOf(sseMessageBuilder().event("starting").data("{\n\"foo\":\ntrue}").build()),
6086
),
6187
SPECIAL_NEW_LINE_CHARACTER(
6288
buildString {
89+
append("event: starting\n")
6390
append("data: {\"content\":\" culpa\"}\n")
6491
append("\n")
92+
append("event: connected\n")
6593
append("data: {\"content\":\" \u2028\"}\n")
6694
append("\n")
95+
append("event: starting\n")
6796
append("data: {\"content\":\"foo\"}\n")
6897
append("\n")
6998
},
7099
listOf(
71-
sseMessageBuilder().data("{\"content\":\" culpa\"}").build(),
72-
sseMessageBuilder().data("{\"content\":\" \u2028\"}").build(),
73-
sseMessageBuilder().data("{\"content\":\"foo\"}").build(),
100+
sseMessageBuilder().event("starting").data("{\"content\":\" culpa\"}").build(),
101+
sseMessageBuilder().event("connected").data("{\"content\":\" \u2028\"}").build(),
102+
sseMessageBuilder().event("starting").data("{\"content\":\"foo\"}").build(),
74103
),
75104
),
76105
MULTI_BYTE_CHARACTER(
77106
buildString {
107+
append("event: starting\n")
78108
append("data: {\"content\":\"\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0438\"}\n")
79109
append("\n")
80110
},
81-
listOf(sseMessageBuilder().data("{\"content\":\"известни\"}").build()),
111+
listOf(sseMessageBuilder().event("starting").data("{\"content\":\"известни\"}").build()),
112+
),
113+
ERROR_EVENT(
114+
buildString {
115+
append("event: error\n")
116+
append("data: {\"errorProperty\":\"42\"}\n")
117+
append("\n")
118+
},
119+
expectedException =
120+
SseException.builder()
121+
.statusCode(0)
122+
.headers(Headers.builder().build())
123+
.body(JsonValue.from(mapOf("errorProperty" to "42")))
124+
.build(),
82125
),
83126
}
84127

0 commit comments

Comments
 (0)