UNPKG

@yihuangdb/storage-object

Version:

A Node.js storage object layer library using Redis OM

296 lines 10.9 kB
"use strict"; /** * Redis OM Pattern Scanner * Finds and manages all keys created by Redis OM for a schema */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RedisOMPatternScanner = void 0; exports.monitorAllSchemas = monitorAllSchemas; exports.cleanupAllSchemas = cleanupAllSchemas; class RedisOMPatternScanner { entityName; client; // Redis OM's predictable patterns REDIS_OM_PATTERNS = [ // Primary patterns '{entity}:*', // Main entity keys 'idx:{entity}', // RediSearch index definition 'idx:{entity}:*', // RediSearch index data // Field indexes '{entity}#*', // Field index pattern // FT Search patterns 'ft:{entity}', // FT index 'ft:{entity}:*', // FT index data 'ft.{entity}', // Alternative FT format // Metadata patterns 'RedisOM:{entity}:*', // Redis OM metadata '_meta:{entity}:*', // Custom metadata '{entity}:index', // Index definition '{entity}:index:*', // Index data // Temporary/Lock patterns '_tmp:{entity}:*', // Temporary keys '_lock:{entity}:*', // Lock keys // With prefix support '*:{entity}:*' // Any prefix ]; constructor(entityName, client) { this.entityName = entityName; this.client = client; } /** * Get all patterns for this entity */ getPatterns() { return this.REDIS_OM_PATTERNS.map(pattern => pattern.replace('{entity}', this.entityName)); } /** * Get keys matching a pattern from registry */ async scanPattern(pattern) { // Extract entity name from pattern if possible // Patterns like: storage:entityName:*, idx:entityName:* const parts = pattern.split(':'); if (parts.length >= 2) { // Try to extract entity name const entityName = parts[1]; if (entityName && !entityName.includes('*')) { // Use cursor-based SCAN to get keys for this entity const { getRedisKeyManager } = require('../redis-key-manager'); const keyManager = getRedisKeyManager(); const entityPattern = `${parts[0]}:${entityName}:*`; return await keyManager.getAllKeysMatching(this.client, entityPattern); } } // For other patterns, use cursor-based SCAN const { getRedisKeyManager } = require('../redis-key-manager'); const keyManager = getRedisKeyManager(); return await keyManager.getAllKeysMatching(this.client, pattern); } /** * Find all keys for this schema */ async findAllSchemaKeys() { const patterns = this.getPatterns(); const results = new Map(); const breakdown = {}; let totalKeys = 0; console.log(`Scanning for ${this.entityName} keys using ${patterns.length} patterns...`); for (const pattern of patterns) { try { const keys = await this.scanPattern(pattern); if (keys.length > 0) { // Deduplicate keys const uniqueKeys = Array.from(new Set(keys)); results.set(pattern, uniqueKeys); breakdown[pattern] = uniqueKeys.length; totalKeys += uniqueKeys.length; console.log(` ✓ Found ${uniqueKeys.length} keys matching: ${pattern}`); } } catch (error) { console.error(` ✗ Error scanning pattern ${pattern}:`, error); } } // Remove duplicates across patterns const allKeys = new Set(); for (const keys of results.values()) { keys.forEach(key => allKeys.add(key)); } console.log(`Total unique keys found: ${allKeys.size}`); return { patterns: results, totalKeys: allKeys.size, breakdown }; } /** * Delete all keys for this schema */ async deleteAllSchemaKeys() { const scanResult = await this.findAllSchemaKeys(); let deleted = 0; const errors = []; const usedPatterns = []; // Collect all unique keys const allKeys = new Set(); for (const keys of scanResult.patterns.values()) { keys.forEach(key => allKeys.add(key)); } if (allKeys.size === 0) { console.log(`No keys found for ${this.entityName}`); return { deleted: 0, patterns: [], errors: [] }; } console.log(`Deleting ${allKeys.size} keys for ${this.entityName}...`); // Delete in batches const keysArray = Array.from(allKeys); const batchSize = 1000; for (let i = 0; i < keysArray.length; i += batchSize) { const batch = keysArray.slice(i, i + batchSize); try { // Use UNLINK for async deletion (non-blocking) await this.client.unlink(batch); deleted += batch.length; if ((i + batchSize) % 10000 === 0) { console.log(` Deleted ${deleted}/${allKeys.size} keys...`); } } catch (error) { const errorMsg = `Failed to delete batch at index ${i}: ${error}`; errors.push(errorMsg); console.error(errorMsg); } } console.log(`✓ Successfully deleted ${deleted} keys`); return { deleted, patterns: Array.from(scanResult.patterns.keys()), errors }; } /** * Get statistics about keys */ async getKeyStats() { const result = await this.findAllSchemaKeys(); // Categorize keys by type const byType = { entities: 0, indexes: 0, search: 0, metadata: 0, temporary: 0, other: 0 }; for (const [pattern, keys] of result.patterns) { const count = keys.length; if (pattern.startsWith(this.entityName + ':')) { byType.entities += count; } else if (pattern.includes('idx') || pattern.includes('index')) { byType.indexes += count; } else if (pattern.includes('ft')) { byType.search += count; } else if (pattern.includes('meta') || pattern.includes('RedisOM')) { byType.metadata += count; } else if (pattern.includes('tmp') || pattern.includes('lock')) { byType.temporary += count; } else { byType.other += count; } } // Estimate memory usage (optional) let memoryUsage; try { // Sample first 100 keys for memory estimation const allKeys = Array.from(result.patterns.values()).flat(); const sampleKeys = allKeys.slice(0, Math.min(100, allKeys.length)); if (sampleKeys.length > 0) { let totalMemory = 0; for (const key of sampleKeys) { try { // @ts-ignore - memoryUsage might not be available const usage = await this.client.memoryUsage(key); totalMemory += usage || 0; } catch (e) { // Memory command might not be available } } // Extrapolate if (totalMemory > 0) { memoryUsage = Math.round(totalMemory * (allKeys.length / sampleKeys.length)); } } } catch (error) { console.warn('Could not estimate memory usage:', error); } return { total: result.totalKeys, byType, memoryUsage }; } /** * Find orphaned keys (keys that don't match expected patterns) */ async findOrphanedKeys() { const orphaned = []; // First, get all keys that contain the entity name const allKeysWithEntity = await this.scanPattern(`*${this.entityName}*`); // Get all keys from known patterns const knownKeys = new Set(); const patterns = this.getPatterns(); for (const pattern of patterns) { const keys = await this.scanPattern(pattern); keys.forEach(key => knownKeys.add(key)); } // Find keys that don't match any known pattern for (const key of allKeysWithEntity) { if (!knownKeys.has(key)) { orphaned.push(key); } } if (orphaned.length > 0) { console.log(`Found ${orphaned.length} orphaned keys for ${this.entityName}`); // Log sample of orphaned keys for debugging console.log('Sample orphaned keys:', orphaned.slice(0, 5)); } return orphaned; } /** * Clean up orphaned keys */ async cleanupOrphans() { const orphaned = await this.findOrphanedKeys(); if (orphaned.length === 0) { return 0; } console.log(`Cleaning up ${orphaned.length} orphaned keys...`); // Delete in batches const batchSize = 1000; for (let i = 0; i < orphaned.length; i += batchSize) { const batch = orphaned.slice(i, i + batchSize); await this.client.unlink(batch); } return orphaned.length; } } exports.RedisOMPatternScanner = RedisOMPatternScanner; /** * Utility to monitor all schemas */ async function monitorAllSchemas(schemas, client) { const report = {}; for (const schemaName of schemas) { const scanner = new RedisOMPatternScanner(schemaName, client); const stats = await scanner.getKeyStats(); report[schemaName] = stats; // Log summary console.log(`${schemaName}: ${stats.total} keys (${Object.entries(stats.byType) .filter(([_, count]) => count > 0) .map(([type, count]) => `${type}: ${count}`) .join(', ')})`); } return report; } /** * Clean up all schemas */ async function cleanupAllSchemas(schemas, client) { const results = {}; for (const schemaName of schemas) { console.log(`\nCleaning up ${schemaName}...`); const scanner = new RedisOMPatternScanner(schemaName, client); results[schemaName] = await scanner.deleteAllSchemaKeys(); } // Summary const totalDeleted = Object.values(results).reduce((sum, r) => sum + r.deleted, 0); console.log(`\nTotal keys deleted across all schemas: ${totalDeleted}`); return results; } //# sourceMappingURL=redis-om-scanner.js.map