# # # Nim's Runtime Library # (c) Copyright 2017 Nim Authors # # See the file "copying.txt", included in this # distribution, for details about the copyright. ## This module implements types and macros for writing asynchronous code ## for the JS backend. It provides tools for interaction with JavaScript async API-s ## and libraries, writing async procedures in Nim and converting callback-based code ## to promises. ## ## A Nim procedure is asynchronous when it includes the `{.async.}` pragma. It ## should always have a `Future[T]` return type or not have a return type at all. ## A `Future[void]` return type is assumed by default. ## ## This is roughly equivalent to the `async` keyword in JavaScript code. ## ## .. code-block:: nim ## proc loadGame(name: string): Future[Game] {.async.} = ## # code ## ## should be equivalent to ## ## .. code-block:: javascript ## async function loadGame(name) { ## // code ## } ## ## A call to an asynchronous procedure usually needs `await` to wait for ## the completion of the `Future`. ## ## .. code-block:: nim ## var game = await loadGame(name) ## ## Often, you might work with callback-based API-s. You can wrap them with ## asynchronous procedures using promises and `newPromise`: ## ## .. code-block:: nim ## proc loadGame(name: string): Future[Game] = ## var promise = newPromise() do (resolve: proc(response: Game)): ## cbBasedLoadGame(name) do (game: Game): ## resolve(game) ## return promise ## ## Forward definitions work properly, you just need to always add the `{.async.}` pragma: ## ## .. code-block:: nim ## proc loadGame(name: string): Future[Game] {.async.} ## ## JavaScript compatibility ## ======================== ## ## Nim currently generates `async/await` JavaScript code which is supported in modern ## EcmaScript and most modern versions of browsers, Node.js and Electron. ## If you need to use this module with older versions of JavaScript, you can ## use a tool that backports the resulting JavaScript code, as babel. # xxx code-block:: javascript above gives `LanguageXNotSupported` warning. when not defined(js) and not defined(nimsuggest): {.fatal: "Module asyncjs is designed to be used with the JavaScript backend.".} import std/jsffi import std/macros type Future*[T] = ref object future*: T ## Wraps the return type of an asynchronous procedure. PromiseJs* {.importjs: "Promise".} = ref object ## A JavaScript Promise. proc replaceReturn(node: var NimNode) = var z = 0 for s in node: var son = node[z] let jsResolve = ident("jsResolve") if son.kind == nnkReturnStmt: let value = if son[0].kind != nnkEmpty: nnkCall.newTree(jsResolve, son[0]) else: jsResolve node[z] = nnkReturnStmt.newTree(value) elif son.kind == nnkAsgn and son[0].kind == nnkIdent and $son[0] == "result": node[z] = nnkAsgn.newTree(son[0], nnkCall.newTree(jsResolve, son[1])) else: replaceReturn(son) inc z proc isFutureVoid(node: NimNode): bool = result = node.kind == nnkBracketExpr and node[0].kind == nnkIdent and $node[0] == "Future" and node[1].kind == nnkIdent and $node[1] == "void" proc generateJsasync(arg: NimNode): NimNode = if arg.kind notin {nnkProcDef, nnkLambda, nnkMethodDef, nnkDo}: error("Cannot transform this node kind into an async proc." & " proc/method definition or lambda node expected.") result = arg var isVoid = false let jsResolve = ident("jsResolve") if arg.params[0].kind == nnkEmpty: result.params[0] = nnkBracketExpr.newTree(ident("Future"), ident("void")) isVoid = true elif isFutureVoid(arg.params[0]): isVoid = true var code = result.body replaceReturn(code) result.body = nnkStmtList.newTree() if len(code) > 0: var awaitFunction = quote: proc await[T](f: Future[T]): T {.importjs: "(await #)", used.} result.body.add(awaitFunction) var resolve: NimNode if isVoid: resolve = quote: var `jsResolve` {.importjs: "undefined".}: Future[void] else: resolve = quote: proc jsResolve[T](a: T): Future[T] {.importjs: "#", used.} proc jsResolve[T](a: Future[T]): Future[T] {.importjs: "#", used.} result.body.add(resolve) else: result.body = newEmptyNode() for child in code: result.body.add(child) if len(code) > 0 and isVoid: var voidFix = quote: return `jsResolve` result.body.add(voidFix) let asyncPragma = quote: {.codegenDecl: "async function $2($3)".} result.addPragma(asyncPragma[0]) macro async*(arg: untyped): untyped = ## Macro which converts normal procedures into ## javascript-compatible async procedures. if arg.kind == nnkStmtList: result = newStmtList() for oneProc in arg: result.add generateJsasync(oneProc) else: result = generateJsasync(arg) proc newPromise*[T](handler: proc(resolve: proc(response: T))): Future[T] {.importjs: "(new Promise(#))".} ## A helper for wrapping callback-based functions ## into promises and async procedures. proc newPromise*(handler: proc(resolve: proc())): Future[void] {.importjs: "(new Promise(#))".} ## A helper for wrapping callback-based functions ## into promises and async procedures. template typeOrVoid[T](a: T): type = # xxx this is useful, make it public in std/typetraits in future work T template maybeFuture(T): untyped = # avoids `Future[Future[T]]` when T is Future: T else: Future[T] when defined(nimExperimentalAsyncjsThen): import std/private/since since (1, 5, 1): #[ TODO: * map `Promise.all()` * proc toString*(a: Error): cstring {.importjs: "#.toString()".} Note: We probably can't have a `waitFor` in js in browser (single threaded), but maybe it would be possible in in nodejs, see https://nodejs.org/api/child_process.html#child_process_child_process_execsync_command_options and https://stackoverflow.com/questions/61377358/javascript-wait-for-async-call-to-finish-before-returning-from-function-witho ]# type Error* {.importjs: "Error".} = ref object of JsRoot ## https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error message*: cstring name*: cstring type OnReject* = proc(reason: Error) proc then*[T](future: Future[T], onSuccess: proc, onReject: OnReject = nil): auto = ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then ## Returns a `Future` from the return type of `onSuccess(T.default)`. runnableExamples("-d:nimExperimentalAsyncjsThen"): from std/sugar import `=>` proc fn(n: int): Future[int] {.async.} = if n >= 7: raise newException(ValueError, "foobar: " & $n) else: result = n * 2 proc asyncFact(n: int): Future[int] {.async.} = if n > 0: result = n * await asyncFact(n-1) else: result = 1 proc main() {.async.} = block: # then assert asyncFact(3).await == 3*2 assert asyncFact(3).then(asyncFact).await == 6*5*4*3*2 let x1 = await fn(3) assert x1 == 3 * 2 let x2 = await fn(4) .then((a: int) => a.float) .then((a: float) => $a) assert x2 == "8.0" block: # then with `onReject` callback var witness = 1 await fn(6).then((a: int) => (witness = 2), (r: Error) => (witness = 3)) assert witness == 2 await fn(7).then((a: int) => (witness = 2), (r: Error) => (witness = 3)) assert witness == 3 template impl(call): untyped = when typeOrVoid(call) is void: var ret: Future[void] else: var ret = default(maybeFuture(typeof(call))) typeof(ret) when T is void: type A = impl(onSuccess()) else: type A = impl(onSuccess(default(T))) var ret: A asm "`ret` = `future`.then(`onSuccess`, `onReject`)" return ret proc catch*[T](future: Future[T], onReject: OnReject): Future[void] = ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch runnableExamples("-d:nimExperimentalAsyncjsThen"): from std/sugar import `=>` from std/strutils import contains proc fn(n: int): Future[int] {.async.} = if n >= 7: raise newException(ValueError, "foobar: " & $n) else: result = n * 2 proc main() {.async.} = var reason: Error await fn(6).catch((r: Error) => (reason = r)) # note: `()` are needed, `=> reason = r` would not work assert reason == nil await fn(7).catch((r: Error) => (reason = r)) assert reason != nil assert "foobar: 7" in $reason.message discard main() asm "`result` = `future`.catch(`onReject`)"