From 794781e582f89ec88eed4f089cdc32c531e2940f Mon Sep 17 00:00:00 2001 From: Roger Chapman Date: Mon, 8 Mar 2021 10:50:29 +1100 Subject: [PATCH] Promise resolver and promise result (#76) * Promise resolver and promise result * update persistent value to match master * Add docs, test and Exception types * remove the exception error functions * add test for when value is not a promise * update changelog --- CHANGELOG.md | 3 + function_template_test.go | 24 ++++++-- promise.go | 87 ++++++++++++++++++++++++++++ promise_test.go | 47 +++++++++++++++ v8go.cc | 118 +++++++++++++++++++++++++++++++++++++- v8go.h | 13 +++++ value.go | 9 ++- value_test.go | 13 +++++ 8 files changed, 307 insertions(+), 7 deletions(-) create mode 100644 promise.go create mode 100644 promise_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8c5adc..2f4a4445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Promise resolver and promise result + ## [v0.5.1] - 2021-02-19 ### Fixed diff --git a/function_template_test.go b/function_template_test.go index 28a82107..d296e529 100644 --- a/function_template_test.go +++ b/function_template_test.go @@ -52,18 +52,32 @@ func ExampleFunctionTemplate() { func ExampleFunctionTemplate_fetch() { iso, _ := v8go.NewIsolate() global, _ := v8go.NewObjectTemplate(iso) + fetchfn, _ := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { args := info.Args() url := args[0].String() - res, _ := http.Get(url) - body, _ := ioutil.ReadAll(res.Body) - val, _ := v8go.NewValue(iso, string(body)) - return val + + resolver, _ := v8go.NewPromiseResolver(info.Context()) + + go func() { + res, _ := http.Get(url) + body, _ := ioutil.ReadAll(res.Body) + val, _ := v8go.NewValue(iso, string(body)) + resolver.Resolve(val) + }() + return resolver.GetPromise().Value }) global.Set("fetch", fetchfn, v8go.ReadOnly) + ctx, _ := v8go.NewContext(iso, global) val, _ := ctx.RunScript("fetch('https://rogchap.com/v8go')", "") - fmt.Printf("%s\n", strings.Split(val.String(), "\n")[0]) + prom, _ := val.AsPromise() + + // wait for the promise to resolve + for prom.State() == v8go.Pending { + continue + } + fmt.Printf("%s\n", strings.Split(prom.Result().String(), "\n")[0]) // Output: // } diff --git a/promise.go b/promise.go new file mode 100644 index 00000000..c1fae698 --- /dev/null +++ b/promise.go @@ -0,0 +1,87 @@ +// Copyright 2021 Roger Chapman and the v8go contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package v8go + +// #include +// #include "v8go.h" +import "C" +import ( + "errors" +) + +// PromiseState is the state of the Promise. +type PromiseState int + +const ( + Pending PromiseState = iota + Fulfilled + Rejected +) + +// PromiseResolver is the resolver object for the promise. +// Most cases will create a new PromiseResolver and return +// the associated Promise from the resolver. +type PromiseResolver struct { + *Object + prom *Promise +} + +// Promise is the JavaScript promise object defined in ES6 +type Promise struct { + *Object +} + +// MewPromiseResolver creates a new Promise resolver for the given context. +// The associated Promise will be in a Pending state. +func NewPromiseResolver(ctx *Context) (*PromiseResolver, error) { + if ctx == nil { + return nil, errors.New("v8go: Context is required") + } + ptr := C.NewPromiseResolver(ctx.ptr) + val := &Value{ptr, ctx} + return &PromiseResolver{&Object{val}, nil}, nil +} + +// GetPromise returns the associated Promise object for this resolver. +// The Promise object is unique to the resolver and returns the same object +// on multiple calls. +func (r *PromiseResolver) GetPromise() *Promise { + if r.prom == nil { + ptr := C.PromiseResolverGetPromise(r.ptr) + val := &Value{ptr, r.ctx} + r.prom = &Promise{&Object{val}} + } + return r.prom +} + +// Resolve invokes the Promise resolve state with the given value. +// The Promise state will transition from Pending to Fulfilled. +func (r *PromiseResolver) Resolve(val Valuer) bool { + r.ctx.register() + defer r.ctx.deregister() + return C.PromiseResolverResolve(r.ptr, val.value().ptr) != 0 +} + +// Reject invokes the Promise reject state with the given value. +// The Promise state will transition from Pending to Rejected. +func (r *PromiseResolver) Reject(err *Value) bool { + r.ctx.register() + defer r.ctx.deregister() + return C.PromiseResolverReject(r.ptr, err.ptr) != 0 +} + +// State returns the current state of the Promise. +func (p *Promise) State() PromiseState { + return PromiseState(C.PromiseState(p.ptr)) +} + +// Result is the value result of the Promise. The Promise must +// NOT be in a Pending state, otherwise may panic. Call promise.State() +// to validate state before calling for the result. +func (p *Promise) Result() *Value { + ptr := C.PromiseResult(p.ptr) + val := &Value{ptr, p.ctx} + return val +} diff --git a/promise_test.go b/promise_test.go new file mode 100644 index 00000000..1ad38ed8 --- /dev/null +++ b/promise_test.go @@ -0,0 +1,47 @@ +// Copyright 2021 Roger Chapman and the v8go contributors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package v8go_test + +import ( + "testing" + + "rogchap.com/v8go" +) + +func TestPromise(t *testing.T) { + t.Parallel() + + iso, _ := v8go.NewIsolate() + ctx, _ := v8go.NewContext(iso) + if _, err := v8go.NewPromiseResolver(nil); err == nil { + t.Error("expected error with Context") + } + + res1, _ := v8go.NewPromiseResolver(ctx) + prom1 := res1.GetPromise() + if s := prom1.State(); s != v8go.Pending { + t.Errorf("unexpected state for Promise, want Pending (0) got: %v", s) + } + + val1, _ := v8go.NewValue(iso, "foo") + res1.Resolve(val1) + + if s := prom1.State(); s != v8go.Fulfilled { + t.Fatalf("unexpected state for Promise, want Fulfilled (1) got: %v", s) + } + + if result := prom1.Result(); result.String() != val1.String() { + t.Errorf("expected the Promise result to match the resolve value, but got: %s", result) + } + + res2, _ := v8go.NewPromiseResolver(ctx) + val2, _ := v8go.NewValue(iso, "Bad Foo") + res2.Reject(val2) + + prom2 := res2.GetPromise() + if s := prom2.State(); s != v8go.Rejected { + t.Fatalf("unexpected state for Promise, want Rejected (2) got: %v", s) + } +} diff --git a/v8go.cc b/v8go.cc index e49fa9f6..64be3f3c 100644 --- a/v8go.cc +++ b/v8go.cc @@ -506,6 +506,7 @@ ValuePtr NewValueInteger(IsolatePtr iso_ptr, int32_t v) { ISOLATE_SCOPE(iso_ptr); m_value* val = new m_value; val->iso = iso; + val->ctx = nullptr; val->ptr = Persistent>( iso, Integer::New(iso, v)); return static_cast(val); @@ -515,6 +516,7 @@ ValuePtr NewValueIntegerFromUnsigned(IsolatePtr iso_ptr, uint32_t v) { ISOLATE_SCOPE(iso_ptr); m_value* val = new m_value; val->iso = iso; + val->ctx = nullptr; val->ptr = Persistent>( iso, Integer::NewFromUnsigned(iso, v)); return static_cast(val); @@ -524,6 +526,7 @@ ValuePtr NewValueString(IsolatePtr iso_ptr, const char* v) { ISOLATE_SCOPE(iso_ptr); m_value* val = new m_value; val->iso = iso; + val->ctx = nullptr; val->ptr = Persistent>( iso, String::NewFromUtf8(iso, v).ToLocalChecked()); return static_cast(val); @@ -533,6 +536,7 @@ ValuePtr NewValueBoolean(IsolatePtr iso_ptr, int v) { ISOLATE_SCOPE(iso_ptr); m_value* val = new m_value; val->iso = iso; + val->ctx = nullptr; val->ptr = Persistent>( iso, Boolean::New(iso, v)); return static_cast(val); @@ -542,6 +546,7 @@ ValuePtr NewValueNumber(IsolatePtr iso_ptr, double v) { ISOLATE_SCOPE(iso_ptr); m_value* val = new m_value; val->iso = iso; + val->ctx = nullptr; val->ptr = Persistent>( iso, Number::New(iso, v)); return static_cast(val); @@ -551,6 +556,7 @@ ValuePtr NewValueBigInt(IsolatePtr iso_ptr, int64_t v) { ISOLATE_SCOPE(iso_ptr); m_value* val = new m_value; val->iso = iso; + val->ctx = nullptr; val->ptr = Persistent>( iso, BigInt::New(iso, v)); return static_cast(val); @@ -560,6 +566,7 @@ ValuePtr NewValueBigIntFromUnsigned(IsolatePtr iso_ptr, uint64_t v) { ISOLATE_SCOPE(iso_ptr); m_value* val = new m_value; val->iso = iso; + val->ctx = nullptr; val->ptr = Persistent>( iso, BigInt::NewFromUnsigned(iso, v)); return static_cast(val); @@ -574,6 +581,7 @@ ValuePtr NewValueBigIntFromWords(IsolatePtr iso_ptr, m_value* val = new m_value; val->iso = iso; + val->ctx = nullptr; MaybeLocal bigint = BigInt::NewFromWords(ctx->ptr.Get(iso), sign_bit, word_count, words); val->ptr = Persistent>( @@ -968,7 +976,7 @@ RtnValue ObjectGet(ValuePtr ptr, const char* key) { m_value* new_val = new m_value; new_val->iso = iso; new_val->ctx = ctx; - new_val->ptr.Reset(iso, Persistent(iso, result.ToLocalChecked())); + new_val->ptr = Persistent>(iso, result.ToLocalChecked()); rtn.value = tracked_value(ctx, new_val); return rtn; @@ -1017,6 +1025,114 @@ int ObjectDeleteIdx(ValuePtr ptr, uint32_t idx) { return obj->Delete(local_ctx, idx).ToChecked(); } +/********** Promise **********/ + +ValuePtr NewPromiseResolver(ContextPtr ctx_ptr) { + LOCAL_CONTEXT(ctx_ptr); + MaybeLocal resolver = Promise::Resolver::New(local_ctx); + m_value* val = new m_value; + val->iso = iso; + val->ctx = ctx; + val->ptr = Persistent>(iso, resolver.ToLocalChecked()); + return tracked_value(ctx, val); +} + +ValuePtr PromiseResolverGetPromise(ValuePtr ptr) { + LOCAL_VALUE(ptr); + Local resolver = value.As(); + Local promise = resolver->GetPromise(); + 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); +} + +int PromiseResolverResolve(ValuePtr ptr, ValuePtr val_ptr) { + LOCAL_VALUE(ptr); + Local resolver = value.As(); + m_value* resolve_val = static_cast(val_ptr); + return resolver->Resolve(local_ctx, resolve_val->ptr.Get(iso)).ToChecked(); +} + +int PromiseResolverReject(ValuePtr ptr, ValuePtr val_ptr) { + LOCAL_VALUE(ptr); + Local resolver = value.As(); + m_value* reject_val = static_cast(val_ptr); + return resolver->Reject(local_ctx, reject_val->ptr.Get(iso)).ToChecked(); +} + +int PromiseState(ValuePtr ptr) { + LOCAL_VALUE(ptr) + Local promise = value.As(); + return promise->State(); +} + +ValuePtr PromiseResult(ValuePtr ptr) { + LOCAL_VALUE(ptr) + Local promise = value.As(); + Local result = promise->Result(); + m_value* result_val = new m_value; + result_val->iso = iso; + result_val->ctx = ctx; + result_val->ptr = Persistent>(iso, result); + return tracked_value(ctx, result_val); +} + +/******** Exceptions *********/ + +ValuePtr ExceptionError(IsolatePtr iso_ptr, const char* message) { + ISOLATE_SCOPE(iso_ptr); + Local msg = String::NewFromUtf8(iso, message).ToLocalChecked(); + m_value* val = new m_value; + val->iso = iso; + val->ctx = nullptr; + // TODO(rogchap): This currently causes a segfault, and I'm not sure why! + // Even a simple error with an empty string causes the error: Exception::Error(String::Empty(iso)) + val->ptr = Persistent>(iso, Exception::Error(msg)); + return static_cast(val); +} + +ValuePtr ExceptionRangeError(IsolatePtr iso_ptr, const char* message) { + ISOLATE_SCOPE(iso_ptr); + Local msg = String::NewFromUtf8(iso, message, NewStringType::kNormal).ToLocalChecked(); + m_value* val = new m_value; + val->iso = iso; + val->ctx = nullptr; + val->ptr = Persistent>(iso, Exception::RangeError(msg)); + return static_cast(val); +} + +ValuePtr ExceptionReferenceError(IsolatePtr iso_ptr, const char* message) { + ISOLATE_SCOPE(iso_ptr); + Local msg = String::NewFromUtf8(iso, message, NewStringType::kNormal).ToLocalChecked(); + m_value* val = new m_value; + val->iso = iso; + val->ctx = nullptr; + val->ptr = Persistent>(iso, Exception::ReferenceError(msg)); + return static_cast(val); +} + +ValuePtr ExceptionSyntaxError(IsolatePtr iso_ptr, const char* message) { + ISOLATE_SCOPE(iso_ptr); + Local msg = String::NewFromUtf8(iso, message, NewStringType::kNormal).ToLocalChecked(); + m_value* val = new m_value; + val->iso = iso; + val->ctx = nullptr; + val->ptr = Persistent>(iso, Exception::SyntaxError(msg)); + return static_cast(val); +} + +ValuePtr ExceptionTypeError(IsolatePtr iso_ptr, const char* message) { + ISOLATE_SCOPE(iso_ptr); + Local msg = String::NewFromUtf8(iso, message, NewStringType::kNormal).ToLocalChecked(); + m_value* val = new m_value; + val->iso = iso; + val->ctx = nullptr; + val->ptr = Persistent>(iso, Exception::TypeError(msg)); + return static_cast(val); +} + /********** Version **********/ const char* Version() { diff --git a/v8go.h b/v8go.h index 1129a6a5..b01975fc 100644 --- a/v8go.h +++ b/v8go.h @@ -166,6 +166,19 @@ int ObjectHasIdx(ValuePtr ptr, uint32_t idx); int ObjectDelete(ValuePtr ptr, const char* key); int ObjectDeleteIdx(ValuePtr ptr, uint32_t idx); +extern ValuePtr NewPromiseResolver(ContextPtr ctx_ptr); +extern ValuePtr PromiseResolverGetPromise(ValuePtr ptr); +int PromiseResolverResolve(ValuePtr ptr, ValuePtr val_ptr); +int PromiseResolverReject(ValuePtr ptr, ValuePtr val_ptr); +int PromiseState(ValuePtr ptr); +extern ValuePtr PromiseResult(ValuePtr ptr); + +extern ValuePtr ExceptionError(IsolatePtr iso_ptr, const char* message); +extern ValuePtr ExceptionRangeError(IsolatePtr iso_ptr, const char* message); +extern ValuePtr ExceptionReferenceError(IsolatePtr iso_ptr, const char* message); +extern ValuePtr ExceptionSyntaxError(IsolatePtr iso_ptr, const char* message); +extern ValuePtr ExceptionTypeError(IsolatePtr iso_ptr, const char* message); + const char* Version(); #ifdef __cplusplus diff --git a/value.go b/value.go index a02fc911..d98e9104 100644 --- a/value.go +++ b/value.go @@ -517,12 +517,19 @@ func (v *Value) IsModuleNamespaceObject() bool { // then an error is returned. Use `value.Object()` to do the JS equivalent of `Object(value)`. func (v *Value) AsObject() (*Object, error) { if !v.IsObject() { - return nil, errors.New("value: unable to cast to Object; value is not an Object") + return nil, errors.New("v8go: value is not an Object") } return &Object{v}, nil } +func (v *Value) AsPromise() (*Promise, error) { + if !v.IsPromise() { + return nil, errors.New("v8go: value is not a Promise") + } + return &Promise{&Object{v}}, nil +} + func (v *Value) finalizer() { C.ValueFree(v.ptr) v.ptr = nil diff --git a/value_test.go b/value_test.go index ae966a8c..37fbd73f 100644 --- a/value_test.go +++ b/value_test.go @@ -364,6 +364,19 @@ func TestValueObject(t *testing.T) { if obj := val.Object(); obj.String() != "1" { t.Errorf("unexpected object value: %v", obj) } +} + +func TestValuePromise(t *testing.T) { + t.Parallel() + + ctx, _ := v8go.NewContext() + val, _ := ctx.RunScript("1", "") + if _, err := val.AsPromise(); err == nil { + t.Error("Expected error but got ") + } + if _, err := ctx.RunScript("new Promise(()=>{})", ""); err != nil { + t.Errorf("Unexpected error: %v", err) + } }