From ea4b71b5f011f5806f92afdcc2b8c9402205701e Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Fri, 6 Mar 2026 16:13:05 +0200 Subject: [PATCH] Implement Promise.allKeyed() and Promise.allSettledKeyed() (await-dictionary) Add support for the TC39 Stage 2.7 await-dictionary proposal which adds Promise.allKeyed() and Promise.allSettledKeyed() static methods. These work like Promise.all/allSettled but accept an object and return an object with the same keys mapped to resolved values. Co-Authored-By: Claude Opus 4.6 --- .../Test262Harness.settings.json | 1 - Jint/Native/Promise/PromiseConstructor.cs | 199 +++++++++++++++++- 2 files changed, 198 insertions(+), 2 deletions(-) diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index 06a949edd3..378084e038 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -5,7 +5,6 @@ "Namespace": "Jint.Tests.Test262", "Parallel": true, "ExcludedFeatures": [ - "await-dictionary", "import-bytes", "import-defer", "regexp-lookbehind", diff --git a/Jint/Native/Promise/PromiseConstructor.cs b/Jint/Native/Promise/PromiseConstructor.cs index bebb47b390..cb8e655c07 100644 --- a/Jint/Native/Promise/PromiseConstructor.cs +++ b/Jint/Native/Promise/PromiseConstructor.cs @@ -38,10 +38,12 @@ protected override void Initialize() { const PropertyFlag PropertyFlags = PropertyFlag.Configurable | PropertyFlag.Writable; const PropertyFlag LengthFlags = PropertyFlag.Configurable; - var properties = new PropertyDictionary(8, checkExistingKeys: false) + var properties = new PropertyDictionary(10, checkExistingKeys: false) { ["all"] = new(new PropertyDescriptor(new ClrFunction(Engine, "all", All, 1, LengthFlags), PropertyFlags)), + ["allKeyed"] = new(new PropertyDescriptor(new ClrFunction(Engine, "allKeyed", AllKeyed, 1, LengthFlags), PropertyFlags)), ["allSettled"] = new(new PropertyDescriptor(new ClrFunction(Engine, "allSettled", AllSettled, 1, LengthFlags), PropertyFlags)), + ["allSettledKeyed"] = new(new PropertyDescriptor(new ClrFunction(Engine, "allSettledKeyed", AllSettledKeyed, 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)), @@ -207,6 +209,201 @@ private JsValue Try(JsValue thisObject, JsCallArguments arguments) return promiseCapability.PromiseInstance; } + // https://tc39.es/proposal-await-dictionary/#sec-promise.allkeyed + private JsValue AllKeyed(JsValue thisObject, JsCallArguments arguments) + { + if (!thisObject.IsObject()) + { + Throw.TypeError(_realm, "Promise.allKeyed called on non-object"); + } + + var capability = NewPromiseCapability(_engine, thisObject); + + ICallable promiseResolve; + try + { + promiseResolve = GetPromiseResolve(thisObject); + } + catch (JavaScriptException e) + { + capability.Reject.Call(Undefined, e.Error); + return capability.PromiseInstance; + } + + try + { + var promises = arguments.At(0); + if (!promises.IsObject()) + { + Throw.TypeError(_realm, "Promise.allKeyed requires an object argument"); + } + + PerformPromiseAllKeyed(allSettled: false, (ObjectInstance) promises, thisObject, capability, promiseResolve); + } + catch (JavaScriptException e) + { + capability.Reject.Call(Undefined, e.Error); + } + + return capability.PromiseInstance; + } + + // https://tc39.es/proposal-await-dictionary/#sec-promise.allsettledkeyed + private JsValue AllSettledKeyed(JsValue thisObject, JsCallArguments arguments) + { + if (!thisObject.IsObject()) + { + Throw.TypeError(_realm, "Promise.allSettledKeyed called on non-object"); + } + + var capability = NewPromiseCapability(_engine, thisObject); + + ICallable promiseResolve; + try + { + promiseResolve = GetPromiseResolve(thisObject); + } + catch (JavaScriptException e) + { + capability.Reject.Call(Undefined, e.Error); + return capability.PromiseInstance; + } + + try + { + var promises = arguments.At(0); + if (!promises.IsObject()) + { + Throw.TypeError(_realm, "Promise.allSettledKeyed requires an object argument"); + } + + PerformPromiseAllKeyed(allSettled: true, (ObjectInstance) promises, thisObject, capability, promiseResolve); + } + catch (JavaScriptException e) + { + capability.Reject.Call(Undefined, e.Error); + } + + return capability.PromiseInstance; + } + + // https://tc39.es/proposal-await-dictionary/#sec-performpromiseallkeyed + private void PerformPromiseAllKeyed( + bool allSettled, + ObjectInstance promises, + JsValue constructor, + PromiseCapability resultCapability, + ICallable promiseResolve) + { + var allKeys = promises.GetOwnPropertyKeys(); + var keys = new List(); + var values = new List(); + var remainingElementsCount = 1; + var index = 0; + + foreach (var key in allKeys) + { + var desc = promises.GetOwnProperty(key); + if (desc == PropertyDescriptor.Undefined || !desc.Enumerable) + { + continue; + } + + keys.Add(key); + values.Add(null!); + + var value = promises.Get(key); + var nextPromise = promiseResolve.Call(constructor, value); + + var capturedIndex = index; + var alreadyCalled = false; + + if (allSettled) + { + var onFulfilled = 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)); + values[capturedIndex] = res; + remainingElementsCount--; + if (remainingElementsCount == 0) + { + var resultObj = CreateKeyedPromiseCombinatorResultObject(keys, values); + resultCapability.Resolve.Call(Undefined, resultObj); + } + } + return Undefined; + }, 1, PropertyFlag.Configurable); + + var onRejected = 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)); + values[capturedIndex] = res; + remainingElementsCount--; + if (remainingElementsCount == 0) + { + var resultObj = CreateKeyedPromiseCombinatorResultObject(keys, values); + resultCapability.Resolve.Call(Undefined, resultObj); + } + } + return Undefined; + }, 1, PropertyFlag.Configurable); + + remainingElementsCount++; + _engine.Invoke(nextPromise, "then", [onFulfilled, onRejected]); + } + else + { + var onFulfilled = new ClrFunction(_engine, "", (_, args) => + { + if (!alreadyCalled) + { + alreadyCalled = true; + values[capturedIndex] = args.At(0); + remainingElementsCount--; + if (remainingElementsCount == 0) + { + var resultObj = CreateKeyedPromiseCombinatorResultObject(keys, values); + resultCapability.Resolve.Call(Undefined, resultObj); + } + } + return Undefined; + }, 1, PropertyFlag.Configurable); + + remainingElementsCount++; + _engine.Invoke(nextPromise, "then", [(JsValue) onFulfilled, resultCapability.RejectObj]); + } + + index++; + } + + remainingElementsCount--; + if (remainingElementsCount == 0) + { + var resultObj = CreateKeyedPromiseCombinatorResultObject(keys, values); + resultCapability.Resolve.Call(Undefined, resultObj); + } + } + + private JsObject CreateKeyedPromiseCombinatorResultObject(List keys, List values) + { + var obj = OrdinaryObjectCreate(_engine, null); + for (var i = 0; i < keys.Count; i++) + { + obj.CreateDataPropertyOrThrow(keys[i], values[i]); + } + return obj; + } + // 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