go.ketch.com/lib/goja@v0.0.1/builtin_promise.go (about) 1 package goja 2 3 import ( 4 "go.ketch.com/lib/goja/unistring" 5 "reflect" 6 ) 7 8 type PromiseState int 9 type PromiseRejectionOperation int 10 11 type promiseReactionType int 12 13 const ( 14 PromiseStatePending PromiseState = iota 15 PromiseStateFulfilled 16 PromiseStateRejected 17 ) 18 19 const ( 20 PromiseRejectionReject PromiseRejectionOperation = iota 21 PromiseRejectionHandle 22 ) 23 24 const ( 25 promiseReactionFulfill promiseReactionType = iota 26 promiseReactionReject 27 ) 28 29 type PromiseRejectionTracker func(p *Promise, operation PromiseRejectionOperation) 30 31 type jobCallback struct { 32 callback func(FunctionCall) Value 33 } 34 35 type promiseCapability struct { 36 promise *Object 37 resolveObj, rejectObj *Object 38 } 39 40 type promiseReaction struct { 41 capability *promiseCapability 42 typ promiseReactionType 43 handler *jobCallback 44 } 45 46 var typePromise = reflect.TypeOf((*Promise)(nil)) 47 48 // Promise is a Go wrapper around ECMAScript Promise. Calling Runtime.ToValue() on it 49 // returns the underlying Object. Calling Export() on a Promise Object returns a Promise. 50 // 51 // Use Runtime.NewPromise() to create one. Calling Runtime.ToValue() on a zero object or nil returns null Value. 52 // 53 // WARNING: Instances of Promise are not goroutine-safe. See Runtime.NewPromise() for more details. 54 type Promise struct { 55 baseObject 56 state PromiseState 57 result Value 58 fulfillReactions []*promiseReaction 59 rejectReactions []*promiseReaction 60 handled bool 61 } 62 63 func (p *Promise) State() PromiseState { 64 return p.state 65 } 66 67 func (p *Promise) Result() Value { 68 return p.result 69 } 70 71 func (p *Promise) toValue(r *Runtime) Value { 72 if p == nil || p.val == nil { 73 return _null 74 } 75 promise := p.val 76 if promise.runtime != r { 77 panic(r.NewTypeError("Illegal runtime transition of a Promise")) 78 } 79 return promise 80 } 81 82 func (p *Promise) createResolvingFunctions() (resolve, reject *Object) { 83 r := p.val.runtime 84 alreadyResolved := false 85 return p.val.runtime.newNativeFunc(func(call FunctionCall) Value { 86 if alreadyResolved { 87 return _undefined 88 } 89 alreadyResolved = true 90 resolution := call.Argument(0) 91 if resolution.SameAs(p.val) { 92 return p.reject(r.NewTypeError("Promise self-resolution")) 93 } 94 if obj, ok := resolution.(*Object); ok { 95 var thenAction Value 96 ex := r.vm.try(func() { 97 thenAction = obj.self.getStr("then", nil) 98 }) 99 if ex != nil { 100 return p.reject(ex.val) 101 } 102 if call, ok := assertCallable(thenAction); ok { 103 job := r.newPromiseResolveThenableJob(p, resolution, &jobCallback{callback: call}) 104 r.enqueuePromiseJob(job) 105 return _undefined 106 } 107 } 108 return p.fulfill(resolution) 109 }, nil, "", nil, 1), 110 p.val.runtime.newNativeFunc(func(call FunctionCall) Value { 111 if alreadyResolved { 112 return _undefined 113 } 114 alreadyResolved = true 115 reason := call.Argument(0) 116 return p.reject(reason) 117 }, nil, "", nil, 1) 118 } 119 120 func (p *Promise) reject(reason Value) Value { 121 reactions := p.rejectReactions 122 p.result = reason 123 p.fulfillReactions, p.rejectReactions = nil, nil 124 p.state = PromiseStateRejected 125 r := p.val.runtime 126 if !p.handled { 127 r.trackPromiseRejection(p, PromiseRejectionReject) 128 } 129 r.triggerPromiseReactions(reactions, reason) 130 return _undefined 131 } 132 133 func (p *Promise) fulfill(value Value) Value { 134 reactions := p.fulfillReactions 135 p.result = value 136 p.fulfillReactions, p.rejectReactions = nil, nil 137 p.state = PromiseStateFulfilled 138 p.val.runtime.triggerPromiseReactions(reactions, value) 139 return _undefined 140 } 141 142 func (p *Promise) exportType() reflect.Type { 143 return typePromise 144 } 145 146 func (p *Promise) export(*objectExportCtx) interface{} { 147 return p 148 } 149 150 func (r *Runtime) newPromiseResolveThenableJob(p *Promise, thenable Value, then *jobCallback) func() { 151 return func() { 152 resolve, reject := p.createResolvingFunctions() 153 ex := r.vm.try(func() { 154 r.callJobCallback(then, thenable, resolve, reject) 155 }) 156 if ex != nil { 157 if fn, ok := reject.self.assertCallable(); ok { 158 fn(FunctionCall{Arguments: []Value{ex.val}}) 159 } 160 } 161 } 162 } 163 164 func (r *Runtime) enqueuePromiseJob(job func()) { 165 r.jobQueue = append(r.jobQueue, job) 166 } 167 168 func (r *Runtime) triggerPromiseReactions(reactions []*promiseReaction, argument Value) { 169 for _, reaction := range reactions { 170 r.enqueuePromiseJob(r.newPromiseReactionJob(reaction, argument)) 171 } 172 } 173 174 func (r *Runtime) newPromiseReactionJob(reaction *promiseReaction, argument Value) func() { 175 return func() { 176 var handlerResult Value 177 fulfill := false 178 if reaction.handler == nil { 179 handlerResult = argument 180 if reaction.typ == promiseReactionFulfill { 181 fulfill = true 182 } 183 } else { 184 ex := r.vm.try(func() { 185 handlerResult = r.callJobCallback(reaction.handler, _undefined, argument) 186 fulfill = true 187 }) 188 if ex != nil { 189 handlerResult = ex.val 190 } 191 } 192 if reaction.capability != nil { 193 if fulfill { 194 reaction.capability.resolve(handlerResult) 195 } else { 196 reaction.capability.reject(handlerResult) 197 } 198 } 199 } 200 } 201 202 func (r *Runtime) newPromise(proto *Object) *Promise { 203 o := &Object{runtime: r} 204 205 po := &Promise{} 206 po.class = classPromise 207 po.val = o 208 po.extensible = true 209 o.self = po 210 po.prototype = proto 211 po.init() 212 return po 213 } 214 215 func (r *Runtime) builtin_newPromise(args []Value, newTarget *Object) *Object { 216 if newTarget == nil { 217 panic(r.needNew("Promise")) 218 } 219 var arg0 Value 220 if len(args) > 0 { 221 arg0 = args[0] 222 } 223 executor := r.toCallable(arg0) 224 225 proto := r.getPrototypeFromCtor(newTarget, r.global.Promise, r.global.PromisePrototype) 226 po := r.newPromise(proto) 227 228 resolve, reject := po.createResolvingFunctions() 229 ex := r.vm.try(func() { 230 executor(FunctionCall{Arguments: []Value{resolve, reject}}) 231 }) 232 if ex != nil { 233 if fn, ok := reject.self.assertCallable(); ok { 234 fn(FunctionCall{Arguments: []Value{ex.val}}) 235 } 236 } 237 return po.val 238 } 239 240 func (r *Runtime) promiseProto_then(call FunctionCall) Value { 241 thisObj := r.toObject(call.This) 242 if p, ok := thisObj.self.(*Promise); ok { 243 c := r.speciesConstructorObj(thisObj, r.global.Promise) 244 resultCapability := r.newPromiseCapability(c) 245 return r.performPromiseThen(p, call.Argument(0), call.Argument(1), resultCapability) 246 } 247 panic(r.NewTypeError("Method Promise.prototype.then called on incompatible receiver %s", r.objectproto_toString(FunctionCall{This: thisObj}))) 248 } 249 250 func (r *Runtime) newPromiseCapability(c *Object) *promiseCapability { 251 pcap := new(promiseCapability) 252 if c == r.global.Promise { 253 p := r.newPromise(r.global.PromisePrototype) 254 pcap.resolveObj, pcap.rejectObj = p.createResolvingFunctions() 255 pcap.promise = p.val 256 } else { 257 var resolve, reject Value 258 executor := r.newNativeFunc(func(call FunctionCall) Value { 259 if resolve != nil { 260 panic(r.NewTypeError("resolve is already set")) 261 } 262 if reject != nil { 263 panic(r.NewTypeError("reject is already set")) 264 } 265 if arg := call.Argument(0); arg != _undefined { 266 resolve = arg 267 } 268 if arg := call.Argument(1); arg != _undefined { 269 reject = arg 270 } 271 return nil 272 }, nil, "", nil, 2) 273 pcap.promise = r.toConstructor(c)([]Value{executor}, c) 274 pcap.resolveObj = r.toObject(resolve) 275 r.toCallable(pcap.resolveObj) // make sure it's callable 276 pcap.rejectObj = r.toObject(reject) 277 r.toCallable(pcap.rejectObj) 278 } 279 return pcap 280 } 281 282 func (r *Runtime) performPromiseThen(p *Promise, onFulfilled, onRejected Value, resultCapability *promiseCapability) Value { 283 var onFulfilledJobCallback, onRejectedJobCallback *jobCallback 284 if f, ok := assertCallable(onFulfilled); ok { 285 onFulfilledJobCallback = &jobCallback{callback: f} 286 } 287 if f, ok := assertCallable(onRejected); ok { 288 onRejectedJobCallback = &jobCallback{callback: f} 289 } 290 fulfillReaction := &promiseReaction{ 291 capability: resultCapability, 292 typ: promiseReactionFulfill, 293 handler: onFulfilledJobCallback, 294 } 295 rejectReaction := &promiseReaction{ 296 capability: resultCapability, 297 typ: promiseReactionReject, 298 handler: onRejectedJobCallback, 299 } 300 switch p.state { 301 case PromiseStatePending: 302 p.fulfillReactions = append(p.fulfillReactions, fulfillReaction) 303 p.rejectReactions = append(p.rejectReactions, rejectReaction) 304 case PromiseStateFulfilled: 305 r.enqueuePromiseJob(r.newPromiseReactionJob(fulfillReaction, p.result)) 306 default: 307 reason := p.result 308 if !p.handled { 309 r.trackPromiseRejection(p, PromiseRejectionHandle) 310 } 311 r.enqueuePromiseJob(r.newPromiseReactionJob(rejectReaction, reason)) 312 } 313 p.handled = true 314 if resultCapability == nil { 315 return _undefined 316 } 317 return resultCapability.promise 318 } 319 320 func (r *Runtime) promiseProto_catch(call FunctionCall) Value { 321 return r.invoke(call.This, "then", _undefined, call.Argument(0)) 322 } 323 324 func (r *Runtime) promiseResolve(c *Object, x Value) *Object { 325 if obj, ok := x.(*Object); ok { 326 xConstructor := nilSafe(obj.self.getStr("constructor", nil)) 327 if xConstructor.SameAs(c) { 328 return obj 329 } 330 } 331 pcap := r.newPromiseCapability(c) 332 pcap.resolve(x) 333 return pcap.promise 334 } 335 336 func (r *Runtime) promiseProto_finally(call FunctionCall) Value { 337 promise := r.toObject(call.This) 338 c := r.speciesConstructorObj(promise, r.global.Promise) 339 onFinally := call.Argument(0) 340 var thenFinally, catchFinally Value 341 if onFinallyFn, ok := assertCallable(onFinally); !ok { 342 thenFinally, catchFinally = onFinally, onFinally 343 } else { 344 thenFinally = r.newNativeFunc(func(call FunctionCall) Value { 345 value := call.Argument(0) 346 result := onFinallyFn(FunctionCall{}) 347 promise := r.promiseResolve(c, result) 348 valueThunk := r.newNativeFunc(func(call FunctionCall) Value { 349 return value 350 }, nil, "", nil, 0) 351 return r.invoke(promise, "then", valueThunk) 352 }, nil, "", nil, 1) 353 354 catchFinally = r.newNativeFunc(func(call FunctionCall) Value { 355 reason := call.Argument(0) 356 result := onFinallyFn(FunctionCall{}) 357 promise := r.promiseResolve(c, result) 358 thrower := r.newNativeFunc(func(call FunctionCall) Value { 359 panic(reason) 360 }, nil, "", nil, 0) 361 return r.invoke(promise, "then", thrower) 362 }, nil, "", nil, 1) 363 } 364 return r.invoke(promise, "then", thenFinally, catchFinally) 365 } 366 367 func (pcap *promiseCapability) resolve(result Value) { 368 pcap.promise.runtime.toCallable(pcap.resolveObj)(FunctionCall{Arguments: []Value{result}}) 369 } 370 371 func (pcap *promiseCapability) reject(reason Value) { 372 pcap.promise.runtime.toCallable(pcap.rejectObj)(FunctionCall{Arguments: []Value{reason}}) 373 } 374 375 func (pcap *promiseCapability) try(f func()) bool { 376 ex := pcap.promise.runtime.vm.try(f) 377 if ex != nil { 378 pcap.reject(ex.val) 379 return false 380 } 381 return true 382 } 383 384 func (r *Runtime) promise_all(call FunctionCall) Value { 385 c := r.toObject(call.This) 386 pcap := r.newPromiseCapability(c) 387 388 pcap.try(func() { 389 promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) 390 iter := r.getIterator(call.Argument(0), nil) 391 var values []Value 392 remainingElementsCount := 1 393 iter.iterate(func(nextValue Value) { 394 index := len(values) 395 values = append(values, _undefined) 396 nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) 397 alreadyCalled := false 398 onFulfilled := r.newNativeFunc(func(call FunctionCall) Value { 399 if alreadyCalled { 400 return _undefined 401 } 402 alreadyCalled = true 403 values[index] = call.Argument(0) 404 remainingElementsCount-- 405 if remainingElementsCount == 0 { 406 pcap.resolve(r.newArrayValues(values)) 407 } 408 return _undefined 409 }, nil, "", nil, 1) 410 remainingElementsCount++ 411 r.invoke(nextPromise, "then", onFulfilled, pcap.rejectObj) 412 }) 413 remainingElementsCount-- 414 if remainingElementsCount == 0 { 415 pcap.resolve(r.newArrayValues(values)) 416 } 417 }) 418 return pcap.promise 419 } 420 421 func (r *Runtime) promise_allSettled(call FunctionCall) Value { 422 c := r.toObject(call.This) 423 pcap := r.newPromiseCapability(c) 424 425 pcap.try(func() { 426 promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) 427 iter := r.getIterator(call.Argument(0), nil) 428 var values []Value 429 remainingElementsCount := 1 430 iter.iterate(func(nextValue Value) { 431 index := len(values) 432 values = append(values, _undefined) 433 nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) 434 alreadyCalled := false 435 reaction := func(status Value, valueKey unistring.String) *Object { 436 return r.newNativeFunc(func(call FunctionCall) Value { 437 if alreadyCalled { 438 return _undefined 439 } 440 alreadyCalled = true 441 obj := r.NewObject() 442 obj.self._putProp("status", status, true, true, true) 443 obj.self._putProp(valueKey, call.Argument(0), true, true, true) 444 values[index] = obj 445 remainingElementsCount-- 446 if remainingElementsCount == 0 { 447 pcap.resolve(r.newArrayValues(values)) 448 } 449 return _undefined 450 }, nil, "", nil, 1) 451 } 452 onFulfilled := reaction(asciiString("fulfilled"), "value") 453 onRejected := reaction(asciiString("rejected"), "reason") 454 remainingElementsCount++ 455 r.invoke(nextPromise, "then", onFulfilled, onRejected) 456 }) 457 remainingElementsCount-- 458 if remainingElementsCount == 0 { 459 pcap.resolve(r.newArrayValues(values)) 460 } 461 }) 462 return pcap.promise 463 } 464 465 func (r *Runtime) promise_any(call FunctionCall) Value { 466 c := r.toObject(call.This) 467 pcap := r.newPromiseCapability(c) 468 469 pcap.try(func() { 470 promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) 471 iter := r.getIterator(call.Argument(0), nil) 472 var errors []Value 473 remainingElementsCount := 1 474 iter.iterate(func(nextValue Value) { 475 index := len(errors) 476 errors = append(errors, _undefined) 477 nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) 478 alreadyCalled := false 479 onRejected := r.newNativeFunc(func(call FunctionCall) Value { 480 if alreadyCalled { 481 return _undefined 482 } 483 alreadyCalled = true 484 errors[index] = call.Argument(0) 485 remainingElementsCount-- 486 if remainingElementsCount == 0 { 487 _error := r.builtin_new(r.global.AggregateError, nil) 488 _error.self._putProp("errors", r.newArrayValues(errors), true, false, true) 489 pcap.reject(_error) 490 } 491 return _undefined 492 }, nil, "", nil, 1) 493 494 remainingElementsCount++ 495 r.invoke(nextPromise, "then", pcap.resolveObj, onRejected) 496 }) 497 remainingElementsCount-- 498 if remainingElementsCount == 0 { 499 _error := r.builtin_new(r.global.AggregateError, nil) 500 _error.self._putProp("errors", r.newArrayValues(errors), true, false, true) 501 pcap.reject(_error) 502 } 503 }) 504 return pcap.promise 505 } 506 507 func (r *Runtime) promise_race(call FunctionCall) Value { 508 c := r.toObject(call.This) 509 pcap := r.newPromiseCapability(c) 510 511 pcap.try(func() { 512 promiseResolve := r.toCallable(c.self.getStr("resolve", nil)) 513 iter := r.getIterator(call.Argument(0), nil) 514 iter.iterate(func(nextValue Value) { 515 nextPromise := promiseResolve(FunctionCall{This: c, Arguments: []Value{nextValue}}) 516 r.invoke(nextPromise, "then", pcap.resolveObj, pcap.rejectObj) 517 }) 518 }) 519 return pcap.promise 520 } 521 522 func (r *Runtime) promise_reject(call FunctionCall) Value { 523 pcap := r.newPromiseCapability(r.toObject(call.This)) 524 pcap.reject(call.Argument(0)) 525 return pcap.promise 526 } 527 528 func (r *Runtime) promise_resolve(call FunctionCall) Value { 529 return r.promiseResolve(r.toObject(call.This), call.Argument(0)) 530 } 531 532 func (r *Runtime) createPromiseProto(val *Object) objectImpl { 533 o := newBaseObjectObj(val, r.global.ObjectPrototype, classObject) 534 o._putProp("constructor", r.global.Promise, true, false, true) 535 536 o._putProp("catch", r.newNativeFunc(r.promiseProto_catch, nil, "catch", nil, 1), true, false, true) 537 o._putProp("finally", r.newNativeFunc(r.promiseProto_finally, nil, "finally", nil, 1), true, false, true) 538 o._putProp("then", r.newNativeFunc(r.promiseProto_then, nil, "then", nil, 2), true, false, true) 539 540 o._putSym(SymToStringTag, valueProp(asciiString(classPromise), false, false, true)) 541 542 return o 543 } 544 545 func (r *Runtime) createPromise(val *Object) objectImpl { 546 o := r.newNativeConstructOnly(val, r.builtin_newPromise, r.global.PromisePrototype, "Promise", 1) 547 548 o._putProp("all", r.newNativeFunc(r.promise_all, nil, "all", nil, 1), true, false, true) 549 o._putProp("allSettled", r.newNativeFunc(r.promise_allSettled, nil, "allSettled", nil, 1), true, false, true) 550 o._putProp("any", r.newNativeFunc(r.promise_any, nil, "any", nil, 1), true, false, true) 551 o._putProp("race", r.newNativeFunc(r.promise_race, nil, "race", nil, 1), true, false, true) 552 o._putProp("reject", r.newNativeFunc(r.promise_reject, nil, "reject", nil, 1), true, false, true) 553 o._putProp("resolve", r.newNativeFunc(r.promise_resolve, nil, "resolve", nil, 1), true, false, true) 554 555 r.putSpeciesReturnThis(o) 556 557 return o 558 } 559 560 func (r *Runtime) initPromise() { 561 r.global.PromisePrototype = r.newLazyObject(r.createPromiseProto) 562 r.global.Promise = r.newLazyObject(r.createPromise) 563 564 r.addToGlobal("Promise", r.global.Promise) 565 } 566 567 func (r *Runtime) wrapPromiseReaction(fObj *Object) func(interface{}) { 568 f, _ := AssertFunction(fObj) 569 return func(x interface{}) { 570 _, _ = f(nil, r.ToValue(x)) 571 } 572 } 573 574 // NewPromise creates and returns a Promise and resolving functions for it. 575 // 576 // WARNING: The returned values are not goroutine-safe and must not be called in parallel with VM running. 577 // 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) 578 // where it can be used like this: 579 // 580 // loop := NewEventLoop() 581 // loop.Start() 582 // defer loop.Stop() 583 // loop.RunOnLoop(func(vm *goja.Runtime) { 584 // p, resolve, _ := vm.NewPromise() 585 // vm.Set("p", p) 586 // go func() { 587 // time.Sleep(500 * time.Millisecond) // or perform any other blocking operation 588 // loop.RunOnLoop(func(*goja.Runtime) { // resolve() must be called on the loop, cannot call it here 589 // resolve(result) 590 // }) 591 // }() 592 // } 593 func (r *Runtime) NewPromise() (promise *Promise, resolve func(result interface{}), reject func(reason interface{})) { 594 p := r.newPromise(r.global.PromisePrototype) 595 resolveF, rejectF := p.createResolvingFunctions() 596 return p, r.wrapPromiseReaction(resolveF), r.wrapPromiseReaction(rejectF) 597 } 598 599 // SetPromiseRejectionTracker registers a function that will be called in two scenarios: when a promise is rejected 600 // without any handlers (with operation argument set to PromiseRejectionReject), and when a handler is added to a 601 // rejected promise for the first time (with operation argument set to PromiseRejectionHandle). 602 // 603 // Setting a tracker replaces any existing one. Setting it to nil disables the functionality. 604 // 605 // See https://tc39.es/ecma262/#sec-host-promise-rejection-tracker for more details. 606 func (r *Runtime) SetPromiseRejectionTracker(tracker PromiseRejectionTracker) { 607 r.promiseRejectionTracker = tracker 608 }