Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

asyncjs: add then, catch for promise pipelining #16871

Merged
merged 8 commits into from
Feb 24, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
timotheecour marked this conversation as resolved.
Show resolved Hide resolved

## Tool changes

- The rst parser now supports markdown table syntax.
Expand Down
8 changes: 7 additions & 1 deletion compiler/nim.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Araq marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
69 changes: 66 additions & 3 deletions lib/js/asyncjs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 RootObj
timotheecour marked this conversation as resolved.
Show resolved Hide resolved
## 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`)"
6 changes: 4 additions & 2 deletions testament/testament.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions tests/config.nims
Original file line number Diff line number Diff line change
Expand Up @@ -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")
80 changes: 62 additions & 18 deletions tests/js/tasync.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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()
22 changes: 22 additions & 0 deletions tests/js/tasyncjs_fail.nim
Original file line number Diff line number Diff line change
@@ -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()