Refactor project structure and remove unused modules

- Removed performance monitoring, tools service, and polyfills modules
- Moved formatToolCall helper function to test file
- Simplified imports in index.ts by removing polyfills import
- Cleaned up unnecessary files and consolidated code structure
This commit is contained in:
jango-blockchained
2025-02-03 15:47:59 +01:00
parent 51c1594f2f
commit ea5c377fa2
8 changed files with 19 additions and 727 deletions

View File

@@ -1,5 +1,11 @@
import { jest, describe, it, expect } from '@jest/globals';
import { formatToolCall } from '../src/helpers.js';
// Helper function moved from src/helpers.ts
const formatToolCall = (obj: any, isError: boolean = false) => {
return {
content: [{ type: "text", text: JSON.stringify(obj, null, 2), isError }],
};
};
describe('helpers', () => {
describe('formatToolCall', () => {

View File

@@ -1,195 +0,0 @@
import { PerformanceMonitor, PerformanceOptimizer, Metric } from '../../src/performance/index.js';
describe('Performance Module', () => {
describe('PerformanceMonitor', () => {
let monitor: PerformanceMonitor;
beforeEach(() => {
monitor = new PerformanceMonitor({
responseTime: 500,
memoryUsage: 1024 * 1024 * 512, // 512MB
cpuUsage: 70
});
});
afterEach(() => {
monitor.stop();
});
it('should collect metrics', () => {
const metricHandler = jest.fn();
monitor.on('metric', metricHandler);
monitor.start();
// Wait for first collection
return new Promise(resolve => setTimeout(() => {
expect(metricHandler).toHaveBeenCalled();
const calls = metricHandler.mock.calls;
// Verify memory metrics
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.heapUsed'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.heapTotal'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.rss'
)).toBe(true);
// Verify CPU metrics
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'cpu.user'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'cpu.system'
)).toBe(true);
resolve(true);
}, 100));
});
it('should emit threshold exceeded events', () => {
const thresholdHandler = jest.fn();
monitor = new PerformanceMonitor({
memoryUsage: 1, // Ensure threshold is exceeded
cpuUsage: 1
});
monitor.on('threshold_exceeded', thresholdHandler);
monitor.start();
return new Promise(resolve => setTimeout(() => {
expect(thresholdHandler).toHaveBeenCalled();
resolve(true);
}, 100));
});
it('should clean old metrics', () => {
const now = Date.now();
const oldMetric: Metric = {
name: 'test',
value: 1,
timestamp: now - 25 * 60 * 60 * 1000 // 25 hours old
};
const newMetric: Metric = {
name: 'test',
value: 2,
timestamp: now - 1000 // 1 second old
};
monitor.addMetric(oldMetric);
monitor.addMetric(newMetric);
const metrics = monitor.getMetrics(now - 24 * 60 * 60 * 1000);
expect(metrics).toHaveLength(1);
expect(metrics[0]).toEqual(newMetric);
});
it('should calculate metric averages', () => {
const now = Date.now();
const metrics: Metric[] = [
{ name: 'test', value: 1, timestamp: now - 3000 },
{ name: 'test', value: 2, timestamp: now - 2000 },
{ name: 'test', value: 3, timestamp: now - 1000 }
];
metrics.forEach(metric => monitor.addMetric(metric));
const average = monitor.calculateAverage(
'test',
now - 5000,
now
);
expect(average).toBe(2);
});
});
describe('PerformanceOptimizer', () => {
it('should process batches correctly', async () => {
const items = [1, 2, 3, 4, 5];
const batchSize = 2;
const processor = jest.fn(async (batch: number[]) =>
batch.map(n => n * 2)
);
const results = await PerformanceOptimizer.processBatch(
items,
batchSize,
processor
);
expect(results).toEqual([2, 4, 6, 8, 10]);
expect(processor).toHaveBeenCalledTimes(3); // 2 + 2 + 1 items
});
it('should debounce function calls', (done) => {
const fn = jest.fn();
const debounced = PerformanceOptimizer.debounce(fn, 100);
debounced();
debounced();
debounced();
setTimeout(() => {
expect(fn).not.toHaveBeenCalled();
}, 50);
setTimeout(() => {
expect(fn).toHaveBeenCalledTimes(1);
done();
}, 150);
});
it('should throttle function calls', (done) => {
const fn = jest.fn();
const throttled = PerformanceOptimizer.throttle(fn, 100);
throttled();
throttled();
throttled();
expect(fn).toHaveBeenCalledTimes(1);
setTimeout(() => {
throttled();
expect(fn).toHaveBeenCalledTimes(2);
done();
}, 150);
});
it('should optimize memory when threshold is exceeded', async () => {
const originalGc = global.gc;
global.gc = jest.fn();
const memoryUsage = process.memoryUsage;
const mockMemoryUsage = () => ({
heapUsed: 900,
heapTotal: 1000,
rss: 2000,
external: 0,
arrayBuffers: 0
});
Object.defineProperty(process, 'memoryUsage', {
value: mockMemoryUsage,
writable: true
});
await PerformanceOptimizer.optimizeMemory();
expect(global.gc).toHaveBeenCalled();
// Cleanup
Object.defineProperty(process, 'memoryUsage', {
value: memoryUsage,
writable: true
});
if (originalGc) {
global.gc = originalGc;
} else {
delete global.gc;
}
});
});
});

View File

@@ -1,5 +0,0 @@
export const formatToolCall = (obj: any, isError: boolean = false) => {
return {
content: [{ type: "text", text: JSON.stringify(obj, null, 2), isError }],
};
}

View File

@@ -8,7 +8,6 @@
* @module index
*/
import './polyfills.js';
import express from 'express';
import { APP_CONFIG } from './config/app.config.js';
import { apiRoutes } from './routes/index.js';

View File

@@ -1,222 +0,0 @@
import { performance } from 'perf_hooks';
import { EventEmitter } from 'events';
// Performance metrics types
export interface Metric {
name: string;
value: number;
timestamp: number;
metadata?: Record<string, any>;
}
export interface PerformanceThresholds {
responseTime: number; // milliseconds
memoryUsage: number; // bytes
cpuUsage: number; // percentage
}
// Performance monitoring class
export class PerformanceMonitor extends EventEmitter {
private metrics: Metric[] = [];
private thresholds: PerformanceThresholds;
private samplingInterval: number;
private retentionPeriod: number;
private intervalId?: NodeJS.Timeout;
constructor(
thresholds: Partial<PerformanceThresholds> = {},
samplingInterval = 5000, // 5 seconds
retentionPeriod = 24 * 60 * 60 * 1000 // 24 hours
) {
super();
this.thresholds = {
responseTime: thresholds.responseTime || 1000, // 1 second
memoryUsage: thresholds.memoryUsage || 1024 * 1024 * 1024, // 1 GB
cpuUsage: thresholds.cpuUsage || 80 // 80%
};
this.samplingInterval = samplingInterval;
this.retentionPeriod = retentionPeriod;
}
// Start monitoring
public start(): void {
this.intervalId = setInterval(() => {
this.collectMetrics();
this.cleanOldMetrics();
}, this.samplingInterval);
}
// Stop monitoring
public stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
// Collect system metrics
private collectMetrics(): void {
const now = Date.now();
const memoryUsage = process.memoryUsage();
const cpuUsage = process.cpuUsage();
// Memory metrics
this.addMetric({
name: 'memory.heapUsed',
value: memoryUsage.heapUsed,
timestamp: now
});
this.addMetric({
name: 'memory.heapTotal',
value: memoryUsage.heapTotal,
timestamp: now
});
this.addMetric({
name: 'memory.rss',
value: memoryUsage.rss,
timestamp: now
});
// CPU metrics
this.addMetric({
name: 'cpu.user',
value: cpuUsage.user,
timestamp: now
});
this.addMetric({
name: 'cpu.system',
value: cpuUsage.system,
timestamp: now
});
// Check thresholds
this.checkThresholds();
}
// Add a metric
public addMetric(metric: Metric): void {
this.metrics.push(metric);
this.emit('metric', metric);
}
// Clean old metrics
private cleanOldMetrics(): void {
const cutoff = Date.now() - this.retentionPeriod;
this.metrics = this.metrics.filter(metric => metric.timestamp > cutoff);
}
// Check if metrics exceed thresholds
private checkThresholds(): void {
const memoryUsage = process.memoryUsage().heapUsed;
if (memoryUsage > this.thresholds.memoryUsage) {
this.emit('threshold_exceeded', {
type: 'memory',
value: memoryUsage,
threshold: this.thresholds.memoryUsage
});
}
const cpuUsage = process.cpuUsage();
const totalCPU = cpuUsage.user + cpuUsage.system;
const cpuPercentage = (totalCPU / (process.uptime() * 1000000)) * 100;
if (cpuPercentage > this.thresholds.cpuUsage) {
this.emit('threshold_exceeded', {
type: 'cpu',
value: cpuPercentage,
threshold: this.thresholds.cpuUsage
});
}
}
// Get metrics for a specific time range
public getMetrics(
startTime: number,
endTime: number = Date.now(),
metricName?: string
): Metric[] {
return this.metrics.filter(metric =>
metric.timestamp >= startTime &&
metric.timestamp <= endTime &&
(!metricName || metric.name === metricName)
);
}
// Calculate average for a metric
public calculateAverage(
metricName: string,
startTime: number,
endTime: number = Date.now()
): number {
const metrics = this.getMetrics(startTime, endTime, metricName);
if (metrics.length === 0) return 0;
return metrics.reduce((sum, metric) => sum + metric.value, 0) / metrics.length;
}
}
// Performance optimization utilities
export class PerformanceOptimizer {
private static readonly GC_THRESHOLD = 0.9; // 90% heap usage
// Optimize memory usage
public static async optimizeMemory(): Promise<void> {
const memoryUsage = process.memoryUsage();
const heapUsageRatio = memoryUsage.heapUsed / memoryUsage.heapTotal;
if (heapUsageRatio > this.GC_THRESHOLD) {
if (global.gc) {
global.gc();
}
}
}
// Batch processing utility
public static async processBatch<T, R>(
items: T[],
batchSize: number,
processor: (batch: T[]) => Promise<R[]>
): Promise<R[]> {
const results: R[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await processor(batch);
results.push(...batchResults);
await new Promise(resolve => setTimeout(resolve, 0)); // Yield to event loop
}
return results;
}
// Debounce utility
public static debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
// Throttle utility
public static throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle = false;
return (...args: Parameters<T>) => {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
}
// Export performance monitoring instance
export const performanceMonitor = new PerformanceMonitor();
// Start monitoring on module load
performanceMonitor.start();

View File

@@ -1,24 +0,0 @@
// Extend global Array interface to include toSorted and toReversed methods
declare global {
interface Array<T> {
toSorted(compareFn?: (a: T, b: T) => number): T[];
toReversed(): T[];
}
}
// Polyfill for toSorted method
if (typeof Array.prototype.toSorted !== 'function') {
Array.prototype.toSorted = function <T>(compareFn?: (a: T, b: T) => number): T[] {
return [...this].sort(compareFn);
};
}
// Polyfill for toReversed method
if (typeof Array.prototype.toReversed !== 'function') {
Array.prototype.toReversed = function <T>(): T[] {
return [...this].reverse();
};
}
// Export an empty object to make this a module
export { };

View File

@@ -1,100 +0,0 @@
import { z } from 'zod';
import { Tool, HassEntity } from '../interfaces/index.js';
import { get_hass } from '../hass/index.js';
import { DomainSchema } from '../schemas.js';
// Define tools array
export const tools: Tool[] = [
{
name: 'list_devices',
description: 'List all devices connected to Home Assistant',
parameters: z.object({
domain: DomainSchema.optional(),
area: z.string().optional(),
floor: z.string().optional()
}),
execute: async (params) => {
const hass = await get_hass();
const states = await hass.states.get();
// Filter by domain if specified
let filteredStates = states;
if (params.domain) {
filteredStates = states.filter((state: HassEntity) => state.entity_id.startsWith(`${params.domain}.`));
}
// Filter by area if specified
if (params.area) {
filteredStates = filteredStates.filter((state: HassEntity) =>
state.attributes.area_id === params.area ||
state.attributes.area === params.area
);
}
// Filter by floor if specified
if (params.floor) {
filteredStates = filteredStates.filter((state: HassEntity) =>
state.attributes.floor === params.floor
);
}
return {
success: true,
devices: filteredStates.map((state: HassEntity) => ({
entity_id: state.entity_id,
state: state.state,
attributes: state.attributes
}))
};
}
},
{
name: 'control',
description: 'Control a Home Assistant device',
parameters: z.object({
command: z.string(),
entity_id: z.string(),
state: z.string().optional(),
brightness: z.number().min(0).max(255).optional(),
color_temp: z.number().optional(),
rgb_color: z.tuple([z.number(), z.number(), z.number()]).optional(),
position: z.number().min(0).max(100).optional(),
tilt_position: z.number().min(0).max(100).optional(),
temperature: z.number().optional(),
target_temp_high: z.number().optional(),
target_temp_low: z.number().optional(),
hvac_mode: z.string().optional(),
fan_mode: z.string().optional(),
humidity: z.number().min(0).max(100).optional()
}),
execute: async (params) => {
const hass = await get_hass();
const domain = params.entity_id.split('.')[0];
const serviceData: Record<string, any> = {
entity_id: params.entity_id
};
// Add optional parameters if they exist
if (params.state) serviceData.state = params.state;
if (params.brightness) serviceData.brightness = params.brightness;
if (params.color_temp) serviceData.color_temp = params.color_temp;
if (params.rgb_color) serviceData.rgb_color = params.rgb_color;
if (params.position) serviceData.position = params.position;
if (params.tilt_position) serviceData.tilt_position = params.tilt_position;
if (params.temperature) serviceData.temperature = params.temperature;
if (params.target_temp_high) serviceData.target_temp_high = params.target_temp_high;
if (params.target_temp_low) serviceData.target_temp_low = params.target_temp_low;
if (params.hvac_mode) serviceData.hvac_mode = params.hvac_mode;
if (params.fan_mode) serviceData.fan_mode = params.fan_mode;
if (params.humidity) serviceData.humidity = params.humidity;
await hass.services.call(domain, params.command, serviceData);
return {
success: true,
message: `Command '${params.command}' executed on ${params.entity_id}`
};
}
}
];

View File

@@ -1,17 +1,15 @@
import { z } from 'zod';
import { get_hass } from '../hass/index.js';
import { Tool } from '../types/index.js';
import { listDevicesTool } from './list-devices.tool.js';
import { controlTool } from './control.tool.js';
import { historyTool } from './history.tool.js';
import { sceneTool } from './scene.tool.js';
import { notifyTool } from './notify.tool.js';
import { automationTool } from './automation.tool.js';
import { addonTool } from './addon.tool.js';
import { packageTool } from './package.tool.js';
import { automationConfigTool } from './automation-config.tool.js';
import { subscribeEventsTool } from './subscribe-events.tool.js';
import { getSSEStatsTool } from './sse-stats.tool.js';
import { Tool } from '../types/index';
import { listDevicesTool } from './list-devices.tool';
import { controlTool } from './control.tool';
import { historyTool } from './history.tool';
import { sceneTool } from './scene.tool';
import { notifyTool } from './notify.tool';
import { automationTool } from './automation.tool';
import { addonTool } from './addon.tool';
import { packageTool } from './package.tool';
import { automationConfigTool } from './automation-config.tool';
import { subscribeEventsTool } from './subscribe-events.tool';
import { getSSEStatsTool } from './sse-stats.tool';
// Tool category types
export enum ToolCategory {
@@ -27,16 +25,6 @@ export enum ToolPriority {
LOW = 'low'
}
interface ToolParameters {
[key: string]: any;
}
interface Tool<Params extends ToolParameters = ToolParameters> {
name: string;
description: string;
execute(params: Params): Promise<any>;
}
interface ToolMetadata {
category: ToolCategory;
platform: string;
@@ -47,161 +35,6 @@ interface ToolMetadata {
};
}
// Enhanced tool interface
export interface EnhancedTool extends Tool {
metadata: ToolMetadata;
validate?: (params: any) => Promise<boolean>;
preExecute?: (params: any) => Promise<void>;
postExecute?: (result: any) => Promise<void>;
}
// Tool registry for managing and organizing tools
export class ToolRegistry {
private tools: Map<string, EnhancedTool> = new Map();
private categories: Map<ToolCategory, Set<string>> = new Map();
private cache: Map<string, { data: any; timestamp: number }> = new Map();
constructor() {
// Initialize categories
Object.values(ToolCategory).forEach(category => {
this.categories.set(category, new Set());
});
}
// Register a new tool
public registerTool(tool: EnhancedTool): void {
this.tools.set(tool.name, tool);
this.categories.get(tool.metadata.category)?.add(tool.name);
}
// Get tool by name
public getTool(name: string): EnhancedTool | undefined {
return this.tools.get(name);
}
// Get all tools in a category
public getToolsByCategory(category: ToolCategory): EnhancedTool[] {
const toolNames = this.categories.get(category);
if (!toolNames) return [];
return Array.from(toolNames).map(name => this.tools.get(name)!);
}
// Execute a tool with validation and hooks
public async executeTool(name: string, params: any): Promise<any> {
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Tool ${name} not found`);
}
// Check cache if enabled
if (tool.metadata.caching?.enabled) {
const cacheKey = `${name}:${JSON.stringify(params)}`;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < tool.metadata.caching.ttl) {
return cached.data;
}
}
// Validate parameters
if (tool.validate) {
const isValid = await tool.validate(params);
if (!isValid) {
throw new Error('Invalid parameters');
}
}
// Pre-execution hook
if (tool.preExecute) {
await tool.preExecute(params);
}
// Execute tool
const result = await tool.execute(params);
// Post-execution hook
if (tool.postExecute) {
await tool.postExecute(result);
}
// Update cache if enabled
if (tool.metadata.caching?.enabled) {
const cacheKey = `${name}:${JSON.stringify(params)}`;
this.cache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
}
return result;
}
// Clean up expired cache entries
public cleanCache(): void {
const now = Date.now();
for (const [key, value] of this.cache.entries()) {
const tool = this.tools.get(key.split(':')[0]);
if (tool?.metadata.caching?.ttl && now - value.timestamp > tool.metadata.caching.ttl) {
this.cache.delete(key);
}
}
}
}
// Create and export the global tool registry
export const toolRegistry = new ToolRegistry();
// Tool decorator for easy registration
function registerTool(metadata: Partial<ToolMetadata>) {
return function (constructor: any) {
return constructor;
};
}
// Example usage:
@registerTool({
category: ToolCategory.DEVICE,
platform: 'hass',
version: '1.0.0',
caching: {
enabled: true,
ttl: 60000
}
})
export class LightControlTool implements EnhancedTool {
name = 'light_control';
description = 'Control light devices';
metadata: ToolMetadata = {
category: ToolCategory.DEVICE,
platform: 'hass',
version: '1.0.0',
caching: {
enabled: true,
ttl: 60000
}
};
parameters = z.object({
command: z.enum(['turn_on', 'turn_off', 'toggle']),
entity_id: z.string(),
brightness: z.number().min(0).max(255).optional(),
color_temp: z.number().optional(),
rgb_color: z.tuple([z.number(), z.number(), z.number()]).optional()
});
async validate(params: any): Promise<boolean> {
try {
this.parameters.parse(params);
return true;
} catch {
return false;
}
}
async execute(params: any): Promise<any> {
// Implementation here
return { success: true };
}
}
// Array to track all tools
const tools: Tool[] = [
listDevicesTool,