about summary refs log tree commit diff stats
path: root/src
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-08-15 19:11:49 +0200
committerbptato <nincsnevem662@gmail.com>2024-08-15 19:23:55 +0200
commit4bf895db711f3d4d229d3f18fbb2145cce2a73af (patch)
tree2e81c7399de03aebb9dfa166eba6ee809a75cd2e /src
parent885a3493b6cad4b4247a200928fe61e41883aaba (diff)
downloadchawan-4bf895db711f3d4d229d3f18fbb2145cce2a73af.tar.gz
xhr: more progress
* add responseText, response
* add net tests
	-> currently sync XHR only; should find a way to do async
	   tests...
* update monoucha
	-> simplified & updated some related code that no longer worked
	   properly
Diffstat (limited to 'src')
-rw-r--r--src/html/chadombuilder.nim5
-rw-r--r--src/html/dom.nim72
-rw-r--r--src/html/env.nim12
-rw-r--r--src/html/xmlhttprequest.nim115
-rw-r--r--src/js/encoding.nim28
-rw-r--r--src/loader/loader.nim2
-rw-r--r--src/loader/response.nim13
-rw-r--r--src/local/pager.nim11
-rw-r--r--src/types/blob.nim21
-rw-r--r--src/version.nim2
10 files changed, 194 insertions, 87 deletions
diff --git a/src/html/chadombuilder.nim b/src/html/chadombuilder.nim
index 5470deb2..bedd0d66 100644
--- a/src/html/chadombuilder.nim
+++ b/src/html/chadombuilder.nim
@@ -349,10 +349,10 @@ proc finish*(wrapper: HTML5ParserWrapper) =
     r.parser = nil
   wrapper.refs.setLen(0)
 
-proc newDOMParser(): DOMParser {.jsctor.} =
+proc newDOMParser*(): DOMParser {.jsctor.} =
   return DOMParser()
 
-proc parseFromString(ctx: JSContext; parser: DOMParser; str, t: string):
+proc parseFromString*(ctx: JSContext; parser: DOMParser; str, t: string):
     JSResult[Document] {.jsfunc.} =
   case t
   of "text/html":
@@ -361,7 +361,6 @@ proc parseFromString(ctx: JSContext; parser: DOMParser; str, t: string):
       window.document.url
     else:
       newURL("about:blank").get
-    #TODO this is probably broken in client (or at least sub-optimal)
     let builder = newChaDOMBuilder(url, window, window.factory, ccIrrelevant)
     var parser = initHTML5Parser(builder, HTML5ParserOpts[Node, CAtom]())
     let res = parser.parseChunk(str)
diff --git a/src/html/dom.nim b/src/html/dom.nim
index 208f8168..7bd3ff9a 100644
--- a/src/html/dom.nim
+++ b/src/html/dom.nim
@@ -464,7 +464,7 @@ proc resetState(state: var DrawingState) =
   state.path = newPath()
 
 proc create2DContext*(jctx: JSContext; target: HTMLCanvasElement;
-    options: Option[JSValue]): CanvasRenderingContext2D =
+    options = JS_UNDEFINED): CanvasRenderingContext2D =
   let ctx = CanvasRenderingContext2D(
     bitmap: target.bitmap,
     canvas: target
@@ -1273,10 +1273,10 @@ func childNodes(node: Node): NodeList {.jsfget.} =
 func length(tokenList: DOMTokenList): uint32 {.jsfget.} =
   return uint32(tokenList.toks.len)
 
-func item(tokenList: DOMTokenList; i: int): Option[string] {.jsfunc.} =
+proc item(ctx: JSContext; tokenList: DOMTokenList; i: int): JSValue {.jsfunc.} =
   if i < tokenList.toks.len:
-    return some(tokenList.element.document.toStr(tokenList.toks[i]))
-  return none(string)
+    return ctx.toJS(tokenList.toks[i])
+  return JS_NULL
 
 func contains*(tokenList: DOMTokenList; a: CAtom): bool =
   return a in tokenList.toks
@@ -1381,8 +1381,12 @@ func supports(tokenList: DOMTokenList; token: string):
 func value(tokenList: DOMTokenList): string {.jsfget.} =
   return $tokenList
 
-func getter(tokenList: DOMTokenList; i: uint32): Option[string] {.jsgetprop.} =
-  return tokenList.item(int(i))
+proc getter(ctx: JSContext; this: DOMTokenList; atom: JSAtom): JSValue
+    {.jsgetprop.} =
+  var u: uint32
+  if ctx.fromJS(atom, u).isSome:
+    return ctx.item(this, int(u))
+  return JS_NULL
 
 # DOMStringMap
 func validateAttributeName(name: string): Err[DOMException] =
@@ -1450,9 +1454,14 @@ func hasprop(nodeList: NodeList; i: uint32): bool {.jshasprop.} =
 func item(nodeList: NodeList; i: int): Node {.jsfunc.} =
   if i < nodeList.len:
     return nodeList.snapshot[i]
+  return nil
 
-func getter(nodeList: NodeList; i: uint32): Option[Node] {.jsgetprop.} =
-  return option(nodeList.item(int(i)))
+func getter(ctx: JSContext; nodeList: NodeList; atom: JSAtom): Node
+    {.jsgetprop.} =
+  var u: uint32
+  if ctx.fromJS(atom, u).isSome:
+    return nodeList.item(int(u))
+  return nil
 
 func names(ctx: JSContext; nodeList: NodeList): JSPropertyEnumList
     {.jspropnames.} =
@@ -1482,21 +1491,21 @@ func namedItem(collection: HTMLCollection; s: string): Element {.jsfunc.} =
       return it
   return nil
 
-func getter(ctx: JSContext; collection: HTMLCollection; atom: JSAtom):
-    Opt[Option[Element]] {.jsgetprop.} =
+func getter(ctx: JSContext; collection: HTMLCollection; atom: JSAtom): Element
+    {.jsgetprop.} =
   var u: uint32
   if ctx.fromJS(atom, u).isSome:
-    return ok(option(collection.item(u)))
+    return collection.item(u)
   var s: string
   if ctx.fromJS(atom, s).isSome:
-    return ok(option(collection.namedItem(s)))
-  return err()
+    return collection.namedItem(s)
+  return nil
 
 func names(ctx: JSContext; collection: HTMLCollection): JSPropertyEnumList
     {.jspropnames.} =
   let L = collection.length
   var list = newJSPropertyEnumList(ctx, L)
-  var ids: OrderedSet[CAtom]
+  var ids = initOrderedSet[CAtom]()
   for u in 0 ..< L:
     list.add(u)
     let elem = collection.item(u)
@@ -1509,25 +1518,32 @@ func names(ctx: JSContext; collection: HTMLCollection): JSPropertyEnumList
   return list
 
 # HTMLAllCollection
-proc length(collection: HTMLAllCollection): uint32 {.jsfget.} =
-  return uint32(collection.len)
+proc length(this: HTMLAllCollection): uint32 {.jsfget.} =
+  return uint32(this.len)
 
-func hasprop(collection: HTMLAllCollection; i: uint32): bool {.jshasprop.} =
-  return int(i) < collection.len
+func hasprop(ctx: JSContext; this: HTMLAllCollection; atom: JSAtom): bool
+    {.jshasprop.} =
+  var u: uint32
+  if ctx.fromJS(atom, u).isSome:
+    return int(u) < this.len
+  return false
 
-func item(collection: HTMLAllCollection; i: uint32): Element {.jsfunc.} =
-  let i = int(i)
-  if i < collection.len:
-    return Element(collection.snapshot[i])
+func item(this: HTMLAllCollection; u: uint32): Element {.jsfunc.} =
+  let i = int(u)
+  if i < this.len:
+    return Element(this.snapshot[i])
   return nil
 
-func getter(collection: HTMLAllCollection; i: uint32): Option[Element]
+func getter(ctx: JSContext; this: HTMLAllCollection; atom: JSAtom): Element
     {.jsgetprop.} =
-  return option(collection.item(i))
+  var u: uint32
+  if ctx.fromJS(atom, u).isSome:
+    return this.item(u)
+  return nil
 
-func names(ctx: JSContext; collection: HTMLAllCollection): JSPropertyEnumList
+func names(ctx: JSContext; this: HTMLAllCollection): JSPropertyEnumList
     {.jspropnames.} =
-  let L = collection.length
+  let L = this.length
   var list = newJSPropertyEnumList(ctx, L)
   for u in 0 ..< L:
     list.add(u)
@@ -3822,7 +3838,7 @@ proc fetchClassicScript(element: HTMLScriptElement; url: URL;
   if response.res != 0:
     element.onComplete(ScriptResult(t: srtNull))
     return
-  window.loader.resume(response.outputId)
+  response.resume()
   let s = response.body.recvAll()
   let cs = if cs == CHARSET_UNKNOWN: CHARSET_UTF_8 else: cs
   let source = s.decodeAll(cs)
@@ -4409,7 +4425,7 @@ func getElementReflectFunctions(): seq[TabGetSet] =
     ))
 
 proc getContext*(jctx: JSContext; this: HTMLCanvasElement; contextId: string;
-    options = none(JSValue)): RenderingContext {.jsfunc.} =
+    options = JS_UNDEFINED): RenderingContext {.jsfunc.} =
   if contextId == "2d":
     if this.ctx2d != nil:
       return this.ctx2d
diff --git a/src/html/env.nim b/src/html/env.nim
index 4ecdc08b..ea55dcbc 100644
--- a/src/html/env.nim
+++ b/src/html/env.nim
@@ -71,12 +71,11 @@ proc item(pluginArray: var PluginArray): JSValue {.jsfunc.} = JS_NULL
 proc length(pluginArray: var PluginArray): uint32 {.jsfget.} = 0
 proc item(mimeTypeArray: var MimeTypeArray): JSValue {.jsfunc.} = JS_NULL
 proc length(mimeTypeArray: var MimeTypeArray): uint32 {.jsfget.} = 0
-proc getter(pluginArray: var PluginArray; i: uint32): Option[JSValue]
+proc getter(pluginArray: var PluginArray; atom: JSAtom): JSValue {.jsgetprop.} =
+  return JS_NULL
+proc getter(mimeTypeArray: var MimeTypeArray; atom: JSAtom): JSValue
     {.jsgetprop.} =
-  discard
-proc getter(mimeTypeArray: var MimeTypeArray; i: uint32): Option[JSValue]
-    {.jsgetprop.} =
-  discard
+  return JS_NULL
 
 # Screen
 proc availWidth(screen: var Screen): int64 {.jsfget.} =
@@ -116,8 +115,7 @@ func key(this: var Storage; i: uint32): Option[string] {.jsfunc.} =
     return some(this.map[int(i)].value)
   return none(string)
 
-func getItem(this: var Storage; s: string): Option[string]
-    {.jsfunc.} =
+func getItem(this: var Storage; s: string): Option[string] {.jsfunc.} =
   let i = this.find(s)
   if i != -1:
     return some(this.map[i].value)
diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim
index ed1b7035..68039978 100644
--- a/src/html/xmlhttprequest.nim
+++ b/src/html/xmlhttprequest.nim
@@ -2,8 +2,10 @@ import std/options
 import std/strutils
 import std/tables
 
+import chagashi/charset
 import chagashi/decoder
 import html/catom
+import html/chadombuilder
 import html/dom
 import html/event
 import html/script
@@ -19,6 +21,7 @@ import monoucha/javascript
 import monoucha/jserror
 import monoucha/quickjs
 import monoucha/tojs
+import types/blob
 import types/opt
 import types/url
 import utils/twtstr
@@ -56,7 +59,9 @@ type
     response: Response
     responseType {.jsget.}: XMLHttpRequestResponseType
     timeout {.jsget.}: uint32
+    responseObject: JSValue
     received: string
+    contentTypeOverride: string
 
   ProgressEvent = ref object of Event
     lengthComputable {.jsget.}: bool
@@ -77,9 +82,13 @@ func newXMLHttpRequest(): XMLHttpRequest {.jsctor.} =
   let upload = XMLHttpRequestUpload()
   return XMLHttpRequest(
     upload: upload,
-    headers: newHeaders()
+    headers: newHeaders(),
+    responseObject: JS_UNDEFINED
   )
 
+proc finalize(rt: JSRuntime; this: XMLHttpRequest) {.jsfin.} =
+  JS_FreeValueRT(rt, this.responseObject)
+
 proc newProgressEvent(ctype: CAtom; init = ProgressEventInit()): ProgressEvent
     {.jsctor.} =
   let event = ProgressEvent(
@@ -170,6 +179,14 @@ proc setRequestHeader(this: XMLHttpRequest; name, value: string):
   this.headers.table[name.toHeaderCase()] = @[value]
   ok()
 
+proc setTimeout(ctx: JSContext; this: XMLHttpRequest; value: uint32):
+    Err[DOMException] {.jsfset: "timeout".} =
+  if ctx.getWindow() != nil and xhrfSync in this.flags:
+    return errDOMException("timeout may not be set on synchronous XHR",
+      "InvalidAccessError")
+  this.timeout = value
+  ok()
+
 proc fireProgressEvent(window: Window; target: EventTarget; name: StaticAtom;
     loaded, length: int64) =
   let event = newProgressEvent(window.factory.toAtom(name), ProgressEventInit(
@@ -316,6 +333,7 @@ proc send(ctx: JSContext; this: XMLHttpRequest; body = JS_NULL): JSResult[void]
       response.opaque = XHROpaque(this: this, window: window, len: len)
       response.onRead = onReadXHR
       response.onFinish = onFinishXHR
+      response.resume()
       #TODO timeout
     )
   else: # sync
@@ -324,6 +342,7 @@ proc send(ctx: JSContext; this: XMLHttpRequest; body = JS_NULL): JSResult[void]
       let response = window.loader.doRequest(request)
       if response.res == 0:
         #TODO timeout
+        response.resume()
         try:
           this.received = response.body.recvAll()
           #TODO report timing
@@ -352,11 +371,40 @@ proc statusText(this: XMLHttpRequest): string {.jsfget.} =
 
 proc getResponseHeader(ctx: JSContext; this: XMLHttpRequest; name: string):
     JSValue {.jsfunc.} =
-  #TODO this can throw, but I don't think the standard allows that?
-  return ctx.get(this.response.headers, name)
+  let res = ctx.get(this.response.headers, name)
+  if JS_IsException(res):
+    return JS_NULL
+  return res
 
 #TODO getAllResponseHeaders
 
+func getCharset(this: XMLHttpRequest): Charset =
+  let override = this.contentTypeOverride.toLowerAscii()
+  let cs = override.getContentTypeAttr("charset").getCharset()
+  if cs != CHARSET_UNKNOWN:
+    return cs
+  return this.response.getCharset(CHARSET_UTF_8)
+
+proc responseText(ctx: JSContext; this: XMLHttpRequest): JSValue {.jsfget.} =
+  if this.responseType notin {xhrtUnknown, xhrtText}:
+    let ex = newDOMException("response type was expected to be '' or 'text'",
+      "InvalidStateError")
+    return JS_Throw(ctx, ctx.toJS(ex))
+  if this.readyState notin {xhrsLoading, xhrsDone}:
+    return ctx.toJS("")
+  let charset = this.getCharset()
+  #TODO XML encoding stuff?
+  return ctx.toJS(this.received.decodeAll(charset))
+
+proc overrideMimeType(this: XMLHttpRequest; s: string): DOMResult[void]
+    {.jsfunc.} =
+  if this.readyState notin {xhrsLoading, xhrsDone}:
+    return errDOMException("readyState must not be loading or done",
+      "InvalidStateError")
+  #TODO parse
+  this.contentTypeOverride = s
+  return ok()
+
 proc setResponseType(ctx: JSContext; this: XMLHttpRequest;
     value: XMLHttpRequestResponseType): Err[DOMException]
     {.jsfset: "responseType".} =
@@ -372,13 +420,60 @@ proc setResponseType(ctx: JSContext; this: XMLHttpRequest;
   this.responseType = value
   ok()
 
-proc setTimeout(ctx: JSContext; this: XMLHttpRequest; value: uint32):
-    Err[DOMException] {.jsfset: "timeout".} =
-  if ctx.getWindow() != nil and xhrfSync in this.flags:
-    return errDOMException("timeout may not be set on synchronous XHR",
-      "InvalidAccessError")
-  this.timeout = value
-  ok()
+func getContentType(this: XMLHttpRequest): string =
+  if this.contentTypeOverride != "":
+    return this.contentTypeOverride
+  return this.response.getContentType()
+
+proc ptrify(s: var string): pointer =
+  if s.len == 0:
+    return nil
+  var sr = new(string)
+  sr[] = move(s)
+  GC_ref(sr)
+  return cast[pointer](sr)
+
+proc deallocPtrified(p: pointer) =
+  if p != nil:
+    let sr = cast[ref string](p)
+    GC_unref(sr)
+
+proc abufFree(rt: JSRuntime; opaque, p: pointer) {.cdecl.} =
+  deallocPtrified(opaque)
+
+proc blobFree(opaque, p: pointer) {.nimcall.} =
+  deallocPtrified(opaque)
+
+proc response(ctx: JSContext; this: XMLHttpRequest): JSValue {.jsfget.} =
+  if this.responseType in {xhrtText, xhrtUnknown}:
+    return ctx.responseText(this)
+  if this.readyState != xhrsDone:
+    return JS_NULL
+  if JS_IsUndefined(this.responseObject):
+    case this.responseType
+    of xhrtArraybuffer:
+      let len = csize_t(this.received.len)
+      let p = cast[ptr UncheckedArray[uint8]](cstring(this.received))
+      let opaque = this.received.ptrify()
+      this.responseObject = JS_NewArrayBuffer(ctx, p, len, abufFree, opaque,
+        false)
+    of xhrtBlob:
+      let len = this.received.len
+      let p = cast[ptr UncheckedArray[uint8]](cstring(this.received))
+      let opaque = this.received.ptrify()
+      let blob = newBlob(p, len, this.getContentType(), blobFree, opaque)
+      this.responseObject = ctx.toJS(blob)
+    of xhrtDocument:
+      #TODO this is certainly not compliant
+      let res = ctx.parseFromString(newDOMParser(), this.received, "text/html")
+      this.responseObject = ctx.toJS(res)
+    of xhrtJSON:
+      this.responseObject = JS_ParseJSON(ctx, cstring(this.received),
+        csize_t(this.received.len), cstring"<input>")
+    else: discard
+  if JS_IsException(this.responseObject):
+    this.responseObject = JS_UNDEFINED
+  return this.responseObject
 
 # Event reflection
 
diff --git a/src/js/encoding.nim b/src/js/encoding.nim
index 7d1bd126..7aae5eb2 100644
--- a/src/js/encoding.nim
+++ b/src/js/encoding.nim
@@ -40,31 +40,29 @@ func newJSTextDecoder(label = "utf-8"; options = TextDecoderOptions()):
 func fatal(this: JSTextDecoder): bool {.jsfget.} =
   return this.errorMode == demFatal
 
-proc decode0(this: JSTextDecoder; ctx: JSContext; input: JSArrayBufferView;
-    stream: bool): JSResult[JSValue] =
-  let H = int(input.abuf.len) - 1
-  var oq = ""
-  for chunk in this.tdctx.decode(input.abuf.p.toOpenArray(0, H), not stream):
-    oq &= chunk
-  if this.tdctx.failed:
-    this.tdctx.failed = false
-    return errTypeError("Failed to decode string")
-  return ok(JS_NewStringLen(ctx, cstring(oq), csize_t(oq.len)))
-
 type TextDecodeOptions = object of JSDict
   stream {.jsdefault.}: bool
 
 #TODO AllowSharedBufferSource
 proc decode(ctx: JSContext; this: JSTextDecoder;
-    input = none(JSArrayBufferView); options = TextDecodeOptions()):
-    JSResult[JSValue] {.jsfunc.} =
+    input = none(JSArrayBufferView); options = TextDecodeOptions()): JSValue
+    {.jsfunc.} =
   if not this.stream:
     this.tdctx = initTextDecoderContext(this.encoding, this.errorMode)
     this.bomSeen = false
   this.stream = options.stream
   if input.isSome:
-    return this.decode0(ctx, input.get, options.stream)
-  return ok(JS_NewString(ctx, ""))
+    let input = input.get
+    let H = int(input.abuf.len) - 1
+    var oq = ""
+    let stream = this.stream
+    for chunk in this.tdctx.decode(input.abuf.p.toOpenArray(0, H), not stream):
+      oq &= chunk
+    if this.tdctx.failed:
+      this.tdctx.failed = false
+      return JS_ThrowTypeError(ctx, "failed to decode string")
+    return JS_NewStringLen(ctx, cstring(oq), csize_t(oq.len))
+  return JS_NewString(ctx, "")
 
 func jencoding(this: JSTextDecoder): string {.jsfget: "encoding".} =
   return $this.encoding
diff --git a/src/loader/loader.nim b/src/loader/loader.nim
index dfc95b8b..c9025153 100644
--- a/src/loader/loader.nim
+++ b/src/loader/loader.nim
@@ -1167,6 +1167,8 @@ proc doRequest*(loader: FileLoader; request: Request): Response =
     r.sread(response.headers) # packet 3
     # Only a stream of the response body may arrive after this point.
     response.body = stream
+    response.resumeFun = proc(outputId: int) =
+      loader.resume(outputId)
   else:
     var msg: string
     r.sread(msg) # packet 1
diff --git a/src/loader/response.nim b/src/loader/response.nim
index 8143ccbf..6463080a 100644
--- a/src/loader/response.nim
+++ b/src/loader/response.nim
@@ -89,13 +89,12 @@ proc close*(response: Response) {.jsfunc.} =
     response.body = nil
 
 func getCharset*(this: Response; fallback: Charset): Charset =
-  if "Content-Type" notin this.headers.table:
-    return fallback
-  let header = this.headers.table["Content-Type"][0].toLowerAscii()
-  let cs = header.getContentTypeAttr("charset").getCharset()
-  if cs == CHARSET_UNKNOWN:
-    return fallback
-  return cs
+  this.headers.table.withValue("Content-Type", p):
+    let header = p[][0].toLowerAscii()
+    let cs = header.getContentTypeAttr("charset").getCharset()
+    if cs != CHARSET_UNKNOWN:
+      return cs
+  return fallback
 
 func getContentType*(this: Response; fallback = "application/octet-stream"):
     string =
diff --git a/src/local/pager.nim b/src/local/pager.nim
index a0c2d221..43f0d8c4 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -234,10 +234,9 @@ proc reflect(ctx: JSContext; this_val: JSValue; argc: cint;
   let fun = func_data[1]
   return JS_Call(ctx, fun, obj, argc, argv)
 
-proc getter(ctx: JSContext; pager: Pager; a: JSAtom): Option[JSValue]
-    {.jsgetprop.} =
+proc getter(ctx: JSContext; pager: Pager; a: JSAtom): JSValue {.jsgetprop.} =
   if pager.container != nil:
-    let cval = toJS(ctx, pager.container)
+    let cval = ctx.toJS(pager.container)
     let val = JS_GetProperty(ctx, cval, a)
     if JS_IsFunction(ctx, val):
       let func_data = @[cval, val]
@@ -245,11 +244,11 @@ proc getter(ctx: JSContext; pager: Pager; a: JSAtom): Option[JSValue]
         func_data.toJSValueArray())
       JS_FreeValue(ctx, cval)
       JS_FreeValue(ctx, val)
-      return some(fun)
+      return fun
     JS_FreeValue(ctx, cval)
     if not JS_IsUndefined(val):
-      return some(val)
-  return none(JSValue)
+      return val
+  return JS_NULL
 
 proc searchNext(pager: Pager; n = 1) {.jsfunc.} =
   if pager.regex.isSome:
diff --git a/src/types/blob.nim b/src/types/blob.nim
index acffedf8..c0278bde 100644
--- a/src/types/blob.nim
+++ b/src/types/blob.nim
@@ -8,12 +8,13 @@ import monoucha/jstypes
 import utils/mimeguess
 
 type
-  DeallocFun = proc(blob: Blob) {.closure, raises: [].}
+  DeallocFun = proc(opaque, p: pointer) {.nimcall, raises: [].}
 
   Blob* = ref object of RootObj
     size* {.jsget.}: uint64
     ctype* {.jsget: "type".}: string
     buffer*: pointer
+    opaque*: pointer
     deallocFun*: DeallocFun
     fd*: Option[FileHandle]
 
@@ -25,19 +26,24 @@ jsDestructor(Blob)
 jsDestructor(WebFile)
 
 proc newBlob*(buffer: pointer; size: int; ctype: string;
-    deallocFun: DeallocFun): Blob =
+    deallocFun: DeallocFun; opaque: pointer = nil): Blob =
   return Blob(
     buffer: buffer,
     size: uint64(size),
     ctype: ctype,
-    deallocFun: deallocFun
+    deallocFun: deallocFun,
+    opaque: opaque
   )
 
+proc deallocBlob*(opaque, p: pointer) =
+  if p != nil:
+    dealloc(p)
+
 proc finalize(blob: Blob) {.jsfin.} =
   if blob.fd.isSome:
     discard close(blob.fd.get)
-  if blob.deallocFun != nil and blob.buffer != nil:
-    blob.deallocFun(blob)
+  if blob.deallocFun != nil:
+    blob.deallocFun(blob.opaque, blob.buffer)
     blob.buffer = nil
 
 proc finalize(file: WebFile) {.jsfin.} =
@@ -58,11 +64,6 @@ type
   FilePropertyBag = object of BlobPropertyBag
     #TODO lastModified: int64
 
-proc deallocBlob*(blob: Blob) =
-  if blob.buffer != nil:
-    dealloc(blob.buffer)
-    blob.buffer = nil
-
 proc newWebFile(ctx: JSContext; fileBits: seq[string]; fileName: string;
     options = FilePropertyBag()): WebFile {.jsctor.} =
   let file = WebFile(
diff --git a/src/version.nim b/src/version.nim
index fe9d331a..fc8d4947 100644
--- a/src/version.nim
+++ b/src/version.nim
@@ -29,4 +29,4 @@ tryImport monoucha/version, "monoucha"
 static:
   checkVersion("chagashi", 0, 5, 4)
   checkVersion("chame", 1, 0, 1)
-  checkVersion("monoucha", 0, 3, 1)
+  checkVersion("monoucha", 0, 4, 0)