diff --git a/mason/breeders_toolbox/create_seedlots_from_trial_dialogs.mas b/mason/breeders_toolbox/create_seedlots_from_trial_dialogs.mas index dfe350040a..2317671994 100644 --- a/mason/breeders_toolbox/create_seedlots_from_trial_dialogs.mas +++ b/mason/breeders_toolbox/create_seedlots_from_trial_dialogs.mas @@ -502,8 +502,7 @@ $timestamp => localtime() * Complete the Select Trial step * - Get details of selected trial */ - function completeSelectTrial() { - let trial_error; + async function completeSelectTrial() { // Make sure trial name was entered let trial_name = jQuery("#create_seedlots_trial_name").val(); @@ -517,57 +516,21 @@ $timestamp => localtime() jQuery("#create_seedlots_trial_complete_select_trial").attr("disabled", true); jQuery("#trial_error_message_container").hide(); - // Get Trial ID - getTrialID(trial_name, function(trial_id) { - if ( trial_id ) { - TRIAL_ID = trial_id; - - // Get the trial details - getTrialDetails(trial_id, function(error, breeding_program_id, accessions, plots, subplots, plants, traits) { - if ( error ) { - trial_error = error; - _finish(); - } - else if ( breeding_program_id && accessions && plots ) { - BREEDING_PROGRAM_ID = breeding_program_id; - ACCESSIONS = accessions; - PLOTS = plots; - SUBPLOTS = subplots; - PLANTS = plants; - TRAITS = traits ? traits : []; - if ( traits ) { - getTraitData(function() { - _finish(true); - }); - } - else { - _finish(true); - } - } - else { - _finish(); - } - }); - - } - else { - _finish(); + try { + TRIAL_ID = await getTrialID(trial_name); + if ( TRIAL_ID ) { + const { breeding_program_id, accessions, plots, subplots, plants, traits } = await getTrialDetails(TRIAL_ID); + BREEDING_PROGRAM_ID = breeding_program_id; + ACCESSIONS = accessions; + PLOTS = plots; + SUBPLOTS = subplots; + PLANTS = plants; + TRAITS = traits || []; + TRAIT_DATA = await getTraitData(TRAITS); } - }); - - /** - * Hide the working modal, renable the button, and continue (if complete) - */ - function _finish(complete) { + // Hide the working modal, renable the button, and continue (if complete) jQuery('#working_modal').modal("hide"); - if ( trial_error ) { - jQuery("#trial_error_message").html(trial_error); - jQuery("#trial_error_message_container").show(); - } - else { - jQuery("#trial_error_message_container").hide(); - } jQuery("#create_seedlots_trial_complete_select_trial").attr("disabled", false); jQuery("#create_seedlots_trial_accession_count").html(ACCESSIONS ? ACCESSIONS.length : ''); jQuery("#create_seedlots_trial_plot_count").html(PLOTS ? PLOTS.length : ''); @@ -577,11 +540,15 @@ $timestamp => localtime() jQuery(".create_seedlots_trial_plants_available").css("display", PLANTS && PLANTS.length > 0 ? 'inline' : 'none'); jQuery(".create_seedlots_trial_subplots_available_list").css("display", SUBPLOTS && SUBPLOTS.length > 0 ? 'list-item' : 'none'); jQuery(".create_seedlots_trial_plants_available_list").css("display", PLANTS && PLANTS.length > 0 ? 'list-item' : 'none'); - if ( complete ) { - Workflow.complete('#create_seedlots_trial_complete_select_trial'); - Workflow.focus("#create_seedlots_trial_workflow", 2); - } + Workflow.complete('#create_seedlots_trial_complete_select_trial'); + Workflow.focus("#create_seedlots_trial_workflow", 2); } + catch (err) { + jQuery('#working_modal').modal("hide"); + jQuery("#create_seedlots_trial_complete_select_trial").attr("disabled", false); + jQuery("#trial_error_message").html(err || 'An Unknown Error Occurred!'); + jQuery("#trial_error_message_container").show(); + }; } /** @@ -704,10 +671,11 @@ $timestamp => localtime() * Complete the seedlot confirmation step * - Start uploading Seedlots to DB */ - function completeSeedlotConfirmation() { + async function completeSeedlotConfirmation() { Workflow.complete('#create_seedlots_trial_complete_confirm_seedlots'); Workflow.focus("#create_seedlots_trial_workflow", 7); - startSeedlotUpload(displayUploadResults); + const { upload_count, errors } = await uploadSeedlots(); + displayUploadResults(errors, upload_count); } @@ -993,68 +961,68 @@ $timestamp => localtime() /** * Upload each of the Seedlots to the Database in batches of up to * UPLOAD_BATCH_SIZE seedlots per batch - * @param {Function} callback Callback function(errors, success_count) + * @return {Promise<{ upload_count, errors }>} Return a count of successfully uploaded seedlots and error messages */ - function startSeedlotUpload(callback) { + function uploadSeedlots() { let stock_type = jQuery("#create_seedlots_trial_stock_type").val(); let contents_type = jQuery("#create_seedlots_trial_contents_type").val(); - jQuery("#create_seedlots_trial_status").html("Creating Seedlots..."); - jQuery("#create_seedlots_trial_progress").css('width', '0%').attr('aria-valuenow', 0); - jQuery("#create_seedlots_trial_results").empty(); + let seedlot_count = 0; // Count of processed seedlots + let seedlot_total = SEEDLOTS.length; // Total number of seedlots + let upload_count = 0; // Count of successfully created/uploaded seedlots + let errors = []; // Array of errors encountered during upload + + return new Promise(async (resolve) => { + jQuery("#create_seedlots_trial_status").html("Creating Seedlots..."); + jQuery("#create_seedlots_trial_progress").css('width', '0%').attr('aria-valuenow', 0); + jQuery("#create_seedlots_trial_results").empty(); + + // Create Results table + let html = ""; + html += "Seedlot"; + html += "Progress"; + html += ""; + html += ""; + for ( let i = 0; i < SEEDLOTS.length; i++ ) { + let status_id = "create_seedlots_trial_results_row_" + i; + html += "" + SEEDLOTS[i].name + "Pending..."; + SEEDLOTS[i].status_id = status_id; + } + jQuery("#create_seedlots_trial_results_table").html(html); - // Create Results table - let html = ""; - html += "Seedlot"; - html += "Progress"; - html += ""; - html += ""; - for ( let i = 0; i < SEEDLOTS.length; i++ ) { - let status_id = "create_seedlots_trial_results_row_" + i; - html += "" + SEEDLOTS[i].name + "Pending..."; - SEEDLOTS[i].status_id = status_id; - } - jQuery("#create_seedlots_trial_results_table").html(html); + // Chunk the Seedlots into batches + let batches = []; + while ( SEEDLOTS.length > 0 ) { + batches.push(SEEDLOTS.splice(0, UPLOAD_BATCH_SIZE)); + } - // Information to return - let upload_count = 0; // Count of successfully created/uploaded seedlots - let errors = []; // Array of errors encountered during upload + // Upload each batch + for ( let i = 0; i < batches.length; i++ ) { + await _uploadBatch(batches[i]); + } - // Set counts of seedlots and batches - let seedlot_count = 0; // Count of completed seedlots - let seedlot_total = SEEDLOTS.length; // Total number of seedlots - let seedlot_batch_count; // Count of completed seedlots, in current batch - let seedlot_batch_total; // Total number of seedlots, in current batch - let batch_count = 0 // Count of completed batches - let batch_total = Math.ceil(seedlot_total/UPLOAD_BATCH_SIZE); // Total number of batches + // Refresh the matviews when complete + jQuery.ajax({ url: '/ajax/breeder/refresh?matviews=stockprop' }); - // Start the first batch - _uploadBatch(); + // Display Upload Results + resolve({ upload_count, errors }); + }); /** * Upload a batch of Seedlots (max set with UPLOAD_BATCH_SIZE) - * - when the seedlot is complete, call _finishSeedlot() */ - function _uploadBatch() { - let seedlot_batch_start = batch_count*UPLOAD_BATCH_SIZE; - let seedlot_batch_end = seedlot_batch_start+UPLOAD_BATCH_SIZE; - if ( seedlot_batch_end > seedlot_total ) seedlot_batch_end = seedlot_total; - seedlot_batch_count = 0; - seedlot_batch_total = seedlot_batch_end - seedlot_batch_start; - for ( let seedlot_index = seedlot_batch_start; seedlot_index < seedlot_batch_end; seedlot_index++ ) { - _uploadSeedlot(seedlot_index, function() { - let p = ((seedlot_count+1)/seedlot_total)*100; - jQuery("#create_seedlots_trial_progress").css('width', p+'%').attr('aria-valuenow', p); - _finishSeedlot(); - }); - } + async function _uploadBatch(batch) { + const requests = []; + batch.forEach((seedlot) => { + requests.push(_processSeedlot(seedlot)); + }); + await Promise.all(requests); } /** - * Upload the specified seedlot + * Process the specified seedlot */ - function _uploadSeedlot(seedlot_index, callback) { - let seedlot = SEEDLOTS[seedlot_index]; - _setStatusInfo(seedlot.status_id, "Creating..."); + async function _processSeedlot(seedlot) { + _setStatusInfo(seedlot, "Creating..."); // Set seedlot request data let data = { @@ -1075,88 +1043,65 @@ $timestamp => localtime() if ( contents_type === 'amount' ) data['seedlot_amount'] = seedlot.contents; if ( contents_type === 'weight' ) data['seedlot_weight'] = seedlot.contents; - jQuery.ajax({ - url: '/ajax/breeders/seedlot-create', - data: data, - success: function(response) { - jQuery('#working_modal').modal('hide'); - if (response.success === 1) { - upload_count++; - _setStatusComplete(seedlot.status_id); - } - if (response.error) { - errors.push({ - index: seedlot_index, - seedlot: seedlot, - error: response.error - }); - _setStatusError(seedlot.status_id, response.error); - } - }, - error: function(response){ - let msg = "AJAX error"; - errors.push({ - index: seedlot_index, - seedlot: seedlot, - error: msg - }); - _setStatusError(seedlot.status_id, msg); - }, - complete: function() { - return callback(); - } - }); - } + // Upload the Seedlot + await _uploadSeedlot(seedlot, data); - /** - * Display an info message for the specified seedlot - */ - function _setStatusInfo(id, info) { - jQuery("#" + id).html(info); + // Update Progress Bar + seedlot_count++; + let p = (seedlot_count/seedlot_total)*100; + jQuery("#create_seedlots_trial_progress").css('width', p+'%').attr('aria-valuenow', p); } /** - * Display a complete message for the specified seedlot + * Upload the specified seedlot */ - function _setStatusComplete(id) { - jQuery("#" + id).html(" Seedlot Created!"); + function _uploadSeedlot(seedlot, data) { + return new Promise((resolve) => { + jQuery.ajax({ + url: '/ajax/breeders/seedlot-create', + data: data, + success: (response) => { + if (response.success === 1) { + _setStatusComplete(seedlot); + } + else if (response.error) { + _setStatusError(seedlot, response.error); + } + resolve(); + }, + error: () => { + _setStatusError(seedlot, "AJAX error"); + resolve(); + } + }); + }); } /** - * Display an error message for the specified seedlot + * Display an info message for the specified seedlot */ - function _setStatusError(id, error) { - jQuery("#" + id).html(" ERROR: " + error); + function _setStatusInfo(seedlot, info) { + jQuery("#" + seedlot.status_id).html(info); } /** - * Finished uploading a seedlot - * - when all of the batch's seedlots have been uploaded, call _finishBatch() + * Display a complete message for the specified seedlot */ - function _finishSeedlot() { - seedlot_count++; - seedlot_batch_count++; - if ( seedlot_batch_count >= seedlot_batch_total ) { - _finishBatch(); - } + function _setStatusComplete(seedlot) { + upload_count++; + _setStatusInfo(seedlot, " Seedlot Created!"); } /** - * Finished uploading a batch of seedlots - * - when all of the batches are complete, refresh the matviews - * - if there are remaining batches, start the next batch + * Display an error message for the specified seedlot */ - function _finishBatch() { - batch_count++; - if ( batch_count >= batch_total ) { - jQuery.ajax({ - url: '/ajax/breeder/refresh?matviews=stockprop' - }); - if ( callback ) return callback(errors, upload_count); - } - else { - _uploadBatch(); - } + function _setStatusError(seedlot, error) { + errors.push({ + index: seedlot.status_id, + seedlot: seedlot, + error: error + }); + _setStatusInfo(seedlot, " ERROR: " + error); } } @@ -1195,39 +1140,33 @@ $timestamp => localtime() /** * Query the database for the trial id (by name) * @param {string} trial_name Trial Name - * @param {Function} callback Callback function(trial_id) + * @return {Promise} the trial id for the named trial */ function getTrialID(trial_name, callback) { - let trial_id; - jQuery.ajax({ - type: "GET", - url: '/ajax/breeders/trial_lookup', - data: { - name: trial_name - }, - success: function(response) { - if (response.error) { - alert(response.error); - } - else if ( response.trial_id ) { - trial_id = response.trial_id; - } - else { - alert('Could not lookup trial'); - } - }, - error: function(response){ - alert('Could not lookup trial'); - }, - complete: function() { - return callback(trial_id); - } + return new Promise((resolve, reject) => { + let trial_id; + jQuery.ajax({ + type: "GET", + url: '/ajax/breeders/trial_lookup', + data: { name: trial_name }, + success: (response) => { + if (response.error) { + reject(response.error); + } + else if ( response.trial_id ) { + resolve(response.trial_id); + } + else { + reject('Could not lookup trial'); + } + }, + error: () => reject('Could not lookup trial') + }); }); } /** * Get required metadata for the Trial: - * - Error message * - Breeding Program ID * - Accessions * - Plots @@ -1235,247 +1174,236 @@ $timestamp => localtime() * - Plants * - Traits * @param {int} trial_id Trial ID - * @param {function} callback Callback function(error, breeding_program_id, accessions, plots, subplots, plants, traits) + * @return {Promise<{ breeding_program_id, accessions, plots, subplots, plants, traits }>} Returns all trial details */ - function getTrialDetails(trial_id, callback) { - let count = 0; - let total = 4; - - let breeding_program_id, accessions, plots, subplots, plants, traits; - let error; - - _getBreedingProgram(); - _getAccessions(); - _getPlots(); - _getTraits(); + async function getTrialDetails(trial_id, callback) { + return new Promise(async (resolve, reject) => { + try { + const [ breeding_program_id, accessions, { plots, subplots, plants }, traits ] = await Promise.all([ + _getBreedingProgram(), + _getAccessions(), + _getPlots(), + _getTraits() + ]); + resolve({ breeding_program_id, accessions, plots, subplots, plants, traits }); + } + catch (err) { + reject(err); + } + }); + // Returns breeding_program_id function _getBreedingProgram() { - jQuery.ajax({ - type: "GET", - url: '/breeders/programs_by_trial/' + trial_id, - success: function(response) { - if (response.error) { - alert(response.error); - } - else if ( response.projects ) { - breeding_program_id = response.projects[0][0]; - } - else { - alert('Could not lookup trial breeding program'); - } - }, - error: function(response){ - alert('Could not lookup trial breeding program'); - }, - complete: function() { - _finish(); - } + return new Promise((resolve, reject) => { + jQuery.ajax({ + type: "GET", + url: '/breeders/programs_by_trial/' + trial_id, + success: (response) => { + if (response.error) { + reject(response.error); + } + else if ( response.projects ) { + resolve(response.projects[0][0]); + } + else { + reject('Could not lookup trial breeding program'); + } + }, + error: () => reject('Could not lookup trial breeding program') + }); }); } + // Returns accessions[] function _getAccessions() { - jQuery.ajax({ - type: "GET", - url: '/ajax/breeders/trial/' + trial_id + '/accessions', - success: function(response) { - if (response.error) { - alert(response.error); - } - else if ( response.accessions ) { - accessions = []; - for ( let i = 0; i < response.accessions[0].length; i++ ) { - if ( response.accessions[0][i].stock_type !== 'accession' ) { - error = `This tool only supports trials with plots that contain accessions. This trial has one or more plots that contain a ${response.accessions[0][i].stock_type}.`; + return new Promise((resolve, reject) => { + jQuery.ajax({ + type: "GET", + url: '/ajax/breeders/trial/' + trial_id + '/accessions', + success: (response) => { + if (response.error) { + reject(response.error); + } + else if ( response.accessions ) { + let accessions = []; + for ( let i = 0; i < response.accessions[0].length; i++ ) { + if ( response.accessions[0][i].stock_type !== 'accession' ) { + error = `This tool only supports trials with plots that contain accessions. This trial has one or more plots that contain a ${response.accessions[0][i].stock_type}.`; + } + accessions.push({ + id: response.accessions[0][i].stock_id, + name: response.accessions[0][i].accession_name + }); } - accessions.push({ - id: response.accessions[0][i].stock_id, - name: response.accessions[0][i].accession_name - }); + resolve(accessions); } - } - else { - alert('Could not lookup trial accessions'); - } - }, - error: function(response){ - alert('Could not lookup trial accessions'); - }, - complete: function() { - _finish(); - } + else { + reject('Could not lookup trial accessions'); + } + }, + error: () => reject('Could not lookup trial accessions') + }); }); } + // Returns { plots[], subplots[], plants[] } function _getPlots() { - jQuery.ajax({ - type: "GET", - url: '/ajax/breeders/trial/' + trial_id + '/layout', - success: function(response) { - if (response.error) { - alert(response.error); - } - else if ( response.design ) { - plots = []; - subplots = []; - plants = []; - for (const [plot, info] of Object.entries(response.design)) { - plots.push({ - plot: { - id: info.plot_id, - number: info.plot_number, - name: info.plot_name, - rep: info.rep_number, - block: info.block_number - }, - accession: { - name: info.accession_name, - id: info.accession_id - } - }); - if ( info.subplot_names && info.subplot_ids && info.subplot_index_numbers ) { - for ( let i = 0; i < info.subplot_names.length; i++ ) { - subplots.push({ - subplot: { - name: info.subplot_names[i], - id: info.subplot_ids[i], - index: info.subplot_index_numbers[i] - }, - plot: { - id: info.plot_id, - number: info.plot_number, - name: info.plot_name, - rep: info.rep_number, - block: info.block_number - }, - accession: { - name: info.accession_name, - id: info.accession_id - } - }); + return new Promise((resolve, reject) => { + jQuery.ajax({ + type: "GET", + url: '/ajax/breeders/trial/' + trial_id + '/layout', + success: (response) => { + if (response.error) { + reject(response.error); + } + else if ( response.design ) { + let plots = []; + let subplots = []; + let plants = []; + for (const [plot, info] of Object.entries(response.design)) { + plots.push({ + plot: { + id: info.plot_id, + number: info.plot_number, + name: info.plot_name, + rep: info.rep_number, + block: info.block_number + }, + accession: { + name: info.accession_name, + id: info.accession_id + } + }); + if ( info.subplot_names && info.subplot_ids && info.subplot_index_numbers ) { + for ( let i = 0; i < info.subplot_names.length; i++ ) { + subplots.push({ + subplot: { + name: info.subplot_names[i], + id: info.subplot_ids[i], + index: info.subplot_index_numbers[i] + }, + plot: { + id: info.plot_id, + number: info.plot_number, + name: info.plot_name, + rep: info.rep_number, + block: info.block_number + }, + accession: { + name: info.accession_name, + id: info.accession_id + } + }); + } } - } - if ( info.plant_names && info.plant_ids && info.plant_index_numbers ) { - for ( let i = 0; i < info.plant_names.length; i++ ) { - plants.push({ - plant: { - name: info.plant_names[i], - id: info.plant_ids[i], - index: info.plant_index_numbers[i] - }, - plot: { - id: info.plot_id, - number: info.plot_number, - name: info.plot_name, - rep: info.rep_number, - block: info.block_number - }, - accession: { - name: info.accession_name, - id: info.accession_id - } - }); + if ( info.plant_names && info.plant_ids && info.plant_index_numbers ) { + for ( let i = 0; i < info.plant_names.length; i++ ) { + plants.push({ + plant: { + name: info.plant_names[i], + id: info.plant_ids[i], + index: info.plant_index_numbers[i] + }, + plot: { + id: info.plot_id, + number: info.plot_number, + name: info.plot_name, + rep: info.rep_number, + block: info.block_number + }, + accession: { + name: info.accession_name, + id: info.accession_id + } + }); + } } + resolve({ plots, subplots, plants }); } } - } - else { - alert('Could not lookup trial plots'); - } - }, - error: function(response){ - alert('Could not lookup trial plots'); - }, - complete: function() { - _finish(); - } + else { + reject('Could not lookup trial plots'); + } + }, + error: () => reject('Could not lookup trial plots') + }); }); } + // Returns traits[] function _getTraits() { - jQuery.ajax({ - type: "GET", - url: '/ajax/breeders/trial/' + trial_id + '/traits_assayed?stock_type=plot', - success: function(response) { - if (response.error) { - alert(response.error); - } - else if ( response.traits_assayed ) { - traits = []; - let html = ""; - for ( let i = 0; i < response.traits_assayed[0].length; i++ ) { - let id = response.traits_assayed[0][i][0]; - let name = response.traits_assayed[0][i][1]; - let variable = "trait_" + parseInt(i+1); - traits.push({ - id: id, - name: name, - variable: variable - }); - html += "{" + variable + "}"; - html += "" + name + ""; + return new Promise((resolve, reject) => { + jQuery.ajax({ + type: "GET", + url: '/ajax/breeders/trial/' + trial_id + '/traits_assayed?stock_type=plot', + success: (response) => { + if (response.error) { + reject(response.error); } - jQuery("#create_seedlots_trial_contents_computed_table").html(html); - } - else { - alert('Could not lookup trial traits'); - } - }, - error: function(response){ - alert('Could not lookup trial traits'); - }, - complete: function() { - _finish(); - } + else if ( response.traits_assayed ) { + let traits = []; + let html = ""; + for ( let i = 0; i < response.traits_assayed[0].length; i++ ) { + let id = response.traits_assayed[0][i][0]; + let name = response.traits_assayed[0][i][1]; + let variable = "trait_" + parseInt(i+1); + traits.push({ + id: id, + name: name, + variable: variable + }); + html += "{" + variable + "}"; + html += "" + name + ""; + } + jQuery("#create_seedlots_trial_contents_computed_table").html(html); + resolve(traits); + } + else { + reject('Could not lookup trial traits'); + } + }, + error: () => reject('Could not lookup trial traits') + }); }); } - - function _finish() { - count++; - if ( count >= total ) { - return callback(error, breeding_program_id, accessions, plots, subplots, plants, traits); - } - } } /** * Set the plot-level trait data for all of the traits - * @param {function} callback Callback function() + * @return {Promise} data for each available trait */ - function getTraitData(callback) { - TRAIT_DATA = []; - - let count = 0; - let total = TRAITS.length; - if ( total > 0 ) { - for ( let i = 0; i < TRAITS.length; i++ ) { - _getTraitData(TRAITS[i].id, TRAITS[i].name); + async function getTraitData(traits = []) { + return new Promise(async (resolve, reject) => { + try { + const requests = []; + traits.forEach((t) => requests.push(_getTraitData(t.id, t.name))); + const data = await Promise.all(requests); + resolve(data ? data.flat() : []); } - } - else { - _finish(); - } + catch (err) { + reject(err); + } + }); function _getTraitData(trait_id, trait_name) { - jQuery.ajax({ - type: "GET", - url: '/ajax/breeders/trial/' + TRIAL_ID + '/trait_phenotypes/?trait=' + trait_id + '&display=plot', - success: function(response) { - if (response.error) { - alert(response.error); - } - else if ( response.status && response.status === 'success' && response.data ) { - _parseTraitData(response.data, trait_id, trait_name); - } - else { - alert('Could not get trait data'); - } - }, - error: function(response){ - alert('Could not get trait data'); - }, - complete: function() { - _finish(); - } + return new Promise((resolve, reject) => { + jQuery.ajax({ + type: "GET", + url: '/ajax/breeders/trial/' + TRIAL_ID + '/trait_phenotypes/?trait=' + trait_id + '&display=plot', + success: (response) => { + if (response.error) { + reject(response.error); + } + else if ( response.status && response.status === 'success' && response.data ) { + const data = _parseTraitData(response.data, trait_id, trait_name); + resolve(data); + } + else { + reject('Could not get trait data'); + } + }, + error: () => reject('Could not get trait data') + }); }); } @@ -1487,21 +1415,16 @@ $timestamp => localtime() if ( headers[i] === 'observationUnitDbId' ) plot_id_col = i; if ( headers[i] === trait_name ) trait_col = i; } + let rtn = []; for ( let i = 1; i < data.length; i++ ) { - TRAIT_DATA.push({ + rtn.push({ accession_id: data[i][accession_id_col], plot_id: data[i][plot_id_col], trait_id: trait_id, trait_value: data[i][trait_col] - }); - } - } - - function _finish() { - count++; - if ( count >= total ) { - return callback(); + }) } + return rtn; } }