Skip to content

Commit

Permalink
feat(zone.js): add Promise.any() implementation
Browse files Browse the repository at this point in the history
Implement `Promise.any()` introduced in ES2021

Close #44393
  • Loading branch information
JiaLiPassion committed Feb 12, 2022
1 parent bbababe commit 1cb3351
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 3 deletions.
53 changes: 52 additions & 1 deletion packages/zone.js/lib/common/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
const promiseState = (promise as any)[symbolState];
const delegate = promiseState ?
(typeof onFulfilled === 'function') ? onFulfilled : forwardResolution :
(typeof onRejected === 'function') ? onRejected : forwardRejection;
(typeof onRejected === 'function') ? onRejected :
forwardRejection;
zone.scheduleMicroTask(source, () => {
try {
const parentPromiseValue = (promise as any)[symbolValue];
Expand Down Expand Up @@ -283,6 +284,19 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr

const noop = function() {};

class ZoneAggregateError extends Error {
errors: Iterable<any>;
constructor(errors: Iterable<any>, message?: string) {
super(message);
this.name = 'AggregateError';
this.errors = errors;
}
}

if (!global.AggregateError) {
global.AggregateError = ZoneAggregateError;
}

class ZoneAwarePromise<R> implements Promise<R> {
static toString() {
return ZONE_AWARE_PROMISE_TO_STRING;
Expand All @@ -296,6 +310,43 @@ Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePr
return resolvePromise(<ZoneAwarePromise<U>>new this(null as any), REJECTED, error);
}

static any<T>(values: Iterable<PromiseLike<T>>): Promise<T> {
if (!values || typeof values[Symbol.iterator] !== 'function') {
return Promise.reject(new ZoneAggregateError([], 'All Promises rejected'));
}
const promises: Promise<PromiseLike<T>>[] = [];
let count = 0;
for (let v of values) {
count++;
promises.push(ZoneAwarePromise.resolve(v));
}
if (count === 0) {
return Promise.reject(new ZoneAggregateError([], 'All Promises rejected'));
}
let finished = false;
const errors: any[] = [];
return new ZoneAwarePromise((resolve, reject) => {
promises.forEach(p => {
p.then(
v => {
if (finished) {
return;
}
finished = true;
resolve(v);
},
err => {
errors.push(err);
count--;
if (count === 0) {
finished = true;
reject(new ZoneAggregateError(errors, 'All Promises rejected'));
}
});
});
});
};

static race<R>(values: PromiseLike<any>[]): Promise<R> {
let resolve: (v: any) => void;
let reject: (v: any) => void;
Expand Down
101 changes: 101 additions & 0 deletions packages/zone.js/test/common/Promise.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,107 @@ describe(
});
});

describe('Promise.any', () => {
const any = (Promise as any).any;
it('undefined parameters', (done: DoneFn) => {
any().then(
() => {
fail('should not get a resolved promise.');
},
(err: any) => {
expect(err.message).toEqual('All Promises rejected');
expect(err.errors).toEqual([]);
done();
});
});
it('empty parameters', (done: DoneFn) => {
any([]).then(
() => {
fail('should not get a resolved promise.');
},
(err: any) => {
expect(err.message).toEqual('All Promises rejected');
expect(err.errors).toEqual([]);
done();
});
});
it('non promises parameters', (done: DoneFn) => {
any([1, 'test'])
.then(
(v: any) => {
expect(v).toBe(1);
done();
},
(err: any) => {
fail('should not get a rejected promise.');
});
});
it('mixed parameters, non promise first', (done: DoneFn) => {
any([1, Promise.resolve(2)])
.then(
(v: any) => {
expect(v).toBe(1);
done();
},
(err: any) => {
fail('should not get a rejected promise.');
});
});
it('mixed parameters, promise first', (done: DoneFn) => {
any([Promise.resolve(1), 2])
.then(
(v: any) => {
expect(v).toBe(1);
done();
},
(err: any) => {
fail('should not get a rejected promise.');
});
});
it('all ok promises', (done: DoneFn) => {
any([Promise.resolve(1), Promise.resolve(2)])
.then(
(v: any) => {
expect(v).toBe(1);
done();
},
(err: any) => {
fail('should not get a rejected promise.');
});
});
it('all promises, first rejected', (done: DoneFn) => {
any([Promise.reject('error'), Promise.resolve(2)])
.then(
(v: any) => {
expect(v).toBe(2);
done();
},
(err: any) => {
fail('should not get a rejected promise.');
});
});
it('all promises, second rejected', (done: DoneFn) => {
any([Promise.resolve(1), Promise.reject('error')])
.then(
(v: any) => {
expect(v).toBe(1);
done();
},
(err: any) => {
fail('should not get a rejected promise.');
});
});
it('all rejected promises', (done: DoneFn) => {
any([
Promise.reject('error1'), Promise.reject('error2')
]).then((v: any) => {fail('should not get a resolved promise.')}, (err: any) => {
expect(err.message).toEqual('All Promises rejected');
expect(err.errors).toEqual(['error1', 'error2']);
done();
});
});
});

describe('Promise.allSettled', () => {
const yes = function makeFulfilledResult(value: any) {
return {status: 'fulfilled', value: value};
Expand Down
4 changes: 2 additions & 2 deletions packages/zone.js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"outDir": "build",
"inlineSourceMap": true,
"inlineSources": true,
Expand Down Expand Up @@ -38,4 +38,4 @@
"test/node_error_entry_point.ts",
"test/node_tests.ts"
]
}
}

0 comments on commit 1cb3351

Please sign in to comment.