about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/buffer/buffer.nim19
-rw-r--r--src/display/client.nim1
-rw-r--r--src/html/dom.nim29
-rw-r--r--src/html/env.nim2
-rw-r--r--src/io/loader.nim23
-rw-r--r--src/io/promise.nim28
-rw-r--r--src/io/readablestream.nim32
-rw-r--r--src/io/request.nim51
-rw-r--r--src/io/response.nim50
-rw-r--r--src/js/javascript.nim27
10 files changed, 164 insertions, 98 deletions
diff --git a/src/buffer/buffer.nim b/src/buffer/buffer.nim
index 7a51792a..6c2a8eda 100644
--- a/src/buffer/buffer.nim
+++ b/src/buffer/buffer.nim
@@ -28,7 +28,6 @@ import img/png
 import io/loader
 import io/posixstream
 import io/promise
-import io/request
 import io/teestream
 import io/window
 import ips/serialize
@@ -559,8 +558,14 @@ proc loadResource(buffer: Buffer, elem: HTMLLinkElement): EmptyPromise =
       let media = parseMediaQueryList(parseListOfComponentValues(newStringStream(media)))
       if not media.applies(document.window): return
     return buffer.loader.fetch(newRequest(url)).then(proc(res: Response) =
-      if res.contenttype == "text/css":
-        elem.sheet = parseStylesheet(res.body))
+      if res.res == 0: #TODO remove res
+        #TODO we should use ReadableStreams for this (which would allow us to
+        # parse CSS asynchronously)
+        # yet another hack: needed because closing a stream before
+        # unregistering breaks
+        if res.contenttype == "text/css":
+          elem.sheet = parseStylesheet(res.body)
+        res.unregisterFun())
 
 proc loadResource(buffer: Buffer, elem: HTMLImageElement): EmptyPromise =
   let document = buffer.document
@@ -569,10 +574,12 @@ proc loadResource(buffer: Buffer, elem: HTMLImageElement): EmptyPromise =
   let url = parseURL(src, document.url.some)
   if url.isSome:
     let url = url.get
-    return buffer.loader.fetch(newRequest(url)).then(proc(res: Response) =
+    return buffer.loader.fetch(newRequest(url)).then(proc(res: Response): Promise[string] =
       if res.contenttype == "image/png":
-        let pngData = res.body.readAll()
-        elem.bitmap = fromPNG(toOpenArrayByte(pngData, 0, pngData.high)))
+        #TODO using text() for PNG is wrong
+        return res.text()
+    ).then(proc(pngData: string) =
+      elem.bitmap = fromPNG(toOpenArrayByte(pngData, 0, pngData.high)))
 
 proc loadResources(buffer: Buffer): EmptyPromise =
   let document = buffer.document
diff --git a/src/display/client.nim b/src/display/client.nim
index 9783650f..88185fb7 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -552,6 +552,7 @@ proc newClient*(config: Config, dispatcher: Dispatcher): Client =
   ctx.addBlobModule()
   ctx.addFormDataModule()
   ctx.addRequestModule()
+  ctx.addResponseModule()
   ctx.addLineEditModule()
   ctx.addConfigModule()
   ctx.addPagerModule()
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 3d901eb9..20b10674 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -15,9 +15,10 @@ import encoding/decoderstream
 import html/tags
 import img/bitmap
 import img/painter
-import img/png
 import img/path
+import img/png
 import io/loader
+import io/promise
 import io/request
 import io/window
 import js/javascript
@@ -2371,26 +2372,30 @@ proc createClassicScript(source: string, baseURL: URL, options: ScriptOptions, m
 
 #TODO settings object
 proc fetchClassicScript(element: HTMLScriptElement, url: URL,
-                        options: ScriptOptions, cors: CORSAttribute,
-                        cs: Charset, onComplete: (proc(element: HTMLScriptElement,
-                                                       res: ScriptResult))) =
+    options: ScriptOptions, cors: CORSAttribute,
+    cs: Charset, onComplete: (proc(element: HTMLScriptElement,
+                                   res: ScriptResult))) =
   if not element.scriptingEnabled:
       element.onComplete(ScriptResult(t: RESULT_NULL))
   else:
     let loader = element.document.window.loader
     if loader.isSome:
       let request = createPotentialCORSRequest(url, RequestDestination.SCRIPT, cors)
-      #TODO this should be async...
-      let r = loader.get.doRequest(request)
-      if r.res != 0 or r.body == nil:
-        element.onComplete(ScriptResult(t: RESULT_NULL))
-      else:
-        #TODO use charset from content-type
+      loader.get.fetch(request).then(proc(r: Response): auto =
+        if r.res != 0 or r.body == nil:
+          #TODO remove res
+          element.onComplete(ScriptResult(t: RESULT_NULL))
+        else:
+          #TODO use charset from content-type
+          #TODO text() should decode
+          return r.text()
+      ).then(proc(s: string) =
+        let ss = newStringStream(s) #TODO unnecessary copy
         let cs = if cs == CHARSET_UNKNOWN: CHARSET_UTF_8 else: cs
-        let source = newDecoderStream(r.body, cs = cs).readAll()
+        let source = newDecoderStream(ss, cs = cs).readAll()
         #TODO use response url
         let script = createClassicScript(source, url, options, false)
-        element.markAsReady(ScriptResult(t: RESULT_SCRIPT, script: script))
+        element.markAsReady(ScriptResult(t: RESULT_SCRIPT, script: script)))
 
 proc log*(console: console, ss: varargs[string]) {.jsfunc.} =
   var s = ""
diff --git a/src/html/env.nim b/src/html/env.nim
index ca7e497d..5883dabf 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -6,6 +6,7 @@ import html/htmlparser
 import io/loader
 import io/promise
 import io/request
+import io/response
 import io/window
 import js/intl
 import js/javascript
@@ -120,6 +121,7 @@ proc addScripting*(window: Window, selector: Selector[int]) =
   ctx.addBlobModule()
   ctx.addFormDataModule()
   ctx.addRequestModule()
+  ctx.addResponseModule()
 
 proc runJSJobs*(window: Window) =
   window.jsrt.runJSJobs(window.console.err)
diff --git a/src/io/loader.nim b/src/io/loader.nim
index f40882fc..1b5d8847 100644
--- a/src/io/loader.nim
+++ b/src/io/loader.nim
@@ -26,6 +26,7 @@ import io/http
 import io/posixstream
 import io/promise
 import io/request
+import io/response
 import io/urlfilter
 import ips/serialize
 import ips/serversocket
@@ -37,6 +38,9 @@ import types/referer
 import types/url
 import utils/twtstr
 
+export request
+export response
+
 type
   FileLoader* = ref object
     process*: Pid
@@ -260,14 +264,6 @@ proc fetch*(loader: FileLoader, input: Request): Promise[Response] =
   )
   return promise
 
-proc newResponse(res: int, request: Request, stream: Stream = nil): Response =
-  return Response(
-    res: res,
-    url: request.url,
-    body: stream,
-    bodyRead: Promise[string]()
-  )
-
 const BufferSize = 4096
 
 proc onConnected*(loader: FileLoader, fd: int) =
@@ -278,12 +274,15 @@ proc onConnected*(loader: FileLoader, fd: int) =
   var res: int
   stream.sread(res)
   if res == 0:
-    let response = newResponse(res, request, stream)
+    let response = newResponse(res, request, fd, stream)
     assert loader.unregisterFun != nil
+    let realCloseImpl = stream.closeImpl
+    stream.closeImpl = nil
     response.unregisterFun = proc() =
       loader.ongoing.del(fd)
       loader.unregistered.add(fd)
       loader.unregisterFun(fd)
+      realCloseImpl(stream)
     stream.sread(response.status)
     stream.sread(response.headers)
     applyHeaders(request, response)
@@ -292,6 +291,7 @@ proc onConnected*(loader: FileLoader, fd: int) =
       response: response,
       readbufsize: BufferSize,
     )
+    SocketStream(stream).source.getFd().setBlocking(false)
     promise.resolve(response)
   else:
     loader.unregisterFun(fd)
@@ -316,10 +316,7 @@ proc onRead*(loader: FileLoader, fd: int) =
         buffer[].buf.setLen(olen + n)
         if response.body.atEnd():
           response.bodyRead.resolve(buffer[].buf)
-          loader.unregisterFun(fd)
-          loader.ongoing.del(fd)
-          loader.unregistered.add(fd)
-          response.body.close()
+          response.unregisterFun()
         break
       except ErrorAgain, ErrorWouldBlock:
         assert buffer.readbufsize > 1
diff --git a/src/io/promise.nim b/src/io/promise.nim
index 0e1b6115..d825c4fa 100644
--- a/src/io/promise.nim
+++ b/src/io/promise.nim
@@ -64,7 +64,9 @@ func empty*(map: PromiseMap): bool =
   map.tab.len == 0
 
 proc then*(promise: EmptyPromise, cb: (proc())): EmptyPromise {.discardable.} =
-  if promise == nil: return
+  if promise == nil:
+    doAssert false
+    return
   promise.cb = cb
   promise.next = EmptyPromise()
   if promise.state == PROMISE_FULFILLED:
@@ -72,7 +74,7 @@ proc then*(promise: EmptyPromise, cb: (proc())): EmptyPromise {.discardable.} =
   return promise.next
 
 proc then*[T](promise: Promise[T], cb: (proc(x: T))): EmptyPromise {.discardable.} =
-  if promise == nil: return
+  doAssert promise != nil
   return promise.then(proc() =
     if promise.get != nil:
       promise.get(promise.opaque, promise.res)
@@ -80,28 +82,32 @@ proc then*[T](promise: Promise[T], cb: (proc(x: T))): EmptyPromise {.discardable
     cb(promise.res))
 
 proc then*[T](promise: EmptyPromise, cb: (proc(): Promise[T])): Promise[T] {.discardable.} =
-  if promise == nil: return
+  doAssert promise != nil
   let next = Promise[T]()
   promise.then(proc() =
-    let p2 = cb()
+    var p2 = cb()
     if p2 != nil:
       p2.then(proc(x: T) =
         next.res = x
-        next.resolve()))
+        next.resolve())
+    else:
+      next.resolve())
   return next
 
 proc then*[T](promise: Promise[T], cb: (proc(x: T): EmptyPromise)): EmptyPromise {.discardable.} =
-  if promise == nil: return
+  doAssert promise != nil
   let next = EmptyPromise()
   promise.then(proc(x: T) =
     let p2 = cb(x)
     if p2 != nil:
       p2.then(proc() =
-        next.resolve()))
+        next.resolve())
+    else:
+      next.resolve())
   return next
 
 proc then*[T, U](promise: Promise[T], cb: (proc(x: T): U)): Promise[U] {.discardable.} =
-  if promise == nil: return
+  doAssert promise != nil
   let next = Promise[U]()
   promise.then(proc(x: T) =
     next.res = cb(x)
@@ -109,14 +115,16 @@ proc then*[T, U](promise: Promise[T], cb: (proc(x: T): U)): Promise[U] {.discard
   return next
 
 proc then*[T, U](promise: Promise[T], cb: (proc(x: T): Promise[U])): Promise[U] {.discardable.} =
-  if promise == nil: return
+  doAssert promise != nil
   let next = Promise[U]()
   promise.then(proc(x: T) =
     let p2 = cb(x)
     if p2 != nil:
       p2.then(proc(y: U) =
         next.res = y
-        next.resolve()))
+        next.resolve())
+    else:
+      next.resolve())
   return next
 
 proc all*(promises: seq[EmptyPromise]): EmptyPromise =
diff --git a/src/io/readablestream.nim b/src/io/readablestream.nim
new file mode 100644
index 00000000..16a650e2
--- /dev/null
+++ b/src/io/readablestream.nim
@@ -0,0 +1,32 @@
+#TODO....
+
+type
+  UnderlyingSourceStartCallback = proc(controller: ReadableStreamController):
+    Option[JSValue] # may be undefined!
+  UnderlyingSourcePullCallback = proc(controller: ReadableStreamController):
+    EmptyPromise
+  UnderlyingSourceCancelCallback = proc(reason = none(JSValue)): EmptyPromise
+
+  ReadableStreamType = enum
+    BYOB = "byob"
+
+  UnderlyingSource* = object
+    start*: Option[UnderlyingSourceStartCallback]
+    pull*: Option[UnderlyingSourcePullCallback]
+    cancel*: Option[UnderlyingSourcePullCallback]
+    #TODO mark real name being type
+    ctype*: Option[ReadableStreamType]
+
+  QueuingStrategySize = proc(chunk: JSValue): float64 # unrestricted
+
+  QueuingStrategy* = object
+    highWaterMark*: float64 # unrestricted
+    size*: QueuingStrategySize
+
+  ReadableStream* = object
+    underlyingSource: UnderlyingSource
+
+proc newReadableStream(underlyingSource = none(UnderlyingSource),
+    strategy = none(QueuingStrategySize)): ReadableStream =
+  let this = ReadableStream()
+  discard
diff --git a/src/io/request.nim b/src/io/request.nim
index 61fee6dd..11e16718 100644
--- a/src/io/request.nim
+++ b/src/io/request.nim
@@ -4,7 +4,6 @@ import strutils
 import tables
 
 import bindings/quickjs
-import io/promise
 import js/javascript
 import types/formdata
 import types/url
@@ -74,18 +73,6 @@ type
     destination* {.jsget.}: RequestDestination
     credentialsMode* {.jsget.}: CredentialsMode
     proxy*: URL #TODO do something with this
-
-  Response* = ref object
-    body*: Stream
-    bodyUsed* {.jsget.}: bool
-    res* {.jsget.}: int
-    contenttype* {.jsget.}: string
-    status* {.jsget.}: int
-    headers* {.jsget.}: Headers
-    redirect*: Request
-    url*: URL #TODO should be urllist?
-    unregisterFun*: proc()
-    bodyRead*: Promise[string]
  
   ReadableStream* = ref object of Stream
     isource*: Stream
@@ -95,19 +82,11 @@ type
   Headers* = ref object
     table* {.jsget.}: Table[string, seq[string]]
 
-proc Request_url(ctx: JSContext, this: JSValue, magic: cint): JSValue {.cdecl.} =
-  let op = getOpaque0(this)
-  if unlikely(not ctx.isInstanceOf(this, "Request") or op == nil):
-    return JS_ThrowTypeError(ctx, "Value is not an instance of %s", "Request")
-  let request = cast[Request](op)
-  return toJS(ctx, $request.url)
+proc js_url(this: Request): string {.jsfget: "url".} =
+  return $this.url
 
-proc Request_referrer(ctx: JSContext, this: JSValue, magic: cint): JSValue {.cdecl.} =
-  let op = getOpaque0(this)
-  if unlikely(not ctx.isInstanceOf(this, "Request") or op == nil):
-    return JS_ThrowTypeError(ctx, "Value is not an instance of %s", "Request")
-  let request = cast[Request](op)
-  return toJS(ctx, $request.referer)
+proc js_referrer(this: Request): string {.jsfget: "referrer".} =
+  return $this.referer
 
 iterator pairs*(headers: Headers): (string, string) =
   for k, vs in headers.table:
@@ -304,22 +283,6 @@ func getOrDefault*(headers: Headers, k: string, default = ""): string =
   else:
     default
 
-#TODO: this should be a property of body
-proc close*(response: Response) {.jsfunc.} =
-  response.bodyUsed = true
-  if response.unregisterFun != nil:
-    response.unregisterFun()
-  if response.body != nil:
-    response.body.close()
-
-proc text*(response: Response): Promise[string] {.jsfunc.} =
-  return response.bodyRead
-
-proc json(ctx: JSContext, this: Response): Promise[JSValue] {.jsfunc.} =
-  return this.text().then(proc(s: string): JSValue =
-    return JS_ParseJSON(ctx, cstring(s), cast[csize_t](s.len),
-      cstring"<input>"))
-
 func credentialsMode*(attribute: CORSAttribute): CredentialsMode =
   case attribute
   of NO_CORS, ANONYMOUS:
@@ -328,9 +291,5 @@ func credentialsMode*(attribute: CORSAttribute): CredentialsMode =
     return INCLUDE
 
 proc addRequestModule*(ctx: JSContext) =
-  ctx.registerType(Request, extra_getset = [
-    TabGetSet(name: "url", get: Request_url),
-    TabGetSet(name: "referrer", get: Request_referrer)
-  ])
-  ctx.registerType(Response)
+  ctx.registerType(Request)
   ctx.registerType(Headers)
diff --git a/src/io/response.nim b/src/io/response.nim
new file mode 100644
index 00000000..7d0f789a
--- /dev/null
+++ b/src/io/response.nim
@@ -0,0 +1,50 @@
+import streams
+
+import bindings/quickjs
+import io/promise
+import io/request
+import js/javascript
+import types/url
+
+type
+  Response* = ref object
+    fd*: int
+    body*: Stream
+    bodyUsed* {.jsget.}: bool
+    res* {.jsget.}: int
+    contenttype* {.jsget.}: string
+    status* {.jsget.}: int
+    headers* {.jsget.}: Headers
+    redirect*: Request
+    url*: URL #TODO should be urllist?
+    unregisterFun*: proc()
+    bodyRead*: Promise[string]
+
+proc newResponse*(res: int, request: Request, fd = -1, stream: Stream = nil):
+    Response =
+  return Response(
+    res: res,
+    url: request.url,
+    body: stream,
+    bodyRead: Promise[string](),
+    fd: fd
+  )
+
+#TODO: this should be a property of body
+proc close*(response: Response) {.jsfunc.} =
+  response.bodyUsed = true
+  if response.unregisterFun != nil:
+    response.unregisterFun()
+  if response.body != nil:
+    response.body.close()
+
+proc text*(response: Response): Promise[string] {.jsfunc.} =
+  return response.bodyRead
+
+proc json(ctx: JSContext, this: Response): Promise[JSValue] {.jsfunc.} =
+  return this.text().then(proc(s: string): JSValue =
+    return JS_ParseJSON(ctx, cstring(s), cast[csize_t](s.len),
+      cstring"<input>"))
+
+proc addResponseModule*(ctx: JSContext) =
+  ctx.registerType(Response)
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index 4de01aa9..9afff65a 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -869,6 +869,7 @@ type
     jsFunCall: NimNode
     jsCallAndRet: NimNode
     minArgs: int
+    actualMinArgs: int # minArgs without JSContext
     i: int # nim parameters accounted for
     j: int # js parameters accounted for (not including fix ones, e.g. `this')
     res: NimNode
@@ -1171,8 +1172,7 @@ proc addFixParam(gen: var JSFuncGenerator, name: string) =
   inc gen.i
 
 proc addRequiredParams(gen: var JSFuncGenerator) =
-  let minArgs = gen.funcParams.getMinArgs()
-  while gen.i < minArgs:
+  while gen.i < gen.minArgs:
     let s = ident("arg_" & $gen.i)
     let tt = gen.funcParams[gen.i][1]
     if tt.typeKind == ntyGenericParam:
@@ -1245,12 +1245,7 @@ export JS_ThrowTypeError, JS_ThrowRangeError, JS_ThrowSyntaxError,
 proc newJSProcBody(gen: var JSFuncGenerator, isva: bool): NimNode =
   let tt = gen.thisType
   let fn = gen.funcName
-  var ma = gen.minArgs
-  if gen.thisname.isSome:
-    ma -= 1
-  if gen.passCtx:
-    ma -= 1
-  assert ma >= 0
+  var ma = gen.actualMinArgs
   result = newStmtList()
   if isva:
     result.add(quote do: 
@@ -1331,6 +1326,15 @@ proc addThisName(gen: var JSFuncGenerator, thisname: Option[string]) =
       gen.thisType = gen.returnType.get.strVal
     gen.newName = ident($gen.t & "_" & gen.funcName)
 
+func getActualMinArgs(gen: var JSFuncGenerator): int =
+  var ma = gen.minArgs
+  if gen.thisname.isSome:
+    dec ma
+  if gen.passCtx:
+    dec ma
+  assert ma >= 0
+  return ma
+
 proc setupGenerator(fun: NimNode, t: BoundFunctionType,
     thisname = some("this"), jsname: string = ""): JSFuncGenerator =
   let jsFunCallList = newStmtList()
@@ -1351,6 +1355,7 @@ proc setupGenerator(fun: NimNode, t: BoundFunctionType,
     jsFunCall: newCall(fun[0])
   )
   gen.addJSContext()
+  gen.actualMinArgs = gen.getActualMinArgs() # must come after passctx is set
   gen.addThisName(thisname)
   return gen
 
@@ -1486,8 +1491,8 @@ macro jsgetprop*(fun: typed) =
 
 macro jsfgetn(jsname: static string, fun: typed) =
   var gen = setupGenerator(fun, GETTER, jsname = jsname)
-  if gen.minArgs != 1 or gen.funcParams.len != gen.minArgs:
-    error("jsfget functions must only have one parameter.")
+  if gen.actualMinArgs != 0 or gen.funcParams.len != gen.minArgs:
+    error("jsfget functions must only accept one parameter.")
   if gen.returnType.isnone:
     error("jsfget functions must have a return type.")
   if gen.newName.strVal in existing_funcs:
@@ -1515,7 +1520,7 @@ macro jsfget*(jsname: static string, fun: typed) =
 
 macro jsfsetn(jsname: static string, fun: typed) =
   var gen = setupGenerator(fun, SETTER, jsname = jsname)
-  if gen.minArgs != 2 or gen.funcParams.len != gen.minArgs:
+  if gen.actualMinArgs != 1 or gen.funcParams.len != gen.minArgs:
     error("jsfset functions must accept two parameters")
   if gen.returnType.issome:
     error("jsfset functions must not have a return type")