asyncjs.nim 8.7 KB

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