Skip to main content
Glama
tide-service.js14.9 kB
/** * Tide Service Module * Port of DataFlood C# TideService.cs * Manages time-based document generation sequences */ import { DocumentGenerator } from '../generator/document-generator.js'; import { DataFloodModel } from '../models/DataFloodModel.js'; import { readFileSync, existsSync } from 'fs'; import path from 'path'; import logger from '../../utils/logger.js'; const log = logger.child('TideService'); /** * TideService manages time-based document generation */ export class TideService { constructor() { this.schemaCache = new Map(); this.random = Math.random; } /** * Execute a tide configuration * @param {Object} request - TideExecutionRequest * @returns {Promise<Object>} TideResponse */ async executeTide(request) { const executionStart = new Date(); try { // Load or validate configuration const config = await this.loadConfiguration(request); if (!config) { return { success: false, message: 'Failed to load configuration' }; } // Validate configuration const validation = await this.validateConfiguration(config); if (!validation.isValid) { return { success: false, message: `Configuration validation failed: ${validation.errors.join(', ')}` }; } if (request.validateOnly) { return { success: true, message: 'Configuration is valid', metadata: this.createMetadata(config, executionStart, new Date(), []) }; } // Apply overrides this.applyRequestOverrides(config, request); // Initialize random seed if provided if (config.seed !== null && config.seed !== undefined) { this.random = this.seededRandom(config.seed); } // Execute the sequence const documents = await this.generateSequenceDocuments(config, request.maxDocuments); const executionEnd = new Date(); return { success: true, message: `Generated ${documents.length} documents`, totalDocuments: documents.length, generationTime: executionEnd - executionStart, documents: request.returnDocuments ? documents : [], metadata: this.createMetadata(config, executionStart, executionEnd, documents) }; } catch (error) { log.error('Tide execution failed', error); return { success: false, message: `Execution failed: ${error.message}`, generationTime: new Date() - executionStart }; } } /** * Validate a tide configuration * @param {Object} configuration - TideConfig * @returns {Promise<Object>} Validation result */ async validateConfiguration(configuration) { const result = { isValid: true, errors: [] }; try { // Basic validation if (!configuration.startTime || !configuration.endTime) { result.errors.push('Start time and end time are required'); result.isValid = false; } else { const startTime = new Date(configuration.startTime); const endTime = new Date(configuration.endTime); if (endTime <= startTime) { result.errors.push('End time must be after start time'); result.isValid = false; } } if (!configuration.intervalMs || configuration.intervalMs <= 0) { result.errors.push('Interval must be positive'); result.isValid = false; } if (!configuration.steps || configuration.steps.length === 0) { result.errors.push('At least one sequence step is required'); result.isValid = false; } // Validate steps const stepIds = new Set(); for (const step of configuration.steps || []) { if (stepIds.has(step.stepId)) { result.errors.push(`Duplicate step ID: ${step.stepId}`); result.isValid = false; } stepIds.add(step.stepId); // Check if model file exists (if path is provided) if (step.modelPath) { const modelPath = this.resolveModelPath(step.modelPath); if (!existsSync(modelPath)) { result.errors.push(`Model file not found: ${step.modelPath}`); result.isValid = false; } else { // Try to load and validate the schema try { await this.loadSchema(modelPath); } catch (error) { result.errors.push(`Invalid schema file ${step.modelPath}: ${error.message}`); result.isValid = false; } } } // Validate probability if (step.generationProbability !== undefined && (step.generationProbability < 0 || step.generationProbability > 1)) { result.errors.push(`Step ${step.stepId}: generationProbability must be between 0 and 1`); result.isValid = false; } // Validate documentsPerInterval if (step.documentsPerInterval !== undefined && step.documentsPerInterval <= 0) { result.errors.push(`Step ${step.stepId}: documentsPerInterval must be positive`); result.isValid = false; } } } catch (error) { result.errors.push(`Validation error: ${error.message}`); result.isValid = false; } return result; } /** * Get statistics for a tide configuration * @param {Object} configuration - TideConfig * @returns {Promise<Object>} Statistics */ async getTideStatistics(configuration) { const startTime = new Date(configuration.startTime); const endTime = new Date(configuration.endTime); const totalDuration = endTime - startTime; const stats = { totalDuration: totalDuration, totalSteps: configuration.steps.length, totalTransactions: (configuration.transactions || []).length, modelsUsed: [...new Set(configuration.steps.map(s => s.modelPath))], estimatedDocuments: 0, stepDurations: {} }; // Calculate estimated documents for absolute time tide const totalIntervals = Math.floor(totalDuration / configuration.intervalMs); for (const step of configuration.steps) { const stepStart = new Date(step.startTime); const stepEnd = step.endTime ? new Date(step.endTime) : endTime; // Calculate step duration const stepDuration = Math.min(stepEnd, endTime) - Math.max(stepStart, startTime); if (stepDuration > 0) { const stepIntervals = Math.floor(stepDuration / configuration.intervalMs); const docsPerInterval = step.documentsPerInterval || 1; const probability = step.generationProbability || 1; stats.estimatedDocuments += Math.floor(stepIntervals * docsPerInterval * probability); stats.stepDurations[step.stepId] = stepDuration; } } return stats; } /** * Load configuration from request */ async loadConfiguration(request) { if (request.configuration) { return request.configuration; } if (request.configurationFile) { return await this.loadConfigurationFromFile(request.configurationFile); } return null; } /** * Load configuration from file */ async loadConfigurationFromFile(configurationFile) { if (!existsSync(configurationFile)) { throw new Error(`Configuration file not found: ${configurationFile}`); } const jsonContent = readFileSync(configurationFile, 'utf8'); return JSON.parse(jsonContent); } /** * Apply request overrides to configuration */ applyRequestOverrides(config, request) { if (request.outputFormatOverride) { config.outputFormat = request.outputFormatOverride; } if (request.seedOverride !== null && request.seedOverride !== undefined) { config.seed = request.seedOverride; } } /** * Generate documents according to sequence configuration */ async generateSequenceDocuments(config, maxDocuments) { const documents = []; const startTime = new Date(config.startTime); const endTime = new Date(config.endTime); let currentTime = new Date(startTime); // Initialize document generators for each step const generators = new Map(); for (const step of config.steps) { const modelPath = this.resolveModelPath(step.modelPath); const schema = await this.loadSchema(modelPath); // Create generator with seed and entropy override let seed = config.seed; if (seed !== null && seed !== undefined) { // Vary seed per step to avoid identical documents seed = seed + step.stepId.charCodeAt(0); } const entropyOverride = step.entropyOverride || config.globalEntropyOverride || null; const generator = new DocumentGenerator(seed, entropyOverride); generators.set(step.stepId, { generator, schema, step }); } // Generate documents at each interval while (currentTime < endTime) { if (maxDocuments && documents.length >= maxDocuments) { break; } // Apply jitter if configured let intervalMs = config.intervalMs; if (config.addJitter) { // Add ±10% random variation const jitter = (this.random() - 0.5) * 0.2 * intervalMs; intervalMs = Math.round(intervalMs + jitter); } // Find active steps for current time const activeSteps = this.getActiveSteps(config, currentTime); // Generate documents for active steps for (const step of activeSteps) { // Check generation probability if (this.random() > (step.generationProbability || 1)) { continue; } const { generator, schema } = generators.get(step.stepId); const docsPerInterval = step.documentsPerInterval || 1; for (let i = 0; i < docsPerInterval; i++) { if (maxDocuments && documents.length >= maxDocuments) { break; } const doc = generator.generateDocument(schema); // Add metadata if requested const tideDocument = { timestamp: new Date(currentTime).toISOString(), stepId: step.stepId, document: doc }; if (config.includeMetadata) { tideDocument.metadata = { modelPath: step.modelPath, tags: step.tags || [], customProperties: step.customProperties || {} }; } documents.push(tideDocument); } } // Advance time currentTime = new Date(currentTime.getTime() + intervalMs); } log.info(`Generated ${documents.length} documents from ${config.steps.length} steps`); return documents; } /** * Get active steps for a given time */ getActiveSteps(config, currentTime) { const activeSteps = []; for (const step of config.steps) { const stepStart = new Date(step.startTime); const stepEnd = step.endTime ? new Date(step.endTime) : new Date(config.endTime); if (currentTime >= stepStart && currentTime < stepEnd) { activeSteps.push(step); } } // If multiple steps are active, use weights to select if (activeSteps.length > 1) { return [this.selectByWeight(activeSteps)]; } return activeSteps; } /** * Select a step based on weights */ selectByWeight(steps) { const totalWeight = steps.reduce((sum, step) => sum + (step.weight || 1), 0); const randomValue = this.random() * totalWeight; let currentWeight = 0; for (const step of steps) { currentWeight += (step.weight || 1); if (randomValue < currentWeight) { return step; } } return steps[0]; } /** * Load schema from file with caching */ async loadSchema(modelPath) { if (this.schemaCache.has(modelPath)) { return this.schemaCache.get(modelPath); } const modelData = JSON.parse(readFileSync(modelPath, 'utf8')); const schema = new DataFloodModel(modelData); this.schemaCache.set(modelPath, schema); return schema; } /** * Resolve model path relative to Test_Data */ resolveModelPath(modelPath) { // If absolute path, use as-is if (path.isAbsolute(modelPath)) { return modelPath; } // Try relative to current directory if (existsSync(modelPath)) { return modelPath; } // Try relative to Test_Data const testDataPath = path.join(process.cwd(), '..', 'Test_Data', modelPath); if (existsSync(testDataPath)) { return testDataPath; } // Return original path (will fail validation if not found) return modelPath; } /** * Create metadata for response */ createMetadata(config, startTime, endTime, documents) { return { configName: config.name, startTime: startTime.toISOString(), endTime: endTime.toISOString(), duration: endTime - startTime, documentsGenerated: documents.length, stepsExecuted: config.steps.length, intervalMs: config.intervalMs, seed: config.seed }; } /** * Create a seeded random number generator */ seededRandom(seed) { let s = seed; return function() { s = Math.sin(s) * 10000; return s - Math.floor(s); }; } } /** * TideConfig class for configuration validation */ export class TideConfig { constructor(data = {}) { this.name = data.name || 'New Sequence'; this.description = data.description || ''; this.startTime = data.startTime || new Date().toISOString(); this.endTime = data.endTime || new Date(Date.now() + 3600000).toISOString(); // +1 hour this.intervalMs = data.intervalMs || 1000; this.seed = data.seed || null; this.addJitter = data.addJitter || false; this.outputFormat = data.outputFormat || 'json'; this.includeMetadata = data.includeMetadata || false; this.steps = data.steps || []; this.transactions = data.transactions || []; this.globalEntropyOverride = data.globalEntropyOverride || null; } } /** * TideStep class for step configuration */ export class TideStep { constructor(data = {}) { this.stepId = data.stepId || ''; this.startTime = data.startTime || new Date().toISOString(); this.endTime = data.endTime || null; this.modelPath = data.modelPath || ''; this.weight = data.weight || 1.0; this.documentsPerInterval = data.documentsPerInterval || 1; this.generationProbability = data.generationProbability || 1.0; this.entropyOverride = data.entropyOverride || null; this.tags = data.tags || []; this.customProperties = data.customProperties || {}; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/smallmindsco/MongTap'

If you have feedback or need assistance with the MCP directory API, please join our Discord server