123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 |
- package goja
- import (
- "github.com/dop251/goja/unistring"
- "reflect"
- )
- type PromiseState int
- type PromiseRejectionOperation int
- type promiseReactionType int
- const (
- PromiseStatePending PromiseState = iota
- PromiseStateFulfilled
- PromiseStateRejected
- )
- const (
- PromiseRejectionReject PromiseRejectionOperation = iota
- PromiseRejectionHandle
- )
- const (
- promiseReactionFulfill promiseReactionType = iota
- promiseReactionReject
- )
- type PromiseRejectionTracker func(p *Promise, operation PromiseRejectionOperation)
- type jobCallback struct {
- callback func(FunctionCall) Value
- }
- type promiseCapability struct {
- promise *Object
- resolveObj, rejectObj *Object
- }
- type promiseReaction struct {
- capability *promiseCapability
- typ promiseReactionType
- handler *jobCallback
- asyncRunner *asyncRunner
- asyncCtx interface{}
- }
- var typePromise = reflect.TypeOf((*Promise)(nil))
- // Promise is a Go wrapper around ECMAScript Promise. Calling Runtime.ToValue() on it
- // returns the underlying Object. Calling Export() on a Promise Object returns a Promise.
- //
- // Use Runtime.NewPromise() to create one. Calling Runtime.ToValue() on a zero object or nil returns null Value.
- //
- // WARNING: Instances of Promise are not goroutine-safe. See Runtime.NewPromise() for more details.
- type Promise struct {
- baseObject
- state PromiseState
- result Value
- fulfillReactions []*promiseReaction
- rejectReactions []*promiseReaction
- handled bool
- }
- func (p *Promise) State() PromiseState {
- return p.state
- }
- func (p *Promise) Result() Value {
- return p.result
- }
- func (p *Promise) toValue(r *Runtime) Value {
- if p == nil || p.val == nil {
- return _null
- }
- promise := p.val
- if promise.runtime != r {
- panic(r.NewTypeError("Illegal runtime transition of a Promise"))
- }
- return promise
- }
- func (p *Promise) createResolvingFunctions() (resolve, reject *Object) {
- r := p.val.runtime
- alreadyResolved := false
- return p.val.runtime.newNativeFunc(func(call FunctionCall) Value {
- if alreadyResolved {
- return _undefined
- }
- alreadyResolved = true
- resolution := call.Argument(0)
- if resolution.SameAs(p.val) {
- return p.reject(r.NewTypeError("Promise self-resolution"))
- }
- if obj, ok := resolution.(*Object); ok {
- var thenAction Value
- ex := r.vm.try(func() {
- thenAction = obj.self.getStr("then", nil)
- })
- if ex != nil {
- return p.reject(ex.val)
- }
- if call, ok := assertCallable(thenAction); ok {
- job := r.newPromiseResolveThenableJob(p, resolution, &jobCallback{callback: call})
- r.enqueuePromiseJob(job)
- return _undefined
- }
- }
- return p.fulfill(resolution)
- }, "", 1),
- p.val.runtime.newNativeFunc(func(call FunctionCall) Value {
- if alreadyResolved {
- return _undefined
- }
- alreadyResolved = true
- reason := call.Argument(0)
- return p.reject(reason)
- }, "", 1)
- }
- func (p *Promise) reject(reason Value) Value {
- reactions := p.rejectReactions
- p.result = reason
- p.fulfillReactions, p.rejectReactions = nil, nil
- p.state = PromiseStateRejected
- r := p.val.runtime
- if !p.handled {
- r.trackPromiseRejection(p, PromiseRejectionReject)
- }
- r.triggerPromiseReactions(reactions, reason)
- return _undefined
- }
- func (p *Promise) fulfill(value Value) Value {
- reactions := p.fulfillReactions
- p.result = value
- p.fulfillReactions, p.rejectReactions = nil, nil
- p.state = PromiseStateFulfilled
- p.val.runtime.triggerPromiseReactions(reactions, value)
- return _undefined
- }
- func (p *Promise) exportType() reflect.Type {
- return typePromise
- }
- func (p *Promise) export(*objectExportCtx) interface{} {
- return p
- }
- func (p *Promise) addReactions(fulfillReaction *promiseReaction, rejectReaction *promiseReaction) {
- r := p.val.runtime
- if tracker := r.asyncContextTracker; tracker != nil {
- ctx := tracker.Grab()
- fulfillReaction.asyncCtx = ctx
- rejectReaction.asyncCtx = ctx
- }
- switch p.state {
- case PromiseStatePending:
- p.fulfillReactions = append(p.fulfillReactions, fulfillReaction)
- p.rejectReactions = append(p.rejectReactions, rejectReaction)
- case PromiseStateFulfilled:
- r.enqueuePromiseJob(r.newPromiseReactionJob(fulfillReaction, p.result))
- default:
- reason := p.result
- if !p.handled {
- r.trackPromiseRejection(p, PromiseRejectionHandle)
- }
- r.enqueuePromiseJob(r.newPromiseReactionJob(rejectReaction, reason))
- }
- p.handled = true
- }
- func (r *Runtime) newPromiseResolveThenableJob(p *Promise, thenable Value, then *jobCallback) func() {
- return func() {
- resolve, reject := p.createResolvingFunctions()
- ex := r.vm.try(func() {
- r.callJobCallback(then, thenable, resolve, reject)
- })
- if ex != nil {
- if fn, ok := reject.self.assertCallable(); ok {
- fn(FunctionCall{Arguments: []Value{ex.val}})
- }
- }
- }
- }
- func (r *Runtime) enqueuePromiseJob(job func()) {
- r.jobQueue = append(r.jobQueue, job)
- }
- func (r *Runtime) triggerPromiseReactions(reactions []*promiseReaction, argument Value) {
- for _, reaction := range reactions {
- r.enqueuePromiseJob(r.newPromiseReactionJob(reaction, argument))
- }
- }
- func (r *Runtime) newPromiseReactionJob(reaction *promiseReaction, argument Value) func() {
- return func() {
- var handlerResult Value
- fulfill := false
- if reaction.handler == nil {
- handlerResult = argument
- if reaction.typ == promiseReactionFulfill {
- fulfill = true
- }
- } else {
- if tracker := r.asyncContextTracker; tracker != nil {
- tracker.Resumed(reaction.asyncCtx)
- }
- ex := r.vm.try(func() {
- handlerResult = r.callJobCallback(reaction.handler, _undefined, argument)
- fulfill = true
- })
- if ex != nil {
- handlerResult = ex.val
- }
- if tracker := r.asyncContextTracker; tracker != nil {
- tracker.Exited()
- }
- }
- if reaction.capability != nil {
- if fulfill {
- reaction.capability.resolve(handlerResult)
- } else {
- reaction.capability.reject(handlerResult)
- }
- }
- }
- }
- func (r *Runtime) newPromise(proto *Object) *Promise {
- o := &Object{runtime: r}
- po := &Promise{}
- po.class = classObject
- po.val = o
- po.extensible = true
- o.self = po
- po.prototype = proto
- po.init()
- return po
- }
- func (r *Runtime) builtin_newPromise(args []Value, newTarget *Object) *Object {
- if newTarget == nil {
- panic(r.needNew("Promise"))
- }
- var arg0 Value
- if len(args) > 0 {
- arg0 = args[0]
- }
- executor := r.toCallable(arg0)
- proto := r.getPrototypeFromCtor(newTarget, r.global.Promise, r.getPromisePrototype())
- po := r.newPromise(proto)
- resolve, reject := po.createResolvingFunctions()
- ex := r.vm.try(func() {
- executor(FunctionCall{Arguments: []Value{resolve, reject}})
- })
- if ex != nil {
- if fn, ok := reject.self.assertCallable(); ok {
- fn(FunctionCall{Arguments: []Value{ex.val}})
- }
- }
- return po.val
- }
- func (r *Runtime) promiseProto_then(call FunctionCall) Value {
- thisObj := r.toObject(call.This)
- if p, ok := thisObj.self.(*Promise); ok {
- c := r.speciesConstructorObj(thisObj, r.getPromise())
- resultCapability := r.newPromiseCapability(c)
- return r.performPromiseThen(p, call.Argument(0), call.Argument(1), resultCapability)
- }
- panic(r.NewTypeError("Method Promise.prototype.then called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj})))
- }
- func (r *Runtime) newPromiseCapability(c *Object) *promiseCapability {
- pcap := new(promiseCapability)
- if c == r.getPromise() {
- p := r.newPromise(r.getPromisePrototype())
- pcap.resolveObj, pcap.rejectObj = p.createResolvingFunctions()
- pcap.promise = p.val
- } else {
- var resolve, reject Value
- executor := r.newNativeFunc(func(call FunctionCall) Value {
- if resolve != nil {
- panic(r.NewTypeError("resolve is already set"))
- }
- if reject != nil {
- panic(r.NewTypeError("reject is already set"))
- }
- if arg := call.Argument(0); arg != _undefined {
- resolve = arg
- }
- if arg := call.Argument(1); arg != _undefined {
- reject = arg
- }
- return nil
- }, "", 2)
- pcap.promise = r.toConstructor(c)([]Value{executor}, c)
- pcap.resolveObj = r.toObject(resolve)
- r.toCallable(pcap.resolveObj) // make sure it's callable
- pcap.rejectObj = r.toObject(reject)
- r.toCallable(pcap.rejectObj)
- }
- return pcap
- }
- func (r *Runtime) performPromiseThen(p *Promise, onFulfilled, onRejected Value, resultCapability *promiseCapability) Value {
- var onFulfilledJobCallback, onRejectedJobCallback *jobCallback
- if f, ok := assertCallable(onFulfilled); ok {
- onFulfilledJobCallback = &jobCallback{callback: f}
- }
- if f, ok := assertCallable(onRejected); ok {
- onRejectedJobCallback = &jobCallback{callback: f}
- }
- fulfillReaction := &promiseReaction{
- capability: resultCapability,
- typ: promiseReactionFulfill,
- handler: onFulfilledJobCallback,
- }
- rejectReaction := &promiseReaction{
- capability: resultCapability,
- typ: promiseReactionReject,
- handler: onRejectedJobCallback,
- }
- p.addReactions(fulfillReaction, rejectReaction)
- if resultCapability == nil {
- return _undefined
- }
- return resultCapability.promise
- }
- func (r *Runtime) promiseProto_catch(call FunctionCall) Value {
- return r.invoke(call.This, "then", _undefined, call.Argument(0))
- }
- func (r *Runtime) promiseResolve(c *Object, x Value) *Object {
- if obj, ok := x.(*Object); ok {
- xConstructor := nilSafe(obj.self.getStr("constructor", nil))
- if xConstructor.SameAs(c) {
- return obj
- }
- }
- pcap := r.newPromiseCapability(c)
- pcap.resolve(x)
- return pcap.promise
- }
- func (r *Runtime) promiseProto_finally(call FunctionCall) Value {
- promise := r.toObject(call.This)
- c := r.speciesConstructorObj(promise, r.getPromise())
- onFinally := call.Argument(0)
- var thenFinally, catchFinally Value
- if onFinallyFn, ok := assertCallable(onFinally); !ok {
- thenFinally, catchFinally = onFinally, onFinally
- } else {
- thenFinally = r.newNativeFunc(func(call FunctionCall) Value {
- value := call.Argument(0)
- result := onFinallyFn(FunctionCall{})
- promise := r.promiseResolve(c, result)
- valueThunk := r.newNativeFunc(func(call FunctionCall) Value {
- return value
- }, "", 0)
- return r.invoke(promise, "then", valueThunk)
- }, "", 1)
- catchFinally = r.newNativeFunc(func(call FunctionCall) Value {
- reason := call.Argument(0)
- result := onFinallyFn(FunctionCall{})
- promise := r.promiseResolve(c, result)
- thrower := r.newNativeFunc(func(call FunctionCall) Value {
- panic(reason)
- }, "", 0)
- return r.invoke(promise, "then", thrower)
- }, "", 1)
- }
- return r.invoke(promise, "then", thenFinally, catchFinally)
- }
- func (pcap *promiseCapability) resolve(result Value) {
- pcap.promise.runtime.toCallable(pcap.resolveObj)(FunctionCall{Arguments: []Value{result}})
- }
- func (pcap *promiseCapability) reject(reason Value) {
- pcap.promise.runtime.toCallable(pcap.rejectObj)(FunctionCall{Arguments: []Value{reason}})
- }
- func (pcap *promiseCapability) try(f func()) bool {
- ex := pcap.promise.runtime.vm.try(f)
- if ex != nil {
- pcap.reject(ex.val)
- return false
- }
- return true
- }
- func (r *Runtime) promise_all(call FunctionCall) Value {
- c := r.toObject(call.This)
- pcap := r.newPromiseCapability(c)
- pcap.try(func() {
- promiseResolve := r.toCallable(c.self.getStr("resolve", nil))
- iter := r.getIterator(call.Argument(0), nil)
- var values []Value
- remainingElementsCount := 1
- iter.iterate(func(nextValue Value) {
- index := len(values)
- values = append(values, _undefined)
- nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}})
- alreadyCalled := false
- onFulfilled := r.newNativeFunc(func(call FunctionCall) Value {
- if alreadyCalled {
- return _undefined
- }
- alreadyCalled = true
- values[index] = call.Argument(0)
- remainingElementsCount--
- if remainingElementsCount == 0 {
- pcap.resolve(r.newArrayValues(values))
- }
- return _undefined
- }, "", 1)
- remainingElementsCount++
- r.invoke(nextPromise, "then", onFulfilled, pcap.rejectObj)
- })
- remainingElementsCount--
- if remainingElementsCount == 0 {
- pcap.resolve(r.newArrayValues(values))
- }
- })
- return pcap.promise
- }
- func (r *Runtime) promise_allSettled(call FunctionCall) Value {
- c := r.toObject(call.This)
- pcap := r.newPromiseCapability(c)
- pcap.try(func() {
- promiseResolve := r.toCallable(c.self.getStr("resolve", nil))
- iter := r.getIterator(call.Argument(0), nil)
- var values []Value
- remainingElementsCount := 1
- iter.iterate(func(nextValue Value) {
- index := len(values)
- values = append(values, _undefined)
- nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}})
- alreadyCalled := false
- reaction := func(status Value, valueKey unistring.String) *Object {
- return r.newNativeFunc(func(call FunctionCall) Value {
- if alreadyCalled {
- return _undefined
- }
- alreadyCalled = true
- obj := r.NewObject()
- obj.self._putProp("status", status, true, true, true)
- obj.self._putProp(valueKey, call.Argument(0), true, true, true)
- values[index] = obj
- remainingElementsCount--
- if remainingElementsCount == 0 {
- pcap.resolve(r.newArrayValues(values))
- }
- return _undefined
- }, "", 1)
- }
- onFulfilled := reaction(asciiString("fulfilled"), "value")
- onRejected := reaction(asciiString("rejected"), "reason")
- remainingElementsCount++
- r.invoke(nextPromise, "then", onFulfilled, onRejected)
- })
- remainingElementsCount--
- if remainingElementsCount == 0 {
- pcap.resolve(r.newArrayValues(values))
- }
- })
- return pcap.promise
- }
- func (r *Runtime) promise_any(call FunctionCall) Value {
- c := r.toObject(call.This)
- pcap := r.newPromiseCapability(c)
- pcap.try(func() {
- promiseResolve := r.toCallable(c.self.getStr("resolve", nil))
- iter := r.getIterator(call.Argument(0), nil)
- var errors []Value
- remainingElementsCount := 1
- iter.iterate(func(nextValue Value) {
- index := len(errors)
- errors = append(errors, _undefined)
- nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}})
- alreadyCalled := false
- onRejected := r.newNativeFunc(func(call FunctionCall) Value {
- if alreadyCalled {
- return _undefined
- }
- alreadyCalled = true
- errors[index] = call.Argument(0)
- remainingElementsCount--
- if remainingElementsCount == 0 {
- _error := r.builtin_new(r.getAggregateError(), nil)
- _error.self._putProp("errors", r.newArrayValues(errors), true, false, true)
- pcap.reject(_error)
- }
- return _undefined
- }, "", 1)
- remainingElementsCount++
- r.invoke(nextPromise, "then", pcap.resolveObj, onRejected)
- })
- remainingElementsCount--
- if remainingElementsCount == 0 {
- _error := r.builtin_new(r.getAggregateError(), nil)
- _error.self._putProp("errors", r.newArrayValues(errors), true, false, true)
- pcap.reject(_error)
- }
- })
- return pcap.promise
- }
- func (r *Runtime) promise_race(call FunctionCall) Value {
- c := r.toObject(call.This)
- pcap := r.newPromiseCapability(c)
- pcap.try(func() {
- promiseResolve := r.toCallable(c.self.getStr("resolve", nil))
- iter := r.getIterator(call.Argument(0), nil)
- iter.iterate(func(nextValue Value) {
- nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}})
- r.invoke(nextPromise, "then", pcap.resolveObj, pcap.rejectObj)
- })
- })
- return pcap.promise
- }
- func (r *Runtime) promise_reject(call FunctionCall) Value {
- pcap := r.newPromiseCapability(r.toObject(call.This))
- pcap.reject(call.Argument(0))
- return pcap.promise
- }
- func (r *Runtime) promise_resolve(call FunctionCall) Value {
- return r.promiseResolve(r.toObject(call.This), call.Argument(0))
- }
- func (r *Runtime) createPromiseProto(val *Object) objectImpl {
- o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject)
- o._putProp("constructor", r.getPromise(), true, false, true)
- o._putProp("catch", r.newNativeFunc(r.promiseProto_catch, "catch", 1), true, false, true)
- o._putProp("finally", r.newNativeFunc(r.promiseProto_finally, "finally", 1), true, false, true)
- o._putProp("then", r.newNativeFunc(r.promiseProto_then, "then", 2), true, false, true)
- o._putSym(SymToStringTag, valueProp(asciiString(classPromise), false, false, true))
- return o
- }
- func (r *Runtime) createPromise(val *Object) objectImpl {
- o := r.newNativeConstructOnly(val, r.builtin_newPromise, r.getPromisePrototype(), "Promise", 1)
- o._putProp("all", r.newNativeFunc(r.promise_all, "all", 1), true, false, true)
- o._putProp("allSettled", r.newNativeFunc(r.promise_allSettled, "allSettled", 1), true, false, true)
- o._putProp("any", r.newNativeFunc(r.promise_any, "any", 1), true, false, true)
- o._putProp("race", r.newNativeFunc(r.promise_race, "race", 1), true, false, true)
- o._putProp("reject", r.newNativeFunc(r.promise_reject, "reject", 1), true, false, true)
- o._putProp("resolve", r.newNativeFunc(r.promise_resolve, "resolve", 1), true, false, true)
- r.putSpeciesReturnThis(o)
- return o
- }
- func (r *Runtime) getPromisePrototype() *Object {
- ret := r.global.PromisePrototype
- if ret == nil {
- ret = &Object{runtime: r}
- r.global.PromisePrototype = ret
- ret.self = r.createPromiseProto(ret)
- }
- return ret
- }
- func (r *Runtime) getPromise() *Object {
- ret := r.global.Promise
- if ret == nil {
- ret = &Object{runtime: r}
- r.global.Promise = ret
- ret.self = r.createPromise(ret)
- }
- return ret
- }
- func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) error {
- f, _ := AssertFunction(fObj)
- 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)
- // where it can be used like this:
- //
- // loop := NewEventLoop()
- // loop.Start()
- // defer loop.Stop()
- // loop.RunOnLoop(func(vm *goja.Runtime) {
- // p, resolve, _ := vm.NewPromise()
- // vm.Set("p", p)
- // 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
- // err := resolve(result)
- // // Handle uncatchable errors (e.g. by stopping the loop, panicking or setting a flag)
- // })
- // }()
- // }
- 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)
- }
- // SetPromiseRejectionTracker registers a function that will be called in two scenarios: when a promise is rejected
- // without any handlers (with operation argument set to PromiseRejectionReject), and when a handler is added to a
- // rejected promise for the first time (with operation argument set to PromiseRejectionHandle).
- //
- // Setting a tracker replaces any existing one. Setting it to nil disables the functionality.
- //
- // See https://tc39.es/ecma262/#sec-host-promise-rejection-tracker for more details.
- func (r *Runtime) SetPromiseRejectionTracker(tracker PromiseRejectionTracker) {
- r.promiseRejectionTracker = tracker
- }
- // SetAsyncContextTracker registers a handler that allows to track async execution contexts. See AsyncContextTracker
- // documentation for more details. Setting it to nil disables the functionality.
- // This method (as Runtime in general) is not goroutine-safe.
- func (r *Runtime) SetAsyncContextTracker(tracker AsyncContextTracker) {
- r.asyncContextTracker = tracker
- }
|