зеркало из https://github.com/mozilla/gecko-dev.git
Backed out 2 changesets (bug 1313049) for various promise-related failures a=backout CLOSED TREE
Backed out changeset 8c08e1aca9ea (bug 1313049) Backed out changeset 72764ba31b81 (bug 1313049)
This commit is contained in:
Родитель
78f5a63960
Коммит
b3a0b0ef15
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -12,31 +12,12 @@
|
|||
|
||||
namespace js {
|
||||
|
||||
enum PromiseSlots {
|
||||
PromiseSlot_Flags = 0,
|
||||
PromiseSlot_ReactionsOrResult,
|
||||
PromiseSlot_RejectFunction,
|
||||
PromiseSlot_AllocationSite,
|
||||
PromiseSlot_ResolutionSite,
|
||||
PromiseSlot_AllocationTime,
|
||||
PromiseSlot_ResolutionTime,
|
||||
PromiseSlot_Id,
|
||||
PromiseSlots,
|
||||
};
|
||||
|
||||
#define PROMISE_FLAG_RESOLVED 0x1
|
||||
#define PROMISE_FLAG_FULFILLED 0x2
|
||||
#define PROMISE_FLAG_HANDLED 0x4
|
||||
#define PROMISE_FLAG_REPORTED 0x8
|
||||
#define PROMISE_FLAG_DEFAULT_RESOLVE_FUNCTION 0x10
|
||||
#define PROMISE_FLAG_DEFAULT_REJECT_FUNCTION 0x20
|
||||
|
||||
class AutoSetNewObjectMetadata;
|
||||
|
||||
class PromiseObject : public NativeObject
|
||||
{
|
||||
public:
|
||||
static const unsigned RESERVED_SLOTS = PromiseSlots;
|
||||
static const unsigned RESERVED_SLOTS = 8;
|
||||
static const Class class_;
|
||||
static const Class protoClass_;
|
||||
static PromiseObject* create(JSContext* cx, HandleObject executor,
|
||||
|
@ -46,7 +27,7 @@ class PromiseObject : public NativeObject
|
|||
static JSObject* unforgeableReject(JSContext* cx, HandleValue value);
|
||||
|
||||
JS::PromiseState state() {
|
||||
int32_t flags = getFixedSlot(PromiseSlot_Flags).toInt32();
|
||||
int32_t flags = getFixedSlot(PROMISE_FLAGS_SLOT).toInt32();
|
||||
if (!(flags & PROMISE_FLAG_RESOLVED)) {
|
||||
MOZ_ASSERT(!(flags & PROMISE_FLAG_FULFILLED));
|
||||
return JS::PromiseState::Pending;
|
||||
|
@ -57,11 +38,11 @@ class PromiseObject : public NativeObject
|
|||
}
|
||||
Value value() {
|
||||
MOZ_ASSERT(state() == JS::PromiseState::Fulfilled);
|
||||
return getFixedSlot(PromiseSlot_ReactionsOrResult);
|
||||
return getFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT);
|
||||
}
|
||||
Value reason() {
|
||||
MOZ_ASSERT(state() == JS::PromiseState::Rejected);
|
||||
return getFixedSlot(PromiseSlot_ReactionsOrResult);
|
||||
return getFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT);
|
||||
}
|
||||
|
||||
MOZ_MUST_USE bool resolve(JSContext* cx, HandleValue resolutionValue);
|
||||
|
@ -69,13 +50,13 @@ class PromiseObject : public NativeObject
|
|||
|
||||
void onSettled(JSContext* cx);
|
||||
|
||||
double allocationTime() { return getFixedSlot(PromiseSlot_AllocationTime).toNumber(); }
|
||||
double resolutionTime() { return getFixedSlot(PromiseSlot_ResolutionTime).toNumber(); }
|
||||
double allocationTime() { return getFixedSlot(PROMISE_ALLOCATION_TIME_SLOT).toNumber(); }
|
||||
double resolutionTime() { return getFixedSlot(PROMISE_RESOLUTION_TIME_SLOT).toNumber(); }
|
||||
JSObject* allocationSite() {
|
||||
return getFixedSlot(PromiseSlot_AllocationSite).toObjectOrNull();
|
||||
return getFixedSlot(PROMISE_ALLOCATION_SITE_SLOT).toObjectOrNull();
|
||||
}
|
||||
JSObject* resolutionSite() {
|
||||
return getFixedSlot(PromiseSlot_ResolutionSite).toObjectOrNull();
|
||||
return getFixedSlot(PROMISE_RESOLUTION_SITE_SLOT).toObjectOrNull();
|
||||
}
|
||||
double lifetime();
|
||||
double timeToResolution() {
|
||||
|
@ -86,36 +67,45 @@ class PromiseObject : public NativeObject
|
|||
uint64_t getID();
|
||||
bool isUnhandled() {
|
||||
MOZ_ASSERT(state() == JS::PromiseState::Rejected);
|
||||
return !(getFixedSlot(PromiseSlot_Flags).toInt32() & PROMISE_FLAG_HANDLED);
|
||||
return !(getFixedSlot(PROMISE_FLAGS_SLOT).toInt32() & PROMISE_FLAG_HANDLED);
|
||||
}
|
||||
void markAsReported() {
|
||||
MOZ_ASSERT(isUnhandled());
|
||||
int32_t flags = getFixedSlot(PromiseSlot_Flags).toInt32();
|
||||
setFixedSlot(PromiseSlot_Flags, Int32Value(flags | PROMISE_FLAG_REPORTED));
|
||||
int32_t flags = getFixedSlot(PROMISE_FLAGS_SLOT).toInt32();
|
||||
setFixedSlot(PROMISE_FLAGS_SLOT, Int32Value(flags | PROMISE_FLAG_REPORTED));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Enqueues resolve/reject reactions in the given Promise's reactions lists
|
||||
* in a content-invisible way.
|
||||
*
|
||||
* Used internally to implement DOM functionality.
|
||||
*
|
||||
* Note: the reactions pushed using this function contain a `promise` field
|
||||
* that can contain null. That field is only ever used by devtools, which have
|
||||
* to treat these reactions specially.
|
||||
* Tells the embedding to enqueue a Promise reaction job, based on six
|
||||
* parameters:
|
||||
* reaction handler - The callback to invoke for this job.
|
||||
argument - The first and only argument to pass to the handler.
|
||||
resolve - The Promise cabability's resolve hook, called upon normal
|
||||
completion of the handler.
|
||||
reject - The Promise cabability's reject hook, called if the handler
|
||||
throws.
|
||||
promise - The associated Promise, or null for some internal uses.
|
||||
objectFromIncumbentGlobal - An object from the global that was the
|
||||
incumbent global when the Promise reaction job
|
||||
was created (not enqueued). Not the global
|
||||
itself because unwrapping that might unwrap an
|
||||
inner to an outer window, which we never want
|
||||
to happen.
|
||||
*/
|
||||
MOZ_MUST_USE bool
|
||||
EnqueuePromiseReactions(JSContext* cx, Handle<PromiseObject*> promise,
|
||||
HandleObject dependentPromise,
|
||||
HandleValue onFulfilled, HandleValue onRejected);
|
||||
bool EnqueuePromiseReactionJob(JSContext* cx, HandleValue handler, HandleValue handlerArg,
|
||||
HandleObject resolve, HandleObject reject,
|
||||
HandleObject promise, HandleObject objectFromIncumbentGlobal);
|
||||
|
||||
MOZ_MUST_USE JSObject*
|
||||
GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises);
|
||||
|
||||
MOZ_MUST_USE JSObject*
|
||||
OriginalPromiseThen(JSContext* cx, Handle<PromiseObject*> promise, HandleValue onFulfilled,
|
||||
HandleValue onRejected);
|
||||
/**
|
||||
* Tells the embedding to enqueue a Promise resolve thenable job, based on six
|
||||
* parameters:
|
||||
* promiseToResolve - The promise to resolve, obviously.
|
||||
* thenable - The thenable to resolve the Promise with.
|
||||
* then - The `then` function to invoke with the `thenable` as the receiver.
|
||||
*/
|
||||
bool EnqueuePromiseResolveThenableJob(JSContext* cx, HandleValue promiseToResolve,
|
||||
HandleValue thenable, HandleValue then);
|
||||
|
||||
/**
|
||||
* A PromiseTask represents a task that can be dispatched to a helper thread
|
||||
|
|
|
@ -2,6 +2,503 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// ES6, 25.4.1.2.
|
||||
// This object is used to verify that an object is a PromiseReaction record.
|
||||
var PromiseReactionRecordProto = {__proto__: null};
|
||||
function PromiseReactionRecord(promise, resolve, reject, fulfillHandler, rejectHandler,
|
||||
incumbentGlobal) {
|
||||
this.promise = promise;
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
this.fulfillHandler = fulfillHandler;
|
||||
this.rejectHandler = rejectHandler;
|
||||
this.incumbentGlobal = incumbentGlobal;
|
||||
}
|
||||
|
||||
MakeConstructible(PromiseReactionRecord, PromiseReactionRecordProto);
|
||||
|
||||
// Used to verify that an object is a PromiseCapability record.
|
||||
var PromiseCapabilityRecordProto = {__proto__: null};
|
||||
|
||||
// ES2016, 25.4.1.3, implemented in Promise.cpp.
|
||||
|
||||
// ES2016, 25.4.1.4, implemented in Promise.cpp.
|
||||
|
||||
// ES2016, 25.4.1.5.
|
||||
// Creates PromiseCapability records, see 25.4.1.1.
|
||||
function NewPromiseCapability(C) {
|
||||
// Steps 1-2.
|
||||
if (!IsConstructor(C))
|
||||
ThrowTypeError(JSMSG_NOT_CONSTRUCTOR, 0);
|
||||
|
||||
// Step 3. Replaced by individual fields, combined in step 11.
|
||||
let resolve;
|
||||
let reject;
|
||||
|
||||
// Steps 4-5.
|
||||
// ES6, 25.4.1.5.1. Inlined here so we can use an upvar instead of a slot to
|
||||
// store promiseCapability.
|
||||
function GetCapabilitiesExecutor(resolve_, reject_) {
|
||||
// Steps 1-2 (implicit).
|
||||
|
||||
// Steps 3-4.
|
||||
if (resolve !== undefined || reject !== undefined)
|
||||
ThrowTypeError(JSMSG_PROMISE_CAPABILITY_HAS_SOMETHING_ALREADY);
|
||||
resolve = resolve_;
|
||||
reject = reject_;
|
||||
}
|
||||
|
||||
// Steps 6-7.
|
||||
let promise = new C(GetCapabilitiesExecutor);
|
||||
|
||||
// Step 8.
|
||||
if (!IsCallable(resolve))
|
||||
ThrowTypeError(JSMSG_PROMISE_RESOLVE_FUNCTION_NOT_CALLABLE);
|
||||
|
||||
// Step 9.
|
||||
if (!IsCallable(reject))
|
||||
ThrowTypeError(JSMSG_PROMISE_REJECT_FUNCTION_NOT_CALLABLE);
|
||||
|
||||
// Steps 10-11.
|
||||
return {
|
||||
__proto__: PromiseCapabilityRecordProto,
|
||||
promise,
|
||||
resolve,
|
||||
reject
|
||||
};
|
||||
}
|
||||
|
||||
// ES2016, 25.4.1.6, implemented in SelfHosting.cpp.
|
||||
|
||||
// ES2016, 25.4.1.7, implemented in Promise.cpp.
|
||||
|
||||
// ES2016, 25.4.1.8, implemented in Promise.cpp.
|
||||
|
||||
// ES2016, 25.4.1.9, implemented in SelfHosting.cpp.
|
||||
|
||||
// ES6, 25.4.2.1.
|
||||
function EnqueuePromiseReactionJob(reaction, jobType, argument) {
|
||||
// Reaction records contain handlers for both fulfillment and rejection.
|
||||
// The `jobType` parameter allows us to retrieves the right one.
|
||||
assert(jobType === PROMISE_JOB_TYPE_FULFILL || jobType === PROMISE_JOB_TYPE_REJECT,
|
||||
"Invalid job type");
|
||||
_EnqueuePromiseReactionJob(reaction[jobType],
|
||||
argument,
|
||||
reaction.resolve,
|
||||
reaction.reject,
|
||||
reaction.promise,
|
||||
reaction.incumbentGlobal || null);
|
||||
}
|
||||
|
||||
// ES6, 25.4.2.2. (Implemented in C++).
|
||||
|
||||
// ES6, 25.4.3.1. (Implemented in C++).
|
||||
|
||||
// ES2016, 25.4.4.1.
|
||||
function Promise_static_all(iterable) {
|
||||
// Step 1.
|
||||
let C = this;
|
||||
|
||||
// Step 2.
|
||||
if (!IsObject(C))
|
||||
ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, "Receiver of Promise.all call");
|
||||
|
||||
// Step 3.
|
||||
let promiseCapability = NewPromiseCapability(C);
|
||||
|
||||
// Steps 4-5.
|
||||
let iterator;
|
||||
try {
|
||||
iterator = GetIterator(iterable);
|
||||
} catch (e) {
|
||||
callContentFunction(promiseCapability.reject, undefined, e);
|
||||
return promiseCapability.promise;
|
||||
}
|
||||
|
||||
// Step 6.
|
||||
let iteratorRecord = {__proto__: null, iterator, done: false};
|
||||
|
||||
// Steps 7-9.
|
||||
try {
|
||||
// Steps 7,9.
|
||||
return PerformPromiseAll(iteratorRecord, C, promiseCapability);
|
||||
} catch (e) {
|
||||
// Step 8.a.
|
||||
// TODO: implement iterator closing.
|
||||
|
||||
// Step 8.b.
|
||||
callContentFunction(promiseCapability.reject, undefined, e);
|
||||
return promiseCapability.promise;
|
||||
}
|
||||
}
|
||||
|
||||
// ES6, 25.4.4.1.1.
|
||||
function PerformPromiseAll(iteratorRecord, constructor, resultCapability) {
|
||||
// Step 1.
|
||||
assert(IsConstructor(constructor), "PerformPromiseAll called with non-constructor");
|
||||
|
||||
// Step 2.
|
||||
assert(IsPromiseCapability(resultCapability), "Invalid promise capability record");
|
||||
|
||||
// Step 3.
|
||||
// Immediately create an Array instead of a List, so we can skip step 6.d.iii.1.
|
||||
//
|
||||
// We might be dealing with a wrapped instance from another Realm. In that
|
||||
// case, we want to create the `values` array in that other Realm so if
|
||||
// it's less-privileged than the current one, code in that Realm can still
|
||||
// work with the array.
|
||||
let values = IsPromise(resultCapability.promise) || !IsWrappedPromise(resultCapability.promise)
|
||||
? []
|
||||
: NewArrayInCompartment(constructor);
|
||||
let valuesCount = 0;
|
||||
|
||||
// Step 4.
|
||||
let remainingElementsCount = {value: 1};
|
||||
|
||||
// Step 5.
|
||||
let index = 0;
|
||||
|
||||
// Step 6.
|
||||
let iterator = iteratorRecord.iterator;
|
||||
let next;
|
||||
let nextValue;
|
||||
let allPromise = resultCapability.promise;
|
||||
while (true) {
|
||||
try {
|
||||
// Step 6.a.
|
||||
next = callContentFunction(iterator.next, iterator);
|
||||
if (!IsObject(next))
|
||||
ThrowTypeError(JSMSG_NEXT_RETURNED_PRIMITIVE);
|
||||
} catch (e) {
|
||||
// Step 6.b.
|
||||
iteratorRecord.done = true;
|
||||
|
||||
// Step 6.c.
|
||||
throw (e);
|
||||
}
|
||||
|
||||
// Step 6.d.
|
||||
if (next.done) {
|
||||
// Step 6.d.i.
|
||||
iteratorRecord.done = true;
|
||||
|
||||
// Step 6.d.ii.
|
||||
remainingElementsCount.value--;
|
||||
assert(remainingElementsCount.value >= 0,
|
||||
"remainingElementsCount mustn't be negative.");
|
||||
|
||||
// Step 6.d.iii.
|
||||
if (remainingElementsCount.value === 0)
|
||||
callContentFunction(resultCapability.resolve, undefined, values);
|
||||
|
||||
// Step 6.d.iv.
|
||||
return allPromise;
|
||||
}
|
||||
try {
|
||||
// Step 6.e.
|
||||
nextValue = next.value;
|
||||
} catch (e) {
|
||||
// Step 6.f.
|
||||
iteratorRecord.done = true;
|
||||
|
||||
// Step 6.g.
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Step 6.h.
|
||||
_DefineDataProperty(values, valuesCount++, undefined);
|
||||
|
||||
// Steps 6.i-j.
|
||||
let nextPromise = callContentFunction(constructor.resolve, constructor, nextValue);
|
||||
|
||||
// Steps 6.k-p.
|
||||
let resolveElement = CreatePromiseAllResolveElementFunction(index, values,
|
||||
resultCapability,
|
||||
remainingElementsCount);
|
||||
|
||||
// Step 6.q.
|
||||
remainingElementsCount.value++;
|
||||
|
||||
// Steps 6.r-s.
|
||||
BlockOnPromise(nextPromise, allPromise, resolveElement, resultCapability.reject);
|
||||
|
||||
// Step 6.t.
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unforgeable version of Promise.all for internal use.
|
||||
*
|
||||
* Takes a dense array of Promise objects and returns a promise that's
|
||||
* resolved with an array of resolution values when all those promises ahve
|
||||
* been resolved, or rejected with the rejection value of the first rejected
|
||||
* promise.
|
||||
*
|
||||
* Asserts if the array isn't dense or one of the entries isn't a Promise.
|
||||
*/
|
||||
function GetWaitForAllPromise(promises) {
|
||||
let resultCapability = NewPromiseCapability(GetBuiltinConstructor('Promise'));
|
||||
let allPromise = resultCapability.promise;
|
||||
|
||||
// Step 3.
|
||||
// Immediately create an Array instead of a List, so we can skip step 6.d.iii.1.
|
||||
let values = [];
|
||||
let valuesCount = 0;
|
||||
|
||||
// Step 4.
|
||||
let remainingElementsCount = {value: 0};
|
||||
|
||||
// Step 6.
|
||||
for (let i = 0; i < promises.length; i++) {
|
||||
// Parts of step 6 for deriving next promise, vastly simplified.
|
||||
assert(callFunction(std_Object_hasOwnProperty, promises, i),
|
||||
"GetWaitForAllPromise must be called with a dense array of promises");
|
||||
let nextPromise = promises[i];
|
||||
assert(IsPromise(nextPromise) || IsWrappedPromise(nextPromise),
|
||||
"promises list must only contain possibly wrapped promises");
|
||||
|
||||
// Step 6.h.
|
||||
_DefineDataProperty(values, valuesCount++, undefined);
|
||||
|
||||
// Steps 6.k-p.
|
||||
let resolveElement = CreatePromiseAllResolveElementFunction(i, values,
|
||||
resultCapability,
|
||||
remainingElementsCount);
|
||||
|
||||
// Step 6.q.
|
||||
remainingElementsCount.value++;
|
||||
|
||||
// Steps 6.r-s, very roughly.
|
||||
EnqueuePromiseReactions(nextPromise, allPromise, resolveElement, resultCapability.reject);
|
||||
}
|
||||
|
||||
if (remainingElementsCount.value === 0)
|
||||
callFunction(resultCapability.resolve, undefined, values);
|
||||
|
||||
return allPromise;
|
||||
}
|
||||
|
||||
// ES6, 25.4.4.1.2.
|
||||
function CreatePromiseAllResolveElementFunction(index, values, promiseCapability,
|
||||
remainingElementsCount)
|
||||
{
|
||||
var alreadyCalled = false;
|
||||
return function PromiseAllResolveElementFunction(x) {
|
||||
// Steps 1-2.
|
||||
if (alreadyCalled)
|
||||
return undefined;
|
||||
|
||||
// Step 3.
|
||||
alreadyCalled = true;
|
||||
|
||||
// Steps 4-7 (implicit).
|
||||
|
||||
// Step 8.
|
||||
// Note: this can't throw because the slot was initialized to `undefined` earlier.
|
||||
values[index] = x;
|
||||
|
||||
// Step 9.
|
||||
remainingElementsCount.value--;
|
||||
assert(remainingElementsCount.value >= 0, "remainingElementsCount mustn't be negative.");
|
||||
|
||||
// Step 10.
|
||||
if (remainingElementsCount.value === 0) {
|
||||
// Step 10.a (implicit).
|
||||
|
||||
// Step 10.b.
|
||||
return callContentFunction(promiseCapability.resolve, undefined, values);
|
||||
}
|
||||
|
||||
// Step 11 (implicit).
|
||||
};
|
||||
}
|
||||
|
||||
// ES2016, 25.4.4.3.
|
||||
function Promise_static_race(iterable) {
|
||||
// Step 1.
|
||||
let C = this;
|
||||
|
||||
// Step 2.
|
||||
if (!IsObject(C))
|
||||
ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, "Receiver of Promise.race call");
|
||||
|
||||
// step 3.
|
||||
let promiseCapability = NewPromiseCapability(C);
|
||||
|
||||
// Steps 4-5.
|
||||
let iterator;
|
||||
try {
|
||||
iterator = GetIterator(iterable);
|
||||
} catch (e) {
|
||||
callContentFunction(promiseCapability.reject, undefined, e);
|
||||
return promiseCapability.promise;
|
||||
}
|
||||
|
||||
// Step 6.
|
||||
let iteratorRecord = {__proto__: null, iterator, done: false};
|
||||
|
||||
// Steps 7-9.
|
||||
try {
|
||||
// Steps 7,9.
|
||||
return PerformPromiseRace(iteratorRecord, promiseCapability, C);
|
||||
} catch (e) {
|
||||
// Step 8.a.
|
||||
// TODO: implement iterator closing.
|
||||
|
||||
// Step 8.b.
|
||||
callContentFunction(promiseCapability.reject, undefined, e);
|
||||
return promiseCapability.promise;
|
||||
}
|
||||
}
|
||||
|
||||
// ES2016, 25.4.4.3.1.
|
||||
function PerformPromiseRace(iteratorRecord, resultCapability, C) {
|
||||
assert(IsConstructor(C), "PerformPromiseRace called with non-constructor");
|
||||
assert(IsPromiseCapability(resultCapability), "Invalid promise capability record");
|
||||
|
||||
// Step 1.
|
||||
let iterator = iteratorRecord.iterator;
|
||||
let racePromise = resultCapability.promise;
|
||||
let next;
|
||||
let nextValue;
|
||||
while (true) {
|
||||
try {
|
||||
// Step 1.a.
|
||||
next = callContentFunction(iterator.next, iterator);
|
||||
if (!IsObject(next))
|
||||
ThrowTypeError(JSMSG_NEXT_RETURNED_PRIMITIVE);
|
||||
} catch (e) {
|
||||
// Step 1.b.
|
||||
iteratorRecord.done = true;
|
||||
|
||||
// Step 1.c.
|
||||
throw (e);
|
||||
}
|
||||
|
||||
// Step 1.d.
|
||||
if (next.done) {
|
||||
// Step 1.d.i.
|
||||
iteratorRecord.done = true;
|
||||
|
||||
// Step 1.d.ii.
|
||||
return racePromise;
|
||||
}
|
||||
try {
|
||||
// Step 1.e.
|
||||
nextValue = next.value;
|
||||
} catch (e) {
|
||||
// Step 1.f.
|
||||
iteratorRecord.done = true;
|
||||
|
||||
// Step 1.g.
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Step 1.h.
|
||||
let nextPromise = callContentFunction(C.resolve, C, nextValue);
|
||||
|
||||
// Steps 1.i.
|
||||
BlockOnPromise(nextPromise, racePromise, resultCapability.resolve,
|
||||
resultCapability.reject);
|
||||
}
|
||||
assert(false, "Shouldn't reach the end of PerformPromiseRace");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls |promise.then| with the provided hooks and adds |blockedPromise| to
|
||||
* its list of dependent promises. Used by |Promise.all| and |Promise.race|.
|
||||
*
|
||||
* If |promise.then| is the original |Promise.prototype.then| function and
|
||||
* the call to |promise.then| would use the original |Promise| constructor to
|
||||
* create the resulting promise, this function skips the call to |promise.then|
|
||||
* and thus creating a new promise that would not be observable by content.
|
||||
*/
|
||||
function BlockOnPromise(promise, blockedPromise, onResolve, onReject) {
|
||||
let then = promise.then;
|
||||
|
||||
// By default, the blocked promise is added as an extra entry to the
|
||||
// rejected promises list.
|
||||
let addToDependent = true;
|
||||
if (then === Promise_then && IsObject(promise) && IsPromise(promise)) {
|
||||
// |then| is the original |Promise.prototype.then|, inline it here.
|
||||
// 25.4.5.3., steps 3-4.
|
||||
let PromiseCtor = GetBuiltinConstructor('Promise');
|
||||
let C = SpeciesConstructor(promise, PromiseCtor);
|
||||
let resultCapability;
|
||||
|
||||
if (C === PromiseCtor) {
|
||||
resultCapability = {
|
||||
__proto__: PromiseCapabilityRecordProto,
|
||||
promise: blockedPromise,
|
||||
reject: NullFunction,
|
||||
resolve: NullFunction
|
||||
};
|
||||
addToDependent = false;
|
||||
} else {
|
||||
// 25.4.5.3., steps 5-6.
|
||||
resultCapability = NewPromiseCapability(C);
|
||||
}
|
||||
|
||||
// 25.4.5.3., step 7.
|
||||
PerformPromiseThen(promise, onResolve, onReject, resultCapability);
|
||||
} else {
|
||||
// Optimization failed, do the normal call.
|
||||
callContentFunction(then, promise, onResolve, onReject);
|
||||
}
|
||||
if (!addToDependent)
|
||||
return;
|
||||
|
||||
// The object created by the |promise.then| call or the inlined version
|
||||
// of it above is visible to content (either because |promise.then| was
|
||||
// overridden by content and could leak it, or because a constructor
|
||||
// other than the original value of |Promise| was used to create it).
|
||||
// To have both that object and |blockedPromise| show up as dependent
|
||||
// promises in the debugger, add a dummy reaction to the list of reject
|
||||
// reactions that contains |blockedPromise|, but otherwise does nothing.
|
||||
// If the object isn't a maybe-wrapped instance of |Promise|, we ignore
|
||||
// it. All this does is lose some small amount of debug information
|
||||
// in scenarios that are highly unlikely to occur in useful code.
|
||||
if (IsPromise(promise))
|
||||
return callFunction(AddDependentPromise, promise, blockedPromise);
|
||||
|
||||
if (IsWrappedPromise(promise))
|
||||
callFunction(CallPromiseMethodIfWrapped, promise, blockedPromise, "AddDependentPromise");
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked with a Promise as the receiver, AddDependentPromise adds an entry
|
||||
* to the reactions list.
|
||||
*
|
||||
* This is only used to make dependent promises visible in the devtools, so no
|
||||
* callbacks are provided. To make handling that case easier elsewhere,
|
||||
* they're all set to NullFunction.
|
||||
*
|
||||
* The reason for the target Promise to be passed as the receiver is so the
|
||||
* same function can be used for wrapped and unwrapped Promise objects.
|
||||
*/
|
||||
function AddDependentPromise(dependentPromise) {
|
||||
assert(IsPromise(this), "AddDependentPromise expects an unwrapped Promise as the receiver");
|
||||
|
||||
if (GetPromiseState(this) !== PROMISE_STATE_PENDING)
|
||||
return;
|
||||
|
||||
let reaction = new PromiseReactionRecord(dependentPromise, NullFunction, NullFunction,
|
||||
NullFunction, NullFunction, null);
|
||||
|
||||
let reactions = UnsafeGetReservedSlot(this, PROMISE_REACTIONS_OR_RESULT_SLOT);
|
||||
|
||||
// The reactions list might not have been allocated yet.
|
||||
if (!reactions)
|
||||
UnsafeSetReservedSlot(dependentPromise, PROMISE_REACTIONS_OR_RESULT_SLOT, [reaction]);
|
||||
else
|
||||
_DefineDataProperty(reactions, reactions.length, reaction);
|
||||
}
|
||||
|
||||
// ES2016, 25.4.4.4 (implemented in C++).
|
||||
|
||||
// ES2016, 25.4.4.5 (implemented in C++).
|
||||
|
||||
// ES6, 25.4.4.6.
|
||||
function Promise_static_get_species() {
|
||||
// Step 1.
|
||||
|
@ -14,3 +511,252 @@ function Promise_catch(onRejected) {
|
|||
// Steps 1-2.
|
||||
return callContentFunction(this.then, this, undefined, onRejected);
|
||||
}
|
||||
|
||||
// ES6, 25.4.5.3.
|
||||
function Promise_then(onFulfilled, onRejected) {
|
||||
// Step 1.
|
||||
let promise = this;
|
||||
|
||||
// Step 2.
|
||||
if (!IsObject(promise))
|
||||
ThrowTypeError(JSMSG_NOT_NONNULL_OBJECT, "Receiver of Promise.prototype.then call");
|
||||
|
||||
let isPromise = IsPromise(promise);
|
||||
let isWrappedPromise = isPromise ? false : IsWrappedPromise(promise);
|
||||
if (!(isPromise || isWrappedPromise))
|
||||
ThrowTypeError(JSMSG_INCOMPATIBLE_PROTO, "Promise", "then", "value");
|
||||
|
||||
// Steps 3-4.
|
||||
let C = SpeciesConstructor(promise, GetBuiltinConstructor('Promise'));
|
||||
|
||||
// Steps 5-6.
|
||||
let resultCapability = NewPromiseCapability(C);
|
||||
|
||||
// Step 7.
|
||||
if (isWrappedPromise) {
|
||||
// See comment above GetPromiseHandlerForwarders for why this is needed.
|
||||
let handlerForwarders = GetPromiseHandlerForwarders(onFulfilled, onRejected);
|
||||
return callFunction(CallPromiseMethodIfWrapped, promise,
|
||||
handlerForwarders[0], handlerForwarders[1],
|
||||
resultCapability.promise, resultCapability.resolve,
|
||||
resultCapability.reject, "UnwrappedPerformPromiseThen");
|
||||
}
|
||||
return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues resolve/reject reactions in the given Promise's reactions lists
|
||||
* in a content-invisible way.
|
||||
*
|
||||
* Used internally to implement DOM functionality.
|
||||
*
|
||||
* Note: the reactions pushed using this function contain a `promise` field
|
||||
* that can contain null. That field is only ever used by devtools, which have
|
||||
* to treat these reactions specially.
|
||||
*/
|
||||
function EnqueuePromiseReactions(promise, dependentPromise, onFulfilled, onRejected) {
|
||||
let isWrappedPromise = false;
|
||||
if (!IsPromise(promise)) {
|
||||
assert(IsWrappedPromise(promise),
|
||||
"EnqueuePromiseReactions must be provided with a possibly wrapped promise");
|
||||
isWrappedPromise = true;
|
||||
}
|
||||
|
||||
assert(dependentPromise === null || IsPromise(dependentPromise),
|
||||
"EnqueuePromiseReactions's dependentPromise argument must be a Promise or null");
|
||||
|
||||
if (isWrappedPromise) {
|
||||
// See comment above GetPromiseHandlerForwarders for why this is needed.
|
||||
let handlerForwarders = GetPromiseHandlerForwarders(onFulfilled, onRejected);
|
||||
return callFunction(CallPromiseMethodIfWrapped, promise, handlerForwarders[0],
|
||||
handlerForwarders[1], dependentPromise, NullFunction, NullFunction,
|
||||
"UnwrappedPerformPromiseThen");
|
||||
}
|
||||
let capability = {
|
||||
__proto__: PromiseCapabilityRecordProto,
|
||||
promise: dependentPromise,
|
||||
resolve: NullFunction,
|
||||
reject: NullFunction
|
||||
};
|
||||
return PerformPromiseThen(promise, onFulfilled, onRejected, capability);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of functions that are (1) self-hosted, and (2) exact
|
||||
* forwarders of the passed-in functions, for use by
|
||||
* UnwrappedPerformPromiseThen.
|
||||
*
|
||||
* When calling `then` on an xray-wrapped promise, the receiver isn't
|
||||
* unwrapped. Instead, Promise_then operates on the wrapped Promise. Just
|
||||
* calling PerformPromiseThen from Promise_then as we normally would doesn't
|
||||
* work in this case: PerformPromiseThen can only deal with unwrapped
|
||||
* Promises. Instead, we use the CallPromiseMethodIfWrapped intrinsic to
|
||||
* switch compartments before calling PerformPromiseThen, via
|
||||
* UnwrappedPerformPromiseThen.
|
||||
*
|
||||
* This is almost enough, but there's an additional wrinkle: when calling the
|
||||
* fulfillment and rejection handlers, we might pass in Object-type arguments
|
||||
* from within the xray-ed, lower-privileged compartment. By default, this
|
||||
* doesn't work, because they're wrapped into wrappers that disallow passing
|
||||
* in Object-typed arguments (so the higher-privileged code doesn't
|
||||
* accidentally operate on objects assuming they're higher-privileged, too.)
|
||||
* So instead UnwrappedPerformPromiseThen adds another level of indirection:
|
||||
* it closes over the, by now cross-compartment-wrapped, handler forwarders
|
||||
* created by GetPromiseHandlerForwarders and creates a second set of
|
||||
* forwarders around them, which use UnsafeCallWrappedFunction to call the
|
||||
* initial forwarders.
|
||||
|
||||
* Note that both above-mentioned guarantees are required: while it may seem
|
||||
* as though the original handlers would always be wrappers once they reach
|
||||
* UnwrappedPerformPromiseThen (because the call to `then` originated in the
|
||||
* higher-privileged compartment, and after unwrapping we end up in the
|
||||
* lower-privileged one), that's not necessarily the case. One or both of the
|
||||
* handlers can originate from the lower-privileged compartment, so they'd
|
||||
* actually be unwrapped functions when they reach
|
||||
* UnwrappedPerformPromiseThen.
|
||||
*/
|
||||
function GetPromiseHandlerForwarders(fulfilledHandler, rejectedHandler) {
|
||||
// If non-callable values are passed, we have to preserve them so steps
|
||||
// 3 and 4 of PerformPromiseThen work as expected.
|
||||
return [
|
||||
IsCallable(fulfilledHandler) ? function onFulfilled(argument) {
|
||||
return callContentFunction(fulfilledHandler, this, argument);
|
||||
} : fulfilledHandler,
|
||||
IsCallable(rejectedHandler) ? function onRejected(argument) {
|
||||
return callContentFunction(rejectedHandler, this, argument);
|
||||
} : rejectedHandler
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwarder used to invoke PerformPromiseThen on an unwrapped Promise, while
|
||||
* wrapping the resolve/reject callbacks into functions that invoke them in
|
||||
* their original compartment. See the comment for GetPromiseHandlerForwarders
|
||||
* for details.
|
||||
*/
|
||||
function UnwrappedPerformPromiseThen(fulfilledHandler, rejectedHandler, promise, resolve, reject) {
|
||||
let resultCapability = {
|
||||
__proto__: PromiseCapabilityRecordProto,
|
||||
promise,
|
||||
resolve(resolution) {
|
||||
// Under some circumstances, we have an unwrapped `resolve`
|
||||
// function here. One way this happens is if the constructor
|
||||
// passed to `NewPromiseCapability` is from the same global as the
|
||||
// Promise object on which `Promise_then` was called, but where
|
||||
// `Promise_then` is from a different global, so we end up here.
|
||||
// In that case, the `resolve` and `reject` functions aren't
|
||||
// wrappers in the current global.
|
||||
if (IsFunctionObject(resolve))
|
||||
return resolve(resolution);
|
||||
return UnsafeCallWrappedFunction(resolve, undefined, resolution);
|
||||
},
|
||||
reject(reason) {
|
||||
// See comment inside `resolve` above.
|
||||
if (IsFunctionObject(reject))
|
||||
return reject(reason);
|
||||
return UnsafeCallWrappedFunction(reject, undefined, reason);
|
||||
}
|
||||
};
|
||||
function onFulfilled(argument) {
|
||||
return UnsafeCallWrappedFunction(fulfilledHandler, undefined, argument);
|
||||
}
|
||||
function onRejected(argument) {
|
||||
return UnsafeCallWrappedFunction(rejectedHandler, undefined, argument);
|
||||
}
|
||||
return PerformPromiseThen(this, IsCallable(fulfilledHandler) ? onFulfilled : fulfilledHandler,
|
||||
IsCallable(rejectedHandler) ? onRejected : rejectedHandler,
|
||||
resultCapability);
|
||||
}
|
||||
|
||||
// ES2016, 25.4.5.3.1.
|
||||
function PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability) {
|
||||
// Step 1.
|
||||
assert(IsPromise(promise), "Can't call PerformPromiseThen on non-Promise objects");
|
||||
|
||||
// Step 2.
|
||||
assert(IsPromiseCapability(resultCapability), "Invalid promise capability record");
|
||||
|
||||
// Step 3.
|
||||
if (!IsCallable(onFulfilled))
|
||||
onFulfilled = PROMISE_HANDLER_IDENTITY;
|
||||
|
||||
// Step 4.
|
||||
if (!IsCallable(onRejected))
|
||||
onRejected = PROMISE_HANDLER_THROWER;
|
||||
|
||||
let incumbentGlobal = _GetObjectFromIncumbentGlobal();
|
||||
// Steps 5,6.
|
||||
// Instead of creating separate reaction records for fulfillment and
|
||||
// rejection, we create a combined record. All places we use the record
|
||||
// can handle that.
|
||||
let reaction = new PromiseReactionRecord(resultCapability.promise,
|
||||
resultCapability.resolve,
|
||||
resultCapability.reject,
|
||||
onFulfilled,
|
||||
onRejected,
|
||||
incumbentGlobal);
|
||||
|
||||
// Step 7.
|
||||
let state = GetPromiseState(promise);
|
||||
let flags = UnsafeGetInt32FromReservedSlot(promise, PROMISE_FLAGS_SLOT);
|
||||
if (state === PROMISE_STATE_PENDING) {
|
||||
// Steps 7.a,b.
|
||||
// We only have a single list for fulfill and reject reactions.
|
||||
let reactions = UnsafeGetReservedSlot(promise, PROMISE_REACTIONS_OR_RESULT_SLOT);
|
||||
if (!reactions)
|
||||
UnsafeSetReservedSlot(promise, PROMISE_REACTIONS_OR_RESULT_SLOT, [reaction]);
|
||||
else
|
||||
_DefineDataProperty(reactions, reactions.length, reaction);
|
||||
}
|
||||
|
||||
// Step 8.
|
||||
else if (state === PROMISE_STATE_FULFILLED) {
|
||||
// Step 8.a.
|
||||
let value = UnsafeGetReservedSlot(promise, PROMISE_REACTIONS_OR_RESULT_SLOT);
|
||||
|
||||
// Step 8.b.
|
||||
EnqueuePromiseReactionJob(reaction, PROMISE_JOB_TYPE_FULFILL, value);
|
||||
}
|
||||
|
||||
// Step 9.
|
||||
else {
|
||||
// Step 9.a.
|
||||
assert(state === PROMISE_STATE_REJECTED, "Invalid Promise state " + state);
|
||||
|
||||
// Step 9.b.
|
||||
let reason = UnsafeGetReservedSlot(promise, PROMISE_REACTIONS_OR_RESULT_SLOT);
|
||||
|
||||
// Step 9.c.
|
||||
if (!(flags & PROMISE_FLAG_HANDLED))
|
||||
HostPromiseRejectionTracker(promise, PROMISE_REJECTION_TRACKER_OPERATION_HANDLE);
|
||||
|
||||
// Step 9.d.
|
||||
EnqueuePromiseReactionJob(reaction, PROMISE_JOB_TYPE_REJECT, reason);
|
||||
}
|
||||
|
||||
// Step 10.
|
||||
UnsafeSetReservedSlot(promise, PROMISE_FLAGS_SLOT, flags | PROMISE_FLAG_HANDLED);
|
||||
|
||||
// Step 11.
|
||||
return resultCapability.promise;
|
||||
}
|
||||
|
||||
/// Utility functions below.
|
||||
function IsPromiseReaction(record) {
|
||||
return std_Reflect_getPrototypeOf(record) === PromiseReactionRecordProto;
|
||||
}
|
||||
|
||||
function IsPromiseCapability(capability) {
|
||||
return std_Reflect_getPrototypeOf(capability) === PromiseCapabilityRecordProto;
|
||||
}
|
||||
|
||||
function GetPromiseState(promise) {
|
||||
let flags = UnsafeGetInt32FromReservedSlot(promise, PROMISE_FLAGS_SLOT);
|
||||
if (!(flags & PROMISE_FLAG_RESOLVED)) {
|
||||
assert(!(flags & PROMISE_STATE_FULFILLED), "Fulfilled promises are resolved, too");
|
||||
return PROMISE_STATE_PENDING;
|
||||
}
|
||||
if (flags & PROMISE_FLAG_FULFILLED)
|
||||
return PROMISE_STATE_FULFILLED;
|
||||
return PROMISE_STATE_REJECTED;
|
||||
}
|
||||
|
|
|
@ -78,6 +78,33 @@
|
|||
#define ITEM_KIND_VALUE 1
|
||||
#define ITEM_KIND_KEY_AND_VALUE 2
|
||||
|
||||
#define PROMISE_FLAGS_SLOT 0
|
||||
#define PROMISE_REACTIONS_OR_RESULT_SLOT 1
|
||||
#define PROMISE_RESOLVE_FUNCTION_SLOT 2
|
||||
#define PROMISE_ALLOCATION_SITE_SLOT 3
|
||||
#define PROMISE_RESOLUTION_SITE_SLOT 4
|
||||
#define PROMISE_ALLOCATION_TIME_SLOT 5
|
||||
#define PROMISE_RESOLUTION_TIME_SLOT 6
|
||||
#define PROMISE_ID_SLOT 7
|
||||
|
||||
#define PROMISE_STATE_PENDING 0
|
||||
#define PROMISE_STATE_FULFILLED 1
|
||||
#define PROMISE_STATE_REJECTED 2
|
||||
|
||||
#define PROMISE_FLAG_RESOLVED 0x1
|
||||
#define PROMISE_FLAG_FULFILLED 0x2
|
||||
#define PROMISE_FLAG_HANDLED 0x4
|
||||
#define PROMISE_FLAG_REPORTED 0x8
|
||||
|
||||
#define PROMISE_HANDLER_IDENTITY 0
|
||||
#define PROMISE_HANDLER_THROWER 1
|
||||
|
||||
#define PROMISE_REJECTION_TRACKER_OPERATION_REJECT false
|
||||
#define PROMISE_REJECTION_TRACKER_OPERATION_HANDLE true
|
||||
|
||||
#define PROMISE_JOB_TYPE_FULFILL "fulfillHandler"
|
||||
#define PROMISE_JOB_TYPE_REJECT "rejectHandler"
|
||||
|
||||
// NB: keep these in sync with the copy in jsfriendapi.h.
|
||||
#define JSITER_OWNONLY 0x8 /* iterate over obj's own properties only */
|
||||
#define JSITER_HIDDEN 0x10 /* also enumerate non-enumerable properties */
|
||||
|
|
|
@ -1451,42 +1451,15 @@ SettlePromiseNow(JSContext* cx, unsigned argc, Value* vp)
|
|||
}
|
||||
|
||||
RootedNativeObject promise(cx, &args[0].toObject().as<NativeObject>());
|
||||
int32_t flags = promise->getFixedSlot(PromiseSlot_Flags).toInt32();
|
||||
promise->setFixedSlot(PromiseSlot_Flags,
|
||||
int32_t flags = promise->getFixedSlot(PROMISE_FLAGS_SLOT).toInt32();
|
||||
promise->setFixedSlot(PROMISE_FLAGS_SLOT,
|
||||
Int32Value(flags | PROMISE_FLAG_RESOLVED | PROMISE_FLAG_FULFILLED));
|
||||
promise->setFixedSlot(PromiseSlot_ReactionsOrResult, UndefinedValue());
|
||||
promise->setFixedSlot(PROMISE_REACTIONS_OR_RESULT_SLOT, UndefinedValue());
|
||||
|
||||
JS::dbg::onPromiseSettled(cx, promise);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
GetWaitForAllPromise(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
if (!args.requireAtLeast(cx, "getWaitForAllPromise", 1))
|
||||
return false;
|
||||
if (!args[0].isObject() || !IsPackedArray(&args[0].toObject())) {
|
||||
JS_ReportErrorASCII(cx, "first argument must be a dense Array of Promise objects");
|
||||
return false;
|
||||
}
|
||||
RootedNativeObject list(cx, &args[0].toObject().as<NativeObject>());
|
||||
AutoObjectVector promises(cx);
|
||||
uint32_t count = list->getDenseInitializedLength();
|
||||
if (!promises.resize(count))
|
||||
return false;
|
||||
|
||||
for (uint32_t i = 0; i < count; i++)
|
||||
promises[i].set(&list->getDenseElement(i).toObject());
|
||||
|
||||
RootedObject resultPromise(cx, JS::GetWaitForAllPromise(cx, promises));
|
||||
if (!resultPromise)
|
||||
return false;
|
||||
|
||||
args.rval().set(ObjectValue(*resultPromise));
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static const js::Class FakePromiseClass = {
|
||||
|
@ -4138,10 +4111,6 @@ static const JSFunctionSpecWithHelp TestingFunctions[] = {
|
|||
" with a value of `undefined` and causes the firing of any onPromiseSettled\n"
|
||||
" hooks set on Debugger instances that are observing the given promise's\n"
|
||||
" global as a debuggee."),
|
||||
JS_FN_HELP("getWaitForAllPromise", GetWaitForAllPromise, 1, 0,
|
||||
"getWaitForAllPromise(densePromisesArray)",
|
||||
" Calls the 'GetWaitForAllPromise' JSAPI function and returns the result\n"
|
||||
" Promise."),
|
||||
#else
|
||||
JS_FN_HELP("makeFakePromise", MakeFakePromise, 0, 0,
|
||||
"makeFakePromise()",
|
||||
|
|
|
@ -4792,56 +4792,97 @@ JS::RejectPromise(JSContext* cx, JS::HandleObject promise, JS::HandleValue rejec
|
|||
}
|
||||
|
||||
JS_PUBLIC_API(JSObject*)
|
||||
JS::CallOriginalPromiseThen(JSContext* cx, JS::HandleObject promiseObj,
|
||||
JS::HandleObject onResolveObj, JS::HandleObject onRejectObj)
|
||||
JS::CallOriginalPromiseThen(JSContext* cx, JS::HandleObject promise,
|
||||
JS::HandleObject onResolve, JS::HandleObject onReject)
|
||||
{
|
||||
AssertHeapIsIdle(cx);
|
||||
CHECK_REQUEST(cx);
|
||||
assertSameCompartment(cx, promiseObj, onResolveObj, onRejectObj);
|
||||
assertSameCompartment(cx, promise, onResolve, onReject);
|
||||
|
||||
MOZ_ASSERT_IF(onResolveObj, IsCallable(onResolveObj));
|
||||
MOZ_ASSERT_IF(onRejectObj, IsCallable(onRejectObj));
|
||||
MOZ_ASSERT(promise->is<PromiseObject>());
|
||||
MOZ_ASSERT(onResolve == nullptr || IsCallable(onResolve));
|
||||
MOZ_ASSERT(onReject == nullptr || IsCallable(onReject));
|
||||
|
||||
Rooted<PromiseObject*> promise(cx, &promiseObj->as<PromiseObject>());
|
||||
RootedValue onFulfilled(cx, ObjectOrNullValue(onResolveObj));
|
||||
RootedValue onRejected(cx, ObjectOrNullValue(onRejectObj));
|
||||
return OriginalPromiseThen(cx, promise, onFulfilled, onRejected);
|
||||
JSObject* obj;
|
||||
{
|
||||
FixedInvokeArgs<2> args(cx);
|
||||
|
||||
args[0].setObjectOrNull(onResolve);
|
||||
args[1].setObjectOrNull(onReject);
|
||||
|
||||
RootedValue thisvOrRval(cx, ObjectValue(*promise));
|
||||
if (!CallSelfHostedFunction(cx, "Promise_then", thisvOrRval, args, &thisvOrRval))
|
||||
return nullptr;
|
||||
|
||||
MOZ_ASSERT(thisvOrRval.isObject());
|
||||
obj = &thisvOrRval.toObject();
|
||||
}
|
||||
|
||||
MOZ_ASSERT(obj->is<PromiseObject>());
|
||||
return obj;
|
||||
}
|
||||
|
||||
JS_PUBLIC_API(bool)
|
||||
JS::AddPromiseReactions(JSContext* cx, JS::HandleObject promiseObj,
|
||||
JS::HandleObject onResolvedObj, JS::HandleObject onRejectedObj)
|
||||
JS::AddPromiseReactions(JSContext* cx, JS::HandleObject promise,
|
||||
JS::HandleObject onResolve, JS::HandleObject onReject)
|
||||
{
|
||||
AssertHeapIsIdle(cx);
|
||||
CHECK_REQUEST(cx);
|
||||
assertSameCompartment(cx, promiseObj, onResolvedObj, onRejectedObj);
|
||||
assertSameCompartment(cx, promise, onResolve, onReject);
|
||||
|
||||
MOZ_ASSERT(IsCallable(onResolvedObj));
|
||||
MOZ_ASSERT(IsCallable(onRejectedObj));
|
||||
MOZ_ASSERT(promise->is<PromiseObject>());
|
||||
MOZ_ASSERT(IsCallable(onResolve));
|
||||
MOZ_ASSERT(IsCallable(onReject));
|
||||
|
||||
Rooted<PromiseObject*> promise(cx, &promiseObj->as<PromiseObject>());
|
||||
RootedValue onResolved(cx, ObjectValue(*onResolvedObj));
|
||||
RootedValue onRejected(cx, ObjectValue(*onRejectedObj));
|
||||
return EnqueuePromiseReactions(cx, promise, nullptr, onResolved, onRejected);
|
||||
FixedInvokeArgs<4> args(cx);
|
||||
|
||||
args[0].setObject(*promise);
|
||||
args[1].setNull();
|
||||
args[2].setObject(*onResolve);
|
||||
args[3].setObject(*onReject);
|
||||
|
||||
RootedValue dummy(cx);
|
||||
return CallSelfHostedFunction(cx, "EnqueuePromiseReactions", UndefinedHandleValue, args,
|
||||
&dummy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unforgeable version of Promise.all for internal use.
|
||||
*
|
||||
* Takes a dense array of Promise objects and returns a promise that's
|
||||
* resolved with an array of resolution values when all those promises ahve
|
||||
* been resolved, or rejected with the rejection value of the first rejected
|
||||
* promise.
|
||||
*
|
||||
* Asserts that the array is dense and all entries are Promise objects.
|
||||
*/
|
||||
JS_PUBLIC_API(JSObject*)
|
||||
JS::GetWaitForAllPromise(JSContext* cx, const JS::AutoObjectVector& promises)
|
||||
{
|
||||
AssertHeapIsIdle(cx);
|
||||
CHECK_REQUEST(cx);
|
||||
|
||||
return js::GetWaitForAllPromise(cx, promises);
|
||||
RootedArrayObject arr(cx, NewDenseFullyAllocatedArray(cx, promises.length()));
|
||||
if (!arr)
|
||||
return nullptr;
|
||||
arr->ensureDenseInitializedLength(cx, 0, promises.length());
|
||||
for (size_t i = 0, len = promises.length(); i < len; i++) {
|
||||
#ifdef DEBUG
|
||||
JSObject* obj = promises[i];
|
||||
assertSameCompartment(cx, obj);
|
||||
if (IsWrapper(obj))
|
||||
obj = UncheckedUnwrap(obj);
|
||||
MOZ_ASSERT(obj->is<PromiseObject>());
|
||||
#endif
|
||||
arr->setDenseElement(i, ObjectValue(*promises[i]));
|
||||
}
|
||||
|
||||
JSObject* obj;
|
||||
{
|
||||
FixedInvokeArgs<1> args(cx);
|
||||
|
||||
args[0].setObject(*arr);
|
||||
|
||||
RootedValue thisvOrRval(cx, UndefinedValue());
|
||||
if (!CallSelfHostedFunction(cx, "GetWaitForAllPromise", thisvOrRval, args, &thisvOrRval))
|
||||
return nullptr;
|
||||
|
||||
MOZ_ASSERT(thisvOrRval.isObject());
|
||||
obj = &thisvOrRval.toObject();
|
||||
}
|
||||
|
||||
MOZ_ASSERT(obj->is<PromiseObject>());
|
||||
return obj;
|
||||
}
|
||||
|
||||
JS_PUBLIC_API(void)
|
||||
|
|
|
@ -1974,47 +1974,6 @@ js::IsStandardPrototype(JSObject* obj, JSProtoKey key)
|
|||
return v.isObject() && obj == &v.toObject();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the original Object.prototype from the embedding-provided incumbent
|
||||
* global.
|
||||
*
|
||||
* Really, we want the incumbent global itself so we can pass it to other
|
||||
* embedding hooks which need it. Specifically, the enqueue promise hook
|
||||
* takes an incumbent global so it can set that on the PromiseCallbackJob
|
||||
* it creates.
|
||||
*
|
||||
* The reason for not just returning the global itself is that we'd need to
|
||||
* wrap it into the current compartment, and later unwrap it. Unwrapping
|
||||
* globals is tricky, though: we might accidentally unwrap through an inner
|
||||
* to its outer window and end up with the wrong global. Plain objects don't
|
||||
* have this problem, so we use the global's Object.prototype. The code using
|
||||
* it - e.g. EnqueuePromiseReactionJob - can then unwrap the object and get
|
||||
* its global without fear of unwrapping too far.
|
||||
*/
|
||||
bool
|
||||
js::GetObjectFromIncumbentGlobal(JSContext* cx, MutableHandleObject obj)
|
||||
{
|
||||
RootedObject globalObj(cx, cx->runtime()->getIncumbentGlobal(cx));
|
||||
if (!globalObj) {
|
||||
obj.set(nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
{
|
||||
AutoCompartment ac(cx, globalObj);
|
||||
obj.set(globalObj->as<GlobalObject>().getOrCreateObjectPrototype(cx));
|
||||
if (!obj)
|
||||
return false;
|
||||
}
|
||||
|
||||
// The object might be from a different compartment, so wrap it.
|
||||
if (obj && !cx->compartment()->wrap(cx, obj))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
JSProtoKey
|
||||
JS::IdentifyStandardInstance(JSObject* obj)
|
||||
{
|
||||
|
@ -3943,16 +3902,6 @@ js::SpeciesConstructor(JSContext* cx, HandleObject obj, HandleValue defaultCtor,
|
|||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
js::SpeciesConstructor(JSContext* cx, HandleObject obj, JSProtoKey ctorKey,
|
||||
MutableHandleValue pctor)
|
||||
{
|
||||
if (!GlobalObject::ensureConstructor(cx, cx->global(), ctorKey))
|
||||
return false;
|
||||
RootedValue defaultCtor(cx, cx->global()->getConstructor(ctorKey));
|
||||
return SpeciesConstructor(cx, obj, defaultCtor, pctor);
|
||||
}
|
||||
|
||||
bool
|
||||
js::Unbox(JSContext* cx, HandleObject obj, MutableHandleValue vp)
|
||||
{
|
||||
|
|
|
@ -1375,12 +1375,6 @@ TestIntegrityLevel(JSContext* cx, HandleObject obj, IntegrityLevel level, bool*
|
|||
extern bool
|
||||
SpeciesConstructor(JSContext* cx, HandleObject obj, HandleValue defaultCtor, MutableHandleValue pctor);
|
||||
|
||||
extern bool
|
||||
SpeciesConstructor(JSContext* cx, HandleObject obj, JSProtoKey ctorKey, MutableHandleValue pctor);
|
||||
|
||||
extern bool
|
||||
GetObjectFromIncumbentGlobal(JSContext* cx, MutableHandleObject obj);
|
||||
|
||||
} /* namespace js */
|
||||
|
||||
#endif /* jsobj_h */
|
||||
|
|
|
@ -5,6 +5,8 @@ if (!this.Promise) {
|
|||
quit(0);
|
||||
}
|
||||
|
||||
let GetWaitForAllPromise = getSelfHostedValue('GetWaitForAllPromise');
|
||||
|
||||
function onResolved(val) {
|
||||
result = 'resolved with ' + val;
|
||||
}
|
||||
|
@ -13,18 +15,18 @@ function onRejected(val) {
|
|||
result = 'rejected with ' + val;
|
||||
}
|
||||
|
||||
// Replacing `Promise#then` shouldn't affect getWaitForAllPromise.
|
||||
// Replacing `Promise#then` shouldn't affect GetWaitForAllPromise.
|
||||
let originalThen = Promise.prototype.then;
|
||||
Promise.prototype.then = 1;
|
||||
|
||||
// Replacing Promise[@@species] shouldn't affect getWaitForAllPromise.
|
||||
// Replacing Promise[@@species] shouldn't affect GetWaitForAllPromise.
|
||||
Promise[Symbol.species] = function(){};
|
||||
|
||||
// Replacing `Promise` shouldn't affect getWaitForAllPromise.
|
||||
// Replacing `Promise` shouldn't affect GetWaitForAllPromise.
|
||||
let PromiseCtor = Promise;
|
||||
Promise = {};
|
||||
|
||||
// Replacing Array[@@iterator] shouldn't affect getWaitForAllPromise.
|
||||
// Replacing Array[@@iterator] shouldn't affect GetWaitForAllPromise.
|
||||
Array.prototype[Symbol.iterator] = function(){};
|
||||
|
||||
let resolveFunctions = [];
|
||||
|
@ -38,7 +40,7 @@ for (let i = 0; i < 3; i++) {
|
|||
promises.push(p);
|
||||
}
|
||||
|
||||
let allPromise = getWaitForAllPromise(promises);
|
||||
let allPromise = GetWaitForAllPromise(promises);
|
||||
let then = originalThen.call(allPromise, onResolved, onRejected);
|
||||
|
||||
resolveFunctions.forEach((fun, i)=>fun(i));
|
||||
|
@ -48,14 +50,14 @@ assertEq(result, 'resolved with 0,1,2');
|
|||
|
||||
// Empty lists result in a promise resolved with an empty array.
|
||||
result = undefined;
|
||||
originalThen.call(getWaitForAllPromise([]), v=>(result = v));
|
||||
originalThen.call(GetWaitForAllPromise([]), v=>(result = v));
|
||||
drainJobQueue();
|
||||
assertEq(result instanceof Array, true);
|
||||
assertEq(result.length, 0);
|
||||
|
||||
//Empty lists result in a promise resolved with an empty array.
|
||||
result = undefined;
|
||||
originalThen.call(getWaitForAllPromise([]), v=>(result = v));
|
||||
originalThen.call(GetWaitForAllPromise([]), v=>(result = v));
|
||||
|
||||
drainJobQueue();
|
||||
|
||||
|
|
|
@ -121,6 +121,7 @@
|
|||
macro(frame, frame, "frame") \
|
||||
macro(from, from, "from") \
|
||||
macro(fulfilled, fulfilled, "fulfilled") \
|
||||
macro(fulfillHandler, fulfillHandler, "fulfillHandler") \
|
||||
macro(futexNotEqual, futexNotEqual, "not-equal") \
|
||||
macro(futexOK, futexOK, "ok") \
|
||||
macro(futexTimedOut, futexTimedOut, "timed-out") \
|
||||
|
@ -253,6 +254,7 @@
|
|||
macro(Reify, Reify, "Reify") \
|
||||
macro(reject, reject, "reject") \
|
||||
macro(rejected, rejected, "rejected") \
|
||||
macro(rejectHandler, rejectHandler, "rejectHandler") \
|
||||
macro(RequireObjectCoercible, RequireObjectCoercible, "RequireObjectCoercible") \
|
||||
macro(resolve, resolve, "resolve") \
|
||||
macro(resumeGenerator, resumeGenerator, "resumeGenerator") \
|
||||
|
|
|
@ -733,8 +733,7 @@ JSRuntime::enqueuePromiseJob(JSContext* cx, HandleFunction job, HandleObject pro
|
|||
// intrinsic_EnqueuePromiseReactionJob for details.
|
||||
if (IsWrapper(promise))
|
||||
unwrappedPromise = UncheckedUnwrap(promise);
|
||||
if (unwrappedPromise->is<PromiseObject>())
|
||||
allocationSite = JS::GetPromiseAllocationSite(unwrappedPromise);
|
||||
allocationSite = JS::GetPromiseAllocationSite(unwrappedPromise);
|
||||
}
|
||||
return cx->runtime()->enqueuePromiseJobCallback(cx, job, allocationSite, incumbentGlobal, data);
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include "builtin/MapObject.h"
|
||||
#include "builtin/ModuleObject.h"
|
||||
#include "builtin/Object.h"
|
||||
#include "builtin/Promise.h"
|
||||
#include "builtin/Reflect.h"
|
||||
#include "builtin/SelfHostingDefines.h"
|
||||
#include "builtin/SIMD.h"
|
||||
|
@ -177,6 +178,59 @@ intrinsic_IsConstructor(JSContext* cx, unsigned argc, Value* vp)
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intrinsic for calling a wrapped self-hosted function without invoking the
|
||||
* wrapper's security checks.
|
||||
*
|
||||
* Takes a wrapped function as the first and the receiver object as the
|
||||
* second argument. Any additional arguments are passed on to the unwrapped
|
||||
* function.
|
||||
*
|
||||
* Xray wrappers prevent lower-privileged code from passing objects to wrapped
|
||||
* functions from higher-privileged realms. In some cases, this check is too
|
||||
* strict, so this intrinsic allows getting around it.
|
||||
*
|
||||
* Note that it's not possible to replace all usages with dedicated intrinsics
|
||||
* as the function in question might be an inner function that closes over
|
||||
* state relevant to its execution.
|
||||
*
|
||||
* Right now, this is used for the Promise implementation to enable creating
|
||||
* resolution functions for xrayed Promises in the privileged realm and then
|
||||
* creating the Promise instance in the non-privileged one. The callbacks have
|
||||
* to be called by non-privileged code in various places, in many cases
|
||||
* passing objects as arguments.
|
||||
*/
|
||||
static bool
|
||||
intrinsic_UnsafeCallWrappedFunction(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
MOZ_ASSERT(args.length() >= 2);
|
||||
MOZ_ASSERT(IsCallable(args[0]));
|
||||
MOZ_ASSERT(IsWrapper(&args[0].toObject()));
|
||||
MOZ_ASSERT(args[1].isObject() || args[1].isUndefined());
|
||||
|
||||
MOZ_RELEASE_ASSERT(args[0].isObject());
|
||||
RootedObject wrappedFun(cx, &args[0].toObject());
|
||||
RootedObject fun(cx, UncheckedUnwrap(wrappedFun));
|
||||
MOZ_RELEASE_ASSERT(fun->is<JSFunction>());
|
||||
MOZ_RELEASE_ASSERT(fun->as<JSFunction>().isSelfHostedOrIntrinsic());
|
||||
|
||||
InvokeArgs args2(cx);
|
||||
if (!args2.init(cx, args.length() - 2))
|
||||
return false;
|
||||
|
||||
args2.setThis(args[1]);
|
||||
|
||||
for (size_t i = 0; i < args2.length(); i++)
|
||||
args2[i].set(args[i + 2]);
|
||||
|
||||
AutoWaivePolicy waivePolicy(cx, wrappedFun, JSID_VOIDHANDLE, BaseProxyHandler::CALL);
|
||||
if (!CrossCompartmentWrapper::singleton.call(cx, wrappedFun, args2))
|
||||
return false;
|
||||
args.rval().set(args2.rval());
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static bool
|
||||
intrinsic_IsInstanceOfBuiltin(JSContext* cx, unsigned argc, Value* vp)
|
||||
|
@ -1831,6 +1885,54 @@ js::ReportIncompatibleSelfHostedMethod(JSContext* cx, const CallArgs& args)
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
intrinsic_EnqueuePromiseReactionJob(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
MOZ_ASSERT(args.length() == 6);
|
||||
|
||||
RootedValue handler(cx, args[0]);
|
||||
MOZ_ASSERT((handler.isNumber() &&
|
||||
(handler.toNumber() == PROMISE_HANDLER_IDENTITY ||
|
||||
handler.toNumber() == PROMISE_HANDLER_THROWER)) ||
|
||||
handler.toObject().isCallable());
|
||||
|
||||
RootedValue handlerArg(cx, args[1]);
|
||||
|
||||
RootedObject resolve(cx, &args[2].toObject());
|
||||
MOZ_ASSERT(IsCallable(resolve));
|
||||
|
||||
RootedObject reject(cx, &args[3].toObject());
|
||||
MOZ_ASSERT(IsCallable(reject));
|
||||
|
||||
RootedObject promise(cx, args[4].toObjectOrNull());
|
||||
RootedObject objectFromIncumbentGlobal(cx, args[5].toObjectOrNull());
|
||||
|
||||
if (!EnqueuePromiseReactionJob(cx, handler, handlerArg, resolve, reject, promise,
|
||||
objectFromIncumbentGlobal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
args.rval().setUndefined();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ES2016, February 12 draft, 25.4.1.9.
|
||||
static bool
|
||||
intrinsic_HostPromiseRejectionTracker(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
MOZ_ASSERT(args.length() == 2);
|
||||
MOZ_ASSERT(args[0].toObject().is<PromiseObject>());
|
||||
|
||||
Rooted<PromiseObject*> promise(cx, &args[0].toObject().as<PromiseObject>());
|
||||
mozilla::DebugOnly<bool> isHandled = args[1].toBoolean();
|
||||
MOZ_ASSERT(isHandled, "HostPromiseRejectionTracker intrinsic currently only marks as handled");
|
||||
cx->runtime()->removeUnhandledRejectedPromise(cx, promise);
|
||||
args.rval().setUndefined();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default locale as a well-formed, but not necessarily canonicalized,
|
||||
* BCP-47 language tag.
|
||||
|
@ -1957,6 +2059,61 @@ intrinsic_NameForTypedArray(JSContext* cx, unsigned argc, Value* vp)
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object created in the embedding-provided incumbent global.
|
||||
*
|
||||
* Really, we want the incumbent global itself so we can pass it to other
|
||||
* embedding hooks which need it. Specifically, the enqueue promise hook
|
||||
* takes an incumbent global so it can set that on the PromiseCallbackJob
|
||||
* it creates.
|
||||
*
|
||||
* The reason for not just returning the global itself is that we'd need to
|
||||
* wrap it into the current compartment, and later unwrap it. Unwrapping
|
||||
* globals is tricky, though: we might accidentally unwrap through an inner
|
||||
* to its outer window and end up with the wrong global. Plain objects don't
|
||||
* have this problem, so we create one and return it. The code using it -
|
||||
* e.g. EnqueuePromiseReactionJob - can then unwrap the object and get its
|
||||
* global without fear of unwrapping too far.
|
||||
*/
|
||||
static bool
|
||||
intrinsic_GetObjectFromIncumbentGlobal(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
MOZ_ASSERT(args.length() == 0);
|
||||
|
||||
RootedObject obj(cx);
|
||||
RootedObject global(cx, cx->runtime()->getIncumbentGlobal(cx));
|
||||
if (global) {
|
||||
MOZ_ASSERT(global->is<GlobalObject>());
|
||||
AutoCompartment ac(cx, global);
|
||||
obj = NewBuiltinClassInstance<PlainObject>(cx);
|
||||
if (!obj)
|
||||
return false;
|
||||
}
|
||||
|
||||
RootedValue objVal(cx, ObjectOrNullValue(obj));
|
||||
|
||||
// The object might be from a different compartment, so wrap it.
|
||||
if (obj && !cx->compartment()->wrap(cx, &objVal))
|
||||
return false;
|
||||
|
||||
args.rval().set(objVal);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
intrinsic_IsWrappedPromiseObject(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
CallArgs args = CallArgsFromVp(argc, vp);
|
||||
MOZ_ASSERT(args.length() == 1);
|
||||
|
||||
RootedObject obj(cx, &args[0].toObject());
|
||||
MOZ_ASSERT(!obj->is<PromiseObject>(),
|
||||
"Unwrapped promises should be filtered out in inlineable code");
|
||||
args.rval().setBoolean(CheckedUnwrap(obj)->is<PromiseObject>());
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
intrinsic_HostResolveImportedModule(JSContext* cx, unsigned argc, Value* vp)
|
||||
{
|
||||
|
@ -2251,6 +2408,7 @@ static const JSFunctionSpec intrinsic_functions[] = {
|
|||
JS_INLINABLE_FN("UnsafeGetBooleanFromReservedSlot", intrinsic_UnsafeGetBooleanFromReservedSlot,2,0,
|
||||
IntrinsicUnsafeGetBooleanFromReservedSlot),
|
||||
|
||||
JS_FN("UnsafeCallWrappedFunction", intrinsic_UnsafeCallWrappedFunction,2,0),
|
||||
JS_FN("NewArrayInCompartment", intrinsic_NewArrayInCompartment, 1,0),
|
||||
|
||||
JS_FN("IsPackedArray", intrinsic_IsPackedArray, 1,0),
|
||||
|
@ -2363,6 +2521,14 @@ static const JSFunctionSpec intrinsic_functions[] = {
|
|||
JS_FN("CallWeakSetMethodIfWrapped",
|
||||
CallNonGenericSelfhostedMethod<Is<WeakSetObject>>, 2, 0),
|
||||
|
||||
JS_FN("_GetObjectFromIncumbentGlobal", intrinsic_GetObjectFromIncumbentGlobal, 0, 0),
|
||||
JS_FN("IsPromise", intrinsic_IsInstanceOfBuiltin<PromiseObject>, 1,0),
|
||||
JS_FN("IsWrappedPromise", intrinsic_IsWrappedPromiseObject, 1, 0),
|
||||
JS_FN("_EnqueuePromiseReactionJob", intrinsic_EnqueuePromiseReactionJob, 2, 0),
|
||||
JS_FN("HostPromiseRejectionTracker", intrinsic_HostPromiseRejectionTracker,2, 0),
|
||||
JS_FN("CallPromiseMethodIfWrapped",
|
||||
CallNonGenericSelfhostedMethod<Is<PromiseObject>>, 2,0),
|
||||
|
||||
// See builtin/TypedObject.h for descriptors of the typedobj functions.
|
||||
JS_FN("NewOpaqueTypedObject", js::NewOpaqueTypedObject, 1, 0),
|
||||
JS_FN("NewDerivedTypedObject", js::NewDerivedTypedObject, 3, 0),
|
||||
|
|
Загрузка…
Ссылка в новой задаче