From db20bc57a8951985d546de044adea53ba4c762c4 Mon Sep 17 00:00:00 2001 From: Rob Figueiredo Date: Thu, 6 May 2021 17:11:55 -0400 Subject: [PATCH] promise: run callbacks on resolution (#118) --- context.go | 8 +++++ promise.go | 38 +++++++++++++++++++++++ promise_test.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++- v8go.cc | 53 ++++++++++++++++++++++++++++++++ v8go.h | 4 +++ 5 files changed, 184 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index 8da3366a..a4190091 100644 --- a/context.go +++ b/context.go @@ -117,6 +117,14 @@ func (c *Context) Global() *Object { return &Object{v} } +// PerformMicrotaskCheckpoint runs the default MicrotaskQueue until empty. +// This is used to make progress on Promises. +func (c *Context) PerformMicrotaskCheckpoint() { + c.register() + defer c.deregister() + C.IsolatePerformMicrotaskCheckpoint(c.iso.ptr) +} + // Close will dispose the context and free the memory. // Access to any values assosiated with the context after calling Close may panic. func (c *Context) Close() { diff --git a/promise.go b/promise.go index c1fae698..7ab63dd3 100644 --- a/promise.go +++ b/promise.go @@ -85,3 +85,41 @@ func (p *Promise) Result() *Value { val := &Value{ptr, p.ctx} return val } + +// Then accepts 1 or 2 callbacks. +// The first is invoked when the promise has been fulfilled. +// The second is invoked when the promise has been rejected. +// The returned Promise resolves after the callback finishes execution. +// +// V8 only invokes the callback when processing "microtasks". +// The default MicrotaskPolicy processes them when the call depth decreases to 0. +// Call (*Context).PerformMicrotaskCheckpoint to trigger it manually. +func (p *Promise) Then(cbs ...FunctionCallback) *Promise { + p.ctx.register() + defer p.ctx.deregister() + + var ptr C.ValuePtr + switch len(cbs) { + case 1: + cbID := p.ctx.iso.registerCallback(cbs[0]) + ptr = C.PromiseThen(p.ptr, C.int(cbID)) + case 2: + cbID1 := p.ctx.iso.registerCallback(cbs[0]) + cbID2 := p.ctx.iso.registerCallback(cbs[1]) + ptr = C.PromiseThen2(p.ptr, C.int(cbID1), C.int(cbID2)) + + default: + panic("1 or 2 callbacks required") + } + return &Promise{&Object{&Value{ptr, p.ctx}}} +} + +// Catch invokes the given function if the promise is rejected. +// See Then for other details. +func (p *Promise) Catch(cb FunctionCallback) *Promise { + p.ctx.register() + defer p.ctx.deregister() + cbID := p.ctx.iso.registerCallback(cb) + ptr := C.PromiseCatch(p.ptr, C.int(cbID)) + return &Promise{&Object{&Value{ptr, p.ctx}}} +} diff --git a/promise_test.go b/promise_test.go index 1ad38ed8..2f5b1ec3 100644 --- a/promise_test.go +++ b/promise_test.go @@ -10,7 +10,7 @@ import ( "rogchap.com/v8go" ) -func TestPromise(t *testing.T) { +func TestPromiseFulfilled(t *testing.T) { t.Parallel() iso, _ := v8go.NewIsolate() @@ -25,6 +25,19 @@ func TestPromise(t *testing.T) { t.Errorf("unexpected state for Promise, want Pending (0) got: %v", s) } + var thenInfo *v8go.FunctionCallbackInfo + prom1thenVal := prom1.Then(func(info *v8go.FunctionCallbackInfo) *v8go.Value { + thenInfo = info + return nil + }) + prom1then, _ := prom1thenVal.AsPromise() + if prom1then.State() != v8go.Pending { + t.Errorf("unexpected state for dependent Promise, want Pending got: %v", prom1then.State()) + } + if thenInfo != nil { + t.Error("unexpected call of Then prior to resolving the promise") + } + val1, _ := v8go.NewValue(iso, "foo") res1.Resolve(val1) @@ -36,6 +49,20 @@ func TestPromise(t *testing.T) { t.Errorf("expected the Promise result to match the resolve value, but got: %s", result) } + if thenInfo == nil { + t.Errorf("expected Then to be called, was not") + } + if len(thenInfo.Args()) != 1 || thenInfo.Args()[0].String() != "foo" { + t.Errorf("expected promise to be called with [foo] args, was: %+v", thenInfo.Args()) + } +} + +func TestPromiseRejected(t *testing.T) { + t.Parallel() + + iso, _ := v8go.NewIsolate() + ctx, _ := v8go.NewContext(iso) + res2, _ := v8go.NewPromiseResolver(ctx) val2, _ := v8go.NewValue(iso, "Bad Foo") res2.Reject(val2) @@ -44,4 +71,57 @@ func TestPromise(t *testing.T) { if s := prom2.State(); s != v8go.Rejected { t.Fatalf("unexpected state for Promise, want Rejected (2) got: %v", s) } + + var thenInfo *v8go.FunctionCallbackInfo + var then2Fulfilled, then2Rejected bool + prom2. + Catch(func(info *v8go.FunctionCallbackInfo) *v8go.Value { + thenInfo = info + return nil + }). + Then( + func(_ *v8go.FunctionCallbackInfo) *v8go.Value { + then2Fulfilled = true + return nil + }, + func(_ *v8go.FunctionCallbackInfo) *v8go.Value { + then2Rejected = true + return nil + }, + ) + ctx.PerformMicrotaskCheckpoint() + if thenInfo == nil { + t.Fatalf("expected Then to be called on already-resolved promise, but was not") + } + if len(thenInfo.Args()) != 1 || thenInfo.Args()[0].String() != val2.String() { + t.Fatalf("expected [%v], was: %+v", val2, thenInfo.Args()) + } + + if then2Fulfilled { + t.Fatalf("unexpectedly called onFulfilled") + } + if !then2Rejected { + t.Fatalf("expected call to onRejected, got none") + } +} + +func TestPromiseThenPanic(t *testing.T) { + t.Parallel() + + iso, _ := v8go.NewIsolate() + ctx, _ := v8go.NewContext(iso) + res, _ := v8go.NewPromiseResolver(ctx) + prom := res.GetPromise() + + t.Run("no callbacks", func(t *testing.T) { + defer func() { recover() }() + prom.Then() + t.Errorf("expected a panic") + }) + t.Run("3 callbacks", func(t *testing.T) { + defer func() { recover() }() + fn := func(_ *v8go.FunctionCallbackInfo) *v8go.Value { return nil } + prom.Then(fn, fn, fn) + t.Errorf("expected a panic") + }) } diff --git a/v8go.cc b/v8go.cc index d25568f4..df4562cc 100644 --- a/v8go.cc +++ b/v8go.cc @@ -160,6 +160,11 @@ IsolatePtr NewIsolate() { return static_cast(iso); } +void IsolatePerformMicrotaskCheckpoint(IsolatePtr ptr) { + ISOLATE_SCOPE(ptr) + iso->PerformMicrotaskCheckpoint(); +} + void IsolateDispose(IsolatePtr ptr) { if (ptr == nullptr) { return; @@ -1077,6 +1082,54 @@ int PromiseState(ValuePtr ptr) { return promise->State(); } +ValuePtr PromiseThen(ValuePtr ptr, int callback_ref) { + LOCAL_VALUE(ptr) + Local promise = value.As(); + Local cbData = Integer::New(iso, callback_ref); + Local func = Function::New(local_ctx, FunctionTemplateCallback, cbData) + .ToLocalChecked(); + Local result = promise->Then(local_ctx, func).ToLocalChecked(); + m_value* promise_val = new m_value; + promise_val->iso = iso; + promise_val->ctx = ctx; + promise_val->ptr = + Persistent>(iso, promise); + return tracked_value(ctx, promise_val); +} + +ValuePtr PromiseThen2(ValuePtr ptr, int on_fulfilled_ref, int on_rejected_ref) { + LOCAL_VALUE(ptr) + Local promise = value.As(); + Local onFulfilledData = Integer::New(iso, on_fulfilled_ref); + Local onFulfilledFunc = Function::New(local_ctx, FunctionTemplateCallback, onFulfilledData) + .ToLocalChecked(); + Local onRejectedData = Integer::New(iso, on_rejected_ref); + Local onRejectedFunc = Function::New(local_ctx, FunctionTemplateCallback, onRejectedData) + .ToLocalChecked(); + Local result = promise->Then(local_ctx, onFulfilledFunc, onRejectedFunc).ToLocalChecked(); + m_value* promise_val = new m_value; + promise_val->iso = iso; + promise_val->ctx = ctx; + promise_val->ptr = + Persistent>(iso, promise); + return tracked_value(ctx, promise_val); +} + +ValuePtr PromiseCatch(ValuePtr ptr, int callback_ref) { + LOCAL_VALUE(ptr) + Local promise = value.As(); + Local cbData = Integer::New(iso, callback_ref); + Local func = Function::New(local_ctx, FunctionTemplateCallback, cbData) + .ToLocalChecked(); + Local result = promise->Catch(local_ctx, func).ToLocalChecked(); + m_value* promise_val = new m_value; + promise_val->iso = iso; + promise_val->ctx = ctx; + promise_val->ptr = + Persistent>(iso, promise); + return tracked_value(ctx, promise_val); +} + ValuePtr PromiseResult(ValuePtr ptr) { LOCAL_VALUE(ptr) Local promise = value.As(); diff --git a/v8go.h b/v8go.h index f7f2df98..638121a3 100644 --- a/v8go.h +++ b/v8go.h @@ -50,6 +50,7 @@ typedef struct { extern void Init(); extern IsolatePtr NewIsolate(); +extern void IsolatePerformMicrotaskCheckpoint(IsolatePtr ptr); extern void IsolateDispose(IsolatePtr ptr); extern void IsolateTerminateExecution(IsolatePtr ptr); extern IsolateHStatistics IsolationGetHeapStatistics(IsolatePtr ptr); @@ -171,6 +172,9 @@ extern ValuePtr PromiseResolverGetPromise(ValuePtr ptr); int PromiseResolverResolve(ValuePtr ptr, ValuePtr val_ptr); int PromiseResolverReject(ValuePtr ptr, ValuePtr val_ptr); int PromiseState(ValuePtr ptr); +ValuePtr PromiseThen(ValuePtr ptr, int callback_ref); +ValuePtr PromiseThen2(ValuePtr ptr, int on_fulfilled_ref, int on_rejected_ref); +ValuePtr PromiseCatch(ValuePtr ptr, int callback_ref); extern ValuePtr PromiseResult(ValuePtr ptr); extern RtnValue FunctionCall(ValuePtr ptr, int argc, ValuePtr argv[]);