Updates
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
Reference in New Issue
Block a user