src/Client.js
import superagent from 'superagent';
import saNoCache from 'superagent-no-cache';
import EventEmitter from 'events';
import utils from './utils/utils.js';
import Service from './Service';
import Job from './Job';
import GSF_API from './utils/GSF_API';
import EVENTS from './utils/EVENTS';
const nocache = utils.isIE() ? saNoCache.withQueryStrings : saNoCache;
/**
* The Client class is used to connect to the server and retrieve information
* about available services and jobs.
* @example
* // Obtain Client object from GSF.
* const Client = GSF.client({address:'MyServer',port:9191});
*/
class Client extends EventEmitter {
/**
* The ClientOptions object contains information about the server.
* @typedef {Object} ClientOptions
* @property {string} ClientOptions.address - The server address/name.
* @property {string} [ClientOptions.port=null] - The server port.
* @property {Object} [ClientOptions.headers={}] - The headers to be used in requests.
* @property {string} [ClientOptions.APIRoot=''] - The API root endpoint.
* @property {string} [ClientOptions.protocol='http'] - The protocol to use.
*/
/**
* @param {ClientOptions} clientOptions - The object containing server information.
* @emits {JobCompleted}
* @emits {JobSucceeded}
* @emits {JobFailed}
* @emits {JobProgress}
* @emits {JobStarted}
* @emits {JobAccepted}
*/
constructor(clientOptions) {
// Init EventEmitter superclass.
super();
/**
* The server address/name.
* @type {string}
*/
this.address = clientOptions.address;
/**
* The server port.
* @type {number}
*/
this.port = clientOptions.port || null;
/**
* The headers to use in requests
* @type {Object}
*/
this.headers = clientOptions.headers || {};
/**
* The API root endpoint. If none, set to empty string.
* @type {string}
*/
this.APIRoot = clientOptions.APIRoot || GSF_API.ROOT_PATH;
/**
* The protocol to use.
* @type {string}
*/
this.protocol = clientOptions.protocol || 'http';
/**
* The server url.
* @type {string}
*/
this.URL = this.protocol + '://' +
this.address + (this.port ? ':' + this.port : '');
/**
* The API root url.
* @type {string}
*/
this.rootURL = (this.APIRoot === '') ? this.URL : [this.URL, this.APIRoot].join('/');
// Allow infinite listeners.
this.setMaxListeners(0);
// Use global EventSource for browsers and the node package for node.
let Eventsource;
if (NODE) { // Webpack defined global
Eventsource = require('eventsource');
} else {
Eventsource = EventSource;
}
// Attach to server sent events and re broadcast.
// Include headers as query strings.
let queryString;
if (this.headers) {
queryString = Object.keys(this.headers).map((key) => {
return encodeURIComponent(key) + '=' +
encodeURIComponent(this.headers[key]);
}).join('&');
}
let url = [this.URL, this.APIRoot,
GSF_API.EVENTS_PATH].filter((v) => (v !== '')).join('/');
url = (queryString) ? (url + '?' + queryString) : (url);
this._events = new Eventsource(url);
// Emit succeeded and failed events.
this.on(EVENTS.completed, (data) => {
this.emit(data.success ? EVENTS.succeeded : EVENTS.failed, data);
});
// Function to handle server sent events.
function handler(type, event) {
try {
const data = JSON.parse(event.data);
this.emit(type, data);
} catch (err) {}
};
// Listen for events from our server. Pass
// them into the handler with job event type.
Object.keys(EVENTS).forEach((key) => {
// Server doesn't emit succeeded or failed events.
if ((EVENTS[key] === EVENTS.succeeded) ||
(EVENTS[key] === EVENTS.failed)) return;
// Add a listener for each of the sse's.
this._events.addEventListener(EVENTS[key],
handler.bind(this, EVENTS[key]));
});
}
/**
* Retrieves an array of available services from the server.
* @return {Promise<Service[], error>} Returns a Promise to an array of available Service objects.
*/
services() {
return new Promise((resolve, reject) => {
// Service url.
const url = [this.rootURL, GSF_API.SERVICES_PATH].join('/');
// Get service list.
superagent
.get(url)
.use(nocache) // Prevents caching of *only* this request
.set(this.headers)
.end((err, res) => {
if (res && res.ok) {
const services = res.body.services;
const serviceList = services
.map(service => new Service(this, service.name));
resolve(serviceList);
} else {
const status = ((err && err.status) ? ': ' + err.status : '');
const text = ((err && err.response && err.response.text) ? ': ' +
err.response.text : '');
reject('Error requesting services' + status + text);
}
});
});
}
/**
* Filtering options for listing jobs.
* @typedef {Object} JobListOptions
* @property {Object} query - Filter jobs by specifying one or more comparison operators per property.
* Comparison operators must be prefixed with '$' and only the following are supported: $eq, $ne,
* $gt, $gte, $lt, $lte. Queries may contain multiple properties and each property may
* contain multiple comparison operators.
* @property {Array} sort - The sort array. This array contains an array for each sort which
* consists of the property to sort by and the direction. To sort in ascending order use 1 and
* to sort in descending order use -1. For example, to sort by jobSubmitted date in ascending
* order: [ [ 'jobSubmitted', 1 ] ]
* @property {number} offset - The number of jobs to skip; useful for pagination.
* @property {number} limit - Limit the number of jobs returned. Set to -1 to
* return all jobs. Note: -1 is not recommended, as it may take a long time
* to retrieve all jobs.
* @property {string} totals - Types of total job counts to include in the response.
* Must be one of: 'all', 'none', or 'default'. The default is 'default'.
* Set to 'none' to exclude totals. Set to 'default' to include total count of all filtered jobs.
* Set to 'all' to include total count of all filtered jobs and the total counts of filtered jobs
* in each job status. Totals will only be visible when used with the jobInfoList() function.
*/
/**
* Retrieves an array of jobs on the server.
* @param {JobListOptions} jobListOptions - Object containing options for
* filtering job list.
* @return {Promise<Job[], error>} Returns a Promise to an array of
* jobs that exist on the server.
*/
jobs(jobListOptions) {
return this
.jobInfoList(jobListOptions)
.then((jobInfoList) => (
jobInfoList.jobs.map((jobInfo) => (new Job(this, jobInfo.jobId)))
));
}
/**
* A list of JobInfo objects with count and total information.
* @typedef {Object} JobInfoList
* @property {JobInfo[]} jobs - An array of JobInfo objects that match the search criteria.
* @property {string} count - The number of filtered jobs in the jobs array.
* @property {string} [total] - The total number of jobs. Enabled by default.
* To disable, set 'totals' to 'none' in the JobListOptions.
* @property {string} [accepted] - The total number of accepted jobs.
* This can be enabled by setting 'totals' to 'all' in the JobListOptions.
* @property {string} [started] - The total number of started jobs.
* This can be enabled by setting 'totals' to 'all' in the JobListOptions.
* @property {string} [succeeded] - The total number of succeeded jobs.
* This can be enabled by setting 'totals' to 'all' in the JobListOptions.
* @property {string} [failed] - The total number of failed jobs.
* This can be enabled by setting 'totals' to 'all' in the JobListOptions.
*/
/**
* Retrieves an array of job info objects.
* @param {JobListOptions} jobListOptions - Object containing options for
* filtering job list.
* @return {Promise<JobInfoList, error>} Returns a Promise to a JobInfoList object.
*/
jobInfoList(jobListOptions) {
return new Promise((resolve, reject) => {
// Service url.
let url = [this.rootURL, GSF_API.JOB_SEARCH_PATH].join('/');
// Get job info list.
superagent
.post(url)
.use(nocache) // Prevents caching of *only* this request
.set(this.headers)
.send(jobListOptions || {})
.then((res) => {
resolve(res.body);
})
.catch((err) => {
const status = ((err && err.status) ? ': ' + err.status : '');
const text = ((err && err.response && err.response.text) ? ': ' +
err.response.text : '');
reject('Error requesting jobs' + status + text);
});
});
}
/**
* Returns the Service object based on service name.
* @param {string} serviceName - The name of the service.
* @return {Service} The Service object.
*/
service(serviceName) {
return new Service(this, serviceName);
}
/**
* Retrieves job object based on the job ID.
* @param {string} jobId - The id of the job from which to retrieve the job object.
* @param {function(info: JobProgressInfo)} [progressCallback] - The callback to handle job progress.
* @param {function(info: JobStartedInfo)} [startedCallback] - The callback that is called when the job starts.
* For more reliable job started information, listen to the GSF JobStarted
* events as this callback may not always get called. In some cases the job
* can start before the callback is registered.
* @return {Job} Returns job object.
*/
job(jobId, progressCallback, startedCallback) {
return new Job(this, jobId, progressCallback, startedCallback);
}
}
export default Client;
/**
* Emitted when a job completes.
* @typedef {Object} JobCompleted
* @property {string} jobId - The job id.
* @property {boolean} success - A boolean set to true if the job succeeds,
* false if it fails.
*/
/**
* Emitted when a job succeeds.
* @typedef {Object} JobSucceeded
* @property {string} jobId - The job id.
*/
/**
* Emitted when a job fails.
* @typedef {Object} JobFailed
* @property {string} jobId - The job id.
*/
/**
* Emitted when job progress is updated.
* @typedef {Object} JobProgress
* @property {string} jobId - The job id.
* @property {number} progress - The job progress percent.
* @property {string} [message] - The job progress message, if any.
*/
/**
* Emitted when a job starts.
* @typedef {Object} JobStarted
* @property {string} jobId - The job id.
*/
/**
* Emitted when a job is accepted.
* @typedef {Object} JobAccepted
* @property {string} jobId - The job id.
*/