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);
}
///
/// https://tc39.es/ecma262/#sec-promise-executor
///
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;
}
///
/// https://tc39.es/ecma262/#sec-promise.resolve
///
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;
}
///
/// https://tc39.es/ecma262/#sec-promise-resolve
///
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;
}
///
/// https://tc39.es/ecma262/#sec-promise.reject
///
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;
}
///
/// https://tc39.es/proposal-promise-try/
///
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();
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
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();
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
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();
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
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);
}
}