asyncjs.nim 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2017 Nim Authors
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. ## This module implements types and macros for writing asynchronous code
  9. ## for the JS backend. It provides tools for interaction with JavaScript async API-s
  10. ## and libraries, writing async procedures in Nim and converting callback-based code
  11. ## to promises.
  12. ##
  13. ## A Nim procedure is asynchronous when it includes the `{.async.}` pragma. It
  14. ## should always have a `Future[T]` return type or not have a return type at all.
  15. ## A `Future[void]` return type is assumed by default.
  16. ##
  17. ## This is roughly equivalent to the `async` keyword in JavaScript code.
  18. ##
  19. ## ```nim
  20. ## proc loadGame(name: string): Future[Game] {.async.} =
  21. ## # code
  22. ## ```
  23. ##
  24. ## should be equivalent to
  25. ##
  26. ## ```javascript
  27. ## async function loadGame(name) {
  28. ## // code
  29. ## }
  30. ## ```
  31. ##
  32. ## A call to an asynchronous procedure usually needs `await` to wait for
  33. ## the completion of the `Future`.
  34. ##
  35. ## ```nim
  36. ## var game = await loadGame(name)
  37. ## ```
  38. ##
  39. ## Often, you might work with callback-based API-s. You can wrap them with
  40. ## asynchronous procedures using promises and `newPromise`:
  41. ##
  42. ## ```nim
  43. ## proc loadGame(name: string): Future[Game] =
  44. ## var promise = newPromise() do (resolve: proc(response: Game)):
  45. ## cbBasedLoadGame(name) do (game: Game):
  46. ## resolve(game)
  47. ## return promise
  48. ## ```
  49. ##
  50. ## Forward definitions work properly, you just need to always add the `{.async.}` pragma:
  51. ##
  52. ## ```nim
  53. ## proc loadGame(name: string): Future[Game] {.async.}
  54. ## ```
  55. ##
  56. ## JavaScript compatibility
  57. ## ========================
  58. ##
  59. ## Nim currently generates `async/await` JavaScript code which is supported in modern
  60. ## EcmaScript and most modern versions of browsers, Node.js and Electron.
  61. ## If you need to use this module with older versions of JavaScript, you can
  62. ## use a tool that backports the resulting JavaScript code, as babel.
  63. # xxx code: javascript above gives `LanguageXNotSupported` warning.
  64. when not defined(js) and not defined(nimsuggest):
  65. {.fatal: "Module asyncjs is designed to be used with the JavaScript backend.".}
  66. import std/jsffi
  67. import std/macros
  68. import std/private/since
  69. type
  70. Future*[T] = ref object
  71. future*: T
  72. ## Wraps the return type of an asynchronous procedure.
  73. PromiseJs* {.importjs: "Promise".} = ref object
  74. ## A JavaScript Promise.
  75. proc replaceReturn(node: var NimNode) =
  76. var z = 0
  77. for s in node:
  78. var son = node[z]
  79. let jsResolve = ident("jsResolve")
  80. if son.kind == nnkReturnStmt:
  81. let value = if son[0].kind != nnkEmpty: nnkCall.newTree(jsResolve, son[0]) else: jsResolve
  82. node[z] = nnkReturnStmt.newTree(value)
  83. elif son.kind == nnkAsgn and son[0].kind == nnkIdent and $son[0] == "result":
  84. node[z] = nnkAsgn.newTree(son[0], nnkCall.newTree(jsResolve, son[1]))
  85. else:
  86. replaceReturn(son)
  87. inc z
  88. proc isFutureVoid(node: NimNode): bool =
  89. result = node.kind == nnkBracketExpr and
  90. node[0].kind == nnkIdent and $node[0] == "Future" and
  91. node[1].kind == nnkIdent and $node[1] == "void"
  92. proc generateJsasync(arg: NimNode): NimNode =
  93. if arg.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}:
  94. error("Cannot transform this node kind into an async proc." &
  95. " proc/method definition or lambda node expected.")
  96. result = arg
  97. var isVoid = false
  98. let jsResolve = ident("jsResolve")
  99. if arg.params[0].kind == nnkEmpty:
  100. result.params[0] = nnkBracketExpr.newTree(ident("Future"), ident("void"))
  101. isVoid = true
  102. elif isFutureVoid(arg.params[0]):
  103. isVoid = true
  104. var code = result.body
  105. replaceReturn(code)
  106. result.body = nnkStmtList.newTree()
  107. if len(code) > 0:
  108. var awaitFunction = quote:
  109. proc await[T](f: Future[T]): T {.importjs: "(await #)", used.}
  110. result.body.add(awaitFunction)
  111. var resolve: NimNode
  112. if isVoid:
  113. resolve = quote:
  114. var `jsResolve` {.importjs: "undefined".}: Future[void]
  115. else:
  116. resolve = quote:
  117. proc jsResolve[T](a: T): Future[T] {.importjs: "#", used.}
  118. proc jsResolve[T](a: Future[T]): Future[T] {.importjs: "#", used.}
  119. result.body.add(resolve)
  120. else:
  121. result.body = newEmptyNode()
  122. for child in code:
  123. result.body.add(child)
  124. if len(code) > 0 and isVoid:
  125. var voidFix = quote:
  126. return `jsResolve`
  127. result.body.add(voidFix)
  128. let asyncPragma = quote:
  129. {.codegenDecl: "async function $2($3)".}
  130. result.addPragma(asyncPragma[0])
  131. macro async*(arg: untyped): untyped =
  132. ## Macro which converts normal procedures into
  133. ## javascript-compatible async procedures.
  134. if arg.kind == nnkStmtList:
  135. result = newStmtList()
  136. for oneProc in arg:
  137. result.add generateJsasync(oneProc)
  138. else:
  139. result = generateJsasync(arg)
  140. proc newPromise*[T](handler: proc(resolve: proc(response: T))): Future[T] {.importjs: "(new Promise(#))".}
  141. ## A helper for wrapping callback-based functions
  142. ## into promises and async procedures.
  143. proc newPromise*(handler: proc(resolve: proc())): Future[void] {.importjs: "(new Promise(#))".}
  144. ## A helper for wrapping callback-based functions
  145. ## into promises and async procedures.
  146. template maybeFuture(T): untyped =
  147. # avoids `Future[Future[T]]`
  148. when T is Future: T
  149. else: Future[T]
  150. since (1, 5, 1):
  151. #[
  152. TODO:
  153. * map `Promise.all()`
  154. * proc toString*(a: Error): cstring {.importjs: "#.toString()".}
  155. Note:
  156. We probably can't have a `waitFor` in js in browser (single threaded), but maybe it would be possible
  157. in in nodejs, see https://nodejs.org/api/child_process.html#child_process_child_process_execsync_command_options
  158. and https://stackoverflow.com/questions/61377358/javascript-wait-for-async-call-to-finish-before-returning-from-function-witho
  159. ]#
  160. type Error* {.importjs: "Error".} = ref object of JsRoot
  161. ## https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
  162. message*: cstring
  163. name*: cstring
  164. type OnReject* = proc(reason: Error)
  165. proc then*[T](future: Future[T], onSuccess: proc, onReject: OnReject = nil): auto =
  166. ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
  167. ## Returns a `Future` from the return type of `onSuccess(T.default)`.
  168. runnableExamples("-r:off"):
  169. from std/sugar import `=>`
  170. proc fn(n: int): Future[int] {.async.} =
  171. if n >= 7: raise newException(ValueError, "foobar: " & $n)
  172. else: result = n * 2
  173. proc asyncFact(n: int): Future[int] {.async.} =
  174. if n > 0: result = n * await asyncFact(n-1)
  175. else: result = 1
  176. proc main() {.async.} =
  177. block: # then
  178. assert asyncFact(3).await == 3*2
  179. assert asyncFact(3).then(asyncFact).await == 6*5*4*3*2
  180. let x1 = await fn(3)
  181. assert x1 == 3 * 2
  182. let x2 = await fn(4)
  183. .then((a: int) => a.float)
  184. .then((a: float) => $a)
  185. assert x2 == "8.0"
  186. block: # then with `onReject` callback
  187. var witness = 1
  188. await fn(6).then((a: int) => (witness = 2), (r: Error) => (witness = 3))
  189. assert witness == 2
  190. await fn(7).then((a: int) => (witness = 2), (r: Error) => (witness = 3))
  191. assert witness == 3
  192. template impl(call): untyped =
  193. # see D20210421T014713
  194. when typeof(block: call) is void:
  195. var ret: Future[void]
  196. else:
  197. var ret = default(maybeFuture(typeof(call)))
  198. typeof(ret)
  199. when T is void:
  200. type A = impl(onSuccess())
  201. else:
  202. type A = impl(onSuccess(default(T)))
  203. var ret: A
  204. asm "`ret` = `future`.then(`onSuccess`, `onReject`)"
  205. return ret
  206. proc catch*[T](future: Future[T], onReject: OnReject): Future[void] =
  207. ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
  208. runnableExamples("-r:off"):
  209. from std/sugar import `=>`
  210. from std/strutils import contains
  211. proc fn(n: int): Future[int] {.async.} =
  212. if n >= 7: raise newException(ValueError, "foobar: " & $n)
  213. else: result = n * 2
  214. proc main() {.async.} =
  215. var reason: Error
  216. await fn(6).catch((r: Error) => (reason = r)) # note: `()` are needed, `=> reason = r` would not work
  217. assert reason == nil
  218. await fn(7).catch((r: Error) => (reason = r))
  219. assert reason != nil
  220. assert "foobar: 7" in $reason.message
  221. discard main()
  222. asm "`result` = `future`.catch(`onReject`)"