I am playing with a javascript library. I am struggling to convert the following function to nim:
function readBlob(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', () => resolve(reader.result));
reader.addEventListener('error', reject)
reader.readAsDataURL(blob);
})
}
I am looking at std/asyncjs.
I understand that readBlob's signature in Nim would be:
proc readBlob(blob):Future[Data] =
##
But I am not sure if the right approach would be something like:
proc newFileReader*():JsObject {.importcpp:"new FileReader()".}
proc readBlob(blob):Future[Data] {.async.} =
let reader = newFileReader()
reader.readAsDataURL(blob)
return reader.result
or should I create a promise for that? This is a bit less clear for me. I don't know how to translate. Would it be something like the following?
proc newFileReader*():JsObject {.importcpp:"new FileReader()".}
proc readBlob(blob):Future[Data] {.async.} =
result = newPromise() do (resolve: proc(response: Data)):
callbackFunc() do ():
let reader = newFileReader()
reader.readAsDataURL(blob)
return reader.result
I got the feeling of being completely off.
The closest experiment I get to is:
# nim js -d:release -d:nodejs -r tmp.nim
import std/[jsffi, asyncjs, dom, strformat, sugar]
type
Data = JsObject
proc newFileReader*():EventTarget {.importcpp:"new FileReader()".}
proc getResult(reader:EventTarget):Data {.importcpp:"#.result".}
proc readAsDataURL(reader:EventTarget; blob:string) {.importcpp:"#.readAsDataURL(#)".}
proc readBlob(blob:string):Future[Data] {.async.} =
let reader = newFileReader()
var p = newPromise() do (resolve: proc(response: Data)):
reader.addEventListener("load".cstring, (event:Event) => resolve(reader.getResult) )
reader.readAsDataURL(blob)
return p
proc main {.async.} =
var tmp = await readBlob("./helloworld.pdf")
discard main()
It compiles but it doesn't run:
var reader_486539320 = new FileReader();
^
ReferenceError: FileReader is not defined
at readBlob_486539306 (/home/jose/src/nimlang/fabricjs/examples/tmp.js:670:28)
at main_486539378 (/home/jose/src/nimlang/fabricjs/examples/tmp.js:684:32)
at Object.<anonymous> (/home/jose/src/nimlang/fabricjs/examples/tmp.js:692:9)
at Module._compile (node:internal/modules/cjs/loader:1159:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1213:10)
at Module.load (node:internal/modules/cjs/loader:1037:32)
at Module._load (node:internal/modules/cjs/loader:878:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:82:12)
at node:internal/main/run_main_module:23:47
Well, finally I got it working. I'll try to explain it on my own words (expect some mis-conceptions) for the non-programmers.
I created and HTML file tmp.html:
<html>
<head>
<meta charset="utf-8" />
</head>
<h1>Example</h1>
<input type="file" accept="application/pdf" onchange="readFile(this)">
<script src="./tmp.js"></script>
</html>
and then I created the following nim file: tmp.nim:
import std/[jsffi, asyncjs, dom, strformat, sugar]
var
console {.importc,nodecl.}: JsObject
type
Data = JsObject
proc getResult(reader:EventTarget):Data {.importcpp:"#.result".}
proc readAsDataURL(reader:EventTarget; blob:Blob) {.importcpp:"#.readAsDataURL(#)".}
proc getData(fname:Blob):Future[Data] {.async.} =
let reader = newFileReader()
var p = newPromise() do (resolve: proc(response: Data)):
reader.addEventListener("load".cstring, (event:Event) => resolve(reader.getResult) )
reader.readAsDataURL(fname)
return p
proc readFile(input:InputElement) {.exportc, async.} =
let file = input.files[0]
console.log(file) # Ahora file tiene tipo File
var data = await getData(file)
console.log(data)
The nim file is compiled:
nim js -d:release tmp.nim
creating the file tmp.js. Now we can view the tmp.html with a browser. If we inspect the page with the Javascript Console, we will see something like the following when we select the PDF file:
File {name: 'helloworld.pdf', lastModified: 1670617073512, lastModifiedDate: Fri Dec 09 2022 21:17:53 GMT+0100 (hora estándar de Europa central), webkitRelativePath: '', size: 678, …}
data:application/pdf;base64,JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwogIC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAvTWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0KPj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAgL1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9udAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2JqCgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJUCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4gCjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAwMDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G
by clicking on the data:application/pdf;base64 link, the PDF file will be opened.
Some explanations (or notes for me for the future):
- the pragma exportc prevents Nim from modifying the name of the function when exporting it to javascript. This is because <input... expect the function with the exact name: readFile.
- the pragma async, among other things, makes await available inside the function's body. The function returns nothing, but due to the use of async, it will return Future[void].
- let file = input.files[0]: we can see in the type definion input.files. The type returned will be a File (a Blob subtype).
- var data = await getData(file): this will wait until the future is resolved, getting the data from the file.
- this will return a javascript Future[T] (a javascript promise new Promise(#)). For that purpose uses: newPromise.
- let reader = newFileReader(): this is defined in std/dom.
- var p = newPromise() do (resolve: proc(response: Data)):: this matches the newPromise definition: proc (resolve: proc (response: T)). We are providing an anonymous function (this is the syntax do) with a parameter which requires another anonymous function named resolve with signature: proc(response: Data). What we do is to attach to reader an EventListener for the event load. We attach the callback: (event:Event) => resolve(reader.getResult): the parameter is the event and it calls the promise's resolve function with the parameter reader.getResult which is another function.