|
| 1 | +-- lua/github_nvim/cache.lua |
| 2 | +local util = require("github_nvim.util") |
| 3 | + |
| 4 | +local M = {} |
| 5 | + |
| 6 | +-- === Configuration: Default settings === -- |
| 7 | +local DEFAULT_TTL = 60 * 60 * 24 -- 24 hours |
| 8 | +local MAX_CACHE_FILES = 50 |
| 9 | +local CACHE_DIR_PREFIX = "github_nvim" -- e.g. "github", "weather", "news" |
| 10 | + |
| 11 | +-- === Internal helpers === -- |
| 12 | + |
| 13 | +-- Get cache directory for a given name |
| 14 | +local function cache_dir(cache_name) |
| 15 | + return vim.fn.stdpath("cache") .. "/" .. (cache_name or CACHE_DIR_PREFIX) |
| 16 | +end |
| 17 | +-- Generate safe filename from key |
| 18 | +local function generate_filename(cache_name, key) |
| 19 | + local sanitized_key = key:gsub("[^%w_]", "_") |
| 20 | + local truncated = string.sub(sanitized_key, 1, 64) |
| 21 | + return string.format("%s/%s_%s.json", cache_dir(cache_name), cache_name, truncated) |
| 22 | +end |
| 23 | + |
| 24 | + |
| 25 | +-- Ensure cache dir exists |
| 26 | +local function ensure_cache_dir(cache_name) |
| 27 | + local path = cache_dir(cache_name) |
| 28 | + if vim.uv.fs_stat(path) then |
| 29 | + return path |
| 30 | + end |
| 31 | + |
| 32 | + util.mkdir_p(path) -- 0755 |
| 33 | + return path |
| 34 | +end |
| 35 | + |
| 36 | +-- Check if file is valid (within TTL) |
| 37 | +local function is_valid(cache_name, key, ttl) |
| 38 | + local file = generate_filename(cache_name, key) |
| 39 | + local stat = vim.uv.fs_stat(file) |
| 40 | + if not stat then |
| 41 | + print(string.format("file %s does not exist.", file)) |
| 42 | + return false |
| 43 | + end |
| 44 | + |
| 45 | + local now = vim.uv.now() |
| 46 | + local age = now - stat.mtime.sec |
| 47 | + return age < (ttl or DEFAULT_TTL) |
| 48 | +end |
| 49 | + |
| 50 | +-- === Public API === -- |
| 51 | + |
| 52 | +-- Load cached data by key in a named cache |
| 53 | +-- @param cache_name (string): e.g. "github", "weather" |
| 54 | +-- @param key (string): unique identifier (e.g. "neovim", "London") |
| 55 | +-- @param ttl (number): override TTL in seconds (optional) |
| 56 | +-- @return table | nil: returns cached data or nil |
| 57 | +function M.load(cache_name, key, ttl) |
| 58 | + local file = generate_filename(cache_name, key) |
| 59 | + local f = io.open(file, "r") |
| 60 | + if not f then |
| 61 | + print(string.format("cannot open %s.", file)) |
| 62 | + return nil |
| 63 | + end |
| 64 | + |
| 65 | + local content = f:read("*a") |
| 66 | + f:close() |
| 67 | + |
| 68 | + local success, data = pcall(vim.json.decode, content) |
| 69 | + if not success then |
| 70 | + print("❌ Invalid JSON in cache file:", file, content) |
| 71 | + return nil |
| 72 | + end |
| 73 | + |
| 74 | +-- print("data is ", vim.inspect(data)) |
| 75 | + if not data or not data.results then |
| 76 | + print(string.format("data decode error, content:%s", content)) |
| 77 | + return nil |
| 78 | + end |
| 79 | + |
| 80 | + if not is_valid(cache_name, key, ttl) then |
| 81 | + return nil -- expired |
| 82 | + end |
| 83 | + |
| 84 | + return data.results |
| 85 | +end |
| 86 | + |
| 87 | +-- Save data to cache |
| 88 | +-- @param cache_name (string): cache namespace (e.g. "github") |
| 89 | +-- @param key (string): unique key |
| 90 | +-- @param results (table): your data (e.g. list of repos) |
| 91 | +-- @param ttl (number): time-to-live in seconds (optional) |
| 92 | +function M.save(cache_name, key, results, ttl) |
| 93 | + local file = generate_filename(cache_name, key) |
| 94 | + local dir = cache_dir(cache_name) |
| 95 | + ensure_cache_dir(cache_name) |
| 96 | + |
| 97 | + local data = { |
| 98 | + results = results, |
| 99 | + timestamp = vim.uv.now(), |
| 100 | + key = key, |
| 101 | + cache_name = cache_name, |
| 102 | + ttl = ttl or DEFAULT_TTL, |
| 103 | + } |
| 104 | + |
| 105 | +-- print("save results: ", vim.inspect(results)) |
| 106 | +-- print("data: ", vim.inspect(data)) |
| 107 | + |
| 108 | + local f = io.open(file, "w") |
| 109 | + if not f then |
| 110 | + print("❌ Failed to write cache file:", file) |
| 111 | + return |
| 112 | + end |
| 113 | + |
| 114 | + local content = vim.json.encode(data) |
| 115 | + print("content is " .. content) |
| 116 | + local err = f:write(content) |
| 117 | + print(vim.inspect(err)) |
| 118 | + if err then |
| 119 | + print("failed to write to file, msg:", err) |
| 120 | + end |
| 121 | + f:close() |
| 122 | + |
| 123 | + -- Optional: cleanup after save |
| 124 | + M.cleanup(cache_name) |
| 125 | +end |
| 126 | + |
| 127 | +-- Cleanup old files in a specific cache |
| 128 | +-- @param cache_name (string): e.g. "github" |
| 129 | +function M.cleanup(cache_name) |
| 130 | + local dir = cache_dir(cache_name) |
| 131 | + local files = vim.split(vim.fn.glob(dir .. "/*.json"), "\n", { plain = true }) |
| 132 | + local valid_files = {} |
| 133 | + |
| 134 | + for _, file in ipairs(files) do |
| 135 | + local stat = vim.uv.fs_stat(file) |
| 136 | + if stat then |
| 137 | + table.insert(valid_files, { file = file, mtime = stat.mtime.sec }) |
| 138 | + end |
| 139 | + end |
| 140 | + |
| 141 | + -- Sort by mtime (oldest first) |
| 142 | + table.sort(valid_files, function(a, b) |
| 143 | + return a.mtime < b.mtime |
| 144 | + end) |
| 145 | + |
| 146 | + -- Remove excess files |
| 147 | + while #valid_files > MAX_CACHE_FILES do |
| 148 | + local old_file = table.remove(valid_files, 1) |
| 149 | + vim.uv.fs_unlink(old_file.file) |
| 150 | + end |
| 151 | +end |
| 152 | + |
| 153 | +-- Clear all files in a cache |
| 154 | +-- @param cache_name (string) |
| 155 | +function M.clear(cache_name) |
| 156 | + local dir = cache_dir(cache_name) |
| 157 | + local files = vim.split(vim.fn.glob(dir .. "/*.json"), "\n", { plain = true }) |
| 158 | + for _, file in ipairs(files) do |
| 159 | + vim.uv.fs_unlink(file) |
| 160 | + end |
| 161 | +end |
| 162 | + |
| 163 | +-- List all cached keys in a cache (for debugging) |
| 164 | +-- @param cache_name (string) |
| 165 | +-- @return table: list of keys |
| 166 | +function M.list_keys(cache_name) |
| 167 | + local dir = cache_dir(cache_name) |
| 168 | + local files = vim.split(vim.fn.glob(dir .. "/*.json"), "\n", { plain = true }) |
| 169 | + local keys = {} |
| 170 | + |
| 171 | + for _, file in ipairs(files) do |
| 172 | + local basename = vim.fn.fnamemodify(file, ":t:r") |
| 173 | + local parts = vim.split(basename, "_", { plain = true }) |
| 174 | + if #parts >= 2 then |
| 175 | + table.insert(keys, parts[2]) |
| 176 | + end |
| 177 | + end |
| 178 | + |
| 179 | + return keys |
| 180 | +end |
| 181 | + |
| 182 | +-- === Utility: Safe JSON decode with fallback === -- |
| 183 | +function M.safe_decode(str) |
| 184 | + local ok, data = pcall(vim.json.decode, str) |
| 185 | + return ok and data or nil |
| 186 | +end |
| 187 | + |
| 188 | +return M |
0 commit comments