* Feat #1130 Adding support for downloading and zipping files @ This is a combination of 12 commits. - Feat #1130 Initial Commit - Feat #1130 Adding support for hierachical lists - Feat #1130 [WIP] Adding support for downloading and zipping files - Feat #1130 Adding support for hierarchical folder structure study, series, instances on generated zip file - Feat #1130 Adding unit tests and addressing code review feedbacks - Feat #1130 POC (adding module to support on progress handling) - Feat #1130 Improving logic from progress tracking utils - Feat #1130 Renaming module with progress tracking utils - Feat #1130 Beta version of Progress Tracking utils - Feat #1130 Adding unit tests for Progress Tracking Utils - Feat #1130 Documenting public methods from progress tracking utils module - Feat #1130 Adding support for download and zip progress reporting * Feat #1130 Addressing code review feedback * Feat #1130 Preventing multiple downloads from executing at once * Feat #1130 Improving error message for concurrent downloads * Remove downloadAndZipSeriesOnViewports Co-authored-by: James Petts <jamesapetts@gmail.com>
This commit is contained in:
1 parent
2412c08595
commit
29d4955980
26 files changed
+1665
No files matched your search
@@ -0,0 +1,66 @@
|
||||
|
||||
export default class Queue {
|
||||
|
||||
constructor(limit) {
|
||||
this.limit = limit;
|
||||
this.size = 0;
|
||||
this.awaiting = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new "proxy" function associated with the current execution queue
|
||||
* instance. When the returned function is invoked, the queue limit is checked
|
||||
* to make sure the limit of scheduled tasks is repected (throwing an
|
||||
* exception when the limit has been reached and before calling the original
|
||||
* function). The original function is only invoked after all the previously
|
||||
* scheduled tasks have finished executing (their returned promises have
|
||||
* resolved/rejected);
|
||||
*
|
||||
* @param {function} task The function whose execution will be associated
|
||||
* with the current Queue instance;
|
||||
* @returns {function} The "proxy" function bound to the current Queue
|
||||
* instance;
|
||||
*/
|
||||
bind(task) {
|
||||
return bind(this, task);
|
||||
}
|
||||
|
||||
bindSafe(task, onError) {
|
||||
const boundTask = bind(this, task);
|
||||
return async function safeTask(...args) {
|
||||
try {
|
||||
return await boundTask(...args);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
function bind(queue, task) {
|
||||
const cleaner = clean.bind(null, queue);
|
||||
return async function boundTask(...args) {
|
||||
if (queue.size >= queue.limit) {
|
||||
throw new Error('Queue limit reached');
|
||||
}
|
||||
const promise = chain(queue.awaiting, task, args);
|
||||
queue.awaiting = promise.then(cleaner, cleaner);
|
||||
queue.size++;
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
function clean(queue) {
|
||||
if (queue.size > 0 && --queue.size === 0) {
|
||||
queue.awaiting = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function chain(prev, task, args) {
|
||||
await prev;
|
||||
return task(...args);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import makeDeferred from './makeDeferred';
|
||||
import Queue from './Queue';
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
function timeout(delay) {
|
||||
const { resolve, promise } = makeDeferred();
|
||||
setTimeout(() => void resolve(Date.now()), delay);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests
|
||||
*/
|
||||
|
||||
describe('Queue', () => {
|
||||
it('should bind functions to the queue', async () => {
|
||||
const queue = new Queue(2);
|
||||
const mockedTimeout = jest.fn(timeout);
|
||||
const timer = queue.bind(mockedTimeout);
|
||||
const start = Date.now();
|
||||
timer(120).then(now => {
|
||||
const elapsed = now - start;
|
||||
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||
});
|
||||
const end = await timer(120);
|
||||
expect(end - start > 240).toBe(true);
|
||||
expect(mockedTimeout).toBeCalledTimes(2);
|
||||
});
|
||||
it('should prevent task execution when queue limit is reached', async () => {
|
||||
const queue = new Queue(1);
|
||||
const mockedTimeout = jest.fn(timeout);
|
||||
const timer = queue.bind(mockedTimeout);
|
||||
const start = Date.now();
|
||||
const promise = timer(120).then(time => time - start);
|
||||
try {
|
||||
await timer(120);
|
||||
} catch (e) {
|
||||
expect(Date.now() - start < 120).toBe(true);
|
||||
expect(e.message).toBe('Queue limit reached');
|
||||
}
|
||||
const elapsed = await promise;
|
||||
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||
expect(mockedTimeout).toBeCalledTimes(1);
|
||||
});
|
||||
it('should safely bind tasks to the queue', async () => {
|
||||
const queue = new Queue(1);
|
||||
const mockedErrorHandler = jest.fn();
|
||||
const mockedTimeout = jest.fn(timeout);
|
||||
const timer = queue.bindSafe(mockedTimeout, mockedErrorHandler);
|
||||
const start = Date.now();
|
||||
const promise = timer(120).then(time => time - start);
|
||||
await timer(120);
|
||||
expect(Date.now() - start < 120).toBe(true);
|
||||
expect(mockedErrorHandler).toBeCalledTimes(1);
|
||||
expect(mockedErrorHandler).nthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ message: 'Queue limit reached' })
|
||||
);
|
||||
const elapsed = await promise;
|
||||
expect(elapsed >= 120 && elapsed < 240).toBe(true);
|
||||
expect(mockedTimeout).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const SEPARATOR = '/';
|
||||
|
||||
/**
|
||||
* API
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add values to a list hierarchically.
|
||||
* @ For example:
|
||||
* addToList([], 'a', 'b', 'c');
|
||||
* will add the following hierarchy to the list:
|
||||
* a > b > c
|
||||
* resulting in the following array:
|
||||
* [['a', [['b', ['c']]]]]
|
||||
* @param {Array} list The target list;
|
||||
* @param {...string} values The values to be hierarchically added to the list;
|
||||
* @returns {Array} Returns the provided list possibly updated with the given
|
||||
* values or null when a bad list (not an actual array) is provided
|
||||
*/
|
||||
function addToList(list, ...values) {
|
||||
if (Array.isArray(list)) {
|
||||
if (values.length > 0) {
|
||||
addValuesToList(list, values);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates through the provided hierarchical list executing the callback
|
||||
* once for each leaf-node of the tree. The ancestors of the leaf-node being
|
||||
* visited are passed to the callback function along with the leaf-node in
|
||||
* the exact same order they appear on the tree (from root to leaf);
|
||||
* @ For example, if the hierachy `a > b > c` appears on the tree ("a" being
|
||||
* the root and "c" being the leaf) the callback function will be called as:
|
||||
* callback('a', 'b', 'c');
|
||||
* @param {Array} list The hierarchical list to be iterated
|
||||
* @param {function} callback The callback which will be exected once for
|
||||
* each leaf-node of the hierarchical list;
|
||||
* @returns {Array} Returns the provided list or null for bad arguments;
|
||||
*/
|
||||
function forEach(list, callback) {
|
||||
if (Array.isArray(list)) {
|
||||
if (typeof callback === 'function') {
|
||||
forEachValue(list, callback);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an item from the given hierarchical list based on an index (number)
|
||||
* or a path (string).
|
||||
* @ For example:
|
||||
* getItem(list, '1/0/4')
|
||||
* will retrieve the fourth grandchild, from the first child of the second
|
||||
* element of the list;
|
||||
* @param {Array} list The source list;
|
||||
* @param {string|number} indexOrPath The index of the element inside list
|
||||
* (number) or the path to reach the desired element (string). The slash "/"
|
||||
* character is cosidered the path separator;
|
||||
*/
|
||||
function getItem(list, indexOrPath) {
|
||||
if (Array.isArray(list)) {
|
||||
let subpath = null;
|
||||
let index = typeof indexOrPath === 'number' ? indexOrPath : -1;
|
||||
if (typeof indexOrPath === 'string') {
|
||||
const separator = indexOrPath.indexOf(SEPARATOR);
|
||||
if (separator > 0) {
|
||||
index = parseInt(indexOrPath.slice(0, separator), 10);
|
||||
if (separator + 1 < indexOrPath.length) {
|
||||
subpath = indexOrPath.slice(separator + 1, indexOrPath.length);
|
||||
}
|
||||
} else {
|
||||
index = parseInt(indexOrPath, 10);
|
||||
}
|
||||
}
|
||||
if (index >= 0 && index < list.length) {
|
||||
const item = list[index];
|
||||
if (isSublist(item)) {
|
||||
if (subpath !== null) {
|
||||
return getItem(item[1], subpath);
|
||||
}
|
||||
return item[0];
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty-print the provided hierarchical list;
|
||||
* @param {Array} list The source list;
|
||||
* @returns {string} The textual representation of the hierarchical list;
|
||||
*/
|
||||
function print(list) {
|
||||
let text = '';
|
||||
if (Array.isArray(list)) {
|
||||
let prev = [];
|
||||
forEachValue(list, function(...args) {
|
||||
let prevLen = prev.length;
|
||||
for (let i = 0, l = args.length; i < l; ++i) {
|
||||
if (i < prevLen && args[i] === prev[i]) {
|
||||
continue;
|
||||
}
|
||||
text += ' '.repeat(i) + args[i] + '\n';
|
||||
}
|
||||
prev = args;
|
||||
});
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
function forEachValue(list, callback) {
|
||||
for (let i = 0, l = list.length; i < l; ++i) {
|
||||
let item = list[i];
|
||||
if (isSublist(item)) {
|
||||
if (item[1].length > 0) {
|
||||
forEachValue(item[1], callback.bind(null, item[0]));
|
||||
continue;
|
||||
}
|
||||
item = item[0];
|
||||
}
|
||||
callback(item);
|
||||
}
|
||||
}
|
||||
|
||||
function addValuesToList(list, values) {
|
||||
let value = values.shift();
|
||||
let index = add(list, value);
|
||||
if (index >= 0) {
|
||||
if (values.length > 0) {
|
||||
let sublist = list[index];
|
||||
if (!isSublist(sublist)) {
|
||||
sublist = toSublist(value);
|
||||
list[index] = sublist;
|
||||
}
|
||||
return addValuesToList(sublist[1], values);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function add(list, value) {
|
||||
let index = find(list, value);
|
||||
if (index === -2) {
|
||||
index = list.push(value) - 1;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
function find(list, value) {
|
||||
if (typeof value === 'string') {
|
||||
for (let i = 0, l = list.length; i < l; ++i) {
|
||||
let item = list[i];
|
||||
if (item === value || (isSublist(item) && item[0] === value)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function isSublist(subject) {
|
||||
return (
|
||||
Array.isArray(subject) &&
|
||||
subject.length === 2 &&
|
||||
typeof subject[0] === 'string' &&
|
||||
Array.isArray(subject[1])
|
||||
);
|
||||
}
|
||||
|
||||
function toSublist(value) {
|
||||
return [value + '', []];
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
export { addToList, getItem, forEach, print };
|
||||
@@ -0,0 +1,96 @@
|
||||
import { addToList, forEach, getItem, print } from './hierarchicalListUtils';
|
||||
|
||||
describe('hierarchicalListUtils', function() {
|
||||
let sharedList;
|
||||
|
||||
beforeEach(function() {
|
||||
sharedList = [
|
||||
['1.2.3.1', ['1.2.3.1.1', '1.2.3.1.2']],
|
||||
'1.2.3.2',
|
||||
['1.2.3.3', ['1.2.3.3.1', ['1.2.3.3.2', ['1.2.3.3.2.1', '1.2.3.3.2.2']]]],
|
||||
];
|
||||
});
|
||||
|
||||
describe('getItem', function() {
|
||||
it('should retrieve elements from a list by index', function() {
|
||||
expect(getItem(sharedList, 0)).toBe('1.2.3.1');
|
||||
expect(getItem(sharedList, 1)).toBe('1.2.3.2');
|
||||
expect(getItem(sharedList, 2)).toBe('1.2.3.3');
|
||||
expect(getItem(sharedList, 3)).toBeUndefined();
|
||||
});
|
||||
it('should retrieve elements from a list by path', function() {
|
||||
expect(getItem(sharedList, '0')).toBe('1.2.3.1');
|
||||
expect(getItem(sharedList, '0/0')).toBe('1.2.3.1.1');
|
||||
expect(getItem(sharedList, '0/1')).toBe('1.2.3.1.2');
|
||||
expect(getItem(sharedList, '0/2')).toBeUndefined();
|
||||
expect(getItem(sharedList, '1')).toBe('1.2.3.2');
|
||||
expect(getItem(sharedList, '2')).toBe('1.2.3.3');
|
||||
expect(getItem(sharedList, '2/0')).toBe('1.2.3.3.1');
|
||||
expect(getItem(sharedList, '2/1')).toBe('1.2.3.3.2');
|
||||
expect(getItem(sharedList, '2/2')).toBeUndefined();
|
||||
expect(getItem(sharedList, '2/1/0')).toBe('1.2.3.3.2.1');
|
||||
expect(getItem(sharedList, '2/1/1')).toBe('1.2.3.3.2.2');
|
||||
expect(getItem(sharedList, '2/1/2')).toBeUndefined();
|
||||
expect(getItem(sharedList, '3')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addToList', function() {
|
||||
it('should support adding elements to a list hierarchically', function() {
|
||||
const list = [];
|
||||
addToList(list, '1.2.3.1', '1.2.3.1.1');
|
||||
addToList(list, '1.2.3.1', '1.2.3.1.2');
|
||||
addToList(list, '1.2.3.2');
|
||||
addToList(list, '1.2.3.3', '1.2.3.3.1');
|
||||
addToList(list, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1');
|
||||
addToList(list, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2');
|
||||
expect(list).toStrictEqual(sharedList);
|
||||
});
|
||||
it('should change leaf nodes into non-leaf nodes', function() {
|
||||
const listw = [];
|
||||
const listx = [['x.1', ['x.1.1', 'x.1.2']], 'x.2'];
|
||||
const listy = [
|
||||
['x.1', [['x.1.1', ['x.1.1.1']], 'x.1.2']],
|
||||
['x.2', ['x.2.1']],
|
||||
];
|
||||
addToList(listw, 'x.1');
|
||||
addToList(listw, 'x.1', 'x.1.1');
|
||||
addToList(listw, 'x.1', 'x.1.2');
|
||||
addToList(listw, 'x.2');
|
||||
expect(listw).toStrictEqual(listx);
|
||||
addToList(listw, 'x.2', 'x.2.1');
|
||||
addToList(listw, 'x.1', 'x.1.1', 'x.1.1.1');
|
||||
expect(listw).toStrictEqual(listy);
|
||||
});
|
||||
});
|
||||
|
||||
describe('forEach', function() {
|
||||
it('should iterate through all leaf nodes of the tree', function() {
|
||||
const fn = jest.fn();
|
||||
forEach(sharedList, fn);
|
||||
expect(fn).toHaveBeenCalledTimes(6);
|
||||
expect(fn).nthCalledWith(1, '1.2.3.1', '1.2.3.1.1');
|
||||
expect(fn).nthCalledWith(2, '1.2.3.1', '1.2.3.1.2');
|
||||
expect(fn).nthCalledWith(3, '1.2.3.2');
|
||||
expect(fn).nthCalledWith(4, '1.2.3.3', '1.2.3.3.1');
|
||||
expect(fn).nthCalledWith(5, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1');
|
||||
expect(fn).nthCalledWith(6, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('print', function() {
|
||||
it('should pretty-print the hierarchical list', function() {
|
||||
expect(print(sharedList)).toBe(
|
||||
'1.2.3.1\n' +
|
||||
' 1.2.3.1.1\n' +
|
||||
' 1.2.3.1.2\n' +
|
||||
'1.2.3.2\n' +
|
||||
'1.2.3.3\n' +
|
||||
' 1.2.3.3.1\n' +
|
||||
' 1.2.3.3.2\n' +
|
||||
' 1.2.3.3.2.1\n' +
|
||||
' 1.2.3.3.2.2\n'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,8 +10,14 @@ import DicomLoaderService from './dicomLoaderService.js';
|
||||
import b64toBlob from './b64toBlob.js';
|
||||
import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js';
|
||||
import * as urlUtil from './urlUtil';
|
||||
import makeDeferred from './makeDeferred';
|
||||
import makeCancelable from './makeCancelable';
|
||||
import hotkeys from './hotkeys';
|
||||
import Queue from './Queue';
|
||||
import isDicomUid from './isDicomUid';
|
||||
import resolveObjectPath from './resolveObjectPath';
|
||||
import * as hierarchicalListUtils from './hierarchicalListUtils';
|
||||
import * as progressTrackingUtils from './progressTrackingUtils';
|
||||
|
||||
const utils = {
|
||||
guid,
|
||||
@@ -26,8 +32,14 @@ const utils = {
|
||||
DicomLoaderService,
|
||||
urlUtil,
|
||||
loadAndCacheDerivedDisplaySets,
|
||||
makeDeferred,
|
||||
makeCancelable,
|
||||
hotkeys,
|
||||
Queue,
|
||||
isDicomUid,
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -43,8 +55,14 @@ export {
|
||||
DicomLoaderService,
|
||||
urlUtil,
|
||||
loadAndCacheDerivedDisplaySets,
|
||||
makeDeferred,
|
||||
makeCancelable,
|
||||
hotkeys,
|
||||
Queue,
|
||||
isDicomUid,
|
||||
resolveObjectPath,
|
||||
hierarchicalListUtils,
|
||||
progressTrackingUtils,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
@@ -15,8 +15,14 @@ describe('Top level exports', () => {
|
||||
'loadAndCacheDerivedDisplaySets',
|
||||
'DicomLoaderService',
|
||||
'urlUtil',
|
||||
'makeDeferred',
|
||||
'makeCancelable',
|
||||
'hotkeys',
|
||||
'Queue',
|
||||
'isDicomUid',
|
||||
'resolveObjectPath',
|
||||
'hierarchicalListUtils',
|
||||
'progressTrackingUtils',
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(utils.default).sort();
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function isDicomUid(subject) {
|
||||
const regex = /^\d+(?:\.\d+)*$/;
|
||||
return typeof subject === 'string' && regex.test(subject.trim());
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import isDicomUid from './isDicomUid';
|
||||
|
||||
describe('isDicomUid', function() {
|
||||
it('should return true for valid DICOM UIDs', function() {
|
||||
expect(isDicomUid('1')).toBe(true);
|
||||
expect(isDicomUid('1.2')).toBe(true);
|
||||
expect(isDicomUid('1.2.3')).toBe(true);
|
||||
expect(isDicomUid('1.2.3.4')).toBe(true);
|
||||
});
|
||||
it('should return false for invalid DICOM UIDs', function() {
|
||||
expect(isDicomUid('x')).toBe(false);
|
||||
expect(isDicomUid('1.')).toBe(false);
|
||||
expect(isDicomUid('1. 2')).toBe(false);
|
||||
expect(isDicomUid('1.2.n.4')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function makeDeferred() {
|
||||
let reject, resolve, promise = new Promise(function (res, rej) {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return Object.freeze({ promise, resolve, reject });
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import makeDeferred from './makeDeferred';
|
||||
|
||||
describe('makeDeferred', () => {
|
||||
it('should provide a promise to be resolved externally', () => {
|
||||
const deferred = makeDeferred();
|
||||
setTimeout(() => void deferred.resolve('Yay!'));
|
||||
return deferred.promise.then(result => void expect(result).toBe('Yay!'));
|
||||
});
|
||||
it('should provide a promise to be rejected externally', () => {
|
||||
const deferred = makeDeferred();
|
||||
setTimeout(() => void deferred.reject('Oops...'));
|
||||
return deferred.promise.catch(error => void expect(error).toBe('Oops...'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,336 @@
|
||||
import makeDeferred from './makeDeferred';
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
const TYPE = Symbol('Type');
|
||||
const TASK = Symbol('Task');
|
||||
const LIST = Symbol('List');
|
||||
|
||||
/**
|
||||
* Public Methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an instance of a task list
|
||||
* @returns {Object} A task list object
|
||||
*/
|
||||
function createList() {
|
||||
return objectWithType(LIST, {
|
||||
head: null,
|
||||
named: Object.create(null),
|
||||
observers: [],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given argument is a List instance
|
||||
* @param {any} subject The value to be tested
|
||||
* @returns {boolean} true if a valid List instance is given, false otherwise
|
||||
*/
|
||||
function isList(subject) {
|
||||
return isOfType(LIST, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of a task
|
||||
* @param {Object} list The List instance related to this task
|
||||
* @param {Object} next The next Task instance to link to
|
||||
* @returns {Object} A task object
|
||||
*/
|
||||
function createTask(list, next) {
|
||||
return objectWithType(TASK, {
|
||||
list: isList(list) ? list : null,
|
||||
next: isTask(next) ? next : null,
|
||||
failed: false,
|
||||
awaiting: null,
|
||||
progress: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given argument is a Task instance
|
||||
* @param {any} subject The value to be tested
|
||||
* @returns {boolean} true if a valid Task instance is given, false otherwise
|
||||
*/
|
||||
function isTask(subject) {
|
||||
return isOfType(TASK, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a new Task to the given List instance and notifies the list observers
|
||||
* @param {Object} list A List instance
|
||||
* @returns {Object} The new Task instance appended to the List or null if the
|
||||
* given List instanc is not valid
|
||||
*/
|
||||
function increaseList(list) {
|
||||
if (isList(list)) {
|
||||
const task = createTask(list, list.head);
|
||||
list.head = task;
|
||||
notify(list, getOverallProgress(list));
|
||||
return task;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the internal progress value of the given Task instance and notifies
|
||||
* the observers of the associated list.
|
||||
* @param {Object} task The Task instance to be updated
|
||||
* @param {number} value A number between 0 (inclusive) and 1 (exclusive)
|
||||
* indicating the progress of the task;
|
||||
* @returns {void} Nothing is returned
|
||||
*/
|
||||
function update(task, value) {
|
||||
if (isTask(task) && isValidProgress(value) && value < 1.0) {
|
||||
if (task.progress !== value) {
|
||||
task.progress = value;
|
||||
if (isList(task.list)) {
|
||||
notify(task.list, getOverallProgress(task.list));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Task instance as finished (progress = 1.0), freezes it in order to
|
||||
* prevent further modifications and notifies the observers of the associated
|
||||
* list.
|
||||
* @param {Object} task The Task instance to be finalized
|
||||
* @returns {void} Nothing is returned
|
||||
*/
|
||||
function finish(task) {
|
||||
if (isTask(task)) {
|
||||
task.progress = 1.0;
|
||||
task.awaiting = null;
|
||||
Object.freeze(task);
|
||||
if (isList(task.list)) {
|
||||
notify(task.list, getOverallProgress(task.list));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a summarized snapshot of the current status of the given task List
|
||||
* @param {Object} list The List instance to be scanned
|
||||
* @returns {Object} An obeject representing the summarized status of the list
|
||||
*/
|
||||
function getOverallProgress(list) {
|
||||
const status = createStatus();
|
||||
if (isList(list)) {
|
||||
let task = list.head;
|
||||
while (isTask(task)) {
|
||||
status.total++;
|
||||
if (isValidProgress(task.progress)) {
|
||||
status.partial += task.progress;
|
||||
if (task.progress === 1.0 && task.failed) status.failures++;
|
||||
}
|
||||
task = task.next;
|
||||
}
|
||||
}
|
||||
if (status.total > 0) {
|
||||
status.progress = status.partial / status.total;
|
||||
}
|
||||
return Object.freeze(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Task instance to the given list that waits on a given "thenable". When
|
||||
* the thenable resolves the "finish" method is called on the newly created
|
||||
* instance thus notifying the observers of the list.
|
||||
* @param {Object} list The List instance to which the new task will be added
|
||||
* @param {Object|Promise} thenable The thenable to be waited on
|
||||
* @returns {Object} A reference to the newly created Task;
|
||||
*/
|
||||
function waitOn(list, thenable) {
|
||||
const task = increaseList(list);
|
||||
if (isTask(task)) {
|
||||
task.awaiting = Promise.resolve(thenable).then(
|
||||
function() {
|
||||
finish(task);
|
||||
},
|
||||
function() {
|
||||
task.failed = true;
|
||||
finish(task);
|
||||
}
|
||||
);
|
||||
return task;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Task instance to the given list using a deferred (a Promise that can
|
||||
* be externally resolved) notifying the observers of the list.
|
||||
* @param {Object} list The List instance to which the new task will be added
|
||||
* @returns {Object} An object with references to the created deferred and task
|
||||
*/
|
||||
function addDeferred(list) {
|
||||
const deferred = makeDeferred();
|
||||
const task = waitOn(list, deferred.promise);
|
||||
return Object.freeze({
|
||||
deferred,
|
||||
task,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a name to a specific task of the list
|
||||
* @param {Object} list The List instance whose task will be named
|
||||
* @param {Object} task The specified Task instance
|
||||
* @param {string} name The name of the task
|
||||
* @returns {boolean} Returns true on success, false otherwise
|
||||
*/
|
||||
function setTaskName(list, task, name) {
|
||||
if (
|
||||
contains(list, task) &&
|
||||
list.named !== null &&
|
||||
typeof list.named === 'object' &&
|
||||
typeof name === 'string'
|
||||
) {
|
||||
list.named[name] = task;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a task by name
|
||||
* @param {Object} list The List instance whose task will be retrieved
|
||||
* @param {string} name The name of the task to be retrieved
|
||||
* @returns {Object} The Task instance or null if not found
|
||||
*/
|
||||
function getTaskByName(list, name) {
|
||||
if (
|
||||
isList(list) &&
|
||||
list.named !== null &&
|
||||
typeof list.named === 'object' &&
|
||||
typeof name === 'string'
|
||||
) {
|
||||
const task = list.named[name];
|
||||
if (isTask(task)) {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an observer (callback function) to a given List instance
|
||||
* @param {Object} list The List instance to which the observer will be appended
|
||||
* @param {Function} observer The observer (function) that will be executed
|
||||
* every time a change happens within the list
|
||||
* @returns {boolean} Returns true on success and false otherewise
|
||||
*/
|
||||
function addObserver(list, observer) {
|
||||
if (
|
||||
isList(list) &&
|
||||
Array.isArray(list.observers) &&
|
||||
typeof observer === 'function'
|
||||
) {
|
||||
list.observers.push(observer);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an observer (callback function) from a given List instance
|
||||
* @param {Object} list The instance List from which the observer will removed
|
||||
* @param {Function} observer The observer function to be removed
|
||||
* @returns {boolean} Returns true on success and false otherewise
|
||||
*/
|
||||
function removeObserver(list, observer) {
|
||||
if (
|
||||
isList(list) &&
|
||||
Array.isArray(list.observers) &&
|
||||
list.observers.length > 0
|
||||
) {
|
||||
const index = list.observers.indexOf(observer);
|
||||
if (index >= 0) {
|
||||
list.observers.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
|
||||
function createStatus() {
|
||||
return Object.seal({
|
||||
total: 0,
|
||||
partial: 0.0,
|
||||
progress: 0.0,
|
||||
failures: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function objectWithType(type, object) {
|
||||
return Object.seal(Object.defineProperty(object, TYPE, { value: type }));
|
||||
}
|
||||
|
||||
function isOfType(type, subject) {
|
||||
return (
|
||||
subject !== null && typeof subject === 'object' && subject[TYPE] === type
|
||||
);
|
||||
}
|
||||
|
||||
function isValidProgress(value) {
|
||||
return typeof value === 'number' && value >= 0.0 && value <= 1.0;
|
||||
}
|
||||
|
||||
function contains(list, task) {
|
||||
if (isList(list) && isTask(task)) {
|
||||
let item = list.head;
|
||||
while (isTask(item)) {
|
||||
if (item === task) {
|
||||
return true;
|
||||
}
|
||||
item = item.next;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function notify(list, data) {
|
||||
if (
|
||||
isList(list) &&
|
||||
Array.isArray(list.observers) &&
|
||||
list.observers.length > 0
|
||||
) {
|
||||
list.observers.slice().forEach(function(observer) {
|
||||
if (typeof observer === 'function') {
|
||||
try {
|
||||
observer(data, list);
|
||||
} catch (e) {
|
||||
/* Oops! */
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
export {
|
||||
createList,
|
||||
isList,
|
||||
createTask,
|
||||
isTask,
|
||||
increaseList,
|
||||
update,
|
||||
finish,
|
||||
getOverallProgress,
|
||||
waitOn,
|
||||
addDeferred,
|
||||
setTaskName,
|
||||
getTaskByName,
|
||||
addObserver,
|
||||
removeObserver,
|
||||
};
|
||||
@@ -0,0 +1,175 @@
|
||||
import * as utils from './progressTrackingUtils';
|
||||
|
||||
describe('progressTrackingUtils', () => {
|
||||
describe('Creation of lists of tasks to be tracked', () => {
|
||||
it('should support creation of task lists', () => {
|
||||
expect(utils.createList()).toBeInstanceOf(Object);
|
||||
});
|
||||
it('should support validation of task lists', () => {
|
||||
const list = utils.createList();
|
||||
expect(utils.isList(list)).toBe(true);
|
||||
expect(utils.isList(JSON.parse(JSON.stringify(list)))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Usage of lists of tasks to be tracked', () => {
|
||||
let context;
|
||||
|
||||
// Mock for download
|
||||
function fakeRequest(callback) {
|
||||
return new Promise(resolve => {
|
||||
let progress = 0.0;
|
||||
setTimeout(function step() {
|
||||
if (progress < 1.0) {
|
||||
progress += 1 / 4;
|
||||
callback(progress);
|
||||
setTimeout(step);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
const list = utils.createList();
|
||||
const observer = jest.fn();
|
||||
utils.addObserver(list, observer);
|
||||
context = { list, observer };
|
||||
});
|
||||
|
||||
it('should call observer twice for each task', () => {
|
||||
const { list, observer } = context;
|
||||
const promises = [
|
||||
Promise.resolve('A'),
|
||||
Promise.resolve('B'),
|
||||
Promise.resolve('C'),
|
||||
];
|
||||
promises.forEach(promise => void utils.waitOn(list, promise));
|
||||
return Promise.all(promises).then(() => {
|
||||
expect(observer).toBeCalledTimes(6);
|
||||
[
|
||||
{
|
||||
failures: 0,
|
||||
partial: 0,
|
||||
progress: 0,
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 0,
|
||||
progress: 0,
|
||||
total: 2,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 0,
|
||||
progress: 0,
|
||||
total: 3,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 1.0,
|
||||
progress: 1 / 3,
|
||||
total: 3,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 2.0,
|
||||
progress: 2 / 3,
|
||||
total: 3,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 3.0,
|
||||
progress: 1.0,
|
||||
total: 3,
|
||||
},
|
||||
].forEach((item, i) => {
|
||||
const result = expect.objectContaining(item);
|
||||
expect(observer).nthCalledWith(i + 1, result, list);
|
||||
});
|
||||
expect(utils.getOverallProgress(list)).toStrictEqual({
|
||||
failures: 0,
|
||||
partial: 3.0,
|
||||
progress: 1.0,
|
||||
total: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should support tasks with internal progress updates', () => {
|
||||
const { list, observer } = context;
|
||||
const download = utils.addDeferred(list);
|
||||
const processing = download.deferred.promise.then(result => result);
|
||||
const update = jest.fn(p => void utils.update(download.task, p));
|
||||
download.deferred.resolve(fakeRequest(update));
|
||||
utils.waitOn(list, processing);
|
||||
return processing.then(() => {
|
||||
expect(update).toBeCalledTimes(4);
|
||||
[0.25, 0.5, 0.75, 1.0].forEach(
|
||||
(value, i) => void expect(update).nthCalledWith(i + 1, value)
|
||||
);
|
||||
expect(observer).toBeCalledTimes(7);
|
||||
[
|
||||
{
|
||||
failures: 0,
|
||||
partial: 0,
|
||||
progress: 0,
|
||||
total: 1,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 0,
|
||||
progress: 0,
|
||||
total: 2,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 0.25,
|
||||
progress: 0.125,
|
||||
total: 2,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 0.5,
|
||||
progress: 0.25,
|
||||
total: 2,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 0.75,
|
||||
progress: 0.375,
|
||||
total: 2,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 1.0,
|
||||
progress: 0.5,
|
||||
total: 2,
|
||||
},
|
||||
{
|
||||
failures: 0,
|
||||
partial: 2.0,
|
||||
progress: 1.0,
|
||||
total: 2,
|
||||
},
|
||||
].forEach((item, i) => {
|
||||
const result = expect.objectContaining(item);
|
||||
expect(observer).nthCalledWith(i + 1, result, list);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Naming of specific tasks', () => {
|
||||
it('should support naming specific tasks', () => {
|
||||
const list = utils.createList();
|
||||
const tasks = [utils.increaseList(list), utils.increaseList(list)];
|
||||
expect(utils.setTaskName(list, tasks[0], 'firstTask')).toBe(true);
|
||||
expect(utils.setTaskName(list, tasks[1], 'secondTask')).toBe(true);
|
||||
expect(utils.getTaskByName(list, 'secondTask')).toBe(tasks[1]);
|
||||
expect(utils.getTaskByName(list, 'firstTask')).toBe(tasks[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export default function resolveObjectPath(root, path, defaultValue) {
|
||||
if (root !== null && typeof root === 'object' && typeof path === 'string') {
|
||||
let value,
|
||||
separator = path.indexOf('.');
|
||||
if (separator >= 0) {
|
||||
return resolveObjectPath(
|
||||
root[path.slice(0, separator)],
|
||||
path.slice(separator + 1, path.length),
|
||||
defaultValue
|
||||
);
|
||||
}
|
||||
value = root[path];
|
||||
return value === undefined && defaultValue !== undefined
|
||||
? defaultValue
|
||||
: value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import resolveObjectPath from './resolveObjectPath';
|
||||
|
||||
describe('resolveObjectPath', function() {
|
||||
let config;
|
||||
|
||||
beforeEach(function() {
|
||||
config = {
|
||||
active: {
|
||||
user: {
|
||||
name: {
|
||||
first: 'John',
|
||||
last: 'Doe',
|
||||
},
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
ipv4: '10.0.0.1',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should safely return deeply nested values from an object', function() {
|
||||
expect(resolveObjectPath(config, 'active.user.name.first')).toBe('John');
|
||||
expect(resolveObjectPath(config, 'active.user.name.last')).toBe('Doe');
|
||||
expect(resolveObjectPath(config, 'active.servers.0.ipv4')).toBe('10.0.0.1');
|
||||
});
|
||||
|
||||
it('should silently return undefined when intermediate values are not valid objects', function() {
|
||||
expect(resolveObjectPath(config, 'active.usr.name.first')).toBeUndefined();
|
||||
expect(resolveObjectPath(config, 'active.name.last')).toBeUndefined();
|
||||
expect(resolveObjectPath(config, 'active.servers.7.ipv4')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -56,6 +56,7 @@
|
||||
"@ohif/extension-dicom-rt": "^0.0.2",
|
||||
"@ohif/extension-dicom-segmentation": "^0.1.2",
|
||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||
"@ohif/extension-dicom-p10-downloader": "^0.0.1",
|
||||
"@ohif/extension-vtk": "^1.3.11",
|
||||
"@ohif/i18n": "^0.52.8",
|
||||
"@ohif/ui": "^1.4.3",
|
||||
|
||||
@@ -29,6 +29,7 @@ import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
||||
import OHIFDicomRtExtension from '@ohif/extension-dicom-rt';
|
||||
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
||||
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
||||
import OHIFDicomP10DownloaderExtension from '@ohif/extension-dicom-p10-downloader';
|
||||
|
||||
/*
|
||||
* Default Settings
|
||||
@@ -48,6 +49,7 @@ const appProps = {
|
||||
OHIFDicomPDFExtension,
|
||||
OHIFDicomSegmentationExtension,
|
||||
OHIFDicomRtExtension,
|
||||
OHIFDicomP10DownloaderExtension,
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user