@openhps/core
Version:
Open Hybrid Positioning System - Core component
122 lines • 4.11 kB
JavaScript
import { ProcessingNode } from '../ProcessingNode';
import { TimeUnit } from '../../utils';
import { TimeService } from '../../service';
import { PushError } from '../../graph/events';
/**
* Merge data frames from two or more sources
* using a certain merge key (e.g. source uid, parent uid, node uid).
* @type {@link http://purl.org/poso/HighLevelFusion}
* @category Flow shape
*/
export class MergeShape extends ProcessingNode {
constructor(mergeFn, groupFn, options) {
super(options);
this._queue = new Map();
this._mergeKeyFn = mergeFn;
this._groupFn = groupFn;
// Merge timeout
this.options.timeout = this.options.timeout || 100;
this.options.timeoutUnit = this.options.timeoutUnit || TimeUnit.MILLISECOND;
this._timeout = this.options.timeoutUnit.convert(this.options.timeout, TimeService.getUnit());
this.once('build', this._start.bind(this));
this.once('destroy', this._stop.bind(this));
}
/**
* Start the timeout timer
* @returns {Promise<void>} Timer promise
*/
_start() {
return new Promise(resolve => {
this.options.minCount = this.options.minCount || this.inlets.length;
this.options.maxCount = this.options.maxCount || this.inlets.length;
const interval = this.options.checkInterval || TimeService.getUnit().convert(this._timeout, TimeUnit.MILLISECOND);
if (this._timeout > 0) {
this._timer = setInterval(this._timerTick.bind(this), interval);
}
resolve();
});
}
_timerTick() {
this._queue.forEach(queue => {
this._purgeQueue(queue);
});
}
_purgeQueue(queue) {
const currentTime = TimeService.now();
if (queue !== undefined && this._queue.has(queue.key) && currentTime - queue.timestamp >= this._timeout && queue.frames.size >= this.options.minCount) {
const frames = Array.from(queue.frames.values());
try {
// Merge node
this.outlets.forEach(outlet => outlet.push(this.merge(frames, queue.key)));
this._queue.delete(queue.key);
// Resolve pending promises
queue.promises.forEach(fn => {
fn(undefined);
});
} catch (ex) {
this.emit('error', new PushError(frames[0].uid, this.uid, ex));
}
return undefined;
} else {
return queue;
}
}
_stop() {
if (this._timer !== undefined) {
clearInterval(this._timer);
}
}
process(frame, options) {
return new Promise(resolve => {
if (this.options.maxCount === 1) {
return resolve(frame);
}
// Merge key(s)
const merge = this._mergeKeyFn(frame, options);
if (merge === undefined) {
return resolve(undefined);
}
(Array.isArray(merge) ? merge : [merge]).forEach(key => {
let queue = this._purgeQueue(this._queue.get(key));
if (queue === undefined) {
// Create a new queued data frame based on the key
queue = new QueuedMerge(key);
queue.promises.push(resolve);
// Group the frames by the grouping function
queue.frames.set(this._groupFn(frame, options), frame);
this._queue.set(key, queue);
} else {
const groupKey = this._groupFn(frame, options);
if (queue.frames.has(groupKey)) {
// Merge frames
queue.frames.set(groupKey, this.merge([queue.frames.get(groupKey), frame]));
} else {
queue.frames.set(groupKey, frame);
}
// Check if there are enough frames
if (queue.frames.size >= this.options.maxCount) {
this._queue.delete(key);
const mergedFrame = this.merge(Array.from(queue.frames.values()), key);
resolve(mergedFrame);
queue.promises.forEach(fn => {
fn(undefined);
});
} else {
queue.promises.push(resolve);
}
}
});
});
}
}
/**
* Queued merge
*/
class QueuedMerge {
constructor(key) {
this.frames = new Map();
this.promises = [];
this.key = key;
this.timestamp = TimeService.now();
}
}