123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739 |
- using Jint.Native.Function;
- using Jint.Native.Iterator;
- using Jint.Native.Object;
- using Jint.Native.Symbol;
- using Jint.Runtime;
- using Jint.Runtime.Descriptors;
- using Jint.Runtime.Interop;
- namespace Jint.Native.Promise;
- internal sealed record PromiseCapability(
- JsValue PromiseInstance,
- ICallable Resolve,
- ICallable Reject,
- JsValue ResolveObj,
- JsValue RejectObj);
- internal sealed class PromiseConstructor : Constructor
- {
- private static readonly JsString _functionName = new JsString("Promise");
- internal PromiseConstructor(
- Engine engine,
- Realm realm,
- FunctionPrototype functionPrototype,
- ObjectPrototype objectPrototype)
- : base(engine, realm, _functionName)
- {
- _prototype = functionPrototype;
- PrototypeObject = new PromisePrototype(engine, realm, this, objectPrototype);
- _length = new PropertyDescriptor(1, PropertyFlag.Configurable);
- _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
- }
- internal PromisePrototype PrototypeObject { get; }
- protected override void Initialize()
- {
- const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
- const PropertyFlag LengthFlags = PropertyFlag.Configurable;
- var properties = new PropertyDictionary(8, checkExistingKeys: false)
- {
- ["all"] = new(new PropertyDescriptor(new ClrFunction(Engine, "all", All, 1, LengthFlags), PropertyFlags)),
- ["allSettled"] = new(new PropertyDescriptor(new ClrFunction(Engine, "allSettled", AllSettled, 1, LengthFlags), PropertyFlags)),
- ["any"] = new(new PropertyDescriptor(new ClrFunction(Engine, "any", Any, 1, LengthFlags), PropertyFlags)),
- ["race"] = new(new PropertyDescriptor(new ClrFunction(Engine, "race", Race, 1, LengthFlags), PropertyFlags)),
- ["reject"] = new(new PropertyDescriptor(new ClrFunction(Engine, "reject", Reject, 1, LengthFlags), PropertyFlags)),
- ["resolve"] = new(new PropertyDescriptor(new ClrFunction(Engine, "resolve", Resolve, 1, LengthFlags), PropertyFlags)),
- ["try"] = new(new PropertyDescriptor(new ClrFunction(Engine, "try", Try, 1, LengthFlags), PropertyFlags)),
- ["withResolvers"] = new(new PropertyDescriptor(new ClrFunction(Engine, "withResolvers", WithResolvers, 0, LengthFlags), PropertyFlags)),
- };
- SetProperties(properties);
- var symbols = new SymbolDictionary(1)
- {
- [GlobalSymbolRegistry.Species] = new GetSetPropertyDescriptor(
- get: new ClrFunction(_engine, "get [Symbol.species]", (thisObj, _) => thisObj, 0, PropertyFlag.Configurable),
- set: Undefined, PropertyFlag.Configurable)
- };
- SetSymbols(symbols);
- }
- /// <summary>
- /// https://tc39.es/ecma262/#sec-promise-executor
- /// </summary>
- public override ObjectInstance Construct(JsCallArguments arguments, JsValue newTarget)
- {
- if (newTarget.IsUndefined())
- {
- Throw.TypeError(_realm, "Constructor Promise requires 'new'");
- }
- if (arguments.At(0) is not ICallable executor)
- {
- Throw.TypeError(_realm, $"Promise executor {(arguments.At(0))} is not a function");
- return null;
- }
- var promise = OrdinaryCreateFromConstructor(
- newTarget,
- static intrinsics => intrinsics.Promise.PrototypeObject,
- static (Engine engine, Realm _, object? _) => new JsPromise(engine));
- var (resolve, reject) = promise.CreateResolvingFunctions();
- try
- {
- executor.Call(Undefined, resolve, reject);
- }
- catch (JavaScriptException e)
- {
- reject.Call(JsValue.Undefined, [e.Error]);
- }
- return promise;
- }
- /// <summary>
- /// https://tc39.es/ecma262/#sec-promise.resolve
- /// </summary>
- internal JsValue Resolve(JsValue thisObject, JsCallArguments arguments)
- {
- if (!thisObject.IsObject())
- {
- Throw.TypeError(_realm, "PromiseResolve called on non-object");
- }
- if (thisObject is not IConstructor)
- {
- Throw.TypeError(_realm, "Promise.resolve invoked on a non-constructor value");
- }
- var x = arguments.At(0);
- return PromiseResolve(thisObject, x);
- }
- private JsObject WithResolvers(JsValue thisObject, JsCallArguments arguments)
- {
- var promiseCapability = NewPromiseCapability(_engine, thisObject);
- var obj = OrdinaryObjectCreate(_engine, _engine.Realm.Intrinsics.Object.PrototypeObject);
- obj.CreateDataPropertyOrThrow("promise", promiseCapability.PromiseInstance);
- obj.CreateDataPropertyOrThrow("resolve", promiseCapability.ResolveObj);
- obj.CreateDataPropertyOrThrow("reject", promiseCapability.RejectObj);
- return obj;
- }
- /// <summary>
- /// https://tc39.es/ecma262/#sec-promise-resolve
- /// </summary>
- private JsValue PromiseResolve(JsValue thisObject, JsValue x)
- {
- if (x.IsPromise())
- {
- var xConstructor = x.Get(CommonProperties.Constructor);
- if (SameValue(xConstructor, thisObject))
- {
- return x;
- }
- }
- var capability = NewPromiseCapability(_engine, thisObject);
- capability.Resolve.Call(Undefined, x);
- return capability.PromiseInstance;
- }
- /// <summary>
- /// https://tc39.es/ecma262/#sec-promise.reject
- /// </summary>
- private JsValue Reject(JsValue thisObject, JsCallArguments arguments)
- {
- if (!thisObject.IsObject())
- {
- Throw.TypeError(_realm, "Promise.reject called on non-object");
- }
- if (thisObject is not IConstructor)
- {
- Throw.TypeError(_realm, "Promise.reject invoked on a non-constructor value");
- }
- var r = arguments.At(0);
- var capability = NewPromiseCapability(_engine, thisObject);
- capability.Reject.Call(Undefined, r);
- return capability.PromiseInstance;
- }
- /// <summary>
- /// https://tc39.es/proposal-promise-try/
- /// </summary>
- private JsValue Try(JsValue thisObject, JsCallArguments arguments)
- {
- if (!thisObject.IsObject())
- {
- Throw.TypeError(_realm, "Promise.try called on non-object");
- }
- var callbackfn = arguments.At(0);
- var promiseCapability = NewPromiseCapability(_engine, thisObject);
- try
- {
- var status = callbackfn.Call(Undefined, arguments.AsSpan().Slice(1).ToArray());
- promiseCapability.Resolve.Call(Undefined, status);
- }
- catch (JavaScriptException e)
- {
- promiseCapability.Reject.Call(Undefined, e.Error);
- }
- return promiseCapability.PromiseInstance;
- }
- // This helper methods executes the first 6 steps in the specs belonging to static Promise methods like all, any etc.
- // If it returns false, that means it has an error and it is already rejected
- // If it returns true, the logic specific to the calling function should continue executing
- private bool TryGetPromiseCapabilityAndIterator(JsValue thisObject, JsCallArguments arguments, string callerName, out PromiseCapability capability, out ICallable promiseResolve, out IteratorInstance iterator)
- {
- if (!thisObject.IsObject())
- {
- Throw.TypeError(_realm, $"{callerName} called on non-object");
- }
- //2. Let promiseCapability be ? NewPromiseCapability(C).
- capability = NewPromiseCapability(_engine, thisObject);
- var reject = capability.Reject;
- //3. Let promiseResolve be GetPromiseResolve(C).
- // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
- try
- {
- promiseResolve = GetPromiseResolve(thisObject);
- }
- catch (JavaScriptException e)
- {
- reject.Call(Undefined, e.Error);
- promiseResolve = null!;
- iterator = null!;
- return false;
- }
- // 5. Let iteratorRecord be GetIterator(iterable).
- // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
- try
- {
- if (arguments.Length == 0)
- {
- Throw.TypeError(_realm, $"no arguments were passed to {callerName}");
- }
- var iterable = arguments.At(0);
- iterator = iterable.GetIterator(_realm);
- }
- catch (JavaScriptException e)
- {
- reject.Call(Undefined, e.Error);
- iterator = null!;
- return false;
- }
- return true;
- }
- // https://tc39.es/ecma262/#sec-promise.all
- private JsValue All(JsValue thisObject, JsCallArguments arguments)
- {
- if (!TryGetPromiseCapabilityAndIterator(thisObject, arguments, "Promise.all", out var capability, out var promiseResolve, out var iterator))
- return capability.PromiseInstance;
- var results = new List<JsValue>();
- bool doneIterating = false;
- void ResolveIfFinished()
- {
- // that means all of them were resolved
- // Note that "Undefined" is not null, thus the logic is sound, even though awkward
- // also note that it is important to check if we are done iterating.
- // if "then" method is sync then it will be resolved BEFORE the next iteration cycle
- if (results.TrueForAll(static x => x is not null) && doneIterating)
- {
- var array = _realm.Intrinsics.Array.ConstructFast(results);
- capability.Resolve.Call(Undefined, array);
- }
- }
- // 27.2.4.1.2 PerformPromiseAll ( iteratorRecord, constructor, resultCapability, promiseResolve )
- // https://tc39.es/ecma262/#sec-performpromiseall
- try
- {
- int index = 0;
- do
- {
- JsValue value;
- try
- {
- if (!iterator.TryIteratorStep(out var nextItem))
- {
- doneIterating = true;
- ResolveIfFinished();
- break;
- }
- value = nextItem.Get(CommonProperties.Value);
- }
- catch (JavaScriptException e)
- {
- capability.Reject.Call(Undefined, e.Error);
- return capability.PromiseInstance;
- }
- // note that null here is important
- // it will help to detect if all inner promises were resolved
- // In F# it would be Option<JsValue>
- results.Add(null!);
- var item = promiseResolve.Call(thisObject, value);
- var thenProps = item.Get("then");
- if (thenProps is ICallable thenFunc)
- {
- var capturedIndex = index;
- var alreadyCalled = false;
- var onSuccess =
- new ClrFunction(_engine, "", (_, args) =>
- {
- if (!alreadyCalled)
- {
- alreadyCalled = true;
- results[capturedIndex] = args.At(0);
- ResolveIfFinished();
- }
- return Undefined;
- }, 1, PropertyFlag.Configurable);
- thenFunc.Call(item, onSuccess, capability.RejectObj);
- }
- else
- {
- Throw.TypeError(_realm, "Passed non Promise-like value");
- }
- index += 1;
- } while (true);
- }
- catch (JavaScriptException e)
- {
- try
- {
- iterator.Close(CompletionType.Throw);
- }
- catch (JavaScriptException)
- {
- // ignore any errors from closing the iterator
- }
- capability.Reject.Call(Undefined, e.Error);
- return capability.PromiseInstance;
- }
- return capability.PromiseInstance;
- }
- // https://tc39.es/ecma262/#sec-promise.allsettled
- private JsValue AllSettled(JsValue thisObject, JsCallArguments arguments)
- {
- if (!TryGetPromiseCapabilityAndIterator(thisObject, arguments, "Promise.allSettled", out var capability, out var promiseResolve, out var iterator))
- return capability.PromiseInstance;
- var results = new List<JsValue>();
- bool doneIterating = false;
- void ResolveIfFinished()
- {
- // that means all of them were resolved
- // Note that "Undefined" is not null, thus the logic is sound, even though awkward
- // also note that it is important to check if we are done iterating.
- // if "then" method is sync then it will be resolved BEFORE the next iteration cycle
- if (results.TrueForAll(static x => x is not null) && doneIterating)
- {
- var array = _realm.Intrinsics.Array.ConstructFast(results);
- capability.Resolve.Call(Undefined, array);
- }
- }
- // 27.2.4.1.2 PerformPromiseAll ( iteratorRecord, constructor, resultCapability, promiseResolve )
- // https://tc39.es/ecma262/#sec-performpromiseall
- try
- {
- int index = 0;
- do
- {
- JsValue value;
- try
- {
- if (!iterator.TryIteratorStep(out var nextItem))
- {
- doneIterating = true;
- ResolveIfFinished();
- break;
- }
- value = nextItem.Get(CommonProperties.Value);
- }
- catch (JavaScriptException e)
- {
- capability.Reject.Call(Undefined, e.Error);
- return capability.PromiseInstance;
- }
- // note that null here is important
- // it will help to detect if all inner promises were resolved
- // In F# it would be Option<JsValue>
- results.Add(null!);
- var item = promiseResolve.Call(thisObject, value);
- var thenProps = item.Get("then");
- if (thenProps is ICallable thenFunc)
- {
- var capturedIndex = index;
- var alreadyCalled = false;
- var onSuccess =
- new ClrFunction(_engine, "", (_, args) =>
- {
- if (!alreadyCalled)
- {
- alreadyCalled = true;
- var res = Engine.Realm.Intrinsics.Object.Construct(2);
- res.FastSetDataProperty("status", "fulfilled");
- res.FastSetDataProperty("value", args.At(0));
- results[capturedIndex] = res;
- ResolveIfFinished();
- }
- return Undefined;
- }, 1, PropertyFlag.Configurable);
- var onFailure =
- new ClrFunction(_engine, "", (_, args) =>
- {
- if (!alreadyCalled)
- {
- alreadyCalled = true;
- var res = Engine.Realm.Intrinsics.Object.Construct(2);
- res.FastSetDataProperty("status", "rejected");
- res.FastSetDataProperty("reason", args.At(0));
- results[capturedIndex] = res;
- ResolveIfFinished();
- }
- return Undefined;
- }, 1, PropertyFlag.Configurable);
- thenFunc.Call(item, onSuccess, onFailure);
- }
- else
- {
- Throw.TypeError(_realm, "Passed non Promise-like value");
- }
- index += 1;
- } while (true);
- }
- catch (JavaScriptException e)
- {
- try
- {
- iterator.Close(CompletionType.Throw);
- }
- catch (JavaScriptException)
- {
- // ignore any errors from closing the iterator
- }
- capability.Reject.Call(Undefined, e.Error);
- return capability.PromiseInstance;
- }
- return capability.PromiseInstance;
- }
- // https://tc39.es/ecma262/#sec-promise.any
- private JsValue Any(JsValue thisObject, JsCallArguments arguments)
- {
- if (!TryGetPromiseCapabilityAndIterator(thisObject, arguments, "Promise.any", out var capability, out var promiseResolve, out var iterator))
- {
- return capability.PromiseInstance;
- }
- var errors = new List<JsValue>();
- var doneIterating = false;
- void RejectIfAllRejected()
- {
- // that means all of them were rejected
- // Note that "Undefined" is not null, thus the logic is sound, even though awkward
- // also note that it is important to check if we are done iterating.
- // if "then" method is sync then it will be resolved BEFORE the next iteration cycle
- if (errors.TrueForAll(static x => x is not null) && doneIterating)
- {
- var array = _realm.Intrinsics.Array.ConstructFast(errors);
- capability.Reject.Call(Undefined, Construct(_realm.Intrinsics.AggregateError, [array]));
- }
- }
- // https://tc39.es/ecma262/#sec-performpromiseany
- try
- {
- var index = 0;
- do
- {
- ObjectInstance? nextItem = null;
- JsValue value;
- try
- {
- if (!iterator.TryIteratorStep(out nextItem))
- {
- doneIterating = true;
- RejectIfAllRejected();
- break;
- }
- value = nextItem.Get(CommonProperties.Value);
- }
- catch (JavaScriptException e)
- {
- if (nextItem?.Get("done")?.AsBoolean() == false)
- {
- throw;
- }
- errors.Add(e.Error);
- continue;
- }
- // note that null here is important
- // it will help to detect if all inner promises were rejected
- // In F# it would be Option<JsValue>
- errors.Add(null!);
- var item = promiseResolve.Call(thisObject, value);
- var thenProps = item.Get("then");
- if (thenProps is ICallable thenFunc)
- {
- var capturedIndex = index;
- var alreadyCalled = false;
- var onError =
- new ClrFunction(_engine, "", (_, args) =>
- {
- if (!alreadyCalled)
- {
- alreadyCalled = true;
- errors[capturedIndex] = args.At(0);
- RejectIfAllRejected();
- }
- return Undefined;
- }, 1, PropertyFlag.Configurable);
- thenFunc.Call(item, capability.ResolveObj, onError);
- }
- else
- {
- Throw.TypeError(_realm, "Passed non Promise-like value");
- }
- index += 1;
- } while (true);
- }
- catch (JavaScriptException e)
- {
- try
- {
- iterator.Close(CompletionType.Throw);
- }
- catch (JavaScriptException)
- {
- // ignore any errors from closing the iterator
- }
- capability.Reject.Call(Undefined, e.Error);
- return capability.PromiseInstance;
- }
- return capability.PromiseInstance;
- }
- // https://tc39.es/ecma262/#sec-promise.race
- private JsValue Race(JsValue thisObject, JsCallArguments arguments)
- {
- if (!TryGetPromiseCapabilityAndIterator(thisObject, arguments, "Promise.race", out var capability, out var promiseResolve, out var iterator))
- return capability.PromiseInstance;
- // 7. Let result be PerformPromiseRace(iteratorRecord, C, promiseCapability, promiseResolve).
- // https://tc39.es/ecma262/#sec-performpromiserace
- try
- {
- do
- {
- JsValue nextValue;
- try
- {
- if (!iterator.TryIteratorStep(out var nextItem))
- {
- break;
- }
- nextValue = nextItem.Get(CommonProperties.Value);
- }
- catch (JavaScriptException e)
- {
- capability.Reject.Call(Undefined, e.Error);
- return capability.PromiseInstance;
- }
- // h. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »).
- var nextPromise = promiseResolve.Call(thisObject, nextValue);
- // i. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], resultCapability.[[Reject]] »).
- _engine.Invoke(nextPromise, "then", [(JsValue) capability.Resolve, capability.RejectObj]);
- } while (true);
- }
- catch (JavaScriptException e)
- {
- try
- {
- iterator.Close(CompletionType.Throw);
- }
- catch (JavaScriptException)
- {
- // ignore any errors from closing the iterator
- }
- capability.Reject.Call(Undefined, e.Error);
- return capability.PromiseInstance;
- }
- // 9. Return Completion(result).
- // Note that PerformPromiseRace returns a Promise instance in success case
- return capability.PromiseInstance;
- }
- // https://tc39.es/ecma262/#sec-getpromiseresolve
- // 27.2.4.1.1 GetPromiseResolve ( promiseConstructor )
- // The abstract operation GetPromiseResolve takes argument promiseConstructor. It performs the following steps when called:
- //
- // 1. Assert: IsConstructor(promiseConstructor) is true.
- // 2. Let promiseResolve be ? Get(promiseConstructor, "resolve").
- // 3. If IsCallable(promiseResolve) is false, throw a TypeError exception.
- // 4. Return promiseResolve.
- private ICallable GetPromiseResolve(JsValue promiseConstructor)
- {
- AssertConstructor(_engine, promiseConstructor);
- var resolveProp = promiseConstructor.Get("resolve");
- if (resolveProp is ICallable resolve)
- {
- return resolve;
- }
- Throw.TypeError(_realm, "resolve is not a function");
- // Note: throws right before return
- return null;
- }
- // https://tc39.es/ecma262/#sec-newpromisecapability
- // The abstract operation NewPromiseCapability takes argument C.
- // It attempts to use C as a constructor in the fashion of the built-in Promise constructor to create a Promise
- // object and extract its resolve and reject functions.
- // The Promise object plus the resolve and reject functions are used to initialize a new PromiseCapability Record.
- // It performs the following steps when called:
- //
- // 1. If IsConstructor(C) is false, throw a TypeError exception.
- // 2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 27.2.3.1).
- // 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }.
- // 4. Let steps be the algorithm steps defined in GetCapabilitiesExecutor Functions.
- // 5. Let length be the number of non-optional parameters of the function definition in GetCapabilitiesExecutor Functions.
- // 6. Let executor be ! CreateBuiltinFunction(steps, length, "", « [[Capability]] »).
- // 7. Set executor.[[Capability]] to promiseCapability.
- // 8. Let promise be ? Construct(C, « executor »).
- // 9. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception.
- // 10. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception.
- // 11. Set promiseCapability.[[Promise]] to promise.
- // 12. Return promiseCapability.
- internal static PromiseCapability NewPromiseCapability(Engine engine, JsValue c)
- {
- var ctor = AssertConstructor(engine, c);
- JsValue? resolveArg = null;
- JsValue? rejectArg = null;
- JsValue Executor(JsValue thisObject, JsCallArguments arguments)
- {
- // 25.4.1.5.1 GetCapabilitiesExecutor Functions
- // 3. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception.
- // 4. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception.
- // 5. Set promiseCapability.[[Resolve]] to resolve.
- // 6. Set promiseCapability.[[Reject]] to reject.
- if (resolveArg is not null && resolveArg != Undefined ||
- rejectArg is not null && rejectArg != Undefined)
- {
- Throw.TypeError(engine.Realm, "executor was already called with not undefined args");
- }
- resolveArg = arguments.At(0);
- rejectArg = arguments.At(1);
- return Undefined;
- }
- var executor = new ClrFunction(engine, "", Executor, 2, PropertyFlag.Configurable);
- var instance = ctor.Construct([executor], c);
- ICallable? resolve = null;
- ICallable? reject = null;
- if (resolveArg is ICallable resFunc)
- {
- resolve = resFunc;
- }
- else
- {
- Throw.TypeError(engine.Realm, "resolve is not a function");
- }
- if (rejectArg is ICallable rejFunc)
- {
- reject = rejFunc;
- }
- else
- {
- Throw.TypeError(engine.Realm, "reject is not a function");
- }
- return new PromiseCapability(
- PromiseInstance: instance,
- Resolve: resolve,
- Reject: reject,
- RejectObj: rejectArg,
- ResolveObj: resolveArg);
- }
- }
|