polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
421 lines • 15.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangeDetector = void 0;
const crypto_1 = require("crypto");
const events_1 = require("events");
class ChangeDetector extends events_1.EventEmitter {
constructor(config = {}) {
super();
this.dataSamples = new Map();
this.lastKnownHashes = new Map();
this.patternCache = new Map();
this.cacheExpiry = new Map();
this.config = {
sampleSize: 100,
minSampleInterval: 1000,
changeThreshold: 0.1,
deepComparison: true,
patternRecognition: true,
...config,
};
}
detectChange(dataSourceId, currentData, previousData) {
const currentHash = this.generateDataHash(currentData);
const previousHash = previousData
? this.generateDataHash(previousData)
: this.lastKnownHashes.get(dataSourceId);
this.lastKnownHashes.set(dataSourceId, currentHash);
const hasBasicChange = currentHash !== previousHash;
let result = {
hasChanged: hasBasicChange,
changeType: 'none',
confidence: 1.0,
currentHash,
previousHash: previousHash || '',
};
if (hasBasicChange && this.config.deepComparison) {
result = this.performDeepComparison(currentData, previousData, result);
}
this.recordSample(dataSourceId, {
timestamp: Date.now(),
dataHash: currentHash,
changeType: result.changeType,
confidence: result.confidence,
size: this.estimateDataSize(currentData),
});
if (result.hasChanged) {
this.emit('changeDetected', {
dataSourceId,
result,
timestamp: Date.now(),
});
}
return result;
}
analyzePattern(dataSourceId) {
const cached = this.patternCache.get(dataSourceId);
const cacheTime = this.cacheExpiry.get(dataSourceId);
if (cached && cacheTime && Date.now() - cacheTime < 60000) {
return cached;
}
const samples = this.dataSamples.get(dataSourceId) || [];
if (samples.length < 10) {
return {
patternType: 'stable',
strength: 0.1,
changeFrequency: 0,
volatility: 0,
};
}
const analysis = this.performPatternAnalysis(samples);
this.patternCache.set(dataSourceId, analysis);
this.cacheExpiry.set(dataSourceId, Date.now());
return analysis;
}
classifyActivity(dataSourceId) {
const pattern = this.analyzePattern(dataSourceId);
const samples = this.dataSamples.get(dataSourceId) || [];
let activityLevel = 'static';
let recommendedInterval = 60000;
if (pattern.volatility > 0.8) {
activityLevel = 'volatile';
recommendedInterval = 1000;
}
else if (pattern.changeFrequency > 30) {
activityLevel = 'high';
recommendedInterval = 2000;
}
else if (pattern.changeFrequency > 10) {
activityLevel = 'medium';
recommendedInterval = 5000;
}
else if (pattern.changeFrequency > 2) {
activityLevel = 'low';
recommendedInterval = 15000;
}
if (pattern.patternType === 'periodic' && pattern.nextChangeTime) {
const timeUntilNextChange = pattern.nextChangeTime - Date.now();
if (timeUntilNextChange > 0) {
recommendedInterval = Math.min(recommendedInterval, timeUntilNextChange * 0.8);
}
}
return {
dataSourceId,
activityLevel,
pattern,
recommendedInterval,
confidence: this.calculateClassificationConfidence(samples),
};
}
getSamples(dataSourceId) {
return [...(this.dataSamples.get(dataSourceId) || [])];
}
clearSamples(dataSourceId) {
this.dataSamples.delete(dataSourceId);
this.lastKnownHashes.delete(dataSourceId);
this.patternCache.delete(dataSourceId);
this.cacheExpiry.delete(dataSourceId);
}
getStatistics() {
const activityDistribution = {
static: 0,
low: 0,
medium: 0,
high: 0,
volatile: 0,
};
const patternDistribution = {
stable: 0,
periodic: 0,
volatile: 0,
trending: 0,
};
let totalSamples = 0;
for (const [dataSourceId, samples] of this.dataSamples) {
totalSamples += samples.length;
const activity = this.classifyActivity(dataSourceId);
activityDistribution[activity.activityLevel] = (activityDistribution[activity.activityLevel] || 0) + 1;
patternDistribution[activity.pattern.patternType] = (patternDistribution[activity.pattern.patternType] || 0) + 1;
}
return {
totalDataSources: this.dataSamples.size,
totalSamples,
activityDistribution,
patternDistribution,
};
}
generateDataHash(data) {
if (data === null || data === undefined) {
return 'null';
}
try {
const seen = new WeakSet();
const serialized = JSON.stringify(data, (_key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
});
return (0, crypto_1.createHash)('sha256').update(serialized).digest('hex');
}
catch (error) {
return (0, crypto_1.createHash)('sha256').update(String(data)).digest('hex');
}
}
performDeepComparison(currentData, previousData, result) {
if (!previousData) {
return {
...result,
changeType: 'major',
confidence: 0.9,
};
}
const changeDetails = {
addedFields: [],
removedFields: [],
modifiedFields: [],
sizeChange: 0,
};
const currentKeys = new Set(Object.keys(currentData || {}));
const previousKeys = new Set(Object.keys(previousData || {}));
for (const key of currentKeys) {
if (!previousKeys.has(key)) {
changeDetails.addedFields.push(key);
}
}
for (const key of previousKeys) {
if (!currentKeys.has(key)) {
changeDetails.removedFields.push(key);
}
}
for (const key of currentKeys) {
if (previousKeys.has(key)) {
const currentValue = currentData[key];
const previousValue = previousData[key];
if (JSON.stringify(currentValue) !== JSON.stringify(previousValue)) {
changeDetails.modifiedFields.push(key);
}
}
}
const currentSize = this.estimateDataSize(currentData);
const previousSize = this.estimateDataSize(previousData);
changeDetails.sizeChange = currentSize - previousSize;
let changeType = 'none';
let confidence = 1.0;
const totalChanges = changeDetails.addedFields.length +
changeDetails.removedFields.length +
changeDetails.modifiedFields.length;
if (changeDetails.addedFields.length > 0 || changeDetails.removedFields.length > 0) {
changeType = 'structural';
confidence = 0.95;
}
else if (totalChanges > currentKeys.size * 0.5) {
changeType = 'major';
confidence = 0.9;
}
else if (totalChanges > 0) {
changeType = 'minor';
confidence = 0.8;
}
return {
...result,
changeType,
confidence,
changeDetails,
};
}
performPatternAnalysis(samples) {
const recentSamples = samples.slice(-this.config.sampleSize);
const timespan = recentSamples.length > 1
? (recentSamples[recentSamples.length - 1]?.timestamp || 0) - (recentSamples[0]?.timestamp || 0)
: 3600000;
const changedSamples = recentSamples.filter(s => s.changeType !== 'none');
const changeFrequency = (changedSamples.length / (timespan / 3600000));
const volatility = this.calculateVolatility(recentSamples);
const patternType = this.detectPatternType(recentSamples);
const strength = this.calculatePatternStrength(recentSamples, patternType);
let nextChangeTime;
if (patternType === 'periodic') {
nextChangeTime = this.predictNextChangeTime(recentSamples);
}
return {
patternType,
strength,
changeFrequency,
volatility,
...(nextChangeTime && { nextChangeTime }),
};
}
calculateVolatility(samples) {
if (samples.length < 3) {
return 0;
}
const intervals = [];
for (let i = 1; i < samples.length; i++) {
const currentSample = samples[i];
const previousSample = samples[i - 1];
if (currentSample && previousSample && currentSample.changeType !== 'none') {
intervals.push(currentSample.timestamp - previousSample.timestamp);
}
}
if (intervals.length < 2) {
return 0;
}
const mean = intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length;
const variance = intervals.reduce((sum, interval) => sum + Math.pow(interval - mean, 2), 0) / intervals.length;
const stdDev = Math.sqrt(variance);
return Math.min(stdDev / mean, 1);
}
detectPatternType(samples) {
if (samples.length < 10) {
return 'stable';
}
const changedSamples = samples.filter(s => s.changeType !== 'none');
if (changedSamples.length === 0) {
return 'stable';
}
if (this.isPeriodicPattern(changedSamples)) {
return 'periodic';
}
if (this.isTrendingPattern(samples)) {
return 'trending';
}
const volatility = this.calculateVolatility(samples);
if (volatility > 0.7) {
return 'volatile';
}
return 'stable';
}
isPeriodicPattern(changedSamples) {
if (changedSamples.length < 4) {
return false;
}
const intervals = [];
for (let i = 1; i < changedSamples.length; i++) {
const currentSample = changedSamples[i];
const previousSample = changedSamples[i - 1];
if (currentSample && previousSample) {
intervals.push(currentSample.timestamp - previousSample.timestamp);
}
}
const mean = intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length;
const consistentIntervals = intervals.filter(interval => Math.abs(interval - mean) < mean * 0.3);
return consistentIntervals.length / intervals.length > 0.7;
}
isTrendingPattern(samples) {
if (samples.length < 10) {
return false;
}
const windowSize = 5;
const windows = [];
for (let i = 0; i <= samples.length - windowSize; i++) {
const window = samples.slice(i, i + windowSize);
const changeCount = window.filter(s => s.changeType !== 'none').length;
windows.push(changeCount);
}
let increasing = 0;
let decreasing = 0;
for (let i = 1; i < windows.length; i++) {
const currentWindow = windows[i];
const previousWindow = windows[i - 1];
if (currentWindow !== undefined && previousWindow !== undefined) {
if (currentWindow > previousWindow) {
increasing++;
}
else if (currentWindow < previousWindow) {
decreasing++;
}
}
}
const totalComparisons = windows.length - 1;
return (increasing / totalComparisons > 0.7) || (decreasing / totalComparisons > 0.7);
}
calculatePatternStrength(samples, patternType) {
switch (patternType) {
case 'stable':
return 1.0 - (samples.filter(s => s.changeType !== 'none').length / samples.length);
case 'periodic':
return this.isPeriodicPattern(samples.filter(s => s.changeType !== 'none')) ? 0.8 : 0.3;
case 'volatile':
return this.calculateVolatility(samples);
case 'trending':
return this.isTrendingPattern(samples) ? 0.7 : 0.3;
default:
return 0.5;
}
}
predictNextChangeTime(samples) {
const changedSamples = samples.filter(s => s.changeType !== 'none');
if (changedSamples.length < 3) {
return undefined;
}
const intervals = [];
for (let i = 1; i < changedSamples.length; i++) {
const currentSample = changedSamples[i];
const previousSample = changedSamples[i - 1];
if (currentSample && previousSample) {
intervals.push(currentSample.timestamp - previousSample.timestamp);
}
}
const averageInterval = intervals.reduce((sum, interval) => sum + interval, 0) / intervals.length;
const lastSample = changedSamples[changedSamples.length - 1];
if (!lastSample) {
return undefined;
}
const lastChangeTime = lastSample.timestamp;
return lastChangeTime + averageInterval;
}
calculateClassificationConfidence(samples) {
if (samples.length < 10) {
return 0.3;
}
if (samples.length < 50) {
return 0.6;
}
return 0.9;
}
estimateDataSize(data) {
if (data === null || data === undefined) {
return 0;
}
try {
const seen = new WeakSet();
const serialized = JSON.stringify(data, (_key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
});
return serialized.length;
}
catch (error) {
return String(data).length;
}
}
recordSample(dataSourceId, sample) {
if (!this.dataSamples.has(dataSourceId)) {
this.dataSamples.set(dataSourceId, []);
}
const samples = this.dataSamples.get(dataSourceId);
if (samples.length > 0) {
const lastSample = samples[samples.length - 1];
if (lastSample && sample.timestamp - lastSample.timestamp < this.config.minSampleInterval) {
return;
}
}
samples.push(sample);
if (samples.length > this.config.sampleSize) {
samples.shift();
}
this.patternCache.delete(dataSourceId);
this.cacheExpiry.delete(dataSourceId);
}
}
exports.ChangeDetector = ChangeDetector;
//# sourceMappingURL=change-detector.js.map