This commit is contained in:
2022-07-18 02:50:52 +00:00
parent befd344ab0
commit 06181b34d6
8569 changed files with 818704 additions and 352705 deletions
+2 -2
View File
@@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { FarmOptions } from './types';
import { FarmOptions, PromiseWithCustomMessage } from './types';
export default class Farm {
private _computeWorkerKey;
private _cacheKeys;
@@ -15,7 +15,7 @@ export default class Farm {
private _offset;
private _queue;
constructor(numOfWorkers: number, callback: Function, computeWorkerKey?: FarmOptions['computeWorkerKey']);
doWork(method: string, ...args: Array<any>): Promise<unknown>;
doWork(method: string, ...args: Array<unknown>): PromiseWithCustomMessage<unknown>;
private _getNextTask;
private _process;
private _enqueue;
+26 -2
View File
@@ -53,7 +53,20 @@ class Farm {
}
doWork(method, ...args) {
return new Promise((resolve, reject) => {
const customMessageListeners = new Set();
const addCustomMessageListener = listener => {
customMessageListeners.add(listener);
return () => {
customMessageListeners.delete(listener);
};
};
const onCustomMessage = message => {
customMessageListeners.forEach(listener => listener(message));
};
const promise = new Promise((resolve, reject) => {
const computeWorkerKey = this._computeWorkerKey;
const request = [_types.CHILD_MESSAGE_CALL, false, method, args];
let worker = null;
@@ -71,6 +84,8 @@ class Farm {
};
const onEnd = (error, result) => {
customMessageListeners.clear();
if (error) {
reject(error);
} else {
@@ -79,6 +94,7 @@ class Farm {
};
const task = {
onCustomMessage,
onEnd,
onStart,
request
@@ -90,6 +106,8 @@ class Farm {
this._push(task);
}
});
promise.UNSTABLE_onCustomMessage = addCustomMessageListener;
return promise;
}
_getNextTask(workerId) {
@@ -126,7 +144,13 @@ class Farm {
this._lock(workerId);
this._callback(workerId, task.request, task.onStart, onEnd);
this._callback(
workerId,
task.request,
task.onStart,
onEnd,
task.onCustomMessage
);
return this;
}
+2 -2
View File
@@ -5,9 +5,9 @@
* LICENSE file in the root directory of this source tree.
*/
import BaseWorkerPool from './base/BaseWorkerPool';
import type { ChildMessage, OnEnd, OnStart, WorkerInterface, WorkerOptions, WorkerPoolInterface } from './types';
import type { ChildMessage, OnCustomMessage, OnEnd, OnStart, WorkerInterface, WorkerOptions, WorkerPoolInterface } from './types';
declare class WorkerPool extends BaseWorkerPool implements WorkerPoolInterface {
send(workerId: number, request: ChildMessage, onStart: OnStart, onEnd: OnEnd): void;
send(workerId: number, request: ChildMessage, onStart: OnStart, onEnd: OnEnd, onCustomMessage: OnCustomMessage): void;
createWorker(workerOptions: WorkerOptions): WorkerInterface;
}
export default WorkerPool;
+3 -3
View File
@@ -22,14 +22,14 @@ const canUseWorkerThreads = () => {
require('worker_threads');
return true;
} catch (_unused) {
} catch {
return false;
}
};
class WorkerPool extends _BaseWorkerPool.default {
send(workerId, request, onStart, onEnd) {
this.getWorkerById(workerId).send(request, onStart, onEnd);
send(workerId, request, onStart, onEnd, onCustomMessage) {
this.getWorkerById(workerId).send(request, onStart, onEnd, onCustomMessage);
}
createWorker(workerOptions) {
+3 -1
View File
@@ -121,12 +121,13 @@ class BaseWorkerPool {
const stdout = (0, _mergeStream().default)();
const stderr = (0, _mergeStream().default)();
const {forkOptions, maxRetries, setupArgs} = options;
const {forkOptions, maxRetries, resourceLimits, setupArgs} = options;
for (let i = 0; i < options.numWorkers; i++) {
const workerOptions = {
forkOptions,
maxRetries,
resourceLimits,
setupArgs,
workerId: i,
workerPath
@@ -177,6 +178,7 @@ class BaseWorkerPool {
worker.send(
[_types().CHILD_MESSAGE_END, false],
emptyMethod,
emptyMethod,
emptyMethod
); // Schedule a force exit in case worker fails to exit gracefully so
// await worker.waitForExit() never takes longer than FORCE_EXIT_DELAY
+3 -1
View File
@@ -5,7 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="node" />
import type { FarmOptions, PoolExitResult } from './types';
import type { FarmOptions, PoolExitResult, PromiseWithCustomMessage } from './types';
export { default as messageParent } from './workers/messageParent';
/**
* The Jest farm (publicly called "Worker") is a class that allows you to queue
* methods across multiple child processes, in order to parallelize work. This
@@ -43,3 +44,4 @@ export default class JestWorker {
getStdout(): NodeJS.ReadableStream;
end(): Promise<PoolExitResult>;
}
export type { PromiseWithCustomMessage };
+48 -9
View File
@@ -3,6 +3,12 @@
Object.defineProperty(exports, '__esModule', {
value: true
});
Object.defineProperty(exports, 'messageParent', {
enumerable: true,
get: function () {
return _messageParent.default;
}
});
exports.default = void 0;
function _os() {
@@ -15,9 +21,11 @@ function _os() {
return data;
}
var _Farm = _interopRequireDefault(require('./Farm'));
var _WorkerPool = _interopRequireDefault(require('./WorkerPool'));
var _Farm = _interopRequireDefault(require('./Farm'));
var _messageParent = _interopRequireDefault(require('./workers/messageParent'));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : {default: obj};
@@ -44,7 +52,7 @@ function getExposedMethods(workerPath, options) {
const module = require(workerPath);
exposedMethods = Object.keys(module).filter(
// @ts-ignore: no index
// @ts-expect-error: no index
name => typeof module[name] === 'function'
);
@@ -83,6 +91,13 @@ function getExposedMethods(workerPath, options) {
class JestWorker {
constructor(workerPath, options) {
var _this$_options$enable,
_this$_options$forkOp,
_this$_options$maxRet,
_this$_options$numWor,
_this$_options$resour,
_this$_options$setupA;
_defineProperty(this, '_ending', void 0);
_defineProperty(this, '_farm', void 0);
@@ -94,16 +109,40 @@ class JestWorker {
this._options = {...options};
this._ending = false;
const workerPoolOptions = {
enableWorkerThreads: this._options.enableWorkerThreads || false,
forkOptions: this._options.forkOptions || {},
maxRetries: this._options.maxRetries || 3,
enableWorkerThreads:
(_this$_options$enable = this._options.enableWorkerThreads) !== null &&
_this$_options$enable !== void 0
? _this$_options$enable
: false,
forkOptions:
(_this$_options$forkOp = this._options.forkOptions) !== null &&
_this$_options$forkOp !== void 0
? _this$_options$forkOp
: {},
maxRetries:
(_this$_options$maxRet = this._options.maxRetries) !== null &&
_this$_options$maxRet !== void 0
? _this$_options$maxRet
: 3,
numWorkers:
this._options.numWorkers || Math.max((0, _os().cpus)().length - 1, 1),
setupArgs: this._options.setupArgs || []
(_this$_options$numWor = this._options.numWorkers) !== null &&
_this$_options$numWor !== void 0
? _this$_options$numWor
: Math.max((0, _os().cpus)().length - 1, 1),
resourceLimits:
(_this$_options$resour = this._options.resourceLimits) !== null &&
_this$_options$resour !== void 0
? _this$_options$resour
: {},
setupArgs:
(_this$_options$setupA = this._options.setupArgs) !== null &&
_this$_options$setupA !== void 0
? _this$_options$setupA
: []
};
if (this._options.WorkerPool) {
// @ts-ignore: constructor target any?
// @ts-expect-error: constructor target any?
this._workerPool = new this._options.WorkerPool(
workerPath,
workerPoolOptions
@@ -129,7 +168,7 @@ class JestWorker {
if (this.constructor.prototype.hasOwnProperty(name)) {
throw new TypeError('Cannot define a method called ' + name);
} // @ts-ignore: dynamic extension of the class instance is expected.
} // @ts-expect-error: dynamic extension of the class instance is expected.
this[name] = this._callFunctionWithArgs.bind(this, name);
});
+52 -24
View File
@@ -5,25 +5,31 @@
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="node" />
import type { EventEmitter } from 'events';
import type { ForkOptions } from 'child_process';
import type { EventEmitter } from 'events';
export interface ResourceLimits {
maxYoungGenerationSizeMb?: number;
maxOldGenerationSizeMb?: number;
codeRangeSizeMb?: number;
}
export declare const CHILD_MESSAGE_INITIALIZE: 0;
export declare const CHILD_MESSAGE_CALL: 1;
export declare const CHILD_MESSAGE_END: 2;
export declare const PARENT_MESSAGE_OK: 0;
export declare const PARENT_MESSAGE_CLIENT_ERROR: 1;
export declare const PARENT_MESSAGE_SETUP_ERROR: 2;
export declare const PARENT_MESSAGE_CUSTOM: 3;
export declare type PARENT_MESSAGE_ERROR = typeof PARENT_MESSAGE_CLIENT_ERROR | typeof PARENT_MESSAGE_SETUP_ERROR;
export interface WorkerPoolInterface {
getStderr(): NodeJS.ReadableStream;
getStdout(): NodeJS.ReadableStream;
getWorkers(): Array<WorkerInterface>;
createWorker(options: WorkerOptions): WorkerInterface;
send(workerId: number, request: ChildMessage, onStart: OnStart, onEnd: OnEnd): void;
send(workerId: number, request: ChildMessage, onStart: OnStart, onEnd: OnEnd, onCustomMessage: OnCustomMessage): void;
end(): Promise<PoolExitResult>;
}
export interface WorkerInterface {
send(request: ChildMessage, onProcessStart: OnStart, onProcessEnd: OnEnd): void;
send(request: ChildMessage, onProcessStart: OnStart, onProcessEnd: OnEnd, onCustomMessage: OnCustomMessage): void;
waitForExit(): Promise<void>;
forceExit(): void;
getWorkerId(): number;
@@ -33,11 +39,15 @@ export interface WorkerInterface {
export declare type PoolExitResult = {
forceExited: boolean;
};
export interface PromiseWithCustomMessage<T> extends Promise<T> {
UNSTABLE_onCustomMessage?: (listener: OnCustomMessage) => () => void;
}
export type { ForkOptions };
export declare type FarmOptions = {
computeWorkerKey?: (method: string, ...args: Array<unknown>) => string | null;
exposedMethods?: ReadonlyArray<string>;
forkOptions?: ForkOptions;
resourceLimits?: ResourceLimits;
setupArgs?: Array<unknown>;
maxRetries?: number;
numWorkers?: number;
@@ -47,12 +57,14 @@ export declare type FarmOptions = {
export declare type WorkerPoolOptions = {
setupArgs: Array<unknown>;
forkOptions: ForkOptions;
resourceLimits: ResourceLimits;
maxRetries: number;
numWorkers: number;
enableWorkerThreads: boolean;
};
export declare type WorkerOptions = {
forkOptions: ForkOptions;
resourceLimits: ResourceLimits;
setupArgs: Array<unknown>;
maxRetries: number;
workerId: number;
@@ -65,34 +77,50 @@ export declare type MessageChannel = {
port1: MessagePort;
port2: MessagePort;
};
export declare type ChildMessageInitialize = [typeof CHILD_MESSAGE_INITIALIZE, // type
boolean, // processed
string, // file
// file
Array<unknown> | undefined, // setupArgs
// setupArgs
MessagePort | undefined];
export declare type ChildMessageCall = [typeof CHILD_MESSAGE_CALL, // type
boolean, // processed
string, // method
Array<unknown>];
export declare type ChildMessageEnd = [typeof CHILD_MESSAGE_END, // type
boolean];
export declare type ChildMessageInitialize = [
typeof CHILD_MESSAGE_INITIALIZE,
boolean,
string,
// file
Array<unknown> | undefined,
// setupArgs
MessagePort | undefined
];
export declare type ChildMessageCall = [
typeof CHILD_MESSAGE_CALL,
boolean,
string,
Array<unknown>
];
export declare type ChildMessageEnd = [
typeof CHILD_MESSAGE_END,
boolean
];
export declare type ChildMessage = ChildMessageInitialize | ChildMessageCall | ChildMessageEnd;
export declare type ParentMessageOk = [typeof PARENT_MESSAGE_OK, // type
unknown];
export declare type ParentMessageError = [PARENT_MESSAGE_ERROR, // type
string, // constructor
string, // message
string, // stack
unknown];
export declare type ParentMessage = ParentMessageOk | ParentMessageError;
export declare type ParentMessageCustom = [
typeof PARENT_MESSAGE_CUSTOM,
unknown
];
export declare type ParentMessageOk = [
typeof PARENT_MESSAGE_OK,
unknown
];
export declare type ParentMessageError = [
PARENT_MESSAGE_ERROR,
string,
string,
string,
unknown
];
export declare type ParentMessage = ParentMessageOk | ParentMessageError | ParentMessageCustom;
export declare type OnStart = (worker: WorkerInterface) => void;
export declare type OnEnd = (err: Error | null, result: unknown) => void;
export declare type OnCustomMessage = (message: Array<unknown> | unknown) => void;
export declare type QueueChildMessage = {
request: ChildMessage;
onStart: OnStart;
onEnd: OnEnd;
onCustomMessage: OnCustomMessage;
};
export declare type QueueItem = {
task: QueueChildMessage;
+5 -1
View File
@@ -3,7 +3,7 @@
Object.defineProperty(exports, '__esModule', {
value: true
});
exports.PARENT_MESSAGE_SETUP_ERROR = exports.PARENT_MESSAGE_CLIENT_ERROR = exports.PARENT_MESSAGE_OK = exports.CHILD_MESSAGE_END = exports.CHILD_MESSAGE_CALL = exports.CHILD_MESSAGE_INITIALIZE = void 0;
exports.PARENT_MESSAGE_CUSTOM = exports.PARENT_MESSAGE_SETUP_ERROR = exports.PARENT_MESSAGE_CLIENT_ERROR = exports.PARENT_MESSAGE_OK = exports.CHILD_MESSAGE_END = exports.CHILD_MESSAGE_CALL = exports.CHILD_MESSAGE_INITIALIZE = void 0;
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
@@ -11,6 +11,8 @@ exports.PARENT_MESSAGE_SETUP_ERROR = exports.PARENT_MESSAGE_CLIENT_ERROR = expor
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// import type {ResourceLimits} from 'worker_threads';
// This is not present in the Node 12 typings
// Because of the dynamic nature of a worker communication process, all messages
// coming from any of the other processes cannot be typed. Thus, many types
// include "unknown" as a TS type, which is (unfortunately) correct here.
@@ -26,3 +28,5 @@ const PARENT_MESSAGE_CLIENT_ERROR = 1;
exports.PARENT_MESSAGE_CLIENT_ERROR = PARENT_MESSAGE_CLIENT_ERROR;
const PARENT_MESSAGE_SETUP_ERROR = 2;
exports.PARENT_MESSAGE_SETUP_ERROR = PARENT_MESSAGE_SETUP_ERROR;
const PARENT_MESSAGE_CUSTOM = 3;
exports.PARENT_MESSAGE_CUSTOM = PARENT_MESSAGE_CUSTOM;
+3 -2
View File
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="node" />
import { ChildMessage, OnEnd, OnStart, WorkerInterface, WorkerOptions } from '../types';
import { ChildMessage, OnCustomMessage, OnEnd, OnStart, WorkerInterface, WorkerOptions } from '../types';
/**
* This class wraps the child process and provides a nice interface to
* communicate with. It takes care of:
@@ -30,6 +30,7 @@ export default class ChildProcessWorker implements WorkerInterface {
private _request;
private _retries;
private _onProcessEnd;
private _onCustomMessage;
private _fakeStream;
private _stdout;
private _stderr;
@@ -40,7 +41,7 @@ export default class ChildProcessWorker implements WorkerInterface {
private _shutdown;
private _onMessage;
private _onExit;
send(request: ChildMessage, onProcessStart: OnStart, onProcessEnd: OnEnd): void;
send(request: ChildMessage, onProcessStart: OnStart, onProcessEnd: OnEnd, onCustomMessage: OnCustomMessage): void;
waitForExit(): Promise<void>;
forceExit(): void;
getWorkerId(): number;
+13 -5
View File
@@ -109,6 +109,8 @@ class ChildProcessWorker {
_defineProperty(this, '_onProcessEnd', void 0);
_defineProperty(this, '_onCustomMessage', void 0);
_defineProperty(this, '_fakeStream', void 0);
_defineProperty(this, '_stdout', void 0);
@@ -214,6 +216,7 @@ class ChildProcessWorker {
}
_onMessage(response) {
// TODO: Add appropriate type check
let error;
switch (response[0]) {
@@ -226,7 +229,7 @@ class ChildProcessWorker {
error = response[4];
if (error != null && typeof error === 'object') {
const extra = error; // @ts-ignore: no index
const extra = error; // @ts-expect-error: no index
const NativeCtor = global[response[1]];
const Ctor = typeof NativeCtor === 'function' ? NativeCtor : Error;
@@ -235,7 +238,6 @@ class ChildProcessWorker {
error.stack = response[3];
for (const key in extra) {
// @ts-ignore: adding custom properties to errors.
error[key] = extra[key];
}
}
@@ -245,8 +247,7 @@ class ChildProcessWorker {
break;
case _types().PARENT_MESSAGE_SETUP_ERROR:
error = new Error('Error when calling setup: ' + response[2]); // @ts-ignore: adding custom properties to errors.
error = new Error('Error when calling setup: ' + response[2]);
error.type = response[1];
error.stack = response[3];
@@ -254,6 +255,11 @@ class ChildProcessWorker {
break;
case _types().PARENT_MESSAGE_CUSTOM:
this._onCustomMessage(response[1]);
break;
default:
throw new TypeError('Unexpected response from worker: ' + response[0]);
}
@@ -275,7 +281,7 @@ class ChildProcessWorker {
}
}
send(request, onProcessStart, onProcessEnd) {
send(request, onProcessStart, onProcessEnd, onCustomMessage) {
onProcessStart(this);
this._onProcessEnd = (...args) => {
@@ -285,6 +291,8 @@ class ChildProcessWorker {
return onProcessEnd(...args);
};
this._onCustomMessage = (...arg) => onCustomMessage(...arg);
this._request = request;
this._retries = 0;
+3 -2
View File
@@ -5,13 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="node" />
import { ChildMessage, OnEnd, OnStart, WorkerInterface, WorkerOptions } from '../types';
import { ChildMessage, OnCustomMessage, OnEnd, OnStart, WorkerInterface, WorkerOptions } from '../types';
export default class ExperimentalWorker implements WorkerInterface {
private _worker;
private _options;
private _request;
private _retries;
private _onProcessEnd;
private _onCustomMessage;
private _fakeStream;
private _stdout;
private _stderr;
@@ -25,7 +26,7 @@ export default class ExperimentalWorker implements WorkerInterface {
private _onExit;
waitForExit(): Promise<void>;
forceExit(): void;
send(request: ChildMessage, onProcessStart: OnStart, onProcessEnd: OnEnd): void;
send(request: ChildMessage, onProcessStart: OnStart, onProcessEnd: OnEnd, onCustomMessage: OnCustomMessage): void;
getWorkerId(): number;
getStdout(): NodeJS.ReadableStream | null;
getStderr(): NodeJS.ReadableStream | null;
+15 -4
View File
@@ -127,6 +127,8 @@ class ExperimentalWorker {
_defineProperty(this, '_onProcessEnd', void 0);
_defineProperty(this, '_onCustomMessage', void 0);
_defineProperty(this, '_fakeStream', void 0);
_defineProperty(this, '_stdout', void 0);
@@ -156,6 +158,8 @@ class ExperimentalWorker {
path().resolve(__dirname, './threadChild.js'),
{
eval: false,
// @ts-expect-error: added in newer versions
resourceLimits: this._options.resourceLimits,
stderr: true,
stdout: true,
workerData: {
@@ -246,7 +250,7 @@ class ExperimentalWorker {
error = response[4];
if (error != null && typeof error === 'object') {
const extra = error; // @ts-ignore: no index
const extra = error; // @ts-expect-error: no index
const NativeCtor = global[response[1]];
const Ctor = typeof NativeCtor === 'function' ? NativeCtor : Error;
@@ -255,7 +259,7 @@ class ExperimentalWorker {
error.stack = response[3];
for (const key in extra) {
// @ts-ignore: no index
// @ts-expect-error: no index
error[key] = extra[key];
}
}
@@ -265,7 +269,7 @@ class ExperimentalWorker {
break;
case _types().PARENT_MESSAGE_SETUP_ERROR:
error = new Error('Error when calling setup: ' + response[2]); // @ts-ignore: adding custom properties to errors.
error = new Error('Error when calling setup: ' + response[2]); // @ts-expect-error: adding custom properties to errors.
error.type = response[1];
error.stack = response[3];
@@ -274,6 +278,11 @@ class ExperimentalWorker {
break;
case _types().PARENT_MESSAGE_CUSTOM:
this._onCustomMessage(response[1]);
break;
default:
throw new TypeError('Unexpected response from worker: ' + response[0]);
}
@@ -301,7 +310,7 @@ class ExperimentalWorker {
this._worker.terminate();
}
send(request, onProcessStart, onProcessEnd) {
send(request, onProcessStart, onProcessEnd, onCustomMessage) {
onProcessStart(this);
this._onProcessEnd = (...args) => {
@@ -311,6 +320,8 @@ class ExperimentalWorker {
return onProcessEnd(...args);
};
this._onCustomMessage = (...arg) => onCustomMessage(...arg);
this._request = request;
this._retries = 0;
+9
View File
@@ -0,0 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="node" />
declare const messageParent: (message: unknown, parentProcess?: NodeJS.Process) => void;
export default messageParent;
+51
View File
@@ -0,0 +1,51 @@
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
exports.default = void 0;
function _types() {
const data = require('../types');
_types = function () {
return data;
};
return data;
}
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const isWorkerThread = () => {
try {
// `Require` here to support Node v10
const {isMainThread, parentPort} = require('worker_threads');
return !isMainThread && parentPort;
} catch {
return false;
}
};
const messageParent = (message, parentProcess = process) => {
try {
if (isWorkerThread()) {
// `Require` here to support Node v10
const {parentPort} = require('worker_threads');
parentPort.postMessage([_types().PARENT_MESSAGE_CUSTOM, message]);
} else if (typeof parentProcess.send === 'function') {
parentProcess.send([_types().PARENT_MESSAGE_CUSTOM, message]);
}
} catch {
throw new Error('"messageParent" can only be used inside a worker');
}
};
var _default = messageParent;
exports.default = _default;
+6 -1
View File
@@ -133,6 +133,11 @@ function execMethod(method, args) {
execFunction(main.setup, main, setupArgs, execHelper, reportInitializeError);
}
const isPromise = obj =>
!!obj &&
(typeof obj === 'object' || typeof obj === 'function') &&
typeof obj.then === 'function';
function execFunction(fn, ctx, args, onResult, onError) {
let result;
@@ -143,7 +148,7 @@ function execFunction(fn, ctx, args, onResult, onError) {
return;
}
if (result && typeof result.then === 'function') {
if (isPromise(result)) {
result.then(onResult, onError);
} else {
onResult(result);
+6 -1
View File
@@ -146,6 +146,11 @@ function execMethod(method, args) {
execFunction(main.setup, main, setupArgs, execHelper, reportInitializeError);
}
const isPromise = obj =>
!!obj &&
(typeof obj === 'object' || typeof obj === 'function') &&
typeof obj.then === 'function';
function execFunction(fn, ctx, args, onResult, onError) {
let result;
@@ -156,7 +161,7 @@ function execFunction(fn, ctx, args, onResult, onError) {
return;
}
if (result && typeof result.then === 'function') {
if (isPromise(result)) {
result.then(onResult, onError);
} else {
onResult(result);