@@ -18,16 +18,27 @@ package org.http4s
18
18
19
19
import cats .effect .kernel .Async
20
20
import cats .effect .kernel .Resource
21
+ import cats .effect .std .Dispatcher
22
+ import cats .effect .std .Queue
23
+ import cats .effect .syntax .all ._
21
24
import cats .syntax .all ._
25
+ import fs2 .Chunk
22
26
import fs2 .Stream
27
+ import org .http4s .headers .`Transfer-Encoding`
23
28
import org .scalajs .dom .Blob
29
+ import org .scalajs .dom .Fetch
24
30
import org .scalajs .dom .File
31
+ import org .scalajs .dom .HttpMethod
25
32
import org .scalajs .dom .ReadableStream
33
+ import org .scalajs .dom .ReadableStreamType
34
+ import org .scalajs .dom .ReadableStreamUnderlyingSource
35
+ import org .scalajs .dom .RequestDuplex
36
+ import org .scalajs .dom .RequestInit
26
37
import org .scalajs .dom .{Headers => DomHeaders }
38
+ import org .scalajs .dom .{Request => DomRequest }
27
39
import org .scalajs .dom .{Response => DomResponse }
28
40
29
41
import scala .scalajs .js
30
- import scala .scalajs .js .JSConverters ._
31
42
import scala .scalajs .js .typedarray .Uint8Array
32
43
33
44
package object dom {
@@ -37,73 +48,124 @@ package object dom {
37
48
38
49
implicit def blobEncoder [F [_]](implicit F : Async [F ]): EntityEncoder [F , Blob ] =
39
50
EntityEncoder .entityBodyEncoder.contramap { blob =>
40
- Stream
41
- .bracketCase {
42
- F .delay(blob.stream())
43
- } { case (rs, exitCase) => closeReadableStream(rs, exitCase) }
44
- .flatMap(fromReadableStream[F ])
51
+ readReadableStream[F ](F .delay(blob.stream()))
45
52
}
46
53
47
54
implicit def readableStreamEncoder [F [_]: Async ]
48
55
: EntityEncoder [F , ReadableStream [Uint8Array ]] =
49
- EntityEncoder .entityBodyEncoder.contramap { rs => fromReadableStream (rs) }
56
+ EntityEncoder .entityBodyEncoder.contramap { rs => readReadableStream (rs.pure ) }
50
57
51
58
private [dom] def fromDomResponse [F [_]](response : DomResponse )(
52
59
implicit F : Async [F ]): F [Response [F ]] =
53
60
F .fromEither(Status .fromInt(response.status)).map { status =>
54
61
Response [F ](
55
62
status = status,
56
63
headers = fromDomHeaders(response.headers),
57
- body = Stream .fromOption(Option (response.body)).flatMap(fromReadableStream[F ])
64
+ body = Stream .fromOption(Option (response.body)).flatMap { rs =>
65
+ readReadableStream[F ](rs.pure)
66
+ }
58
67
)
59
68
}
60
69
61
- private [dom] def toDomHeaders (headers : Headers ): DomHeaders =
62
- new DomHeaders (
63
- headers
64
- .headers
65
- .view
66
- .map {
67
- case Header .Raw (name, value) =>
68
- name.toString -> value
69
- }
70
- .toMap
71
- .toJSDictionary)
70
+ private [dom] def toDomHeaders (headers : Headers , request : Boolean ): DomHeaders = {
71
+ val domHeaders = new DomHeaders ()
72
+ headers.foreach {
73
+ case Header .Raw (name, value) =>
74
+ val skip = request && name == `Transfer-Encoding`.name
75
+ if (! skip) domHeaders.append(name.toString, value)
76
+ }
77
+ domHeaders
78
+ }
72
79
73
80
private [dom] def fromDomHeaders (headers : DomHeaders ): Headers =
74
81
Headers (
75
82
headers.map { header => header(0 ) -> header(1 ) }.toList
76
83
)
77
84
78
- private [dom] def fromReadableStream [F [_]](rs : ReadableStream [Uint8Array ])(
79
- implicit F : Async [F ]): Stream [F , Byte ] =
80
- Stream .bracket(F .delay(rs.getReader()))(r => F .delay(r.releaseLock())).flatMap { reader =>
81
- Stream .unfoldChunkEval(reader) { reader =>
82
- F .fromPromise(F .delay(reader.read())).map { chunk =>
83
- if (chunk.done)
84
- None
85
- else
86
- Some ((fs2.Chunk .uint8Array(chunk.value), reader))
85
+ private [dom] def readReadableStream [F [_]](
86
+ readableStream : F [ReadableStream [Uint8Array ]]
87
+ )(implicit F : Async [F ]): Stream [F , Byte ] = {
88
+ def read (readableStream : ReadableStream [Uint8Array ]) =
89
+ Stream
90
+ .bracket(F .delay(readableStream.getReader()))(r => F .delay(r.releaseLock()))
91
+ .flatMap { reader =>
92
+ Stream .unfoldChunkEval(reader) { reader =>
93
+ F .fromPromise(F .delay(reader.read())).map { chunk =>
94
+ if (chunk.done)
95
+ None
96
+ else
97
+ Some ((fs2.Chunk .uint8Array(chunk.value), reader))
98
+ }
99
+ }
87
100
}
88
- }
89
- }
90
101
91
- private [dom] def closeReadableStream [F [_], A ](
102
+ Stream .bracketCase(readableStream)(cancelReadableStream(_, _)).flatMap(read(_))
103
+ }
104
+
105
+ private [dom] def cancelReadableStream [F [_], A ](
92
106
rs : ReadableStream [A ],
93
- exitCase : Resource .ExitCase )(implicit F : Async [F ]): F [Unit ] = F .fromPromise {
107
+ exitCase : Resource .ExitCase
108
+ )(implicit F : Async [F ]): F [Unit ] = F .fromPromise {
94
109
F .delay {
95
110
// Best guess: Firefox internally locks a ReadableStream after it is "drained"
96
111
// This checks if the stream is locked before canceling it to avoid an error
97
112
if (! rs.locked) exitCase match {
98
113
case Resource .ExitCase .Succeeded =>
99
114
rs.cancel(js.undefined)
100
115
case Resource .ExitCase .Errored (ex) =>
101
- rs.cancel(ex.getLocalizedMessage ())
116
+ rs.cancel(ex.toString ())
102
117
case Resource .ExitCase .Canceled =>
103
118
rs.cancel(js.undefined)
104
119
}
105
120
else js.Promise .resolve[Unit ](())
106
121
}
107
- }.void
122
+ }
123
+
124
+ private [dom] def toReadableStream [F [_]](in : Stream [F , Byte ])(
125
+ implicit F : Async [F ]): Resource [F , ReadableStream [Uint8Array ]] =
126
+ Dispatcher .sequential.flatMap { dispatcher =>
127
+ Resource .eval(Queue .synchronous[F , Option [Chunk [Byte ]]]).flatMap { chunks =>
128
+ in.enqueueNoneTerminatedChunks(chunks).compile.drain.background.evalMap { _ =>
129
+ F .delay {
130
+ val source = new ReadableStreamUnderlyingSource [Uint8Array ] {
131
+ `type` = ReadableStreamType .bytes
132
+ pull = js.defined { controller =>
133
+ dispatcher.unsafeToPromise {
134
+ chunks.take.flatMap {
135
+ case Some (chunk) =>
136
+ F .delay(controller.enqueue(chunk.toUint8Array))
137
+ case None => F .delay(controller.close())
138
+ }
139
+ }
140
+ }
141
+ }
142
+ ReadableStream [Uint8Array ](source)
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ private [dom] lazy val supportsRequestStreams = {
149
+ val request = new DomRequest (
150
+ " data:a/a;charset=utf-8," ,
151
+ new RequestInit {
152
+ body = ReadableStream ()
153
+ method = HttpMethod .POST
154
+ duplex = RequestDuplex .half
155
+ }
156
+ )
157
+
158
+ val supportsStreamsInRequestObjects = ! request.headers.has(" Content-Type" )
159
+
160
+ if (! supportsStreamsInRequestObjects)
161
+ js.Promise .resolve[Boolean ](false )
162
+ else
163
+ Fetch
164
+ .fetch(request)
165
+ .`then`[Boolean ](
166
+ _ => true ,
167
+ (_ => false ): js.Function1 [Any , Boolean ]
168
+ )
169
+ }
108
170
109
171
}
0 commit comments