endpoints4s.fetch.ChunkedEntities.scala Maven / Gradle / Ivy
package endpoints4s.fetch
import endpoints4s.algebra
import org.scalajs.dom
import org.scalajs.dom.RequestDuplex
import scala.scalajs.js
import scala.scalajs.js.JSConverters._
import scala.scalajs.js.typedarray.Uint8Array
import scala.scalajs.js.|
trait ChunkedEntities extends ChunkedRequestEntities with ChunkedResponseEntities
trait ChunkedRequestEntities
extends algebra.ChunkedRequestEntities
with EndpointsWithCustomErrors
with Chunks {
def chunksRequestDuplex: RequestDuplex
def textChunksRequest: RequestEntity[this.Chunks[String]] =
chunkedRequestEntity(string => new TextEncoder("utf-8").encode(string))
def bytesChunksRequest: RequestEntity[this.Chunks[Array[Byte]]] =
chunkedRequestEntity(byteArray => Uint8Array.from(byteArray.map(_.toShort).toJSArray))
private[fetch] def chunkedRequestEntity[A](
toUint8Array: A => Uint8Array,
framing: dom.ReadableStream[Uint8Array] => dom.ReadableStream[Uint8Array] = identity
): RequestEntity[this.Chunks[A]] = { (as, request) =>
val readableStream = dom.ReadableStream[Uint8Array](
new dom.ReadableStreamUnderlyingSource[Uint8Array] {
start = js.defined((controller: dom.ReadableStreamController[Uint8Array]) => {
def read(reader: dom.ReadableStreamReader[A]): js.Promise[Unit] = {
reader
.read()
.`then`((chunk: dom.Chunk[A]) => {
if (chunk.done) {
controller.close(): Unit | js.Thenable[Unit]
} else {
controller.enqueue(toUint8Array(chunk.value))
read(reader): Unit | js.Thenable[Unit]
}
})
}
read(as.getReader()): js.UndefOr[js.Promise[Unit]]
}): js.UndefOr[
js.Function1[dom.ReadableStreamController[Uint8Array], js.UndefOr[js.Promise[Unit]]]
]
}
)
request.body = framing(readableStream)
request.duplex = chunksRequestDuplex
}
}
trait ChunkedResponseEntities
extends algebra.ChunkedResponseEntities
with EndpointsWithCustomErrors
with Chunks {
def textChunksResponse: ResponseEntity[this.Chunks[String]] =
chunkedResponseEntity(uint8array => Right(new TextDecoder("utf-8").decode(uint8array)))
def bytesChunksResponse: ResponseEntity[this.Chunks[Array[Byte]]] =
chunkedResponseEntity(uint8array => Right(uint8array.toArray.map(_.toByte)))
private[fetch] def chunkedResponseEntity[A](
fromUint8Array: Uint8Array => Either[Throwable, A],
framing: dom.ReadableStream[Uint8Array] => dom.ReadableStream[Uint8Array] = identity
): ResponseEntity[this.Chunks[A]] = { response =>
val readableStream = dom.ReadableStream[A](
new dom.ReadableStreamUnderlyingSource[A] {
start = js.defined((controller: dom.ReadableStreamController[A]) => {
def read(reader: dom.ReadableStreamReader[Uint8Array]): js.Promise[Unit] = {
reader
.read()
.`then`((chunk: dom.Chunk[Uint8Array]) => {
if (chunk.done) {
controller.close(): Unit | js.Thenable[Unit]
} else {
fromUint8Array(chunk.value)
.fold(
error => js.Promise.reject(error),
value => {
controller.enqueue(value)
read(reader): Unit | js.Thenable[Unit]
}
): Unit | js.Thenable[Unit]
}
})
}
read(framing(response.body).getReader()): js.UndefOr[js.Promise[Unit]]
}): js.UndefOr[js.Function1[dom.ReadableStreamController[A], js.UndefOr[js.Promise[Unit]]]]
}
)
js.Promise.resolve[Either[Throwable, dom.ReadableStream[A]]](
Right(readableStream)
)
}
}
trait Chunks {
type Chunks[A] = dom.ReadableStream[A]
}
trait Framing extends algebra.Framing {
class Framing(
val request: dom.ReadableStream[Uint8Array] => dom.ReadableStream[Uint8Array],
val response: dom.ReadableStream[Uint8Array] => dom.ReadableStream[Uint8Array]
)
override lazy val newLineDelimiterFraming: Framing = new Framing(
readableStream =>
dom.ReadableStream[Uint8Array](
new dom.ReadableStreamUnderlyingSource[Uint8Array] {
start = js.defined((controller: dom.ReadableStreamController[Uint8Array]) => {
def read(
reader: dom.ReadableStreamReader[Uint8Array]
): js.Promise[Unit] = {
reader
.read()
.`then`((chunk: dom.Chunk[Uint8Array]) => {
if (chunk.done) {
controller.close(): Unit | js.Thenable[Unit]
} else {
controller.enqueue(chunk.value)
controller.enqueue(new TextEncoder("utf-8").encode("\n"))
read(reader): Unit | js.Thenable[Unit]
}
})
}
read(readableStream.getReader()): js.UndefOr[
js.Promise[Unit]
]
}): js.UndefOr[
js.Function1[dom.ReadableStreamController[Uint8Array], js.UndefOr[js.Promise[Unit]]]
]
}
),
readableStream =>
dom.ReadableStream[Uint8Array](
new dom.ReadableStreamUnderlyingSource[Uint8Array] {
start = js.defined((controller: dom.ReadableStreamController[Uint8Array]) => {
def read(
reader: dom.ReadableStreamReader[Uint8Array],
buffer: String,
firstChunk: Boolean
): js.Promise[Unit] = {
reader
.read()
.`then`((chunk: dom.Chunk[Uint8Array]) => {
if (chunk.done) {
if (!firstChunk) {
controller.enqueue(new TextEncoder("utf-8").encode(buffer))
}
controller.close(): Unit | js.Thenable[Unit]
} else {
val newBuffer = (buffer + new TextDecoder("utf-8").decode(chunk.value))
.foldLeft("") { case (tmpBuffer, char) =>
if (char == '\n') {
controller.enqueue(new TextEncoder("utf-8").encode(tmpBuffer))
""
} else {
tmpBuffer + char
}
}
read(reader, newBuffer, firstChunk = false): Unit | js.Thenable[Unit]
}
})
}
read(readableStream.getReader(), "", firstChunk = true): js.UndefOr[
js.Promise[Unit]
]
}): js.UndefOr[
js.Function1[dom.ReadableStreamController[Uint8Array], js.UndefOr[js.Promise[Unit]]]
]
}
)
)
protected lazy val noopFraming: Framing = new Framing(identity, identity)
}
trait ChunkedJsonEntities extends ChunkedJsonRequestEntities with ChunkedJsonResponseEntities
trait ChunkedJsonRequestEntities
extends algebra.ChunkedJsonRequestEntities
with ChunkedRequestEntities
with JsonEntitiesFromCodecs
with Framing {
def jsonChunksRequest[A](implicit
codec: JsonCodec[A]
): RequestEntity[this.Chunks[A]] = jsonChunksRequest(noopFraming)
override def jsonChunksRequest[A](framing: this.Framing)(implicit
codec: JsonCodec[A]
): RequestEntity[this.Chunks[A]] = {
val encoder = stringCodec(codec)
chunkedRequestEntity(
{ a =>
val string = encoder.encode(a)
new TextEncoder("utf-8").encode(string)
},
framing.request
)
}
}
trait ChunkedJsonResponseEntities
extends algebra.ChunkedJsonResponseEntities
with ChunkedResponseEntities
with JsonEntitiesFromCodecs
with Framing {
def jsonChunksResponse[A](implicit
codec: JsonCodec[A]
): ResponseEntity[this.Chunks[A]] = jsonChunksResponse(noopFraming)
override def jsonChunksResponse[A](framing: this.Framing)(implicit
codec: JsonCodec[A]
): ResponseEntity[this.Chunks[A]] = {
val decoder = stringCodec(codec)
chunkedResponseEntity(
{ uint8array =>
val string = new TextDecoder("utf-8").decode(uint8array)
decoder
.decode(string)
.toEither
.left
.map(errors => new Throwable(errors.mkString(". ")))
},
framing.response
)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy