major revamp of http client functionalit

This commit is contained in:
Dave Horton
2020-02-14 12:45:28 -05:00
parent ff531e6964
commit 446000ee97
35 changed files with 906 additions and 433 deletions

View File

@@ -2,25 +2,39 @@ const Emitter = require('events');
const debug = require('debug')('jambonz:feature-server');
const assert = require('assert');
const {TaskPreconditions} = require('../utils/constants');
const normalizeJamones = require('../utils/normalize-jamones');
const makeTask = require('./make_task');
const specs = new Map();
const _specData = require('./specs');
for (const key in _specData) {specs.set(key, _specData[key]);}
/**
* @classdesc Represents a jambonz verb. This is a superclass that is extended
* by a subclass for each verb.
* @extends Emitter
*/
class Task extends Emitter {
constructor(logger, data) {
super();
this.preconditions = TaskPreconditions.None;
this.logger = logger;
this.data = data;
this.actionHook = this.data.actionHook;
this._killInProgress = false;
this._completionPromise = new Promise((resolve) => this._completionResolver = resolve);
}
/**
* @property {boolean} killed - true if the task has been killed
*/
get killed() {
return this._killInProgress;
}
/**
* @property {CallSession} callSession - the CallSession this task is executing within
*/
get callSession() {
return this.cs;
}
@@ -29,13 +43,13 @@ class Task extends Emitter {
return this.data;
}
/**
* Execute the task. Subclasses must implement this method, but should always call
* the superclass implementation first.
* @param {CallSession} cs - the CallSession that the Task will be executing within.
*/
async exec(cs) {
this.cs = cs;
// N.B. need to require it down here rather than at top to avoid recursion in require of this module
const {actionHook, notifyHook} = require('../utils/notifiers')(this.logger, cs.callInfo);
this.actionHook = actionHook;
this.notifyHook = notifyHook;
}
/**
@@ -48,29 +62,47 @@ class Task extends Emitter {
// no-op
}
/**
* when a subclass Task has completed its work, it should call this method
*/
notifyTaskDone() {
this._completionResolver();
}
/**
* when a subclass task has launched various async activities and is now simply waiting
* for them to complete it should call this method to block until that happens
*/
awaitTaskDone() {
return this._completionPromise;
}
/**
* provided as a convenience for tasks, this simply calls CallSession#normalizeUrl
*/
normalizeUrl(url, method, auth) {
return this.callSession.normalizeUrl(url, method, auth);
}
async performAction(method, auth, results, expectResponse = true) {
if (this.action) {
const hook = this.normalizeUrl(this.action, method, auth);
const tasks = await this.actionHook(hook, results, expectResponse);
if (expectResponse && tasks && Array.isArray(tasks)) {
this.logger.debug({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
this.callSession.replaceApplication(tasks);
async performAction(results, expectResponse = true) {
if (this.actionHook) {
const params = results ? Object.assign(results, this.cs.callInfo) : this.cs.callInfo;
const json = await this.cs.requestor.request(this.actionHook, params);
if (expectResponse && json && Array.isArray(json)) {
const tasks = normalizeJamones(this.logger, json).map((tdata) => makeTask(this.logger, tdata));
if (tasks && tasks.length > 0) {
this.logger.info({tasks: tasks}, `${this.name} replacing application with ${tasks.length} tasks`);
this.callSession.replaceApplication(tasks);
}
}
}
}
/**
* validate that the JSON task description is valid
* @param {string} name - verb name
* @param {object} data - verb properties
*/
static validate(name, data) {
debug(`validating ${name} with data ${JSON.stringify(data)}`);
// validate the instruction is supported
@@ -94,6 +126,12 @@ class Task extends Emitter {
else if (typeof dSpec === 'string' && dSpec === 'array') {
if (!Array.isArray(dVal)) throw new Error(`${name}: property ${dKey} is not an array`);
}
else if (typeof dSpec === 'string' && dSpec.includes('|')) {
const types = dSpec.split('|').map((t) => t.trim());
if (!types.includes(typeof dVal)) {
throw new Error(`${name}: property ${dKey} has invalid data type, must be one of ${types}`);
}
}
else if (Array.isArray(dSpec) && dSpec[0].startsWith('#')) {
const name = dSpec[0].slice(1);
for (const item of dVal) {