Skip to content

Commit

Permalink
WIP: top-level-await with error handling and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mstoykov committed Oct 22, 2024
1 parent 51378fb commit 667e0c5
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 11 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) {
cal, _ := sobek.AssertFunction(rt.ToValue(promise).ToObject(rt).Get("then"))
handler := rt.ToValue(func(_ sobek.Value) {})
_, _ = cal(rt.ToValue(promise), handler, handler)
}
32 changes: 24 additions & 8 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,13 +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:
return mod, nil
return nil, false, nil
}
}

Expand Down
9 changes: 8 additions & 1 deletion js/tc39/tc39_test.go
Original file line number Diff line number Diff line change
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
1 change: 1 addition & 0 deletions vendor/github.com/grafana/sobek/modules_sourcetext.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 667e0c5

Please sign in to comment.