Added an async task manager to the visual console client

Former-commit-id: 4f5c92a976ae976e77cc11505c7a57bf31121930
This commit is contained in:
Alejandro Gallardo Escobar 2019-05-16 11:04:03 +02:00
parent 2b2daf2b8b
commit a25a08f48d
7 changed files with 247 additions and 113 deletions

View File

@ -1,5 +1,5 @@
// TODO: Add Artica ST header. // TODO: Add Artica ST header.
/* globals jQuery, VisualConsole */ /* globals jQuery, VisualConsole, AsyncTaskManager */
/* /*
* ********************* * *********************
@ -28,63 +28,68 @@ function createVisualConsole(
updateInterval, updateInterval,
onUpdate onUpdate
) { ) {
var visualConsole = null;
var linkedVCRequest = null;
var updateVCRequest = null;
if (container == null || props == null || items == null) return null; if (container == null || props == null || items == null) return null;
if (baseUrl == null) baseUrl = ""; if (baseUrl == null) baseUrl = "";
// Code which will be executed between intervals. var visualConsole = null;
var intervalRef = null; var asyncTaskManager = new AsyncTaskManager();
var stopInterval = function() {
if (intervalRef !== null) window.clearInterval(intervalRef);
};
var startInterval = function() {
if (updateInterval == null || updateInterval <= 0) return;
stopInterval();
intervalRef = window.setInterval(function() { function updateVisualConsole(visualConsoleId, updateInterval) {
if (updateVCRequest !== null) updateVCRequest.abort(); asyncTaskManager.add(
updateVCRequest = loadVisualConsoleData( "visual-console",
baseUrl, function(done) {
visualConsole.props.id, var abortable = loadVisualConsoleData(
function(error, data) { baseUrl,
if (error) { visualConsoleId,
console.log( function(error, data) {
"[ERROR]", if (error) {
"[VISUAL-CONSOLE-CLIENT]", console.log(
"[API]", "[ERROR]",
error.message "[VISUAL-CONSOLE-CLIENT]",
); "[API]",
return; error.message
);
done();
return;
}
// Replace Visual Console.
if (data != null && data.props != null && data.items != null) {
try {
var props =
typeof data.props === "string"
? JSON.parse(data.props)
: data.props;
var items =
typeof data.items === "string"
? JSON.parse(data.items)
: data.items;
var prevProps = visualConsole.props;
// Update the data structure.
visualConsole.props = props;
// Update the items.
visualConsole.updateElements(items);
// Emit the VC update event.
if (onUpdate) onUpdate(prevProps, visualConsole.props);
} catch (ignored) {} // eslint-disable-line no-empty
done();
}
} }
);
// Replace Visual Console. return {
if (data != null && data.props != null && data.items != null) { cancel: function() {
try { abortable.abort();
var props =
typeof data.props === "string"
? JSON.parse(data.props)
: data.props;
var items =
typeof data.items === "string"
? JSON.parse(data.items)
: data.items;
var prevProps = visualConsole.props;
// Update the data structure.
visualConsole.props = props;
// Update the items.
visualConsole.updateElements(items);
// Emit the VC update event.
if (onUpdate) onUpdate(prevProps, visualConsole.props);
} catch (ignored) {} // eslint-disable-line no-empty
} }
} };
); },
}, updateInterval); updateInterval
}; );
asyncTaskManager.init("visual-console");
}
// Initialize the Visual Console. // Initialize the Visual Console.
try { try {
@ -102,64 +107,23 @@ function createVisualConsole(
) { ) {
// Stop the current link behavior. // Stop the current link behavior.
e.nativeEvent.preventDefault(); e.nativeEvent.preventDefault();
// Fetch and update the old VC with the new. // Fetch and update the old VC with the new.
if (linkedVCRequest !== null) linkedVCRequest.abort(); updateVisualConsole(e.data.linkedLayoutId, updateInterval);
linkedVCRequest = loadVisualConsoleData(
baseUrl,
e.data.linkedLayoutId,
function(error, data) {
if (error) {
console.log(
"[ERROR]",
"[VISUAL-CONSOLE-CLIENT]",
"[API]",
error.message
);
return;
}
// Replace Visual Console.
if (data != null && data.props != null && data.items != null) {
// Cancel the old VC updates.
stopInterval();
try {
var props =
typeof data.props === "string"
? JSON.parse(data.props)
: data.props;
var items =
typeof data.items === "string"
? JSON.parse(data.items)
: data.items;
if (updateVCRequest !== null) updateVCRequest.abort();
// Save the old props.
var prevProps = visualConsole.props;
// Update the data structure.
visualConsole.props = props;
// Update the items.
visualConsole.updateElements(items);
// Emit the VC update event.
if (onUpdate) onUpdate(prevProps, visualConsole.props);
} catch (ignored) {} // eslint-disable-line no-empty
// Restart the updates.
startInterval();
}
}
);
} }
}); });
// Start an interval to update the Visual Console. // Start an interval to update the Visual Console.
startInterval(); updateVisualConsole(props.id, updateInterval);
} catch (error) { } catch (error) {
console.log("[ERROR]", "[VISUAL-CONSOLE-CLIENT]", error.message); console.log("[ERROR]", "[VISUAL-CONSOLE-CLIENT]", error.message);
} }
return visualConsole; return {
visualConsole: visualConsole,
changeUpdateInterval: function(updateInterval) {
updateVisualConsole(visualConsole.props.id, updateInterval);
}
};
} }
/** /**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,8 +7,12 @@
import "./main.css"; // CSS import. import "./main.css"; // CSS import.
import VisualConsole from "./VisualConsole"; import VisualConsole from "./VisualConsole";
import AsyncTaskManager from "./lib/AsyncTaskManager";
// Export the VisualConsole class to the global object. // Export the VisualConsole class to the global object.
// eslint-disable-next-line // eslint-disable-next-line
(window as any).VisualConsole = VisualConsole; (window as any).VisualConsole = VisualConsole;
// Export the AsyncTaskManager class to the global object.
// eslint-disable-next-line
(window as any).AsyncTaskManager = AsyncTaskManager;

View File

@ -0,0 +1,167 @@
import TypedEvent, { Disposable, Listener } from "../TypedEvent";
interface Cancellable {
cancel(): void;
}
type AsyncTaskStatus = "waiting" | "started" | "cancelled" | "finished";
type AsyncTaskInitiator = (done: () => void) => Cancellable;
/**
* Defines an async task which can be started and cancelled.
* It's possible to observe the status changes of the task.
*/
class AsyncTask {
private readonly taskInitiator: AsyncTaskInitiator;
private cancellable: Cancellable = { cancel: () => {} };
private _status: AsyncTaskStatus = "waiting";
// Event manager for status change events.
private readonly statusChangeEventManager = new TypedEvent<AsyncTaskStatus>();
// List of references to clean the event listeners.
private readonly disposables: Disposable[] = [];
public constructor(taskInitiator: AsyncTaskInitiator) {
this.taskInitiator = taskInitiator;
}
/**
* Public setter of the `status` property.
* @param status.
*/
public set status(status: AsyncTaskStatus) {
this._status = status;
this.statusChangeEventManager.emit(status);
}
/**
* Public accessor of the `status` property.
* @return status.
*/
public get status() {
return this._status;
}
/**
* Start the async task.
*/
public init(): void {
this.cancellable = this.taskInitiator(() => {
this.status = "finished";
});
this.status = "started";
}
/**
* Cancel the async task.
*/
public cancel(): void {
this.cancellable.cancel();
this.status = "cancelled";
}
/**
* Add an event handler to the status change.
* @param listener Function which is going to be executed when the status changes.
*/
public onStatusChange(listener: Listener<AsyncTaskStatus>): Disposable {
/*
* The '.on' function returns a function which will clean the event
* listener when executed. We store all the 'dispose' functions to
* call them when the item should be cleared.
*/
const disposable = this.statusChangeEventManager.on(listener);
this.disposables.push(disposable);
return disposable;
}
}
/**
* Wrap an async task into another which will execute that task indefinitely
* every time the tash finnish and the chosen period ends.
* Will last until cancellation.
*
* @param task Async task to execute.
* @param period Time in milliseconds to wait until the next async esecution.
*
* @return A new async task.
*/
function asyncPeriodic(task: AsyncTask, period: number): AsyncTask {
return new AsyncTask(() => {
let ref: number | null = null;
task.onStatusChange(status => {
if (status === "finished") {
ref = window.setTimeout(() => {
task.init();
}, period);
}
});
task.init();
return {
cancel: () => {
if (ref) clearTimeout(ref);
task.cancel();
}
};
});
}
/**
* Manages a list of async tasks.
*/
export default class AsyncTaskManager {
private tasks: { [identifier: string]: AsyncTask } = {};
/**
* Adds an async task to the manager.
*
* @param identifier Unique identifier.
* @param taskInitiator Function to initialize the async task.
* Should return a structure to cancel the task.
* @param period Optional period to repeat the task indefinitely.
*/
public add(
identifier: string,
taskInitiator: AsyncTaskInitiator,
period: number | null = null
): AsyncTask {
if (this.tasks[identifier] && this.tasks[identifier].status === "started") {
this.tasks[identifier].cancel();
}
const asyncTask =
period !== null
? asyncPeriodic(new AsyncTask(taskInitiator), period)
: new AsyncTask(taskInitiator);
this.tasks[identifier] = asyncTask;
return this.tasks[identifier];
}
/**
* Starts an async task.
*
* @param identifier Unique identifier.
*/
public init(identifier: string) {
if (this.tasks[identifier] && this.tasks[identifier].status === "waiting") {
this.tasks[identifier].init();
}
}
/**
* Cancel a running async task.
*
* @param identifier Unique identifier.
*/
public cancel(identifier: string) {
if (this.tasks[identifier] && this.tasks[identifier].status === "started") {
this.tasks[identifier].cancel();
}
}
}

View File

@ -6,7 +6,7 @@ import {
WithModuleProps, WithModuleProps,
LinkedVisualConsoleProps, LinkedVisualConsoleProps,
LinkedVisualConsolePropsStatus LinkedVisualConsolePropsStatus
} from "./types"; } from "../types";
/** /**
* Return a number or a default value from a raw value. * Return a number or a default value from a raw value.
@ -14,8 +14,7 @@ import {
* @param defaultValue Default value to use if we cannot extract a valid number. * @param defaultValue Default value to use if we cannot extract a valid number.
* @return A valid number or the default value. * @return A valid number or the default value.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any export function parseIntOr<T>(value: unknown, defaultValue: T): number | T {
export function parseIntOr<T>(value: any, defaultValue: T): number | T {
if (typeof value === "number") return value; if (typeof value === "number") return value;
if (typeof value === "string" && value.length > 0 && !isNaN(parseInt(value))) if (typeof value === "string" && value.length > 0 && !isNaN(parseInt(value)))
return parseInt(value); return parseInt(value);
@ -28,8 +27,7 @@ export function parseIntOr<T>(value: any, defaultValue: T): number | T {
* @param defaultValue Default value to use if we cannot extract a valid number. * @param defaultValue Default value to use if we cannot extract a valid number.
* @return A valid number or the default value. * @return A valid number or the default value.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any export function parseFloatOr<T>(value: unknown, defaultValue: T): number | T {
export function parseFloatOr<T>(value: any, defaultValue: T): number | T {
if (typeof value === "number") return value; if (typeof value === "number") return value;
if ( if (
typeof value === "string" && typeof value === "string" &&
@ -55,8 +53,10 @@ export function stringIsEmpty(value?: string | null): boolean {
* @param defaultValue Default value to use if we cannot extract a non empty string. * @param defaultValue Default value to use if we cannot extract a non empty string.
* @return A non empty string or the default value. * @return A non empty string or the default value.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any export function notEmptyStringOr<T>(
export function notEmptyStringOr<T>(value: any, defaultValue: T): string | T { value: unknown,
defaultValue: T
): string | T {
return typeof value === "string" && value.length > 0 ? value : defaultValue; return typeof value === "string" && value.length > 0 ? value : defaultValue;
} }
@ -65,8 +65,7 @@ export function notEmptyStringOr<T>(value: any, defaultValue: T): string | T {
* @param value Raw value from which we will try to extract the boolean. * @param value Raw value from which we will try to extract the boolean.
* @return A valid boolean value. false by default. * @return A valid boolean value. false by default.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any export function parseBoolean(value: unknown): boolean {
export function parseBoolean(value: any): boolean {
if (typeof value === "boolean") return value; if (typeof value === "boolean") return value;
else if (typeof value === "number") return value > 0; else if (typeof value === "number") return value > 0;
else if (typeof value === "string") return value === "1" || value === "true"; else if (typeof value === "string") return value === "1" || value === "true";

View File

@ -8,7 +8,7 @@ import {
humanDate, humanDate,
humanTime, humanTime,
replaceMacros replaceMacros
} from "./lib"; } from ".";
describe("function parseIntOr", () => { describe("function parseIntOr", () => {
it("should retrieve valid int or a default value", () => { it("should retrieve valid int or a default value", () => {