mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-28 17:04:41 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`)"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user