summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorTimothee Cour <timothee.cour2@gmail.com>2021-02-24 12:03:21 -0800
committerGitHub <noreply@github.com>2021-02-24 21:03:21 +0100
commita4e6b242d56d80df004bf625643fcb3c9d3f1b7f (patch)
tree32298bbbeeae4aa4bfa5bed4a3305a744f7041ee
parent99633d768236ea325c7c6482445ba71c62b30fb0 (diff)
downloadNim-a4e6b242d56d80df004bf625643fcb3c9d3f1b7f.tar.gz
asyncjs: add `then`, `catch` for promise pipelining (#16871)
* asyncjs: add then
* improve tests, changelog, API
* fix cryptic windows error: The parameter is incorrect
* address comments
-rw-r--r--changelog.md2
-rw-r--r--compiler/nim.nim8
-rw-r--r--lib/js/asyncjs.nim69
-rw-r--r--testament/testament.nim6
-rw-r--r--tests/config.nims3
-rw-r--r--tests/js/tasync.nim80
-rw-r--r--tests/js/tasyncjs_fail.nim22
7 files changed, 166 insertions, 24 deletions
diff --git a/changelog.md b/changelog.md
index 8c6d2b3c4..6fe730ffb 100644
--- a/changelog.md
+++ b/changelog.md
@@ -225,6 +225,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 46654e352..5ec891816 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 76b948e6a..45053fbaa 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 8637f9464..1307c19ef 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 ac90d37e8..47a303e85 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 318237651..e676ba14b 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 000000000..b1e5a7bc3
--- /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()