276 lines
6.7 KiB
Text
276 lines
6.7 KiB
Text
# 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" }
|
|
}
|
|
}
|