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:
parent
99633d7682
commit
a4e6b242d5
7 changed files with 166 additions and 24 deletions
|
|
@ -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 `-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
|
## Tool changes
|
||||||
|
|
||||||
- The rst parser now supports markdown table syntax.
|
- The rst parser now supports markdown table syntax.
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,15 @@ proc handleCmdLine(cache: IdentCache; conf: ConfigRef) =
|
||||||
var cmdPrefix = ""
|
var cmdPrefix = ""
|
||||||
case conf.backend
|
case conf.backend
|
||||||
of backendC, backendCpp, backendObjc: discard
|
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
|
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)
|
||||||
|
# execExternalProgram(conf, cmdPrefix & ' ' & output.quoteShell & ' ' & conf.arguments)
|
||||||
of cmdDocLike, cmdRst2html, cmdRst2tex: # bugfix(cmdRst2tex was missing)
|
of cmdDocLike, cmdRst2html, cmdRst2tex: # bugfix(cmdRst2tex was missing)
|
||||||
if conf.arguments.len > 0:
|
if conf.arguments.len > 0:
|
||||||
# reserved for future use
|
# reserved for future use
|
||||||
|
|
|
||||||
|
|
@ -57,12 +57,13 @@
|
||||||
## If you need to use this module with older versions of JavaScript, you can
|
## 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.
|
## 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):
|
when not defined(js) and not defined(nimsuggest):
|
||||||
{.fatal: "Module asyncjs is designed to be used with the JavaScript backend.".}
|
{.fatal: "Module asyncjs is designed to be used with the JavaScript backend.".}
|
||||||
|
|
||||||
|
import std/jsffi
|
||||||
|
import std/macros
|
||||||
|
import std/private/since
|
||||||
|
|
||||||
type
|
type
|
||||||
Future*[T] = ref object
|
Future*[T] = ref object
|
||||||
future*: T
|
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(#))".}
|
proc newPromise*(handler: proc(resolve: proc())): Future[void] {.importcpp: "(new Promise(#))".}
|
||||||
## A helper for wrapping callback-based functions
|
## A helper for wrapping callback-based functions
|
||||||
## into promises and async procedures.
|
## 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`)"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
import
|
import
|
||||||
strutils, pegs, os, osproc, streams, json, std/exitprocs,
|
strutils, pegs, os, osproc, streams, json, std/exitprocs,
|
||||||
backend, parseopt, specs, htmlgen, browsers, terminal,
|
backend, parseopt, specs, htmlgen, browsers, terminal,
|
||||||
algorithm, times, md5, sequtils, azure, intsets, macros
|
algorithm, times, md5, azure, intsets, macros
|
||||||
from std/sugar import dup
|
from std/sugar import dup
|
||||||
import compiler/nodejs
|
import compiler/nodejs
|
||||||
import lib/stdtest/testutils
|
import lib/stdtest/testutils
|
||||||
|
|
@ -501,7 +501,8 @@ proc testSpecHelper(r: var TResults, test: var TTest, expected: TSpec,
|
||||||
var args = test.args
|
var args = test.args
|
||||||
if isJsTarget:
|
if isJsTarget:
|
||||||
exeCmd = nodejs
|
exeCmd = nodejs
|
||||||
args = concat(@[exeFile], args)
|
# see D20210217T215950
|
||||||
|
args = @["--unhandled-rejections=strict", exeFile] & args
|
||||||
else:
|
else:
|
||||||
exeCmd = exeFile.dup(normalizeExe)
|
exeCmd = exeFile.dup(normalizeExe)
|
||||||
if expected.useValgrind != disabled:
|
if expected.useValgrind != disabled:
|
||||||
|
|
@ -510,6 +511,7 @@ proc testSpecHelper(r: var TResults, test: var TTest, expected: TSpec,
|
||||||
valgrindOptions.add "--leak-check=yes"
|
valgrindOptions.add "--leak-check=yes"
|
||||||
args = valgrindOptions & exeCmd & args
|
args = valgrindOptions & exeCmd & args
|
||||||
exeCmd = "valgrind"
|
exeCmd = "valgrind"
|
||||||
|
# xxx honor `testament --verbose` here
|
||||||
var (_, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input)
|
var (_, buf, exitCode) = execCmdEx2(exeCmd, args, input = expected.input)
|
||||||
# Treat all failure codes from nodejs as 1. Older versions of nodejs used
|
# 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.
|
# to return other codes, but for us it is sufficient to know that it's not 0.
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,6 @@ hint("Processing", off)
|
||||||
# switch("define", "nimTestsEnableFlaky")
|
# switch("define", "nimTestsEnableFlaky")
|
||||||
|
|
||||||
# switch("hint", "ConvFromXtoItselfNotNeeded")
|
# switch("hint", "ConvFromXtoItselfNotNeeded")
|
||||||
|
|
||||||
|
# experimental API's are enabled in testament, refs https://github.com/timotheecour/Nim/issues/575
|
||||||
|
switch("define", "nimExperimentalAsyncjsThen")
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,18 @@ discard """
|
||||||
output: '''
|
output: '''
|
||||||
x
|
x
|
||||||
e
|
e
|
||||||
|
done
|
||||||
'''
|
'''
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncjs
|
#[
|
||||||
|
xxx move this to tests/stdlib/tasyncjs.nim
|
||||||
|
]#
|
||||||
|
|
||||||
# demonstrate forward definition
|
import std/asyncjs
|
||||||
# for js
|
|
||||||
|
block:
|
||||||
|
# demonstrate forward definition for js
|
||||||
proc y(e: int): Future[string] {.async.}
|
proc y(e: int): Future[string] {.async.}
|
||||||
|
|
||||||
proc e: int {.discardable.} =
|
proc e: int {.discardable.} =
|
||||||
|
|
@ -28,6 +33,45 @@ proc y(e: int): Future[string] {.async.} =
|
||||||
else:
|
else:
|
||||||
return "x"
|
return "x"
|
||||||
|
|
||||||
|
|
||||||
discard x(2)
|
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 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
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
|
||||||
22
tests/js/tasyncjs_fail.nim
Normal file
22
tests/js/tasyncjs_fail.nim
Normal 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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue