134 lines
3.3 KiB
JavaScript
134 lines
3.3 KiB
JavaScript
/**
|
|
* Atomic JSON cache under ~/.config/peardata/cache/{name}.json
|
|
* (+ optional localStorage mirror / legacy migration).
|
|
*/
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { getPeardataCacheDir } from './paths.js'
|
|
|
|
export const JSON_CACHE_VERSION = 1
|
|
export const JSON_CACHE_LS_PREFIX = 'peardata.cache.'
|
|
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
export function sanitizeCacheName(name) {
|
|
const s = String(name || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9._-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
if (!s || s === '.' || s === '..') throw new Error(`Invalid cache name: ${name}`)
|
|
return s
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
export function getCacheFilePath(name) {
|
|
return path.join(getPeardataCacheDir(), `${sanitizeCacheName(name)}.json`)
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
export function getCacheLocalStorageKey(name) {
|
|
return JSON_CACHE_LS_PREFIX + sanitizeCacheName(name)
|
|
}
|
|
|
|
/**
|
|
* @param {string} file
|
|
* @param {object} payload
|
|
*/
|
|
export function atomicWriteJson(file, payload) {
|
|
const dir = path.dirname(file)
|
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
const json = JSON.stringify(payload, null, 2)
|
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
fs.writeFileSync(tmp, json, { mode: 0o600 })
|
|
fs.renameSync(tmp, file)
|
|
try {
|
|
fs.chmodSync(file, 0o600)
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {unknown} data
|
|
* @param {{ mirrorLocalStorage?: boolean }} [opts]
|
|
*/
|
|
export function saveJsonCache(name, data, opts = {}) {
|
|
const file = getCacheFilePath(name)
|
|
const payload = {
|
|
version: JSON_CACHE_VERSION,
|
|
updatedAt: new Date().toISOString(),
|
|
data,
|
|
}
|
|
try {
|
|
atomicWriteJson(file, payload)
|
|
} catch (err) {
|
|
console.warn('[WARN] jsonCache: write failed', name, err?.message || err)
|
|
}
|
|
if (opts.mirrorLocalStorage !== false) {
|
|
try {
|
|
if (typeof localStorage !== 'undefined') {
|
|
localStorage.setItem(getCacheLocalStorageKey(name), JSON.stringify(payload))
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {{ legacyLocalStorageKey?: string, defaultValue?: unknown }} [opts]
|
|
*/
|
|
export function loadJsonCache(name, opts = {}) {
|
|
const file = getCacheFilePath(name)
|
|
try {
|
|
if (fs.existsSync(file)) {
|
|
const raw = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
if (raw && typeof raw === 'object' && 'data' in raw) return raw.data
|
|
return raw
|
|
}
|
|
} catch {
|
|
// fall through
|
|
}
|
|
|
|
// Legacy localStorage migration
|
|
if (opts.legacyLocalStorageKey) {
|
|
try {
|
|
if (typeof localStorage !== 'undefined') {
|
|
const legacy = localStorage.getItem(opts.legacyLocalStorageKey)
|
|
if (legacy) {
|
|
const parsed = JSON.parse(legacy)
|
|
const data =
|
|
parsed && typeof parsed === 'object' && 'data' in parsed ? parsed.data : parsed
|
|
saveJsonCache(name, data ?? opts.defaultValue ?? null)
|
|
return data ?? opts.defaultValue ?? null
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (typeof localStorage !== 'undefined') {
|
|
const mir = localStorage.getItem(getCacheLocalStorageKey(name))
|
|
if (mir) {
|
|
const parsed = JSON.parse(mir)
|
|
return parsed && typeof parsed === 'object' && 'data' in parsed ? parsed.data : parsed
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
return opts.defaultValue ?? null
|
|
}
|