Skip to content
Permalink
Browse files

Fixes to youi-client to properly support Scala.js

  • Loading branch information...
darkfrog26 committed Sep 19, 2019
1 parent d1d90ff commit 9134f22adb9a0448e78efb0dd1a6ecd22fdf9edd
@@ -10,4 +10,6 @@ object ClientPlatform {
keepAlive: FiniteDuration = ConnectionPool.keepAlive): ConnectionPool = {
JSConnectionPool(maxIdleConnections, keepAlive)
}

def defaultSaveDirectory: String = "/"
}
@@ -8,7 +8,7 @@ import io.youi.net.ContentType
import scala.concurrent.{ExecutionContext, Future}

class JSHttpClientImplementation(config: HttpClientConfig) extends HttpClientImplementation(config) {
private val HeaderRegex = """(.+)[=](.+)""".r
private val HeaderRegex = """(.+)[:](.+)""".r

override def send(request: HttpRequest, executionContext: ExecutionContext): Future[HttpResponse] = {
implicit val implicitContext: ExecutionContext = executionContext
@@ -23,15 +23,16 @@ class JSHttpClientImplementation(config: HttpClientConfig) extends HttpClientImp
)
val action = new AjaxAction(ajaxRequest)
manager.enqueue(action).map { xmlHttpRequest =>
val headers: Map[String, List[String]] = xmlHttpRequest.getAllResponseHeaders().split('&').map {
case HeaderRegex(key, value) => key -> value
val headers: Map[String, List[String]] = xmlHttpRequest.getAllResponseHeaders().split('\n').map(_.trim).map {
case HeaderRegex(key, value) => key.trim -> value.trim
case s => throw new RuntimeException(s"Invalid Header: [$s]")
}.groupBy(_._1).map {
case (key, array) => key -> array.toList.map(_._2)
}
val content = xmlHttpRequest.responseType match {
case null | "" => None
case null => None
case _ => {
val `type` = ContentType.parse(xmlHttpRequest.responseType)
val `type` = if (xmlHttpRequest.responseType == "") ContentType.`text/plain` else ContentType.parse(xmlHttpRequest.responseType)
Some(Content.string(xmlHttpRequest.responseText, `type`))
}
}
@@ -9,4 +9,6 @@ object ClientPlatform {
keepAlive: FiniteDuration = ConnectionPool.keepAlive): ConnectionPool = {
JVMConnectionPool(maxIdleConnections, keepAlive)
}

def defaultSaveDirectory: String = System.getProperty("java.io.tmpdir")
}
@@ -195,7 +195,7 @@ class JVMHttpClientImplementation(config: HttpClientConfig) extends HttpClientIm
} else if (contentToBytes(contentType, contentLength)) {
Content.bytes(responseBody.bytes(), contentType)
} else {
val file = File.createTempFile("youi", "client", config.saveDirectory.toFile)
val file = File.createTempFile("youi", "client", new File(config.saveDirectory))
IO.stream(responseBody.byteStream(), file)
Content.file(file, contentType)
}
@@ -6,6 +6,7 @@ import io.youi.http.cookie.RequestCookie
import io.youi.http._
import io.youi.http.content.{Content, StringContent}
import io.youi.net.{ContentType, Path, URL}
import io.youi.util.Time

import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future}
@@ -102,7 +103,7 @@ case class HttpClient(request: HttpRequest,
future.recoverWith {
case t: Throwable if retries > 0 => {
scribe.warn(s"Request to ${request.url} failed (${t.getMessage}). Retrying after $retryDelay...")
RateLimiter.delayedFuture(retryDelay, send(retries - 1))
Time.delay(retryDelay).flatMap(_ => send(retries - 1))
}
}.map { response =>
sessionManager.foreach { sm =>
@@ -1,7 +1,5 @@
package io.youi.client

import java.nio.file.{Path, Paths}

import io.youi.client.intercept.Interceptor
import reactify.Var

@@ -11,7 +9,7 @@ case class HttpClientConfig(retries: Int = 0,
retryDelay: FiniteDuration = 5.seconds,
interceptor: Interceptor = Interceptor.empty,
connectionPool: ConnectionPool = ConnectionPool.default,
saveDirectory: Path = Paths.get(System.getProperty("java.io.tmpdir")),
saveDirectory: String = ClientPlatform.defaultSaveDirectory,
timeout: FiniteDuration = 15.seconds,
pingInterval: Option[FiniteDuration] = None,
dns: DNS = DNS.default,
@@ -23,7 +21,7 @@ case class HttpClientConfig(retries: Int = 0,
def retryDelay(retryDelay: FiniteDuration): HttpClientConfig = copy(retryDelay = retryDelay)
def interceptor(interceptor: Interceptor): HttpClientConfig = copy(interceptor = interceptor)
def connectionPool(connectionPool: ConnectionPool): HttpClientConfig = copy(connectionPool = connectionPool)
def saveDirectory(saveDirectory: Path): HttpClientConfig = copy(saveDirectory = saveDirectory)
def saveDirectory(saveDirectory: String): HttpClientConfig = copy(saveDirectory = saveDirectory)
def timeout(timeout: FiniteDuration): HttpClientConfig = copy(timeout = timeout)
def pingInterval(pingInterval: Option[FiniteDuration]): HttpClientConfig = copy(pingInterval = pingInterval)
def dns(dns: DNS): HttpClientConfig = copy(dns = dns)
@@ -1,8 +1,7 @@
package io.youi.client.intercept

import java.util.concurrent.{Executors, ThreadFactory}

import io.youi.http.{HttpRequest, HttpResponse}
import io.youi.util.Time

import scala.concurrent.duration._
import scala.concurrent.{Future, Promise}
@@ -22,7 +21,7 @@ case class RateLimiter(perRequestDelay: FiniteDuration) extends InterceptorAdapt
val elapsed = now - _lastTime
val delay = maxDelay - elapsed
if (delay > 0L) {
RateLimiter.delayedFuture(delay.millis, Future.successful(request)).onComplete {
Time.delay(delay.millis).map(_ => request).onComplete {
case Success(v) => p.success(v)
case Failure(exception) => p.failure(exception)
}
@@ -40,25 +39,4 @@ case class RateLimiter(perRequestDelay: FiniteDuration) extends InterceptorAdapt

super.after(request, response)
}
}

object RateLimiter {
private lazy val scheduler = Executors.newScheduledThreadPool(1, new ThreadFactory {
override def newThread(r: Runnable): Thread = {
val t = new Thread(r)
t.setDaemon(true)
t
}
})

def delayedFuture[T](delay: FiniteDuration, t: => Future[T]): Future[T] = {
val promise = Promise[T]
scheduler.schedule(new Runnable {
override def run(): Unit = t.onComplete {
case Success(value) => promise.success(value)
case Failure(exception) => promise.failure(exception)
}
}, delay.length, delay.unit)
promise.future
}
}
@@ -2,13 +2,32 @@ package io.youi

import scala.concurrent.{Future, Promise}
import org.scalajs.dom._
import perfolation._

import scala.scalajs.js.Date

object YouIPlatform {
private val Regex = """(.{3}), (.{2}) (.{3}) (.{4}) (.{2}):(.{2}):(.{2}) GMT""".r
private val DayNames = Vector("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
private val MonthNames = Vector("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")

def delay(millis: Long): Future[Unit] = {
val promise = Promise[Unit]
window.setTimeout(() => {
promise.success(())
}, millis)
promise.future
}

def parseHTTPDate(date: String): Option[Long] = date match {
case Regex(_, day, month, year, hour, minute, second) => {
val date = Date.parse(s"$day $month $year $hour:$minute:$second GMT")
Some(date.toLong)
}
}

def toHTTPDate(time: Long): String = {
val date = new Date(time)
s"${DayNames(date.getUTCDay())}, ${date.getUTCDate().f(i = 2)} ${MonthNames(date.getUTCMonth())} ${date.getUTCFullYear()} ${date.getUTCHours().f(i = 2)}:${date.getUTCMinutes().f(i = 2)}:${date.getUTCSeconds().f(i = 2)} GMT"
}
}
@@ -1,6 +1,7 @@
package io.youi

import java.util.{Timer, TimerTask}
import java.text.SimpleDateFormat
import java.util.{Locale, Timer, TimerTask}

import scala.concurrent.{Future, Promise}

@@ -14,4 +15,21 @@ object YouIPlatform {
}, millis)
promise.future
}

def parseHTTPDate(date: String): Option[Long] = {
val parser = new SimpleDateFormat("EEE, dd MMMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
try {
Some(parser.parse(date.replace('-', ' ')).getTime)
} catch {
case t: Throwable => {
scribe.warn(s"Unable to parse date header: $date (${t.getMessage})")
None
}
}
}

def toHTTPDate(time: Long): String = {
val parser = new SimpleDateFormat("EEE, dd MMMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
parser.format(time)
}
}
@@ -1,8 +1,5 @@
package io.youi.http

import java.text.SimpleDateFormat
import java.util.Locale

class DateHeaderKey(val key: String, val commaSeparated: Boolean = false) extends TypedHeaderKey[Long] {
import DateHeaderKey._

@@ -12,20 +9,7 @@ class DateHeaderKey(val key: String, val commaSeparated: Boolean = false) extend
}

object DateHeaderKey {
def parse(date: String): Option[Long] = {
val parser = new SimpleDateFormat("EEE, dd MMMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
try {
Some(parser.parse(date.replace('-', ' ')).getTime)
} catch {
case t: Throwable => {
scribe.warn(s"Unable to parse date header: $date (${t.getMessage})")
None
}
}
}
def parse(date: String): Option[Long] = io.youi.YouIPlatform.parseHTTPDate(date)

def format(date: Long): String = {
val parser = new SimpleDateFormat("EEE, dd MMMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
parser.format(date)
}
def format(date: Long): String = io.youi.YouIPlatform.toHTTPDate(date)
}
@@ -45,7 +45,7 @@ object Headers {
case object `Accept-Encoding` extends StringHeaderKey("Accept-Encoding")
case object `Accept-Language` extends StringHeaderKey("Accept-Language")
case object `Authorization` extends StringHeaderKey("Authorization")
def `Cookie` = CookieHeader
def `Cookie`: CookieHeader.type = CookieHeader
case object `If-Modified-Since` extends DateHeaderKey("If-Modified-Since")
case object `Origin` extends StringHeaderKey("Origin")
case object `User-Agent` extends StringHeaderKey("User-Agent", commaSeparated = false)
@@ -63,7 +63,7 @@ object Headers {
}
}
case object `Server` extends StringHeaderKey("Server")
def `Set-Cookie` = SetCookie
def `Set-Cookie`: SetCookie.type = SetCookie
case object `Content-Disposition` extends HeaderKey {
override def key: String = "Content-Disposition"
override protected def commaSeparated: Boolean = false

0 comments on commit 9134f22

Please sign in to comment.
You can’t perform that action at this time.