asyncjs.nim 8.7 KB

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