Skip to content

Commit

Permalink
Bug 1033406 - Promise.jsm promise should still work after Object.free…
Browse files Browse the repository at this point in the history
…ze. r=paolo

Fix by moving internal properties into N_INTERNALS.
  • Loading branch information
neojski committed Aug 20, 2014
1 parent c7cd7bf commit a44e895
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 50 deletions.
99 changes: 49 additions & 50 deletions toolkit/modules/Promise-backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,13 @@ const STATUS_PENDING = 0;
const STATUS_RESOLVED = 1;
const STATUS_REJECTED = 2;

// These "private names" allow some properties of the Promise object to be
// This N_INTERNALS name allow internal properties of the Promise to be
// accessed only by this module, while still being visible on the object
// manually when using a debugger. They don't strictly guarantee that the
// manually when using a debugger. This doesn't strictly guarantee that the
// properties are inaccessible by other code, but provide enough protection to
// avoid using them by mistake.
const salt = Math.floor(Math.random() * 100);
const Name = (n) => "{private:" + n + ":" + salt + "}";
const N_STATUS = Name("status");
const N_VALUE = Name("value");
const N_HANDLERS = Name("handlers");
const N_WITNESS = Name("witness");

const N_INTERNALS = "{private:internals:" + salt + "}";

/////// Warn-upon-finalization mechanism
//
Expand Down Expand Up @@ -308,35 +303,39 @@ this.Promise = function Promise(aExecutor)
}

/*
* Internal status of the promise. This can be equal to STATUS_PENDING,
* STATUS_RESOLVED, or STATUS_REJECTED.
*/
Object.defineProperty(this, N_STATUS, { value: STATUS_PENDING,
writable: true });

/*
* When the N_STATUS property is STATUS_RESOLVED, this contains the final
* resolution value, that cannot be a promise, because resolving with a
* promise will cause its state to be eventually propagated instead. When the
* N_STATUS property is STATUS_REJECTED, this contains the final rejection
* reason, that could be a promise, even if this is uncommon.
*/
Object.defineProperty(this, N_VALUE, { writable: true });

/*
* Array of Handler objects registered by the "then" method, and not processed
* yet. Handlers are removed when the promise is resolved or rejected.
*/
Object.defineProperty(this, N_HANDLERS, { value: [] });

/**
* When the N_STATUS property is STATUS_REJECTED and until there is
* a rejection callback, this contains an array
* - {string} id An id for use with |PendingErrors|;
* - {FinalizationWitness} witness A witness broadcasting |id| on
* notification "promise-finalization-witness".
* Object holding all of our internal values we associate with the promise.
*/
Object.defineProperty(this, N_WITNESS, { writable: true });
Object.defineProperty(this, N_INTERNALS, { value: {
/*
* Internal status of the promise. This can be equal to STATUS_PENDING,
* STATUS_RESOLVED, or STATUS_REJECTED.
*/
status: STATUS_PENDING,

/*
* When the status property is STATUS_RESOLVED, this contains the final
* resolution value, that cannot be a promise, because resolving with a
* promise will cause its state to be eventually propagated instead. When the
* status property is STATUS_REJECTED, this contains the final rejection
* reason, that could be a promise, even if this is uncommon.
*/
value: undefined,

/*
* Array of Handler objects registered by the "then" method, and not processed
* yet. Handlers are removed when the promise is resolved or rejected.
*/
handlers: [],

/**
* When the status property is STATUS_REJECTED and until there is
* a rejection callback, this contains an array
* - {string} id An id for use with |PendingErrors|;
* - {FinalizationWitness} witness A witness broadcasting |id| on
* notification "promise-finalization-witness".
*/
witness: undefined
}});

Object.seal(this);

Expand Down Expand Up @@ -398,16 +397,16 @@ this.Promise = function Promise(aExecutor)
Promise.prototype.then = function (aOnResolve, aOnReject)
{
let handler = new Handler(this, aOnResolve, aOnReject);
this[N_HANDLERS].push(handler);
this[N_INTERNALS].handlers.push(handler);

// Ensure the handler is scheduled for processing if this promise is already
// resolved or rejected.
if (this[N_STATUS] != STATUS_PENDING) {
if (this[N_INTERNALS].status != STATUS_PENDING) {

// This promise is not the last in the chain anymore. Remove any watchdog.
if (this[N_WITNESS] != null) {
let [id, witness] = this[N_WITNESS];
this[N_WITNESS] = null;
if (this[N_INTERNALS].witness != null) {
let [id, witness] = this[N_INTERNALS].witness;
this[N_INTERNALS].witness = null;
witness.forget();
PendingErrors.unregister(id);
}
Expand Down Expand Up @@ -649,7 +648,7 @@ this.PromiseWalker = {
completePromise: function (aPromise, aStatus, aValue)
{
// Do nothing if the promise is already resolved or rejected.
if (aPromise[N_STATUS] != STATUS_PENDING) {
if (aPromise[N_INTERNALS].status != STATUS_PENDING) {
return;
}

Expand All @@ -663,17 +662,17 @@ this.PromiseWalker = {
}

// Change the promise status and schedule our handlers for processing.
aPromise[N_STATUS] = aStatus;
aPromise[N_VALUE] = aValue;
if (aPromise[N_HANDLERS].length > 0) {
aPromise[N_INTERNALS].status = aStatus;
aPromise[N_INTERNALS].value = aValue;
if (aPromise[N_INTERNALS].handlers.length > 0) {
this.schedulePromise(aPromise);
} else if (aStatus == STATUS_REJECTED) {
// This is a rejection and the promise is the last in the chain.
// For the time being we therefore have an uncaught error.
let id = PendingErrors.register(aValue);
let witness =
FinalizationWitnessService.make("promise-finalization-witness", id);
aPromise[N_WITNESS] = [id, witness];
aPromise[N_INTERNALS].witness = [id, witness];
}
},

Expand All @@ -698,10 +697,10 @@ this.PromiseWalker = {
schedulePromise: function (aPromise)
{
// Migrate the handlers from the provided promise to the global list.
for (let handler of aPromise[N_HANDLERS]) {
for (let handler of aPromise[N_INTERNALS].handlers) {
this.handlers.push(handler);
}
aPromise[N_HANDLERS].length = 0;
aPromise[N_INTERNALS].handlers.length = 0;

// Schedule the walker loop on the next tick of the event loop.
if (!this.walkerLoopScheduled) {
Expand Down Expand Up @@ -854,8 +853,8 @@ Handler.prototype = {
process: function()
{
// The state of this promise is propagated unless a handler is defined.
let nextStatus = this.thisPromise[N_STATUS];
let nextValue = this.thisPromise[N_VALUE];
let nextStatus = this.thisPromise[N_INTERNALS].status;
let nextValue = this.thisPromise[N_INTERNALS].value;

try {
// If a handler is defined for either resolution or rejection, invoke it
Expand Down
11 changes: 11 additions & 0 deletions toolkit/modules/tests/xpcshell/test_Promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,17 @@ make_promise_test(function test_caught_is_not_reported() {
);
}));

// Bug 1033406 - Make sure Promise works even after freezing.
tests.push(
make_promise_test(function test_freezing_promise(test) {
var p = new Promise(function executor(resolve) {
do_execute_soon(resolve);
});
Object.freeze(p);
return p;
})
);

function run_test()
{
do_test_pending();
Expand Down

0 comments on commit a44e895

Please sign in to comment.