asyncjs.nim 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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, nnkProcTy}:
  94. error("Cannot transform this node kind into an async proc." &
  95. " proc/method definition or lambda node expected.")
  96. # Transform type X = proc (): something {.async.}
  97. # into type X = proc (): Future[something]
  98. if arg.kind == nnkProcTy:
  99. result = arg
  100. if arg[0][0].kind == nnkEmpty:
  101. result[0][0] = quote do: Future[void]
  102. return result
  103. result = arg
  104. var isVoid = false
  105. let jsResolve = ident("jsResolve")
  106. if arg.params[0].kind == nnkEmpty:
  107. result.params[0] = nnkBracketExpr.newTree(ident("Future"), ident("void"))
  108. isVoid = true
  109. elif isFutureVoid(arg.params[0]):
  110. isVoid = true
  111. var code = result.body
  112. replaceReturn(code)
  113. result.body = nnkStmtList.newTree()
  114. if len(code) > 0:
  115. var awaitFunction = quote:
  116. proc await[T](f: Future[T]): T {.importjs: "(await #)", used.}
  117. result.body.add(awaitFunction)
  118. var resolve: NimNode
  119. if isVoid:
  120. resolve = quote:
  121. var `jsResolve` {.importjs: "undefined".}: Future[void]
  122. else:
  123. resolve = quote:
  124. proc jsResolve[T](a: T): Future[T] {.importjs: "#", used.}
  125. proc jsResolve[T](a: Future[T]): Future[T] {.importjs: "#", used.}
  126. result.body.add(resolve)
  127. else:
  128. result.body = newEmptyNode()
  129. for child in code:
  130. result.body.add(child)
  131. if len(code) > 0 and isVoid:
  132. var voidFix = quote:
  133. return `jsResolve`
  134. result.body.add(voidFix)
  135. let asyncPragma = quote:
  136. {.codegenDecl: "async function $2($3)".}
  137. result.addPragma(asyncPragma[0])
  138. macro async*(arg: untyped): untyped =
  139. ## Macro which converts normal procedures into
  140. ## javascript-compatible async procedures.
  141. if arg.kind == nnkStmtList:
  142. result = newStmtList()
  143. for oneProc in arg:
  144. result.add generateJsasync(oneProc)
  145. else:
  146. result = generateJsasync(arg)
  147. proc newPromise*[T](handler: proc(resolve: proc(response: T))): Future[T] {.importjs: "(new Promise(#))".}
  148. ## A helper for wrapping callback-based functions
  149. ## into promises and async procedures.
  150. proc newPromise*(handler: proc(resolve: proc())): Future[void] {.importjs: "(new Promise(#))".}
  151. ## A helper for wrapping callback-based functions
  152. ## into promises and async procedures.
  153. template maybeFuture(T): untyped =
  154. # avoids `Future[Future[T]]`
  155. when T is Future: T
  156. else: Future[T]
  157. since (1, 5, 1):
  158. #[
  159. TODO:
  160. * map `Promise.all()`
  161. * proc toString*(a: Error): cstring {.importjs: "#.toString()".}
  162. Note:
  163. We probably can't have a `waitFor` in js in browser (single threaded), but maybe it would be possible
  164. in in nodejs, see https://nodejs.org/api/child_process.html#child_process_child_process_execsync_command_options
  165. and https://stackoverflow.com/questions/61377358/javascript-wait-for-async-call-to-finish-before-returning-from-function-witho
  166. ]#
  167. type Error* {.importjs: "Error".} = ref object of JsRoot
  168. ## https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
  169. message*: cstring
  170. name*: cstring
  171. type OnReject* = proc(reason: Error)
  172. proc then*[T](future: Future[T], onSuccess: proc, onReject: OnReject = nil): auto =
  173. ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then
  174. ## Returns a `Future` from the return type of `onSuccess(T.default)`.
  175. runnableExamples("-r:off"):
  176. from std/sugar import `=>`
  177. proc fn(n: int): Future[int] {.async.} =
  178. if n >= 7: raise newException(ValueError, "foobar: " & $n)
  179. else: result = n * 2
  180. proc asyncFact(n: int): Future[int] {.async.} =
  181. if n > 0: result = n * await asyncFact(n-1)
  182. else: result = 1
  183. proc main() {.async.} =
  184. block: # then
  185. assert asyncFact(3).await == 3*2
  186. assert asyncFact(3).then(asyncFact).await == 6*5*4*3*2
  187. let x1 = await fn(3)
  188. assert x1 == 3 * 2
  189. let x2 = await fn(4)
  190. .then((a: int) => a.float)
  191. .then((a: float) => $a)
  192. assert x2 == "8.0"
  193. block: # then with `onReject` callback
  194. var witness = 1
  195. await fn(6).then((a: int) => (witness = 2), (r: Error) => (witness = 3))
  196. assert witness == 2
  197. await fn(7).then((a: int) => (witness = 2), (r: Error) => (witness = 3))
  198. assert witness == 3
  199. template impl(call): untyped =
  200. # see D20210421T014713
  201. when typeof(block: call) is void:
  202. var ret: Future[void]
  203. else:
  204. var ret = default(maybeFuture(typeof(call)))
  205. typeof(ret)
  206. when T is void:
  207. type A = impl(onSuccess())
  208. else:
  209. type A = impl(onSuccess(default(T)))
  210. var ret: A
  211. asm "`ret` = `future`.then(`onSuccess`, `onReject`)"
  212. return ret
  213. proc catch*[T](future: Future[T], onReject: OnReject): Future[void] =
  214. ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
  215. runnableExamples("-r:off"):
  216. from std/sugar import `=>`
  217. from std/strutils import contains
  218. proc fn(n: int): Future[int] {.async.} =
  219. if n >= 7: raise newException(ValueError, "foobar: " & $n)
  220. else: result = n * 2
  221. proc main() {.async.} =
  222. var reason: Error
  223. await fn(6).catch((r: Error) => (reason = r)) # note: `()` are needed, `=> reason = r` would not work
  224. assert reason == nil
  225. await fn(7).catch((r: Error) => (reason = r))
  226. assert reason != nil
  227. assert "foobar: 7" in $reason.message
  228. discard main()
  229. asm "`result` = `future`.catch(`onReject`)"