Skip to content

Commit

Permalink
promise: run callbacks on resolution (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob Figueiredo authored May 6, 2021
1 parent 4918644 commit db20bc5
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 1 deletion.
8 changes: 8 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
38 changes: 38 additions & 0 deletions promise.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}}
}
82 changes: 81 additions & 1 deletion promise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"rogchap.com/v8go"
)

func TestPromise(t *testing.T) {
func TestPromiseFulfilled(t *testing.T) {
t.Parallel()

iso, _ := v8go.NewIsolate()
Expand All @@ -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)

Expand All @@ -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)
Expand All @@ -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")
})
}
53 changes: 53 additions & 0 deletions v8go.cc
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ IsolatePtr NewIsolate() {
return static_cast<IsolatePtr>(iso);
}

void IsolatePerformMicrotaskCheckpoint(IsolatePtr ptr) {
ISOLATE_SCOPE(ptr)
iso->PerformMicrotaskCheckpoint();
}

void IsolateDispose(IsolatePtr ptr) {
if (ptr == nullptr) {
return;
Expand Down Expand Up @@ -1077,6 +1082,54 @@ int PromiseState(ValuePtr ptr) {
return promise->State();
}

ValuePtr PromiseThen(ValuePtr ptr, int callback_ref) {
LOCAL_VALUE(ptr)
Local<Promise> promise = value.As<Promise>();
Local<Integer> cbData = Integer::New(iso, callback_ref);
Local<Function> func = Function::New(local_ctx, FunctionTemplateCallback, cbData)
.ToLocalChecked();
Local<Promise> 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<Value, CopyablePersistentTraits<Value>>(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> promise = value.As<Promise>();
Local<Integer> onFulfilledData = Integer::New(iso, on_fulfilled_ref);
Local<Function> onFulfilledFunc = Function::New(local_ctx, FunctionTemplateCallback, onFulfilledData)
.ToLocalChecked();
Local<Integer> onRejectedData = Integer::New(iso, on_rejected_ref);
Local<Function> onRejectedFunc = Function::New(local_ctx, FunctionTemplateCallback, onRejectedData)
.ToLocalChecked();
Local<Promise> 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<Value, CopyablePersistentTraits<Value>>(iso, promise);
return tracked_value(ctx, promise_val);
}

ValuePtr PromiseCatch(ValuePtr ptr, int callback_ref) {
LOCAL_VALUE(ptr)
Local<Promise> promise = value.As<Promise>();
Local<Integer> cbData = Integer::New(iso, callback_ref);
Local<Function> func = Function::New(local_ctx, FunctionTemplateCallback, cbData)
.ToLocalChecked();
Local<Promise> 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<Value, CopyablePersistentTraits<Value>>(iso, promise);
return tracked_value(ctx, promise_val);
}

ValuePtr PromiseResult(ValuePtr ptr) {
LOCAL_VALUE(ptr)
Local<Promise> promise = value.As<Promise>();
Expand Down
4 changes: 4 additions & 0 deletions v8go.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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[]);
Expand Down

0 comments on commit db20bc5

Please sign in to comment.