Skip to content
This repository has been archived by the owner on Oct 9, 2023. It is now read-only.

Avoid mutating the global Promise #135

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"pretests-only": "npm run build",
"tests-only": "npm run test:quick",
"precoverage": "npm run build",
"coverage": "babel-node node_modules/.bin/istanbul cover --report html node_modules/.bin/_mocha -- -R tap test/init.js test/*-test.js",
"coverage": "babel-node node_modules/.bin/istanbul cover --report html node_modules/.bin/_mocha -- -R tap test/init.js test/*-test.js test/**/*-test.js",
"postcoverage": "npm run cover:check",
"cover:check": "istanbul check-coverage && echo code coverage thresholds met, achievement unlocked!",
"test:quick": "babel-node node_modules/.bin/_mocha -R tap test/init.js test/*-test.js"
Expand Down
25 changes: 2 additions & 23 deletions src/environment.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,5 @@
/* eslint func-names:0 no-extra-parens:0 */
import 'airbnb-js-shims';
import Promise from 'bluebird';
import StrictPromise from './utils/strict-promise';

const es6methods = ['then', 'catch', 'constructor'];
const es6StaticMethods = ['all', 'race', 'resolve', 'reject', 'cast'];

function isNotMethod(name) {
return !(es6methods.includes(name) || es6StaticMethods.includes(name) || name.charAt(0) === '_');
}

function del(obj) {
/* eslint no-param-reassign: 0 */
return (key) => { delete obj[key]; };
}

function toFastProperties(obj) {
(function () {}).prototype = obj;
}

Object.keys(Promise.prototype).filter(isNotMethod).forEach(del(Promise.prototype));
Object.keys(Promise).filter(isNotMethod).forEach(del(Promise));
toFastProperties(Promise);
toFastProperties(Promise.prototype);

global.Promise = Promise;
global.Promise = StrictPromise;
69 changes: 69 additions & 0 deletions src/utils/strict-promise.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable no-underscore-dangle */

import Promise from 'bluebird';

export default class StrictPromise {
constructor(executor) {
this._promise = new Promise(executor);
}

static all(iterable) {
const newPromise = Promise.all(iterable);

const strictPromise = new StrictPromise((resolve, reject) => {
newPromise.then(resolve, reject);
});

return strictPromise;
}

static race(iterable) {
const newPromise = Promise.race(iterable);

const strictPromise = new StrictPromise((resolve, reject) => {
newPromise.then(resolve, reject);
});

return strictPromise;
}

static reject(value) {
const newPromise = Promise.reject(value);

const strictPromise = new StrictPromise((resolve, reject) => {
newPromise.then(resolve, reject);
});

return strictPromise;
}

static resolve(value) {
const newPromise = Promise.resolve(value);

const strictPromise = new StrictPromise((resolve, reject) => {
newPromise.then(resolve, reject);
});

return strictPromise;
}

then(onFulfilled, onRejected) {
const newPromise = this._promise.then(onFulfilled, onRejected);

const strictPromise = new StrictPromise((resolve, reject) => {
newPromise.then(resolve, reject);
});

return strictPromise;
}

catch(onRejected) {
const newPromise = this._promise.catch(onRejected);

const strictPromise = new StrictPromise((resolve, reject) => {
newPromise.then(resolve, reject);
});

return strictPromise;
}
}
19 changes: 19 additions & 0 deletions test/environment-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { assert } from 'chai';
import '../src/environment';

describe('environment', () => {
describe('global promise', () => {
it('has ES6 methods', () => {
assert.isFunction(global.Promise.prototype.then);
assert.isFunction(global.Promise.prototype.catch);
assert.isFunction(global.Promise.prototype.constructor);
});

it('has ES6 static methods', () => {
assert.isFunction(global.Promise.all);
assert.isFunction(global.Promise.race);
assert.isFunction(global.Promise.resolve);
assert.isFunction(global.Promise.reject);
});
});
});
170 changes: 170 additions & 0 deletions test/utils/strict-promise-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { assert } from 'chai';
import StrictPromise from '../../src/utils/strict-promise';

describe('StrictPromise', () => {
describe('static all', () => {
it('resolves when the iterable is empty', (done) => {
StrictPromise.all([]).then(() => {
done();
});
});

it('resolves when the whole iterable is resolved', (done) => {
const resolveWiths = [];
const messages = [
'First resolved.',
'Second resolved.',
'Third resolved',
];

StrictPromise.all([
new StrictPromise((resolve) => {
resolveWiths.push(resolve);
}),
new StrictPromise((resolve) => {
resolveWiths.push(resolve);
}),
new StrictPromise((resolve) => {
resolveWiths.push(resolve);
}),
]).then((resolvedWiths) => {
assert.strictEqual(resolvedWiths[0], messages[0]);
assert.strictEqual(resolvedWiths[1], messages[1]);
assert.strictEqual(resolvedWiths[2], messages[2]);
done();
});

resolveWiths[0](messages[0]);
resolveWiths[1](messages[1]);
resolveWiths[2](messages[2]);
});

it('rejects with the first rejected iterable', (done) => {
let rejectWith;
const message = 'Only one rejected.';

StrictPromise.all([
new StrictPromise(() => {}),
new StrictPromise((resolve, reject) => {
rejectWith = reject;
}),
new StrictPromise(() => {}),
]).then(() => {}, (rejectedWith) => {
assert.strictEqual(rejectedWith, message);
done();
});

rejectWith(message);
});
});

describe('static race', () => {
it('resolves with the first resolved iterable', (done) => {
let resolveWith;
const message = 'It is a resolved race!';

StrictPromise.race([
new StrictPromise(() => {}),
new StrictPromise((resolve) => {
resolveWith = resolve;
}),
new StrictPromise(() => {}),
]).then((resolvedWith) => {
assert.strictEqual(resolvedWith, message);
done();
});

resolveWith(message);
});

it('rejects with the first rejected iterable', (done) => {
let rejectWith;
const message = 'It is a rejected race!';

StrictPromise.race([
new StrictPromise(() => {}),
new StrictPromise((resolve, reject) => {
rejectWith = reject;
}),
new StrictPromise(() => {}),
]).then(() => {}, (rejectedWith) => {
assert.strictEqual(rejectedWith, message);
done();
});

rejectWith(message);
});
});

describe('static reject', () => {
it('rejects with a given reason', (done) => {
const message = 'rejected';

StrictPromise.reject(message).catch((rejectedWith) => {
assert.strictEqual(rejectedWith, message);
done();
});
});
});

describe('static resolve', () => {
it('resolves with a given reason', (done) => {
const message = 'fulfilled';

StrictPromise.resolve(message).then((resolvedWith) => {
assert.strictEqual(resolvedWith, message);
done();
});
});
});

describe('thennable', () => {
it('has a fulfillment callback', (done) => {
let resolveWith;
const message = 'fulfilled';

const promise = new StrictPromise((resolve) => {
resolveWith = resolve;
});

promise.then((resolvedWith) => {
assert.strictEqual(resolvedWith, message);
done();
});

resolveWith(message);
});

it('has a rejection callback', (done) => {
let rejectWith;
const message = 'rejected';

const promise = new StrictPromise((resolve, reject) => {
rejectWith = reject;
});

promise.then(() => {}, (rejectedWith) => {
assert.strictEqual(rejectedWith, message);
done();
});

rejectWith(message);
});
});

it('is catchable', (done) => {
let rejectWith;
const message = 'rejected';

const promise = new StrictPromise((resolve, reject) => {
rejectWith = reject;
});

promise.catch((rejectedWith) => {
assert.strictEqual(rejectedWith, message);
done();
});

rejectWith(message);
});
});