Explorar o código

Promise resolve and reject now return Interrupt errors (#624)

* Promise resolve and reject now return Interrupt errors

This is so that it can be handled at all.

Previously the error was just not propagated which lead to interrupts
not really working with asynchronous code.

Fixes #623

* Apply suggestions from code review

Co-authored-by: Dmitry Panov <[email protected]>

* fixup! Promise resolve and reject now return Interrupt errors

---------

Co-authored-by: Dmitry Panov <[email protected]>
Mihail Stoykov hai 11 meses
pai
achega
79f3a7efcd
Modificáronse 2 ficheiros con 46 adicións e 5 borrados
  1. 9 5
      builtin_promise.go
  2. 37 0
      runtime_test.go

+ 9 - 5
builtin_promise.go

@@ -595,14 +595,17 @@ func (r *Runtime) getPromise() *Object {
 	return ret
 }
 
-func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) {
+func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) error {
 	f, _ := AssertFunction(fObj)
-	return func(x interface{}) {
-		_, _ = f(nil, r.ToValue(x))
+	return func(x interface{}) error {
+		_, err := f(nil, r.ToValue(x))
+		return err
 	}
 }
 
 // NewPromise creates and returns a Promise and resolving functions for it.
+// The returned errors will be uncatchable errors, such as InterruptedError or StackOverflowError, which should be propagated upwards.
+// Exceptions are handled through [PromiseRejectionTracker].
 //
 // WARNING: The returned values are not goroutine-safe and must not be called in parallel with VM running.
 // In order to make use of this method you need an event loop such as the one in goja_nodejs (https://github.com/dop251/goja_nodejs)
@@ -617,11 +620,12 @@ func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) {
 //	    go func() {
 //	        time.Sleep(500 * time.Millisecond)   // or perform any other blocking operation
 //	        loop.RunOnLoop(func(*goja.Runtime) { // resolve() must be called on the loop, cannot call it here
-//	            resolve(result)
+//	            err := resolve(result)
+//	            // Handle uncatchable errors (e.g. by stopping the loop, panicking or setting a flag)
 //	        })
 //	    }()
 //	}
-func (r *Runtime) NewPromise() (promise *Promise, resolve func(result interface{}), reject func(reason interface{})) {
+func (r *Runtime) NewPromise() (promise *Promise, resolve, reject func(reason interface{}) error) {
 	p := r.newPromise(r.getPromisePrototype())
 	resolveF, rejectF := p.createResolvingFunctions()
 	return p, r.wrapPromiseReaction(resolveF), r.wrapPromiseReaction(rejectF)

+ 37 - 0
runtime_test.go

@@ -1792,6 +1792,43 @@ func TestInterruptInWrappedFunctionExpectStackOverflowError(t *testing.T) {
 	}
 }
 
+func TestInterruptWithPromises(t *testing.T) {
+	rt := New()
+	rt.SetMaxCallStackSize(5)
+	// this test panics as otherwise goja will recover and possibly loop
+	rt.Set("abort", rt.ToValue(func() {
+		// panic("waty")
+		rt.Interrupt("abort this")
+	}))
+	var queue = make(chan func() error, 10)
+	rt.Set("myPromise", func() Value {
+		p, resolve, _ := rt.NewPromise()
+		queue <- func() error {
+			return resolve("some value")
+		}
+
+		return rt.ToValue(p)
+	})
+
+	_, err := rt.RunString(`
+		let p = myPromise()
+		p.then(() => { abort() });
+	`)
+	if err != nil {
+		t.Fatal("expected noerror but got error")
+	}
+	f := <-queue
+	err = f()
+	if err == nil {
+		t.Fatal("expected error but got no error")
+	}
+	t.Log(err)
+	var soErr *InterruptedError
+	if !errors.As(err, &soErr) {
+		t.Fatalf("Wrong error type: %T", err)
+	}
+}
+
 func TestRunLoopPreempt(t *testing.T) {
 	vm := New()
 	v, err := vm.RunString("(function() {for (;;) {}})")