@invisiblecities/sidequest-cqo
Version:
Configuration-agnostic TypeScript and ESLint orchestrator with real-time watch mode, SQLite persistence, and intelligent terminal detection
296 lines • 12.5 kB
JavaScript
/**
* Polling Service for Code Quality Orchestrator
* Handles rule execution scheduling and coordination
*/
import { EventEmitter } from "node:events";
// ============================================================================
// Polling Service Implementation
// ============================================================================
// eslint-disable-next-line unicorn/prefer-event-target
export class PollingService extends EventEmitter {
storageService;
isActive = false;
isPaused = false;
pollingInterval = undefined;
activeChecks = new Map();
// Configuration
defaultFrequencyMs = 30_000; // 30 seconds
maxConcurrentChecks = 3;
// private adaptivePollingEnabled = true;
pollingIntervalMs = 5000; // Check for scheduled rules every 5 seconds
constructor(storageService) {
super();
this.storageService = storageService;
}
// ========================================================================
// Lifecycle Management
// ========================================================================
start() {
if (this.isActive) {
console.log("[PollingService] Already running");
return Promise.resolve();
}
console.log("[PollingService] Starting polling service...");
this.isActive = true;
this.isPaused = false;
// Start the main polling loop
this.pollingInterval = setInterval(async () => {
if (!this.isPaused) {
await this.executePollCycle();
}
}, this.pollingIntervalMs);
console.log("[PollingService] Polling service started");
return Promise.resolve();
}
async stop() {
if (!this.isActive) {
console.log("[PollingService] Already stopped");
return;
}
console.log("[PollingService] Stopping polling service...");
this.isActive = false;
// Clear polling interval
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = undefined;
}
// Wait for active checks to complete
if (this.activeChecks.size > 0) {
console.log(`[PollingService] Waiting for ${this.activeChecks.size} active checks to complete...`);
await Promise.allSettled(this.activeChecks.values());
this.activeChecks.clear();
}
console.log("[PollingService] Polling service stopped");
}
pause() {
if (!this.isActive) {
throw new Error("Cannot pause: polling service is not running");
}
console.log("[PollingService] Pausing polling service...");
this.isPaused = true;
return Promise.resolve();
}
resume() {
if (!this.isActive) {
throw new Error("Cannot resume: polling service is not running");
}
if (!this.isPaused) {
console.log("[PollingService] Already running (not paused)");
return Promise.resolve();
}
console.log("[PollingService] Resuming polling service...");
this.isPaused = false;
return Promise.resolve();
}
isRunning() {
return this.isActive && !this.isPaused;
}
// ========================================================================
// Rule Scheduling
// ========================================================================
async scheduleRule(rule, engine, frequencyMs) {
const frequency = frequencyMs || this.defaultFrequencyMs;
console.log(`[PollingService] Scheduling rule ${rule} (${engine}) with frequency ${frequency}ms`);
await this.storageService.upsertRuleSchedule({
rule_id: rule,
engine,
enabled: 1,
priority: 1,
check_frequency_ms: frequency,
});
}
async unscheduleRule(rule, engine) {
console.log(`[PollingService] Unscheduling rule ${rule} (${engine})`);
await this.storageService.upsertRuleSchedule({
rule_id: rule,
engine,
enabled: 0,
priority: 999,
check_frequency_ms: this.defaultFrequencyMs,
});
}
async getScheduledRules() {
return await this.storageService.getNextRulesToCheck(100); // Get all enabled rules
}
// ========================================================================
// Execution Control
// ========================================================================
async executeRule(rule, engine) {
const checkKey = `${rule}:${engine}`;
// Check if this rule is already running
if (this.activeChecks.has(checkKey)) {
console.log(`[PollingService] Rule ${rule} (${engine}) is already running`);
return await this.activeChecks.get(checkKey);
}
// Start the rule check
const checkPromise = this.performRuleCheck(rule, engine);
this.activeChecks.set(checkKey, checkPromise);
try {
const result = await checkPromise;
return result;
}
finally {
this.activeChecks.delete(checkKey);
}
}
async executeNextRules(maxConcurrent) {
const maxRules = maxConcurrent || this.maxConcurrentChecks;
const availableSlots = maxRules - this.activeChecks.size;
if (availableSlots <= 0) {
console.log("[PollingService] No available slots for rule execution");
return [];
}
// Get next rules to check
const nextRules = await this.storageService.getNextRulesToCheck(availableSlots);
if (nextRules.length === 0) {
return [];
}
console.log(`[PollingService] Executing ${nextRules.length} rules...`);
// Execute rules concurrently
const promises = nextRules.map((rule) => this.executeRule(rule.rule_id, rule.engine));
const results = await Promise.allSettled(promises);
// Extract successful results
const successfulResults = results
.filter((result) => result.status === "fulfilled")
.map((result) => result.value);
// Log any failures
results
.filter((result) => result.status === "rejected")
.forEach((result, index) => {
const rule = nextRules[index];
if (rule) {
console.error(`[PollingService] Failed to execute rule ${rule.rule_id} (${rule.engine}):`, result.reason);
this.emit("ruleFailed", rule.rule_id, rule.engine, result.reason);
}
});
return successfulResults;
}
// ========================================================================
// Configuration
// ========================================================================
setDefaultFrequency(frequencyMs) {
if (frequencyMs < 1000) {
throw new Error("Default frequency must be at least 1000ms");
}
this.defaultFrequencyMs = frequencyMs;
console.log(`[PollingService] Default frequency set to ${frequencyMs}ms`);
}
setMaxConcurrentChecks(max) {
if (max < 1) {
throw new Error("Max concurrent checks must be at least 1");
}
this.maxConcurrentChecks = max;
console.log(`[PollingService] Max concurrent checks set to ${max}`);
}
enableAdaptivePolling(enabled) {
// this.adaptivePollingEnabled = enabled;
console.log(`[PollingService] Adaptive polling ${enabled ? "enabled" : "disabled"}`);
}
// ========================================================================
// Private Implementation
// ========================================================================
async executePollCycle() {
try {
const startTime = performance.now();
// Execute next scheduled rules
const results = await this.executeNextRules();
if (results.length > 0) {
const executionTime = performance.now() - startTime;
console.log(`[PollingService] Poll cycle completed: ${results.length} rules executed in ${Math.round(executionTime)}ms`);
// Record performance metric
await this.storageService.recordPerformanceMetric("polling_cycle", executionTime, "ms", `rules: ${results.length}`);
// Emit cycle completed event
this.emit("cycleCompleted", results);
}
}
catch (error) {
console.error("[PollingService] Error in poll cycle:", error);
}
}
async performRuleCheck(rule, engine) {
const startTime = performance.now();
console.log(`[PollingService] Starting rule check: ${rule} (${engine})`);
this.emit("ruleStarted", rule, engine);
// Start the rule check in storage
const checkId = await this.storageService.startRuleCheck(rule, engine);
try {
// Simulate rule execution (replace with actual rule execution logic)
const executionResult = await this.simulateRuleExecution(rule, engine);
const executionTime = performance.now() - startTime;
// Complete the rule check
await this.storageService.completeRuleCheck(checkId, executionResult.violationsFound, Math.round(executionTime), executionResult.filesChecked, executionResult.filesWithViolations);
const result = {
rule,
engine,
checkId,
success: true,
violationCount: executionResult.violationsFound,
executionTime: Math.round(executionTime),
filesChecked: executionResult.filesChecked,
filesWithViolations: executionResult.filesWithViolations,
violations: executionResult.violations || [],
};
console.log(`[PollingService] Rule check completed: ${rule} (${engine}) - ${result.violationCount} violations in ${result.executionTime}ms`);
this.emit("ruleCompleted", result);
return result;
}
catch (error) {
const executionTime = performance.now() - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
// Mark rule check as failed
await this.storageService.failRuleCheck(checkId, errorMessage);
const result = {
rule,
engine,
checkId,
success: false,
error: errorMessage,
executionTime: Math.round(executionTime),
violationCount: 0,
violations: [],
};
console.error(`[PollingService] Rule check failed: ${rule} (${engine}) - ${errorMessage}`);
this.emit("ruleFailed", rule, engine, error);
return result;
}
}
async simulateRuleExecution(_rule, _engine) {
// Simulate execution time based on rule complexity
const baseTime = Math.random() * 200 + 50; // 50-250ms
await new Promise((resolve) => setTimeout(resolve, baseTime));
// Simulate different outcomes based on rule
const isFlaky = Math.random() < 0.1; // 10% chance of flaky behavior
const violationsFound = isFlaky ? 0 : Math.floor(Math.random() * 10);
const filesChecked = Math.floor(Math.random() * 50) + 10;
const filesWithViolations = Math.min(violationsFound, filesChecked);
return {
violationsFound,
filesChecked,
filesWithViolations,
};
}
}
// ============================================================================
// Service Factory
// ============================================================================
let pollingServiceInstance;
/**
* Get or create polling service instance
*/
export function getPollingService(storageService) {
if (!pollingServiceInstance) {
pollingServiceInstance = new PollingService(storageService);
}
return pollingServiceInstance;
}
/**
* Reset polling service instance (useful for testing)
*/
export function resetPollingService() {
if (pollingServiceInstance && // Stop the service if it's running
pollingServiceInstance.isRunning()) {
pollingServiceInstance.stop().catch(console.error);
}
pollingServiceInstance = undefined;
}
//# sourceMappingURL=polling-service.js.map