diff --git a/changelog.md b/changelog.md index 059f8d9863b5..6b8c8f39e423 100644 --- a/changelog.md +++ b/changelog.md @@ -203,6 +203,8 @@ provided by the operating system. - Added `-d:nimStrictMode` in CI in several places to ensure code doesn't have certain hints/warnings +- Added `then`, `catch` to `asyncjs`, for now hidden behind `-d:nimExperimentalAsyncjsThen`. + ## Tool changes - The rst parser now supports markdown table syntax. diff --git a/compiler/nim.nim b/compiler/nim.nim index 46654e352f35..5ec891816486 100644 --- a/compiler/nim.nim +++ b/compiler/nim.nim @@ -95,9 +95,15 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) = var cmdPrefix = "" case conf.backend of backendC, backendCpp, backendObjc: discard - of backendJs: cmdPrefix = findNodeJs() & " " + of backendJs: + # D20210217T215950:here this flag is needed for node < v15.0.0, otherwise + # tasyncjs_fail` would fail, refs https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode + cmdPrefix = findNodeJs() & " --unhandled-rejections=strict " else: doAssert false, $conf.backend + # No space before command otherwise on windows you'd get a cryptic: + # `The parameter is incorrect` execExternalProgram(conf, cmdPrefix & output.quoteShell & ' ' & conf.arguments) + # execExternalProgram(conf, cmdPrefix & ' ' & output.quoteShell & ' ' & conf.arguments) of cmdDocLike, cmdRst2html, cmdRst2tex: # bugfix(cmdRst2tex was missing) if conf.arguments.len > 0: # reserved for future use diff --git a/lib/js/asyncjs.nim b/lib/js/asyncjs.nim index 76b948e6a29f..45053fbaa923 100644 --- a/lib/js/asyncjs.nim +++ b/lib/js/asyncjs.nim @@ -57,12 +57,13 @@ ## 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. -import std/jsffi -import std/macros - 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 +import std/private/since + type Future*[T] = ref object future*: T @@ -154,3 +155,65 @@ proc newPromise*[T](handler: proc(resolve: proc(response: T))): Future[T] {.impo proc newPromise*(handler: proc(resolve: proc())): Future[void] {.importcpp: "(new Promise(#))".} ## A helper for wrapping callback-based functions ## into promises and async procedures. + +when defined(nimExperimentalAsyncjsThen): + 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, T2](future: Future[T], onSuccess: proc(value: T): T2, onReject: OnReject = nil): Future[T2] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + + proc then*[T](future: Future[T], onSuccess: proc(value: T), onReject: OnReject = nil): Future[void] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + + proc then*(future: Future[void], onSuccess: proc(), onReject: OnReject = nil): Future[void] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + + proc then*[T2](future: Future[void], onSuccess: proc(): T2, onReject: OnReject = nil): Future[T2] = + ## See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + asm "`result` = `future`.then(`onSuccess`, `onReject`)" + + 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: + 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.} = + 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" + + var reason: Error + await fn(6).catch((r: Error) => (reason = r)) + 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`)" diff --git a/testament/testament.nim b/testament/testament.nim index 8637f946413f..1307c19efc9d 100644 --- a/testament/testament.nim +++ b/testament/testament.nim @@ -12,7 +12,7 @@ import strutils, pegs, os, osproc, streams, json, std/exitprocs, backend, parseopt, specs, htmlgen, browsers, terminal, - algorithm, times, md5, sequtils, azure, intsets, macros + algorithm, times, md5, azure, intsets, macros from std/sugar import dup import compiler/nodejs import lib/stdtest/testutils @@ -501,7 +501,8 @@ proc testSpecHelper(r: var TResults, test: var TTest, expected: TSpec, var args = test.args if isJsTarget: exeCmd = nodejs - args = concat(@[exeFile], args) + # see D20210217T215950 + args = @["--unhandled-rejections=strict", exeFile] & args else: exeCmd = exeFile.dup(normalizeExe) if expected.useValgrind != disabled: @@ -510,6 +511,7 @@ proc testSpecHelper(r: var TResults, test: var TTest, expected: TSpec, valgrindOptions.add "--leak-check=yes" args = valgrindOptions & exeCmd & args exeCmd = "valgrind" + # xxx honor `testament --verbose` here var (_, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input) # Treat all failure codes from nodejs as 1. Older versions of nodejs used # to return other codes, but for us it is sufficient to know that it's not 0. diff --git a/tests/config.nims b/tests/config.nims index ac90d37e86c5..47a303e852a6 100644 --- a/tests/config.nims +++ b/tests/config.nims @@ -23,3 +23,6 @@ hint("Processing", off) # switch("define", "nimTestsEnableFlaky") # switch("hint", "ConvFromXtoItselfNotNeeded") + +# experimental API's are enabled in testament, refs https://github.com/timotheecour/Nim/issues/575 +switch("define", "nimExperimentalAsyncjsThen") diff --git a/tests/js/tasync.nim b/tests/js/tasync.nim index 31823765195f..e676ba14b2c7 100644 --- a/tests/js/tasync.nim +++ b/tests/js/tasync.nim @@ -2,32 +2,76 @@ discard """ output: ''' x e +done ''' """ -import asyncjs +#[ +xxx move this to tests/stdlib/tasyncjs.nim +]# -# demonstrate forward definition -# for js -proc y(e: int): Future[string] {.async.} +import std/asyncjs -proc e: int {.discardable.} = - echo "e" - return 2 +block: + # demonstrate forward definition for js + proc y(e: int): Future[string] {.async.} -proc x(e: int): Future[void] {.async.} = - var s = await y(e) - if e > 2: - return - echo s - e() + proc e: int {.discardable.} = + echo "e" + return 2 -proc y(e: int): Future[string] {.async.} = - if e > 0: - return await y(0) + proc x(e: int): Future[void] {.async.} = + var s = await y(e) + if e > 2: + return + echo s + e() + + proc y(e: int): Future[string] {.async.} = + if e > 0: + return await y(0) + else: + return "x" + + discard x(2) + +import std/sugar +from std/strutils import contains + +var witness: seq[string] + +proc fn(n: int): Future[int] {.async.} = + if n >= 7: + raise newException(ValueError, "foobar: " & $n) + if n > 0: + var ret = 1 + await fn(n-1) + witness.add $(n, ret) + return ret else: - return "x" + return 10 + +proc main() {.async.} = + block: # then + let x = await fn(4) + .then((a: int) => a.float) + .then((a: float) => $a) + doAssert x == "14.0" + doAssert witness == @["(1, 11)", "(2, 12)", "(3, 13)", "(4, 14)"] + + doAssert (await fn(2)) == 12 + + let x2 = await fn(4).then((a: int) => (discard)).then(() => 13) + doAssert x2 == 13 + block: # catch + var reason: Error + await fn(6).then((a: int) => (witness.add $a)).catch((r: Error) => (reason = r)) + doAssert reason == nil -discard x(2) + await fn(7).then((a: int) => (discard)).catch((r: Error) => (reason = r)) + doAssert reason != nil + doAssert reason.name == "Error" + doAssert "foobar: 7" in $reason.message + echo "done" # justified here to make sure we're running this, since it's inside `async` +discard main() diff --git a/tests/js/tasyncjs_fail.nim b/tests/js/tasyncjs_fail.nim new file mode 100644 index 000000000000..b1e5a7bc33fa --- /dev/null +++ b/tests/js/tasyncjs_fail.nim @@ -0,0 +1,22 @@ +discard """ + exitCode: 1 + outputsub: "Error: unhandled exception: foobar: 13" +""" + +# note: this needs `--unhandled-rejections=strict`, see D20210217T215950 + +import std/asyncjs +from std/sugar import `=>` + +proc fn(n: int): Future[int] {.async.} = + if n >= 7: raise newException(ValueError, "foobar: " & $n) + else: result = n + +proc main() {.async.} = + let x1 = await fn(6) + doAssert x1 == 6 + await fn(7).catch((a: Error) => (discard)) + let x3 = await fn(13) + doAssert false # shouldn't go here, should fail before + +discard main()