provisioning-core/nulib/domain/cache/core.nu

159 lines
6.1 KiB
Text
Raw Permalink Normal View History

# Cache Core — reads from the shared plugin cache directory.
# Written by ncl-sync daemon; read by this module and nu_plugin_nickel.
# Single writer principle: Nu NEVER writes to the cache dir directly.
# cache/metadata star-import was dead — dropped (ADR-025 Phase 3 Layer 2).
# Check if a directory has workspace markers.
def is-ws-dir [path: string]: nothing -> bool {
if ($path | is-empty) or (not ($path | path exists)) { return false }
let has_infra = ($path | path join "infra" | path exists)
let has_config = ($path | path join "config" "provisioning.ncl" | path exists)
let has_onto = ($path | path join ".ontology" | path exists)
$has_infra or $has_config or $has_onto
}
# Walk up from PWD to find workspace root (recursive).
def find-ws-up [path: string]: nothing -> string {
if ($path | is-empty) or $path == "/" { return "" }
if (is-ws-dir $path) { return $path }
let parent = ($path | path dirname)
if $parent == $path { return "" }
find-ws-up $parent
}
# Global cache directory (shared across workspaces, for files under $PROVISIONING).
def get-global-cache-dir []: nothing -> string {
let home = ($env.HOME? | default "~" | path expand)
let host_info = (do { sys host } | complete)
let is_mac = if $host_info.exit_code == 0 {
($host_info.stdout | get name | str downcase | str contains "darwin")
or ($host_info.stdout | get name | str downcase | str contains "macos")
} else {
($home | path join "Library" | path exists)
}
if $is_mac {
$home | path join "Library" "Caches" "provisioning" "config-cache"
} else {
$home | path join ".cache" "provisioning" "config-cache"
}
}
# Resolve cache directory FOR A SPECIFIC FILE. Priority:
# 1. $NCL_CACHE_DIR (explicit override, for CI/tests)
# 2. File under $PROVISIONING → global cache (extensions, schemas — shared)
# 3. File under a workspace (walk up from file path) → <ws>/.ncl-cache/
# 4. Fallback: global cache
#
# Must match resolve_cache_dir_for_file() in ncl-sync + plugin.
def get-cache-dir-for-file [file_path: string]: nothing -> string {
if ($env.NCL_CACHE_DIR? | is-not-empty) {
return $env.NCL_CACHE_DIR
}
# File under $PROVISIONING → global cache
let prov = ($env.PROVISIONING? | default "")
if ($prov | is-not-empty) and ($file_path | str starts-with $prov) {
return (get-global-cache-dir)
}
# File under a workspace → workspace-local cache
let ws = (find-ws-up ($file_path | path dirname))
if ($ws | is-not-empty) {
return ($ws | path join ".ncl-cache")
}
get-global-cache-dir
}
# Legacy helper (CWD-based) — kept for backwards compat in code paths that don't have
# the file path at hand. Prefer get-cache-dir-for-file.
def get-cache-base-dir []: nothing -> string {
if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR }
let ws = (find-ws-up $env.PWD)
if ($ws | is-not-empty) { return ($ws | path join ".ncl-cache") }
get-global-cache-dir
}
# Lookup a cache entry by pre-computed key.
# Only "nickel" type is backed by the shared plugin cache.
# Returns: { valid: bool, reason: string, data: any }
export def cache-lookup [
cache_type: string
cache_key: string
--ttl: int = 0
]: nothing -> record {
if $cache_type != "nickel" {
return { valid: false, reason: "type_not_supported", data: null }
}
let cache_file = ((get-cache-base-dir) | path join $"($cache_key).json")
if not ($cache_file | path exists) {
return { valid: false, reason: "cache_miss", data: null }
}
let result = (do { open $cache_file } | complete)
if $result.exit_code != 0 {
return { valid: false, reason: "read_error", data: null }
}
{ valid: true, reason: "hit", data: ($result.stdout | from json) }
}
# Signal ncl-sync daemon to (re-)export a list of NCL files.
# Nu never writes to the cache directly — only signals the daemon.
# Uses pid-unique sidecar + atomic rename to prevent concurrent-write corruption.
export def cache-write [
cache_type: string
cache_key: string
data: any
source_files: list
--ttl: int = 0
]: nothing -> nothing {
if $cache_type != "nickel" { return }
write-sync-request ($source_files | each {|f| { path: $f, import_paths: [] }})
}
# Write a sync-request sidecar file for ncl-sync to process.
# Each Nu process writes .sync-<pid>.tmp then renames to .sync-<pid>.json atomically.
export def write-sync-request [
requests: list # list of {path: string, import_paths: list}
]: nothing -> nothing {
let cache_dir = (get-cache-base-dir)
if not ($cache_dir | path exists) { return }
let pid = $nu.pid
let tmp_file = ($cache_dir | path join $".sync-($pid).tmp")
let json_file = ($cache_dir | path join $".sync-($pid).json")
$requests | to json | save --force $tmp_file
^mv $tmp_file $json_file
}
# Cache stats — count entries and total size in the shared cache dir.
export def get-cache-stats []: nothing -> record {
let cache_dir = (get-cache-base-dir)
if not ($cache_dir | path exists) {
return { total_entries: 0, total_size_mb: 0.0, by_type: {} }
}
let files = (do { ls $cache_dir } | complete)
if $files.exit_code != 0 {
return { total_entries: 0, total_size_mb: 0.0, by_type: {} }
}
let entries = ($files.stdout | where name =~ '\.json$' | where name !~ 'manifest' | length)
let size_bytes = ($files.stdout | where name =~ '\.json$' | get size | math sum)
{
total_entries: $entries,
total_size_mb: ($size_bytes / 1_048_576 | math round -p 2),
by_type: { nickel: $entries }
}
}
# Clear the shared cache directory (removes all .json files except manifest).
export def cache-clear-type [cache_type: string]: nothing -> nothing {
if $cache_type != "nickel" { return }
let cache_dir = (get-cache-base-dir)
if not ($cache_dir | path exists) { return }
do {
ls $cache_dir
| where name =~ '\.json$'
| where name !~ 'manifest'
| each {|f| rm $f.name}
} | ignore
}
# No-op — eviction is handled by ncl-sync daemon.
export def cleanup-expired-cache [max_size_mb: int = 100]: nothing -> nothing {}