@signumjs/monitor
Version:
Monitor transactions on Signum Network blockchain
288 lines (256 loc) • 8.27 kB
text/typescript
/**
* Copyright (c) 2020 Burst Apps Team
*/
import {voidLogger} from './typings/voidLogger';
import {Logger} from './typings/logger';
import {EventEmitter} from './internal/eventEmitter';
import {
MonitorFetchFunction,
MonitorArgs,
MonitorPredicateFunction,
} from './typings/args/monitorArgs';
interface KeyArgs {
key: string;
}
interface FulfilledArgs<T> extends KeyArgs {
data: T;
}
type TimeoutFunction = (args: KeyArgs) => void;
type FulfilledFunction<T = any> = (args: FulfilledArgs<T>) => void;
const MonitorEvents = {
Fulfilled: '@signumjs/monitor/fulfilled',
Timeout: '@signumjs/monitor/timeout',
};
/**
* The generic monitor class.
*
* A monitor can be used to check periodically for a certain situation, e.g. confirmation of a transaction,
* activation on an account, or even something completely different.
*
* Example: (checking for the existence of an account aka account activation)
* ```ts
* // A method that checks if an account exists
* // > IMPORTANT: Do not use closures, when you need to serialize the monitor
* async function tryFetchAccount() {
* const Api = composeApi({ nodeHost: 'https://testnet.signum.network:6876/'})
* try{
* const {account} = await Api.account.getAccount('1234')
* return account;
* }catch (e){
* // ignore error
* return null;
* }
*}
*
* // A comparing function to check if a certain condition for the returned data from fetch function
* // is true. If it's true the monitor stops
* function checkIfAccountExists(account) {
* return account !== null;
*}
*
* // Create your monitor
* const monitor = new Monitor<Account>({
* asyncFetcherFn: tryFetchAccount,
* compareFn: checkIfAccountExists,
* intervalSecs: 10, // polling interval in seconds
* key: 'monitor-account',
* timeoutSecs: 2 * 240 // when reached timeout the monitor stops
*})
* .onFulfilled(() => {
* // called when `checkIfAccountExists` returns true
* console.log('Yay, account active');
*})
* .onTimeout(() => {
* // called when `timeoutSecs` is reached
* console.log('Hmm, something went wrong');
*}).start();
*
*```
* @module monitor
*/
export class Monitor<T> {
private readonly _timeoutSecs: number = -1;
private readonly _asyncFetcher: MonitorFetchFunction<T>;
private readonly _compareFn: MonitorPredicateFunction<T>;
private readonly _emitter = new EventEmitter();
private readonly _intervalSecs: number = -1;
private readonly _key: string;
private _startTime = -1;
private _logger: Logger;
private _handle: any = undefined;
/**
* The monitors constructor
* @param args The arguments
*/
constructor(args: MonitorArgs<T>) {
const {
asyncFetcherFn,
compareFn,
intervalSecs,
key,
logger,
timeoutSecs,
} = args;
if (intervalSecs < 1) {
throw new Error('interval must be greater than zero');
}
this._key = key;
this._intervalSecs = intervalSecs;
this._timeoutSecs = timeoutSecs;
this._asyncFetcher = asyncFetcherFn;
this._compareFn = compareFn;
this._logger = logger || voidLogger;
}
/**
* The start timestamp if started, or -1
*/
public get startTime(): number {
return this._startTime;
}
/**
* The interval
*/
public get intervalSecs(): number {
return this._intervalSecs;
}
/**
* The key aka identifier
*/
public get key(): string {
return this._key;
}
/**
* The timeout
*/
public get timeoutSecs(): number {
return this._timeoutSecs;
}
/**
* Deserializes a serialized monitor
* @see [[Monitor.serialize]]
* @param serializedMonitor The serialized monitor
* @param autoStart If monitor was started on serialization the monitor starts automatically, if set true (default)
* @return The monitor instance
*/
public static deserialize<T>(serializedMonitor: string, autoStart = true): Monitor<T> {
const args = JSON.parse(serializedMonitor);
const monitor = new Monitor<T>({
...args,
asyncFetcherFn: Monitor._deserializeFunction<MonitorFetchFunction>(args.asyncFetcherFn),
compareFn: Monitor._deserializeFunction<MonitorPredicateFunction>(args.compareFn),
});
monitor._startTime = args.startTime;
if (autoStart && args.startTime > -1) {
monitor.start();
}
return monitor;
}
private static _serializeFunction(fn): string {
return fn.toString().replace(/\s+/g, ' ');
}
private static _deserializeFunction<T>(serialized: string): T {
// tslint:disable-next-line:no-eval
return eval(serialized) as T;
}
/**
* Serializes the monitor, such it can be stored.
* This serializes also the `asyncFetcher` and `compareFn`
* It is important that these functions are not closures, i.e. the must not reference
* outer data/variables, otherwise the behavior on deserialization is not deterministic
*/
public serialize(): string {
return JSON.stringify({
intervalSecs: this._intervalSecs,
timeoutSecs: this._timeoutSecs,
key: this._key,
startTime: this._startTime,
asyncFetcherFn: Monitor._serializeFunction(this._asyncFetcher),
compareFn: Monitor._serializeFunction(this._compareFn),
});
}
_debug(msg) {
this._logger.debug(`[${this._key}] - ${msg}`);
}
_resetStartTime() {
this._startTime = -1;
}
/**
* @return true, if monitor was started and is running
*/
hasStarted(): boolean {
return this.startTime !== -1;
}
/**
* @returns true, if a started monitor timed out.
*/
isExpired(): boolean {
return this.hasStarted()
? (Date.now() - this._startTime) / 1000 >= this._timeoutSecs
: false;
}
/**
* Starts the monitor
*/
start(): Monitor<T> {
this._debug('Monitoring...');
if (this.isExpired()) {
this._debug('Monitor expired');
this.stop();
return this;
}
// @ts-ignore
this._handle = setTimeout(async () => {
try {
const data = await this._asyncFetcher();
this._debug(`Fetched: ${JSON.stringify(data, null, '\t')}`);
const predicateFulfilled = this._compareFn(data);
if (predicateFulfilled) {
this._debug('Monitor predicate fulfilled');
this._emitter.emit(MonitorEvents.Fulfilled, {
key: this.key,
data
});
this._resetStartTime();
} else if (!this.isExpired()) {
this.start();
} else {
this._debug('Monitor timed out');
this._emitter.emit(MonitorEvents.Timeout, {
key: this.key,
});
this._resetStartTime();
}
} catch (e) {
this._debug(`Monitor failed: ${e}`);
}
}, this._intervalSecs * 1000);
if (!this.hasStarted()) {
this._startTime = Date.now();
}
return this;
}
/**
* Stops the monitor
*/
stop(): void {
// @ts-ignore
clearTimeout(this._handle);
this._startTime = -1;
}
/**
* Callback function for timeout event. You can add multiple event listener if you want
* @param fn The callback
*/
onTimeout(fn: TimeoutFunction): Monitor<T> {
this._emitter.on(MonitorEvents.Timeout, fn);
return this;
}
/**
* Callback function for fulfilled event. You can add multiple event listener if you want
* @param fn The callback
*/
onFulfilled(fn: FulfilledFunction<T>): Monitor<T> {
this._emitter.on(MonitorEvents.Fulfilled, fn);
return this;
}
}