# Cache Metadata Management # Handles metadata creation, loading, validation, and mtime comparison # Follows Nushell 0.109.0+ guidelines # Helper: Get current ISO8601 timestamp def get-iso-timestamp [] { date now | format date "%Y-%m-%dT%H:%M:%SZ" } # Helper: Parse ISO8601 timestamp def parse-iso-timestamp [timestamp: string] { # Convert ISO8601 to Unix timestamp $timestamp } # Helper: Check if timestamp is expired def is-expired [ expires_at: string now: string ] { $now > $expires_at } # ============================================================================ # PUBLIC API: Metadata Operations # ============================================================================ # Create metadata for cache entry export def create-metadata [ source_files: list # List of file paths to track ttl_seconds: int # Time-to-live in seconds data_hash: string # Hash of cached data ] { let now = (get-iso-timestamp) let expires_at = (((date now) + ($ttl_seconds | into duration)) | format date "%Y-%m-%dT%H:%M:%SZ") mut source_mtimes = {} for file in $source_files { let mtime = if ($file | path exists) { $file | stat | get modified | into int } else { (-1) } $source_mtimes = ($source_mtimes | insert $file $mtime) } { created_at: $now, ttl_seconds: $ttl_seconds, expires_at: $expires_at, source_files: $source_files, source_mtimes: $source_mtimes, hash: $data_hash, cache_version: "1.0" } } # Load and validate metadata export def load-metadata [ meta_file: string ] { let load_result = (do { if ($meta_file | path exists) { open $meta_file } else { error make { msg: $"Metadata file not found: ($meta_file)" } } } | complete) if $load_result.exit_code != 0 { return { valid: false, reason: "metadata_load_error", data: null } } { valid: true, reason: "metadata_loaded", data: $load_result.stdout } } # Validate metadata (check version, format, etc.) export def validate-metadata [ metadata: record ] { # Check required fields let required_fields = ["created_at" "ttl_seconds" "expires_at" "source_files" "hash" "cache_version"] for field in $required_fields { if not ($metadata | has -c $field) { return { valid: false, reason: $"missing_field: ($field)" } } } # Check cache version compatibility if $metadata.cache_version != "1.0" { return { valid: false, reason: "incompatible_cache_version" } } { valid: true, reason: "metadata_valid" } } # Get file modification times export def get-source-mtimes [ source_files: list ] { mut mtimes = {} for file in $source_files { let mtime_result = (do { if ($file | path exists) { $file | stat | get modified | into int } else { -1 } } | complete) if $mtime_result.exit_code == 0 { $mtimes = ($mtimes | insert $file $mtime_result.stdout) } else { $mtimes = ($mtimes | insert $file (-1)) } } $mtimes } # Compare cached vs current mtimes export def compare-mtimes [ cached_mtimes: record current_mtimes: record ] { mut all_match = true mut mismatches = [] for file in ($cached_mtimes | columns) { let cached = ($cached_mtimes | get $file) let current = ($current_mtimes | get --optional $file | default (-1)) if $cached != $current { $all_match = false $mismatches = ($mismatches | append { file: $file, cached: $cached, current: $current }) } } { match: $all_match, mismatches: $mismatches } } # Check if metadata is expired export def is-metadata-expired [ metadata: record now: string ] { is-expired $metadata.expires_at $now } # Get metadata expiration time remaining export def get-metadata-ttl-remaining [ metadata: record now: string ] { # Parse both timestamps and calculate difference let now_ts = (parse-iso-timestamp $now) let expires_ts = (parse-iso-timestamp $metadata.expires_at) if $expires_ts > $now_ts { $expires_ts - $now_ts } else { 0 } } # Get comprehensive metadata status export def get-metadata-status [ metadata: record current_source_mtimes: record ] { let now = (get-iso-timestamp) let is_expired = (is-metadata-expired $metadata $now) let mtime_comparison = (compare-mtimes $metadata.source_mtimes $current_source_mtimes) let ttl_remaining = (get-metadata-ttl-remaining $metadata $now) { valid: ((not $is_expired) and $mtime_comparison.match), expired: $is_expired, ttl_remaining_seconds: $ttl_remaining, mtime_status: $mtime_comparison, created_at: $metadata.created_at, expires_at: $metadata.expires_at, source_files_count: ($metadata.source_files | length), mtime_matches: (($mtime_comparison.mismatches | length) == 0) } } # Update metadata (refresh TTL) export def update-metadata [ metadata: record ttl_seconds: int = 0 # If 0, keep original TTL ] { let new_ttl = if $ttl_seconds > 0 { $ttl_seconds } else { $metadata.ttl_seconds } let now = (get-iso-timestamp) let expires_at = (((date now) + ($new_ttl | into duration)) | format date "%Y-%m-%dT%H:%M:%SZ") $metadata | insert created_at $now | insert ttl_seconds $new_ttl | insert expires_at $expires_at } # Rotate metadata (create new with updated mtimes) export def rotate-metadata [ metadata: record new_source_mtimes: record ] { let now = (get-iso-timestamp) let expires_at = (((date now) + ($metadata.ttl_seconds | into duration)) | format date "%Y-%m-%dT%H:%M:%SZ") $metadata | insert created_at $now | insert expires_at $expires_at | insert source_mtimes $new_source_mtimes } # Export metadata to JSON export def export-metadata [ metadata: record --pretty = false ] { if $pretty { $metadata | to json --indent 2 } else { $metadata | to json } } # Import metadata from JSON export def import-metadata [ json_string: string ] { let result = (do { $json_string | from json } | complete) if $result.exit_code == 0 { $result.stdout } else { error make { msg: "Failed to parse metadata JSON" } } }