PromiseConstructor.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. using System.Collections.Generic;
  2. using Jint.Collections;
  3. using Jint.Native.Function;
  4. using Jint.Native.Iterator;
  5. using Jint.Native.Object;
  6. using Jint.Native.Symbol;
  7. using Jint.Runtime;
  8. using Jint.Runtime.Descriptors;
  9. using Jint.Runtime.Interop;
  10. namespace Jint.Native.Promise
  11. {
  12. internal sealed record PromiseCapability(
  13. JsValue PromiseInstance,
  14. ICallable Resolve,
  15. ICallable Reject,
  16. JsValue RejectObj
  17. );
  18. public sealed class PromiseConstructor : FunctionInstance, IConstructor
  19. {
  20. private static readonly JsString _functionName = new JsString("Promise");
  21. internal PromisePrototype PrototypeObject { get; private set; }
  22. internal PromiseConstructor(
  23. Engine engine,
  24. Realm realm,
  25. FunctionPrototype functionPrototype,
  26. ObjectPrototype objectPrototype)
  27. : base(engine, realm, _functionName)
  28. {
  29. _prototype = functionPrototype;
  30. PrototypeObject = new PromisePrototype(engine, realm, this, objectPrototype);
  31. _length = new PropertyDescriptor(1, PropertyFlag.Configurable);
  32. _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
  33. }
  34. protected override void Initialize()
  35. {
  36. const PropertyFlag propertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable;
  37. const PropertyFlag lengthFlags = PropertyFlag.Configurable;
  38. var properties = new PropertyDictionary(5, checkExistingKeys: false)
  39. {
  40. ["resolve"] =
  41. new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "resolve", Resolve, 1, lengthFlags),
  42. propertyFlags)),
  43. ["reject"] =
  44. new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "reject", Reject, 1, lengthFlags),
  45. propertyFlags)),
  46. ["all"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "all", All, 1, lengthFlags),
  47. propertyFlags)),
  48. ["race"] = new(new PropertyDescriptor(new ClrFunctionInstance(Engine, "race", Race, 1, lengthFlags),
  49. propertyFlags)),
  50. };
  51. SetProperties(properties);
  52. var symbols = new SymbolDictionary(1)
  53. {
  54. [GlobalSymbolRegistry.Species] = new GetSetPropertyDescriptor(
  55. get: new ClrFunctionInstance(_engine, "get [Symbol.species]", (thisObj, _) => thisObj, 0,
  56. PropertyFlag.Configurable),
  57. set: Undefined, PropertyFlag.Configurable)
  58. };
  59. SetSymbols(symbols);
  60. }
  61. public override JsValue Call(JsValue thisObject, JsValue[] arguments)
  62. {
  63. ExceptionHelper.ThrowTypeError(_realm, "Constructor Promise requires 'new'");
  64. return null;
  65. }
  66. public ObjectInstance Construct(JsValue[] arguments, JsValue newTarget)
  67. {
  68. if (newTarget.IsUndefined())
  69. {
  70. ExceptionHelper.ThrowTypeError(_realm, "Constructor Promise requires 'new'");
  71. }
  72. var promiseExecutor = arguments.At(0) as ICallable;
  73. if (promiseExecutor is null)
  74. {
  75. ExceptionHelper.ThrowTypeError(_realm, $"Promise executor {(arguments.At(0))} is not a function");
  76. }
  77. var instance = OrdinaryCreateFromConstructor(
  78. newTarget,
  79. static intrinsics => intrinsics.Promise.PrototypeObject,
  80. static(engine, realm, _) => new PromiseInstance(engine));
  81. var (resolve, reject) = instance.CreateResolvingFunctions();
  82. promiseExecutor.Call(Undefined, new JsValue[] {resolve, reject});
  83. return instance;
  84. }
  85. // The abstract operation PromiseResolve takes arguments C (a constructor) and x (an ECMAScript language value).
  86. // It returns a new promise resolved with x. It performs the following steps when called:
  87. //
  88. // 1. Assert: Type(C) is Object.
  89. // 2. If IsPromise(x) is true, then
  90. // a. Let xConstructor be ? Get(x, "constructor").
  91. // b. If SameValue(xConstructor, C) is true, return x.
  92. // 3. Let promiseCapability be ? NewPromiseCapability(C).
  93. // 4. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »).
  94. // 5. Return promiseCapability.[[Promise]].
  95. internal JsValue Resolve(JsValue thisObj, JsValue[] arguments)
  96. {
  97. if (!thisObj.IsObject())
  98. {
  99. ExceptionHelper.ThrowTypeError(_realm, "PromiseResolve called on non-object");
  100. }
  101. if (thisObj is not IConstructor)
  102. {
  103. ExceptionHelper.ThrowTypeError(_realm, "Promise.resolve invoked on a non-constructor value");
  104. }
  105. JsValue x = arguments.At(0);
  106. if (x.IsPromise())
  107. {
  108. var xConstructor = x.Get(CommonProperties.Constructor);
  109. if (SameValue(xConstructor, thisObj))
  110. {
  111. return x;
  112. }
  113. }
  114. var (instance, resolve, _, _) = NewPromiseCapability(_engine, thisObj);
  115. resolve.Call(Undefined, new[] {x});
  116. return instance;
  117. }
  118. private JsValue Reject(JsValue thisObj, JsValue[] arguments)
  119. {
  120. if (!thisObj.IsObject())
  121. {
  122. ExceptionHelper.ThrowTypeError(_realm, "Promise.reject called on non-object");
  123. }
  124. if (thisObj is not IConstructor)
  125. {
  126. ExceptionHelper.ThrowTypeError(_realm, "Promise.reject invoked on a non-constructor value");
  127. }
  128. var r = arguments.At(0);
  129. var (instance, _, reject, _) = NewPromiseCapability(_engine, thisObj);
  130. reject.Call(Undefined, new[] {r});
  131. return instance;
  132. }
  133. // https://tc39.es/ecma262/#sec-promise.all
  134. // The all function returns a new promise which is fulfilled with an array of fulfillment values for the passed promises,
  135. // or rejects with the reason of the first passed promise that rejects. It resolves all elements of the passed iterable to promises as it runs this algorithm.
  136. //
  137. // 1. Let C be the this value.
  138. // 2. Let promiseCapability be ? NewPromiseCapability(C).
  139. // 3. Let promiseResolve be GetPromiseResolve(C).
  140. // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
  141. // 5. Let iteratorRecord be GetIterator(iterable).
  142. // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
  143. // 7. Let result be PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve).
  144. // 8. If result is an abrupt completion, then
  145. // a. If iteratorRecord.[[Done]] is false, set result to IteratorClose(iteratorRecord, result).
  146. // b. IfAbruptRejectPromise(result, promiseCapability).
  147. // 9. Return Completion(result)
  148. private JsValue All(JsValue thisObj, JsValue[] arguments)
  149. {
  150. if (!thisObj.IsObject())
  151. {
  152. ExceptionHelper.ThrowTypeError(_realm, "Promise.all called on non-object");
  153. }
  154. //2. Let promiseCapability be ? NewPromiseCapability(C).
  155. var (resultingPromise, resolve, reject, rejectObj) = NewPromiseCapability(_engine, thisObj);
  156. //3. Let promiseResolve be GetPromiseResolve(C).
  157. // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
  158. ICallable promiseResolve;
  159. try
  160. {
  161. promiseResolve = GetPromiseResolve(thisObj);
  162. }
  163. catch (JavaScriptException e)
  164. {
  165. reject.Call(Undefined, new[] {e.Error});
  166. return resultingPromise;
  167. }
  168. IIterator iterator;
  169. // 5. Let iteratorRecord be GetIterator(iterable).
  170. // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
  171. try
  172. {
  173. if (arguments.Length == 0)
  174. {
  175. ExceptionHelper.ThrowTypeError(_realm, "no arguments were passed to Promise.all");
  176. }
  177. var iterable = arguments.At(0);
  178. iterator = iterable.GetIterator(_realm);
  179. }
  180. catch (JavaScriptException e)
  181. {
  182. reject.Call(Undefined, new[] {e.Error});
  183. return resultingPromise;
  184. }
  185. var results = new List<JsValue>();
  186. bool doneIterating = false;
  187. void ResolveIfFinished()
  188. {
  189. // that means all of them were resolved
  190. // Note that "Undefined" is not null, thus the logic is sound, even though awkward
  191. // also note that it is important to check if we are done iterating.
  192. // if "then" method is sync then it will be resolved BEFORE the next iteration cycle
  193. if (results.TrueForAll(static x => x != null) && doneIterating)
  194. {
  195. var array = _realm.Intrinsics.Array.ConstructFast(results);
  196. resolve.Call(Undefined, new JsValue[] { array });
  197. }
  198. }
  199. // 27.2.4.1.2 PerformPromiseAll ( iteratorRecord, constructor, resultCapability, promiseResolve )
  200. // https://tc39.es/ecma262/#sec-performpromiseall
  201. try
  202. {
  203. int index = 0;
  204. do
  205. {
  206. JsValue value;
  207. try
  208. {
  209. if (!iterator.TryIteratorStep(out var nextItem))
  210. {
  211. doneIterating = true;
  212. ResolveIfFinished();
  213. break;
  214. }
  215. value = nextItem.Get(CommonProperties.Value);
  216. }
  217. catch (JavaScriptException e)
  218. {
  219. reject.Call(Undefined, new[] {e.Error});
  220. return resultingPromise;
  221. }
  222. // note that null here is important
  223. // it will help to detect if all inner promises were resolved
  224. // In F# it would be Option<JsValue>
  225. results.Add(null);
  226. var item = promiseResolve.Call(thisObj, new JsValue[] {value});
  227. var thenProps = item.Get("then");
  228. if (thenProps is ICallable thenFunc)
  229. {
  230. var capturedIndex = index;
  231. var fulfilled = false;
  232. var onSuccess =
  233. new ClrFunctionInstance(_engine, "", (_, args) =>
  234. {
  235. if (!fulfilled)
  236. {
  237. fulfilled = true;
  238. results[capturedIndex] = args.At(0);
  239. ResolveIfFinished();
  240. }
  241. return Undefined;
  242. }, 1, PropertyFlag.Configurable);
  243. thenFunc.Call(item, new JsValue[] {onSuccess, rejectObj});
  244. }
  245. else
  246. {
  247. ExceptionHelper.ThrowTypeError(_realm, "Passed non Promise-like value");
  248. }
  249. index += 1;
  250. } while (true);
  251. }
  252. catch (JavaScriptException e)
  253. {
  254. iterator.Close(CompletionType.Throw);
  255. reject.Call(Undefined, new[] {e.Error});
  256. return resultingPromise;
  257. }
  258. // if there were not items but the iteration was successful
  259. // e.g. "[]" empty array as an example
  260. // resolve the promise sync
  261. if (results.Count == 0)
  262. {
  263. resolve.Call(Undefined, new JsValue[] {_realm.Intrinsics.Array.ConstructFast(0)});
  264. }
  265. return resultingPromise;
  266. }
  267. // https://tc39.es/ecma262/#sec-promise.race
  268. private JsValue Race(JsValue thisObj, JsValue[] arguments)
  269. {
  270. if (!thisObj.IsObject())
  271. {
  272. ExceptionHelper.ThrowTypeError(_realm, "Promise.all called on non-object");
  273. }
  274. // 2. Let promiseCapability be ? NewPromiseCapability(C).
  275. var (resultingPromise, resolve, reject, rejectObj) = NewPromiseCapability(_engine, thisObj);
  276. // 3. Let promiseResolve be GetPromiseResolve(C).
  277. // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability).
  278. ICallable promiseResolve;
  279. try
  280. {
  281. promiseResolve = GetPromiseResolve(thisObj);
  282. }
  283. catch (JavaScriptException e)
  284. {
  285. reject.Call(Undefined, new[] {e.Error});
  286. return resultingPromise;
  287. }
  288. IIterator iterator;
  289. // 5. Let iteratorRecord be GetIterator(iterable).
  290. // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability).
  291. try
  292. {
  293. if (arguments.Length == 0)
  294. {
  295. ExceptionHelper.ThrowTypeError(_realm, "no arguments were passed to Promise.all");
  296. }
  297. var iterable = arguments.At(0);
  298. iterator = iterable.GetIterator(_realm);
  299. }
  300. catch (JavaScriptException e)
  301. {
  302. reject.Call(Undefined, new[] {e.Error});
  303. return resultingPromise;
  304. }
  305. // 7. Let result be PerformPromiseRace(iteratorRecord, C, promiseCapability, promiseResolve).
  306. // https://tc39.es/ecma262/#sec-performpromiserace
  307. try
  308. {
  309. do
  310. {
  311. JsValue nextValue;
  312. try
  313. {
  314. if (!iterator.TryIteratorStep(out var nextItem))
  315. {
  316. break;
  317. }
  318. nextValue = nextItem.Get(CommonProperties.Value);
  319. }
  320. catch (JavaScriptException e)
  321. {
  322. reject.Call(Undefined, new[] {e.Error});
  323. return resultingPromise;
  324. }
  325. // h. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »).
  326. var nextPromise = promiseResolve.Call(thisObj, new JsValue[] {nextValue});
  327. // i. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], resultCapability.[[Reject]] »).
  328. _engine.Invoke(nextPromise, "then", new[] {resolve as JsValue, rejectObj});
  329. } while (true);
  330. }
  331. catch (JavaScriptException e)
  332. {
  333. // 8. If result is an abrupt completion, then
  334. // a. If iteratorRecord.[[Done]] is false, set result to IteratorClose(iteratorRecord, result).
  335. // b. IfAbruptRejectPromise(result, promiseCapability).
  336. iterator.Close(CompletionType.Throw);
  337. reject.Call(Undefined, new[] {e.Error});
  338. return resultingPromise;
  339. }
  340. // 9. Return Completion(result).
  341. // Note that PerformPromiseRace returns a Promise instance in success case
  342. return resultingPromise;
  343. }
  344. // https://tc39.es/ecma262/#sec-getpromiseresolve
  345. // 27.2.4.1.1 GetPromiseResolve ( promiseConstructor )
  346. // The abstract operation GetPromiseResolve takes argument promiseConstructor. It performs the following steps when called:
  347. //
  348. // 1. Assert: IsConstructor(promiseConstructor) is true.
  349. // 2. Let promiseResolve be ? Get(promiseConstructor, "resolve").
  350. // 3. If IsCallable(promiseResolve) is false, throw a TypeError exception.
  351. // 4. Return promiseResolve.
  352. private ICallable GetPromiseResolve(JsValue promiseConstructor)
  353. {
  354. AssertConstructor(_engine, promiseConstructor);
  355. var resolveProp = promiseConstructor.Get("resolve");
  356. if (resolveProp is ICallable resolve)
  357. {
  358. return resolve;
  359. }
  360. ExceptionHelper.ThrowTypeError(_realm, "resolve is not a function");
  361. // Note: throws right before return
  362. return null;
  363. }
  364. // https://tc39.es/ecma262/#sec-newpromisecapability
  365. // The abstract operation NewPromiseCapability takes argument C.
  366. // It attempts to use C as a constructor in the fashion of the built-in Promise constructor to create a Promise
  367. // object and extract its resolve and reject functions.
  368. // The Promise object plus the resolve and reject functions are used to initialize a new PromiseCapability Record.
  369. // It performs the following steps when called:
  370. //
  371. // 1. If IsConstructor(C) is false, throw a TypeError exception.
  372. // 2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 27.2.3.1).
  373. // 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }.
  374. // 4. Let steps be the algorithm steps defined in GetCapabilitiesExecutor Functions.
  375. // 5. Let length be the number of non-optional parameters of the function definition in GetCapabilitiesExecutor Functions.
  376. // 6. Let executor be ! CreateBuiltinFunction(steps, length, "", « [[Capability]] »).
  377. // 7. Set executor.[[Capability]] to promiseCapability.
  378. // 8. Let promise be ? Construct(C, « executor »).
  379. // 9. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception.
  380. // 10. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception.
  381. // 11. Set promiseCapability.[[Promise]] to promise.
  382. // 12. Return promiseCapability.
  383. internal static PromiseCapability NewPromiseCapability(Engine engine, JsValue c)
  384. {
  385. var ctor = AssertConstructor(engine, c);
  386. JsValue resolveArg = null;
  387. JsValue rejectArg = null;
  388. JsValue Executor(JsValue thisObj, JsValue[] arguments)
  389. {
  390. // 25.4.1.5.1 GetCapabilitiesExecutor Functions
  391. // 3. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception.
  392. // 4. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception.
  393. // 5. Set promiseCapability.[[Resolve]] to resolve.
  394. // 6. Set promiseCapability.[[Reject]] to reject.
  395. if (resolveArg != null && resolveArg != Undefined ||
  396. rejectArg != null && rejectArg != Undefined)
  397. {
  398. ExceptionHelper.ThrowTypeError(engine.Realm, "executor was already called with not undefined args");
  399. }
  400. resolveArg = arguments.At(0);
  401. rejectArg = arguments.At(1);
  402. return Undefined;
  403. }
  404. var executor = new ClrFunctionInstance(engine, "", Executor, 2, PropertyFlag.Configurable);
  405. var instance = ctor.Construct(new JsValue[] {executor}, c);
  406. ICallable resolve = null;
  407. ICallable reject = null;
  408. if (resolveArg is ICallable resFunc)
  409. {
  410. resolve = resFunc;
  411. }
  412. else
  413. {
  414. ExceptionHelper.ThrowTypeError(engine.Realm, "resolve is not a function");
  415. }
  416. if (rejectArg is ICallable rejFunc)
  417. {
  418. reject = rejFunc;
  419. }
  420. else
  421. {
  422. ExceptionHelper.ThrowTypeError(engine.Realm, "reject is not a function");
  423. }
  424. return new PromiseCapability(instance, resolve, reject, rejectArg);
  425. }
  426. }
  427. }