Skip to content

Commit

Permalink
Top level await (#4007)
Browse files Browse the repository at this point in the history
Co-authored-by: Joan López de la Franca Beltran <[email protected]>
  • Loading branch information
mstoykov and joanlopez authored Oct 28, 2024
1 parent 582eb9e commit 3c56028
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 13 deletions.
11 changes: 10 additions & 1 deletion js/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,10 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*BundleInstance
env: b.preInitState.RuntimeOptions.Env,
moduleVUImpl: vuImpl,
}
var result *modules.RunSourceDataResult
callback := func() error { // this exists so that Sobek catches uncatchable panics such as Interrupt
var err error
bi.mainModule, err = modSys.RunSourceData(b.sourceData)
result, err = modSys.RunSourceData(b.sourceData)
return err
}

Expand All @@ -346,6 +347,14 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*BundleInstance

<-initDone

if err == nil {
var finished bool
bi.mainModule, finished, err = result.Result()
if !finished {
return nil, errors.New("initializing the main module hasn't finished, this is a bug in k6 please report it")
}
}

if err != nil {
var exception *sobek.Exception
if errors.As(err, &exception) {
Expand Down
17 changes: 16 additions & 1 deletion js/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func TestNewBundle(t *testing.T) {
t.Run("InvalidExports", func(t *testing.T) {
t.Parallel()
_, err := getSimpleBundle(t, "/script.js", `module.exports = null`)
require.EqualError(t, err, "GoError: CommonJS's exports must not be null\n") // TODO: try to remove the GoError from herer
require.EqualError(t, err, "CommonJS's exports must not be null")
})
t.Run("DefaultUndefined", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -983,3 +983,18 @@ func TestGlobalTimers(t *testing.T) {
})
}
}

func TestTopLevelAwaitErrors(t *testing.T) {
t.Parallel()
data := `
const delay = (delayInms) => {
return new Promise(resolve => setTimeout(resolve, delayInms));
}
await delay(10).then(() => {something});
export default () => {}
`

_, err := getSimpleBundle(t, "/script.js", data)
require.ErrorContains(t, err, "ReferenceError: something is not defined")
}
10 changes: 10 additions & 0 deletions js/modules/require_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/grafana/sobek"

"go.k6.io/k6/loader"
)

Expand Down Expand Up @@ -47,6 +48,7 @@ func (ms *ModuleSystem) Require(specifier string) (*sobek.Object, error) {
} else {
panic(fmt.Sprintf("expected sobek.CyclicModuleRecord, but for some reason got a %T", m))
}
promisesThenIgnore(rt, promise)
switch promise.State() {
case sobek.PromiseStateRejected:
err = promise.Result().Export().(error) //nolint:forcetypeassert
Expand Down Expand Up @@ -195,3 +197,11 @@ func getPreviousRequiringFile(vu VU) (string, error) {
}
return result, nil
}

// sets the provided promise in such way as to ignore falures
// this is mostly needed as failures are handled separately and we do not want those to lead to stopping the event loop
func promisesThenIgnore(rt *sobek.Runtime, promise *sobek.Promise) {
call, _ := sobek.AssertFunction(rt.ToValue(promise).ToObject(rt).Get("then"))
handler := rt.ToValue(func(_ sobek.Value) {})
_, _ = call(rt.ToValue(promise), handler, handler)
}
33 changes: 24 additions & 9 deletions js/modules/resolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,7 @@ func NewModuleSystem(resolver *ModuleResolver, vu VU) *ModuleSystem {
// RunSourceData runs the provided sourceData and adds it to the cache.
// If a module with the same specifier as the source is already cached
// it will be used instead of reevaluating the source from the provided SourceData.
//
// TODO: this API will likely change as native ESM support will likely not let us have the exports
// as one big sobek.Value that we can manipulate
func (ms *ModuleSystem) RunSourceData(source *loader.SourceData) (sobek.ModuleRecord, error) {
func (ms *ModuleSystem) RunSourceData(source *loader.SourceData) (*RunSourceDataResult, error) {
specifier := source.URL.String()
pwd := source.URL.JoinPath("../")
if _, err := ms.resolver.resolveLoaded(pwd, specifier, source.Data); err != nil {
Expand All @@ -256,14 +253,32 @@ func (ms *ModuleSystem) RunSourceData(source *loader.SourceData) (sobek.ModuleRe
}
rt := ms.vu.Runtime()
promise := rt.CyclicModuleRecordEvaluate(ci, ms.resolver.sobekModuleResolver)
switch promise.State() {

promisesThenIgnore(rt, promise)

return &RunSourceDataResult{
promise: promise,
mod: mod,
}, nil
}

// RunSourceDataResult helps with the asynchronous nature of ESM
// it wraps the promise that is returned from Sobek while at the same time allowing access to the module record
type RunSourceDataResult struct {
promise *sobek.Promise
mod sobek.ModuleRecord
}

// Result returns either the underlying module or error if the promise has been completed and true,
// or false if the promise still hasn't been completed
func (r *RunSourceDataResult) Result() (sobek.ModuleRecord, bool, error) {
switch r.promise.State() {
case sobek.PromiseStateRejected:
return nil, promise.Result().Export().(error) //nolint:forcetypeassert
return nil, true, r.promise.Result().Export().(error) //nolint:forcetypeassert
case sobek.PromiseStateFulfilled:
return mod, nil
return r.mod, true, nil
default:
// TODO(@mstoykov): this will require having some callbacks through the code, but should be doable, just not pretty
panic("TLA not supported in k6 at the moment")
return nil, false, nil
}
}

Expand Down
2 changes: 2 additions & 0 deletions js/tc39/breaking_test_errors-experimental_enhanced.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"test/language/module-code/instn-star-err-not-found.js-strict:true": "test/language/module-code/instn-star-err-not-found.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/parse-err-hoist-lex-fun.js-strict:true": "test/language/module-code/parse-err-hoist-lex-fun.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/parse-err-return.js-strict:true": "test/language/module-code/parse-err-return.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/top-level-await/new-await.js-strict:true": "test/language/module-code/top-level-await/new-await.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/top-level-await/no-operand.js-strict:true": "test/language/module-code/top-level-await/no-operand.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/reserved-words/await-module.js-strict:true": "test/language/reserved-words/await-module.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/statements/class/class-name-ident-await-escaped-module.js-strict:true": "test/language/statements/class/class-name-ident-await-escaped-module.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/statements/class/class-name-ident-await-module.js-strict:true": "test/language/statements/class/class-name-ident-await-module.js: error is not an object (Test262: This statement should not be evaluated.)",
Expand Down
2 changes: 2 additions & 0 deletions js/tc39/breaking_test_errors-extended.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"test/language/module-code/instn-star-err-not-found.js-strict:true": "test/language/module-code/instn-star-err-not-found.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/parse-err-hoist-lex-fun.js-strict:true": "test/language/module-code/parse-err-hoist-lex-fun.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/parse-err-return.js-strict:true": "test/language/module-code/parse-err-return.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/top-level-await/new-await.js-strict:true": "test/language/module-code/top-level-await/new-await.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/top-level-await/no-operand.js-strict:true": "test/language/module-code/top-level-await/no-operand.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/reserved-words/await-module.js-strict:true": "test/language/reserved-words/await-module.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/statements/class/class-name-ident-await-escaped-module.js-strict:true": "test/language/statements/class/class-name-ident-await-escaped-module.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/statements/class/class-name-ident-await-module.js-strict:true": "test/language/statements/class/class-name-ident-await-module.js: error is not an object (Test262: This statement should not be evaluated.)",
Expand Down
11 changes: 9 additions & 2 deletions js/tc39/tc39_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ var (
featuresBlockList = []string{
"IsHTMLDDA", // not supported at all
"async-iteration", // not supported at all
"top-level-await", // not supported at all
"String.prototype.replaceAll", // not supported at all, Stage 4 since 2020
"dynamic-import", // not support in k6

// from Sobek
"Symbol.asyncIterator",
Expand Down Expand Up @@ -737,10 +737,17 @@ func (ctx *tc39TestCtx) runTC39Module(name, src string, includes []string, vm *s
moduleRuntime.VU.InitEnvField.CWD = base

early = false
_, err = ms.RunSourceData(&loader.SourceData{
result, err := ms.RunSourceData(&loader.SourceData{
Data: []byte(src),
URL: u,
})
if err == nil {
var finished bool
_, finished, err = result.Result()
if !finished {
panic("tc39 has no tests where this should happen")
}
}

return early, err
}
Expand Down

0 comments on commit 3c56028

Please sign in to comment.