From 45bb1c4ff94c84597acf1bc2de767c8d6e3077bc Mon Sep 17 00:00:00 2001 From: Denis Pushkarev Date: Mon, 22 May 2023 15:02:24 +0700 Subject: [PATCH] add `Promise.withResolvers` Stage 2 proposal https://github.com/tc39/proposal-promise-with-resolvers --- CHANGELOG.md | 6 +- README.md | 21 ++++++- packages/core-js-compat/src/data.mjs | 2 + .../src/modules-by-versions.mjs | 1 + packages/core-js/full/promise/index.js | 3 +- .../core-js/full/promise/with-resolvers.js | 13 +++++ .../modules/esnext.promise.with-resolvers.js | 16 ++++++ .../proposals/promise-with-resolvers.js | 2 + packages/core-js/stage/2.js | 1 + tests/compat/tests.js | 3 + tests/entries/unit.mjs | 2 + .../esnext.promise.with-resolvers.js | 56 +++++++++++++++++++ .../esnext.promise.with-resolvers.js | 55 ++++++++++++++++++ 13 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 packages/core-js/full/promise/with-resolvers.js create mode 100644 packages/core-js/modules/esnext.promise.with-resolvers.js create mode 100644 packages/core-js/proposals/promise-with-resolvers.js create mode 100644 tests/unit-global/esnext.promise.with-resolvers.js create mode 100644 tests/unit-pure/esnext.promise.with-resolvers.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ad913a3a9b0e..4a750ca2ee74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,8 @@ ##### Unreleased - [Well-formed unicode strings proposal](https://github.com/tc39/proposal-is-usv-string): - Methods: - - `String.prototype.isWellFormed` - - `String.prototype.toWellFormed` + - `String.prototype.isWellFormed` method + - `String.prototype.toWellFormed` method - Moved to stable ES, [May 2023 TC39 meeting](https://github.com/tc39/proposal-is-usv-string/pull/31) - Added `es.` namespace modules, `/es/` and `/stable/` namespaces entries - [Decorator Metadata proposal](https://github.com/tc39/proposal-decorator-metadata): @@ -13,6 +13,8 @@ - [Iterator Helpers Stage 3 proposal](https://github.com/tc39/proposal-iterator-helpers): - Changed `Symbol.iterator` fallback from callable check to `undefined` / `null` check, May 2023 TC39 meeting, [proposal-iterator-helpers/272](https://github.com/tc39/proposal-iterator-helpers/pull/272) - Removed `IsCallable` check on `NextMethod`, deferring errors to `Call` site, May 2023 TC39 meeting, [proposal-iterator-helpers/274](https://github.com/tc39/proposal-iterator-helpers/pull/274) +- Added [`Promise.withResolvers` Stage 2 proposal](https://github.com/tc39/proposal-promise-with-resolvers): + - `Promise.withResolvers` method - [`Symbol` predicates stage 2 proposal](https://github.com/tc39/proposal-symbol-predicates): - The methods renamed to end with `Symbol`, [May 2023 TC39 meeting](https://github.com/babel/proposals/issues/88#issuecomment-1548219580): - `Symbol.isRegistered` -> `Symbol.isRegisteredSymbol` diff --git a/README.md b/README.md index 0a13b379e0f9..f40ad4118489 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ structuredClone(new Set([1, 2, 3])); // => new Set([1, 2, 3]) - [Stage 2 proposals](#stage-2-proposals) - [`AsyncIterator` helpers](#asynciterator-helpers) - [`Iterator.range`](#iteratorrange) + - [`Promise.withResolvers`](#promisewithresolvers) - [`Map.prototype.emplace`](#mapprototypeemplace) - [`Array.isTemplateObject`](#arrayistemplateobject) - [`String.dedent`](#stringdedent) @@ -1149,7 +1150,7 @@ class Promise { static reject(r: any): Promise; static all(iterable: Iterable): Promise; static allSettled(iterable: Iterable): Promise; - static any(promises: Iterable): Promise; + static any(promises: Iterable): Promise; static race(iterable: Iterable): Promise; } ``` @@ -2512,6 +2513,24 @@ for (const i of Iterator.range(1, 10, { step: 3, inclusive: true })) { console.log(i); // => 1, 4, 7, 10 } ``` +##### [`Promise.withResolvers`](https://github.com/tc39/proposal-promise-with-resolvers)[⬆](#index) +Module [`esnext.promise.with-resolvers`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/esnext.promise.with-resolvers.js) +```js +class Promise { + static withResolvers(): { promise: Promise, resolve: function, reject: function }; +} +``` +[*CommonJS entry points:*](#commonjs-api) +```js +core-js/proposals/promise-with-resolvers +core-js(-pure)/full/promise/with-resolvers +``` +[*Examples*](https://tinyurl.com/2gx4t3xu): +```js +const d = Promise.withResolvers(); +d.resolve(42); +d.promise.then(console.log); // => 42 +``` ##### [`Map.prototype.emplace`](https://github.com/thumbsupep/proposal-upsert)[⬆](#index) Modules [`esnext.map.emplace`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/esnext.map.emplace.js) and [`esnext.weak-map.emplace`](https://github.com/zloirock/core-js/blob/master/packages/core-js/modules/esnext.weak-map.emplace.js) ```js diff --git a/packages/core-js-compat/src/data.mjs b/packages/core-js-compat/src/data.mjs index 9f7f66f72161..f53de9676bfe 100644 --- a/packages/core-js-compat/src/data.mjs +++ b/packages/core-js-compat/src/data.mjs @@ -2128,6 +2128,8 @@ export const data = { // TODO: Remove from `core-js@4` 'esnext.promise.try': { }, + 'esnext.promise.with-resolvers': { + }, // TODO: Remove from `core-js@4` 'esnext.reflect.define-metadata': { }, diff --git a/packages/core-js-compat/src/modules-by-versions.mjs b/packages/core-js-compat/src/modules-by-versions.mjs index 66bb7c5526c3..f13b1820a067 100644 --- a/packages/core-js-compat/src/modules-by-versions.mjs +++ b/packages/core-js-compat/src/modules-by-versions.mjs @@ -206,6 +206,7 @@ export default { 'es.string.is-well-formed', 'es.string.to-well-formed', 'esnext.function.metadata', + 'esnext.promise.with-resolvers', 'esnext.symbol.is-registered-symbol', 'esnext.symbol.is-well-known-symbol', ], diff --git a/packages/core-js/full/promise/index.js b/packages/core-js/full/promise/index.js index c45bb5883d37..deabfaed987d 100644 --- a/packages/core-js/full/promise/index.js +++ b/packages/core-js/full/promise/index.js @@ -1,6 +1,7 @@ var parent = require('../../actual/promise'); -require('../../modules/esnext.aggregate-error'); +require('../../modules/esnext.promise.with-resolvers'); // TODO: Remove from `core-js@4` +require('../../modules/esnext.aggregate-error'); require('../../modules/esnext.promise.all-settled'); require('../../modules/esnext.promise.try'); require('../../modules/esnext.promise.any'); diff --git a/packages/core-js/full/promise/with-resolvers.js b/packages/core-js/full/promise/with-resolvers.js new file mode 100644 index 000000000000..008e2c985293 --- /dev/null +++ b/packages/core-js/full/promise/with-resolvers.js @@ -0,0 +1,13 @@ +'use strict'; +require('../../modules/es.promise'); +require('../../modules/esnext.promise.with-resolvers'); +var call = require('../../internals/function-call'); +var isCallable = require('../../internals/is-callable'); +var path = require('../../internals/path'); + +var Promise = path.Promise; +var promiseWithResolvers = Promise.withResolvers; + +module.exports = { withResolvers: function withResolvers() { + return call(promiseWithResolvers, isCallable(this) ? this : Promise); +} }.withResolvers; diff --git a/packages/core-js/modules/esnext.promise.with-resolvers.js b/packages/core-js/modules/esnext.promise.with-resolvers.js new file mode 100644 index 000000000000..82734606e2c2 --- /dev/null +++ b/packages/core-js/modules/esnext.promise.with-resolvers.js @@ -0,0 +1,16 @@ +'use strict'; +var $ = require('../internals/export'); +var newPromiseCapabilityModule = require('../internals/new-promise-capability'); + +// `Promise.withResolvers` method +// https://github.com/tc39/proposal-promise-with-resolvers +$({ target: 'Promise', stat: true, forced: true }, { + withResolvers: function withResolvers() { + var promiseCapability = newPromiseCapabilityModule.f(this); + return { + promise: promiseCapability.promise, + resolve: promiseCapability.resolve, + reject: promiseCapability.reject + }; + } +}); diff --git a/packages/core-js/proposals/promise-with-resolvers.js b/packages/core-js/proposals/promise-with-resolvers.js new file mode 100644 index 000000000000..56249aa7b45b --- /dev/null +++ b/packages/core-js/proposals/promise-with-resolvers.js @@ -0,0 +1,2 @@ +// https://github.com/tc39/proposal-promise-with-resolvers +require('../modules/esnext.promise.with-resolvers'); diff --git a/packages/core-js/stage/2.js b/packages/core-js/stage/2.js index 5b0190ab3369..07c6591b2718 100644 --- a/packages/core-js/stage/2.js +++ b/packages/core-js/stage/2.js @@ -5,6 +5,7 @@ require('../proposals/async-explicit-resource-management'); require('../proposals/async-iterator-helpers'); require('../proposals/iterator-range'); require('../proposals/map-upsert-stage-2'); +require('../proposals/promise-with-resolvers'); require('../proposals/string-dedent'); require('../proposals/symbol-predicates-v2'); // TODO: Obsolete versions, remove from `core-js@4` diff --git a/tests/compat/tests.js b/tests/compat/tests.js index f1c82e4e3d66..5a72572ba51d 100644 --- a/tests/compat/tests.js +++ b/tests/compat/tests.js @@ -1703,6 +1703,9 @@ GLOBAL.tests = { 'esnext.observable.of': function () { return Observable.of; }, + 'esnext.promise.with-resolvers': [PROMISES_SUPPORT, function () { + return Promise.withResolvers; + }], 'esnext.set.add-all': function () { return Set.prototype.addAll; }, diff --git a/tests/entries/unit.mjs b/tests/entries/unit.mjs index eeb32d220c05..9a66513c5977 100644 --- a/tests/entries/unit.mjs +++ b/tests/entries/unit.mjs @@ -808,6 +808,7 @@ for (PATH of ['core-js-pure', 'core-js']) { ok(typeof load(NS, 'reflect/has-own-metadata') == 'function'); ok(typeof load(NS, 'reflect/metadata') == 'function'); ok(load(NS, 'promise/try')(() => 42) instanceof load(NS, 'promise')); + ok(load(NS, 'promise/with-resolvers')().promise instanceof load(NS, 'promise')); ok(load(NS, 'set/add-all')(new Set([1, 2, 3]), 4, 5).size === 5); ok(load(NS, 'set/delete-all')(new Set([1, 2, 3]), 4, 5) === false); ok(load(NS, 'set/every')(new Set([1, 2, 3]), it => typeof it == 'number')); @@ -940,6 +941,7 @@ for (PATH of ['core-js-pure', 'core-js']) { load('proposals/promise-any'); load('proposals/promise-finally'); load('proposals/promise-try'); + load('proposals/promise-with-resolvers'); load('proposals/reflect-metadata'); load('proposals/regexp-dotall-flag'); load('proposals/regexp-named-groups'); diff --git a/tests/unit-global/esnext.promise.with-resolvers.js b/tests/unit-global/esnext.promise.with-resolvers.js new file mode 100644 index 000000000000..b786a6bcbcd9 --- /dev/null +++ b/tests/unit-global/esnext.promise.with-resolvers.js @@ -0,0 +1,56 @@ +const { getPrototypeOf } = Object; + +QUnit.test('Promise.withResolvers', assert => { + const { withResolvers } = Promise; + assert.isFunction(withResolvers); + assert.arity(withResolvers, 0); + assert.name(withResolvers, 'withResolvers'); + assert.nonEnumerable(Promise, 'withResolvers'); + assert.looksNative(withResolvers); + + const d1 = Promise.withResolvers(); + assert.same(getPrototypeOf(d1), Object.prototype, 'proto is Object.prototype'); + assert.true(d1.promise instanceof Promise, 'promise is promise'); + assert.isFunction(d1.resolve, 'resolve is function'); + assert.isFunction(d1.reject, 'reject is function'); + + const promise = {}; + const resolve = () => { /* empty */ }; + let reject = () => { /* empty */ }; + + function P(exec) { + exec(resolve, reject); + return promise; + } + + const d2 = withResolvers.call(P); + assert.same(d2.promise, promise, 'promise is promise #2'); + assert.same(d2.resolve, resolve, 'resolve is resolve #2'); + assert.same(d2.reject, reject, 'reject is reject #2'); + + reject = {}; + + assert.throws(() => withResolvers.call(P), TypeError, 'broken resolver'); + assert.throws(() => withResolvers.call({}), TypeError, 'broken constructor #1'); + assert.throws(() => withResolvers.call(null), TypeError, 'broken constructor #2'); +}); + +QUnit.test('Promise.withResolvers, resolve', assert => { + const d = Promise.withResolvers(); + d.resolve(42); + return d.promise.then(it => { + assert.same(it, 42, 'resolved as expected'); + }, () => { + assert.avoid(); + }); +}); + +QUnit.test('Promise.withResolvers, reject', assert => { + const d = Promise.withResolvers(); + d.reject(42); + return d.promise.then(() => { + assert.avoid(); + }, error => { + assert.same(error, 42, 'rejected as expected'); + }); +}); diff --git a/tests/unit-pure/esnext.promise.with-resolvers.js b/tests/unit-pure/esnext.promise.with-resolvers.js new file mode 100644 index 000000000000..10b02a2ca412 --- /dev/null +++ b/tests/unit-pure/esnext.promise.with-resolvers.js @@ -0,0 +1,55 @@ +import Promise from 'core-js-pure/full/promise'; +import getPrototypeOf from 'core-js-pure/es/object/get-prototype-of'; + +QUnit.test('Promise.withResolvers', assert => { + const { withResolvers } = Promise; + assert.isFunction(withResolvers); + assert.arity(withResolvers, 0); + assert.name(withResolvers, 'withResolvers'); + + const d1 = Promise.withResolvers(); + assert.same(getPrototypeOf(d1), Object.prototype, 'proto is Object.prototype'); + assert.true(d1.promise instanceof Promise, 'promise is promise'); + assert.isFunction(d1.resolve, 'resolve is function'); + assert.isFunction(d1.reject, 'reject is function'); + + const promise = {}; + const resolve = () => { /* empty */ }; + let reject = () => { /* empty */ }; + + function P(exec) { + exec(resolve, reject); + return promise; + } + + const d2 = withResolvers.call(P); + assert.same(d2.promise, promise, 'promise is promise #2'); + assert.same(d2.resolve, resolve, 'resolve is resolve #2'); + assert.same(d2.reject, reject, 'reject is reject #2'); + + reject = {}; + + assert.throws(() => withResolvers.call(P), TypeError, 'broken resolver'); + assert.throws(() => withResolvers.call({}), TypeError, 'broken constructor #1'); + assert.throws(() => withResolvers.call(null), TypeError, 'broken constructor #2'); +}); + +QUnit.test('Promise.withResolvers, resolve', assert => { + const d = Promise.withResolvers(); + d.resolve(42); + return d.promise.then(it => { + assert.same(it, 42, 'resolved as expected'); + }, () => { + assert.avoid(); + }); +}); + +QUnit.test('Promise.withResolvers, reject', assert => { + const d = Promise.withResolvers(); + d.reject(42); + return d.promise.then(() => { + assert.avoid(); + }, error => { + assert.same(error, 42, 'rejected as expected'); + }); +});