Skip to content

Commit 436f51c

Browse files
committed
Core: do not rely on an extendable window.Promise
1 parent 4994064 commit 436f51c

File tree

2 files changed

+59
-127
lines changed

2 files changed

+59
-127
lines changed

src/utils/promise.js

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import {getGlobal} from '../prebidGlobal.js';
2-
31
const SUCCESS = 0;
42
const FAIL = 1;
53

64
/**
75
* A version of Promise that runs callbacks synchronously when it can (i.e. after it's been fulfilled or rejected).
86
*/
9-
export class GreedyPromise extends (getGlobal().Promise || Promise) {
7+
export class GreedyPromise {
108
#result;
119
#callbacks;
12-
#parent = null;
1310

1411
/**
1512
* Convenience wrapper for setTimeout; takes care of returning an already fulfilled GreedyPromise when the delay is zero.
@@ -24,53 +21,33 @@ export class GreedyPromise extends (getGlobal().Promise || Promise) {
2421
}
2522

2623
constructor(resolver) {
24+
if (typeof resolver !== 'function') {
25+
throw new Error('resolver not a function');
26+
}
2727
const result = [];
2828
const callbacks = [];
29-
function handler(type, resolveFn) {
29+
let [resolve, reject] = [SUCCESS, FAIL].map((type) => {
3030
return function (value) {
31-
if (!result.length) {
31+
if (type === SUCCESS && typeof value?.then === 'function') {
32+
value.then(resolve, reject);
33+
} else if (!result.length) {
3234
result.push(type, value);
3335
while (callbacks.length) callbacks.shift()();
34-
resolveFn(value);
3536
}
3637
}
38+
});
39+
try {
40+
resolver(resolve, reject);
41+
} catch (e) {
42+
reject(e);
3743
}
38-
super(
39-
typeof resolver !== 'function'
40-
? resolver // let super throw an error
41-
: (resolve, reject) => {
42-
const rejectHandler = handler(FAIL, reject);
43-
const resolveHandler = (() => {
44-
const done = handler(SUCCESS, resolve);
45-
return value =>
46-
typeof value?.then === 'function' ? value.then(done, rejectHandler) : done(value);
47-
})();
48-
try {
49-
resolver(resolveHandler, rejectHandler);
50-
} catch (e) {
51-
rejectHandler(e);
52-
}
53-
}
54-
);
5544
this.#result = result;
5645
this.#callbacks = callbacks;
5746
}
47+
5848
then(onSuccess, onError) {
59-
if (typeof onError === 'function') {
60-
// if an error handler is provided, attach a dummy error handler to super,
61-
// and do the same for all promises without an error handler that precede this one in a chain.
62-
// This is to avoid unhandled rejection events / warnings for errors that were, in fact, handled;
63-
// since we are not using super's callback mechanisms we need to make it aware of this separately.
64-
let node = this;
65-
while (node) {
66-
super.then.call(node, null, () => null);
67-
const next = node.#parent;
68-
node.#parent = null; // since we attached a handler already, we are no longer interested in what will happen later in the chain
69-
node = next;
70-
}
71-
}
7249
const result = this.#result;
73-
const res = new GreedyPromise((resolve, reject) => {
50+
return new this.constructor((resolve, reject) => {
7451
const continuation = () => {
7552
let value = result[1];
7653
let [handler, resolveFn] = result[0] === SUCCESS ? [onSuccess, resolve] : [onError, reject];
@@ -87,8 +64,50 @@ export class GreedyPromise extends (getGlobal().Promise || Promise) {
8764
}
8865
result.length ? continuation() : this.#callbacks.push(continuation);
8966
});
90-
res.#parent = this;
91-
return res;
67+
}
68+
69+
catch(onError) {
70+
return this.then(null, onError);
71+
}
72+
73+
finally(onFinally) {
74+
let val;
75+
return this.then(
76+
(v) => { val = v; return onFinally(); },
77+
(e) => { val = this.constructor.reject(e); return onFinally() }
78+
).then(() => val);
79+
}
80+
81+
static #collect(promises, collector, done) {
82+
let cnt = promises.length;
83+
function clt() {
84+
collector.apply(this, arguments);
85+
if (--cnt === 0 && done) done();
86+
}
87+
promises.forEach((p, i) => this.resolve(p).then(
88+
(val) => clt(true, val, i),
89+
(err) => clt(false, err, i)
90+
));
91+
}
92+
93+
static race(promises) {
94+
return new this((resolve, reject) => {
95+
this.#collect(promises, (success, result) => success ? resolve(result) : reject(result));
96+
})
97+
}
98+
99+
static all(promises) {
100+
return new this((resolve, reject) => {
101+
let res = [];
102+
this.#collect(promises, (success, val, i) => success ? res[i] = val : reject(val), () => resolve(res));
103+
})
104+
}
105+
106+
static allSettled(promises) {
107+
return new this((resolve) => {
108+
let res = [];
109+
this.#collect(promises, (success, val, i) => res[i] = success ? {status: 'fulfilled', value: val} : {status: 'rejected', reason: val}, () => resolve(res))
110+
})
92111
}
93112

94113
static resolve(value) {

test/spec/unit/utils/promise_spec.js

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -19,93 +19,6 @@ describe('GreedyPromise', () => {
1919
})
2020
});
2121

22-
describe('unhandled rejections', () => {
23-
let unhandled, done, stop;
24-
25-
function reset(expectUnhandled) {
26-
let pending = expectUnhandled;
27-
let resolver;
28-
unhandled.reset();
29-
unhandled.callsFake(() => {
30-
pending--;
31-
if (pending === 0) {
32-
resolver();
33-
}
34-
})
35-
done = new Promise((resolve) => {
36-
resolver = resolve;
37-
stop = function () {
38-
if (expectUnhandled === 0) {
39-
resolve()
40-
} else {
41-
resolver = resolve;
42-
}
43-
}
44-
})
45-
}
46-
47-
before(() => {
48-
unhandled = sinon.stub();
49-
window.addEventListener('unhandledrejection', unhandled);
50-
});
51-
52-
after(() => {
53-
window.removeEventListener('unhandledrejection', unhandled);
54-
});
55-
56-
function getUnhandledErrors() {
57-
return unhandled.args.map((args) => args[0].reason);
58-
}
59-
60-
Object.entries({
61-
'simple reject': [1, (P) => { P.reject('err'); stop() }],
62-
'caught reject': [0, (P) => P.reject('err').catch((e) => { stop(); return e })],
63-
'unhandled reject with finally': [1, (P) => P.reject('err').finally(() => 'finally')],
64-
'error handler that throws': [1, (P) => P.reject('err').catch((e) => { stop(); throw e })],
65-
'rejection handled later in the chain': [0, (P) => P.reject('err').then((v) => v).catch((e) => { stop(); return e })],
66-
'multiple errors in one chain': [1, (P) => P.reject('err').then((v) => v).catch((e) => e).then((v) => { stop(); return P.reject(v) })],
67-
'multiple errors in one chain, all handled': [0, (P) => P.reject('err').then((v) => v).catch((e) => e).then((v) => P.reject(v)).catch((e) => { stop(); return e })],
68-
'separate chains for rejection and handling': [1, (P) => {
69-
const p = P.reject('err');
70-
p.catch((e) => { stop(); return e; })
71-
p.then((v) => v);
72-
}],
73-
'separate rejections merged without handling': [2, (P) => {
74-
const p1 = P.reject('err1');
75-
const p2 = P.reject('err2');
76-
p1.then(() => p2).finally(stop);
77-
}],
78-
'separate rejections merged for handling': [0, (P) => {
79-
const p1 = P.reject('err1');
80-
const p2 = P.reject('err2');
81-
P.all([p1, p2]).catch((e) => { stop(); return e });
82-
}],
83-
// eslint-disable-next-line no-throw-literal
84-
'exception in resolver': [1, (P) => new P(() => { stop(); throw 'err'; })],
85-
// eslint-disable-next-line no-throw-literal
86-
'exception in resolver, caught': [0, (P) => new P(() => { throw 'err' }).catch((e) => { stop(); return e })],
87-
'errors from nested promises': [1, (P) => new P((resolve) => setTimeout(() => { resolve(P.reject('err')); stop(); }))],
88-
'errors from nested promises, caught': [0, (P) => new P((resolve) => setTimeout(() => resolve(P.reject('err')))).catch((e) => { stop(); return e })],
89-
}).forEach(([t, [expectUnhandled, op]]) => {
90-
describe(`on ${t}`, () => {
91-
it('should match vanilla Promises', () => {
92-
let vanillaUnhandled;
93-
reset(expectUnhandled);
94-
op(Promise);
95-
return done.then(() => {
96-
vanillaUnhandled = getUnhandledErrors();
97-
reset(expectUnhandled);
98-
op(GreedyPromise);
99-
return done;
100-
}).then(() => {
101-
const actualUnhandled = getUnhandledErrors();
102-
expect(actualUnhandled.length).to.eql(expectUnhandled);
103-
expect(actualUnhandled).to.eql(vanillaUnhandled);
104-
})
105-
})
106-
})
107-
});
108-
});
10922

11023
describe('idioms', () => {
11124
let makePromise, pendingFailure, pendingSuccess;

0 commit comments

Comments
 (0)