UNPKG

@sailboat-computer/data-storage

Version:

Shared data storage library for sailboat computer v3

311 lines 11.1 kB
"use strict"; /** * Time-based downsampling strategy implementation */ Object.defineProperty(exports, "__esModule", { value: true }); exports.createTimeBasedDownsampler = exports.TimeBasedDownsampler = void 0; /** * Time-based downsampler implementation */ class TimeBasedDownsampler { constructor() { this.type = 'time-based'; } /** * Downsample data using time-based strategy * * @param data - Data to downsample * @param strategy - Time-based strategy * @returns Downsampled data */ async downsample(data, strategy) { if (data.length === 0) { return []; } try { // Sort data by timestamp const sortedData = [...data].sort((a, b) => { const aTime = new Date(a.metadata.timestamp).getTime(); const bTime = new Date(b.metadata.timestamp).getTime(); return aTime - bTime; }); // Group data by age threshold const now = new Date(); const dataByAgeThreshold = {}; // Initialize age threshold groups for (const level of strategy.levels) { dataByAgeThreshold[level.ageThreshold] = []; } // Assign data to age threshold groups for (const item of sortedData) { const itemTime = new Date(item.metadata.timestamp).getTime(); const ageInDays = (now.getTime() - itemTime) / (24 * 60 * 60 * 1000); // Find the appropriate age threshold let assignedThreshold = -1; for (const level of strategy.levels) { if (ageInDays >= level.ageThreshold && level.ageThreshold > assignedThreshold) { assignedThreshold = level.ageThreshold; } } if (assignedThreshold >= 0) { dataByAgeThreshold[assignedThreshold].push(item); } else { // Data is newer than any threshold, keep as is dataByAgeThreshold[-1] = dataByAgeThreshold[-1] || []; dataByAgeThreshold[-1].push(item); } } // Process each age threshold group const results = []; // Add data that doesn't match any threshold (newer than all thresholds) if (dataByAgeThreshold[-1]) { results.push(...dataByAgeThreshold[-1]); } // Process each threshold group for (const level of strategy.levels) { const thresholdData = dataByAgeThreshold[level.ageThreshold]; if (!thresholdData || thresholdData.length === 0) { continue; } // Apply downsampling based on interval const downsampledData = this.downsampleByInterval(thresholdData, level.interval, level.aggregations); results.push(...downsampledData); } return results; } catch (error) { console.error('Failed to apply time-based downsampling:', error); // Return original data on error return data; } } /** * Downsample data by interval * * @param data - Data to downsample * @param interval - Interval string (e.g., '1m', '5m', '1h') * @param aggregations - Aggregation configurations * @returns Downsampled data */ downsampleByInterval(data, interval, aggregations) { // Parse interval const intervalMs = this.parseInterval(interval); if (intervalMs <= 0) { return data; } // Group data by interval const dataByInterval = {}; for (const item of data) { const timestamp = new Date(item.metadata.timestamp).getTime(); const intervalKey = Math.floor(timestamp / intervalMs) * intervalMs; if (!dataByInterval[intervalKey]) { dataByInterval[intervalKey] = []; } dataByInterval[intervalKey].push(item); } // Apply aggregations to each interval const results = []; for (const [intervalKey, intervalData] of Object.entries(dataByInterval)) { // Group by specified fields if any const groupByFields = aggregations[0]?.groupBy || []; if (groupByFields.length > 0) { const groupedData = this.groupByFields(intervalData, groupByFields); for (const groupData of Object.values(groupedData)) { const aggregatedItem = this.applyAggregations(groupData, aggregations); if (aggregatedItem) { results.push(aggregatedItem); } } } else { // No grouping, apply aggregations to all interval data const aggregatedItem = this.applyAggregations(intervalData, aggregations); if (aggregatedItem) { results.push(aggregatedItem); } } } return results; } /** * Parse interval string to milliseconds * * @param interval - Interval string (e.g., '1m', '5m', '1h') * @returns Interval in milliseconds */ parseInterval(interval) { const match = interval.match(/^(\d+)([smhd])$/); if (!match) { console.warn(`Invalid interval format: ${interval}`); return 0; } const value = parseInt(match[1]); const unit = match[2]; switch (unit) { case 's': return value * 1000; case 'm': return value * 60 * 1000; case 'h': return value * 60 * 60 * 1000; case 'd': return value * 24 * 60 * 60 * 1000; default: return 0; } } /** * Group data by specified fields * * @param data - Data to group * @param fields - Fields to group by * @returns Grouped data */ groupByFields(data, fields) { const result = {}; for (const item of data) { const groupKey = fields.map(field => { const value = item.data[field]; return `${field}:${value}`; }).join('|'); if (!result[groupKey]) { result[groupKey] = []; } result[groupKey].push(item); } return result; } /** * Apply aggregations to data * * @param data - Data to aggregate * @param aggregations - Aggregation configurations * @returns Aggregated data */ applyAggregations(data, aggregations) { if (data.length === 0) { return null; } // Use the first item as a template const template = data[0]; const result = { data: { ...template.data }, metadata: { ...template.metadata } }; // Apply each aggregation for (const agg of aggregations) { const field = agg.field; const outputField = agg.outputField || field; switch (agg.function) { case 'avg': result.data[outputField] = this.calculateAverage(data, field); break; case 'sum': result.data[outputField] = this.calculateSum(data, field); break; case 'min': result.data[outputField] = this.calculateMin(data, field); break; case 'max': result.data[outputField] = this.calculateMax(data, field); break; case 'count': result.data[outputField] = data.length; break; case 'first': result.data[outputField] = data[0].data[field]; break; case 'last': result.data[outputField] = data[data.length - 1].data[field]; break; default: console.warn(`Unknown aggregation function: ${agg.function}`); } } // Update metadata result.metadata.timestamp = this.calculateAverageTimestamp(data); // Add downsampling metadata result.metadata.tags = { ...result.metadata.tags, downsampled: 'true', downsampledFrom: data.length.toString(), downsamplingStrategy: 'time-based' }; return result; } /** * Calculate average of field values * * @param data - Data to calculate average from * @param field - Field to calculate average of * @returns Average value */ calculateAverage(data, field) { const sum = this.calculateSum(data, field); return sum / data.length; } /** * Calculate sum of field values * * @param data - Data to calculate sum from * @param field - Field to calculate sum of * @returns Sum value */ calculateSum(data, field) { return data.reduce((sum, item) => { const value = item.data[field]; return sum + (typeof value === 'number' ? value : 0); }, 0); } /** * Calculate minimum of field values * * @param data - Data to calculate minimum from * @param field - Field to calculate minimum of * @returns Minimum value */ calculateMin(data, field) { return data.reduce((min, item) => { const value = item.data[field]; return typeof value === 'number' ? Math.min(min, value) : min; }, Infinity); } /** * Calculate maximum of field values * * @param data - Data to calculate maximum from * @param field - Field to calculate maximum of * @returns Maximum value */ calculateMax(data, field) { return data.reduce((max, item) => { const value = item.data[field]; return typeof value === 'number' ? Math.max(max, value) : max; }, -Infinity); } /** * Calculate average timestamp * * @param data - Data to calculate average timestamp from * @returns Average timestamp as ISO string */ calculateAverageTimestamp(data) { const sum = data.reduce((sum, item) => { return sum + new Date(item.metadata.timestamp).getTime(); }, 0); const avg = sum / data.length; return new Date(avg).toISOString(); } } exports.TimeBasedDownsampler = TimeBasedDownsampler; /** * Create a new time-based downsampler * * @returns Time-based downsampler */ function createTimeBasedDownsampler() { return new TimeBasedDownsampler(); } exports.createTimeBasedDownsampler = createTimeBasedDownsampler; //# sourceMappingURL=time-based.js.map