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
This commit is contained in:
Timothee Cour
2021-02-24 12:03:21 -08:00
committed by GitHub
parent 99633d7682
commit a4e6b242d5
7 changed files with 166 additions and 24 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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`)"

View File

@@ -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.

View File

@@ -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")

View File

@@ -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)"]
discard x(2)
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
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()

View File

@@ -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()