main.okhttp3.sse.internal.ServerSentEventReader.kt Maven / Gradle / Ivy
/*
* Copyright (C) 2018 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.sse.internal
import java.io.IOException
import okhttp3.internal.toLongOrDefault
import okio.Buffer
import okio.BufferedSource
import okio.ByteString.Companion.encodeUtf8
import okio.Options
class ServerSentEventReader(
private val source: BufferedSource,
private val callback: Callback,
) {
private var lastId: String? = null
interface Callback {
fun onEvent(
id: String?,
type: String?,
data: String,
)
fun onRetryChange(timeMs: Long)
}
/**
* Process the next event. This will result in a single call to [Callback.onEvent] *unless* the
* data section was empty. Any number of calls to [Callback.onRetryChange] may occur while
* processing an event.
*
* @return false when EOF is reached
*/
@Throws(IOException::class)
fun processNextEvent(): Boolean {
var id = lastId
var type: String? = null
val data = Buffer()
while (true) {
when (source.select(options)) {
in 0..2 -> {
completeEvent(id, type, data)
return true
}
in 3..4 -> {
source.readData(data)
}
in 5..7 -> {
data.writeByte('\n'.code) // 'data' on a line of its own.
}
in 8..9 -> {
id = source.readUtf8LineStrict().takeIf { it.isNotEmpty() }
}
in 10..12 -> {
id = null // 'id' on a line of its own.
}
in 13..14 -> {
type = source.readUtf8LineStrict().takeIf { it.isNotEmpty() }
}
in 15..17 -> {
type = null // 'event' on a line of its own
}
in 18..19 -> {
val retryMs = source.readRetryMs()
if (retryMs != -1L) {
callback.onRetryChange(retryMs)
}
}
-1 -> {
val lineEnd = source.indexOfElement(CRLF)
if (lineEnd != -1L) {
// Skip the line and newline
source.skip(lineEnd)
source.select(options)
} else {
return false // No more newlines.
}
}
else -> throw AssertionError()
}
}
}
@Throws(IOException::class)
private fun completeEvent(
id: String?,
type: String?,
data: Buffer,
) {
if (data.size != 0L) {
lastId = id
data.skip(1L) // Leading newline.
callback.onEvent(id, type, data.readUtf8())
}
}
companion object {
val options =
Options.of(
// 0
"\r\n".encodeUtf8(),
// 1
"\r".encodeUtf8(),
// 2
"\n".encodeUtf8(),
// 3
"data: ".encodeUtf8(),
// 4
"data:".encodeUtf8(),
// 5
"data\r\n".encodeUtf8(),
// 6
"data\r".encodeUtf8(),
// 7
"data\n".encodeUtf8(),
// 8
"id: ".encodeUtf8(),
// 9
"id:".encodeUtf8(),
// 10
"id\r\n".encodeUtf8(),
// 11
"id\r".encodeUtf8(),
// 12
"id\n".encodeUtf8(),
// 13
"event: ".encodeUtf8(),
// 14
"event:".encodeUtf8(),
// 15
"event\r\n".encodeUtf8(),
// 16
"event\r".encodeUtf8(),
// 17
"event\n".encodeUtf8(),
// 18
"retry: ".encodeUtf8(),
// 19
"retry:".encodeUtf8(),
)
private val CRLF = "\r\n".encodeUtf8()
@Throws(IOException::class)
private fun BufferedSource.readData(data: Buffer) {
data.writeByte('\n'.code)
readFully(data, indexOfElement(CRLF))
select(options) // Skip the newline bytes.
}
@Throws(IOException::class)
private fun BufferedSource.readRetryMs(): Long {
val retryString = readUtf8LineStrict()
return retryString.toLongOrDefault(-1L)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy