signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
165 lines • 6.22 kB
JavaScript
;
/*
* Copyright 2026 Signal K contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DETAIL_TTL = exports.LIST_TTL = void 0;
exports.createCache = createCache;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const debug_1 = require("../debug");
const safe_name_1 = require("./safe-name");
const debug = (0, debug_1.createDebug)('signalk-server:appstore:cache');
const LIST_TTL_MS = 60 * 60 * 1000; // 1 hour
const DETAIL_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
function createCache(configPath) {
const root = path_1.default.join(configPath, 'appstore-cache');
const listFile = path_1.default.join(root, 'list.json');
const pluginsDir = path_1.default.join(root, 'plugins');
function ensureRoot() {
try {
fs_1.default.mkdirSync(pluginsDir, { recursive: true });
}
catch (err) {
debug.enabled &&
debug('failed to create cache dir %s: %O', pluginsDir, err);
}
}
function pluginDir(name) {
return path_1.default.join(pluginsDir, (0, safe_name_1.safeName)(name));
}
function isFresh(writtenAt, ttl, installed = false) {
// Cached detail for an installed plugin is treated as fresh
// indefinitely. The on-disk plugin tarball is the source of truth
// for installed plugins (we render its README/CHANGELOG locally),
// so the cache only ages out when the plugin is uninstalled or
// the user clicks Refresh. This also keeps the detail tab usable
// offline — npm being unreachable doesn't blank the cached
// README/Changelog/Indicators for plugins the user already runs.
if (installed)
return true;
return Date.now() - writtenAt < ttl;
}
function readListRaw() {
try {
if (!fs_1.default.existsSync(listFile))
return undefined;
const raw = fs_1.default.readFileSync(listFile, 'utf8');
return JSON.parse(raw);
}
catch (err) {
debug.enabled && debug('readList failed: %O', err);
return undefined;
}
}
return {
readList() {
const parsed = readListRaw();
if (!parsed)
return undefined;
if (!isFresh(parsed.writtenAt, LIST_TTL_MS))
return undefined;
return parsed;
},
// Bypass the TTL — offline.ts uses this when the live store is
// unreachable so we can still serve a stale snapshot rather than
// empty results.
readListIgnoringTtl() {
return readListRaw();
},
writeList(payload) {
ensureRoot();
const record = { writtenAt: Date.now(), payload };
try {
fs_1.default.writeFileSync(listFile, JSON.stringify(record), 'utf8');
}
catch (err) {
debug.enabled && debug('writeList failed: %O', err);
}
},
readPluginDetail(name) {
try {
const file = path_1.default.join(pluginDir(name), 'detail.json');
if (!fs_1.default.existsSync(file))
return undefined;
const raw = fs_1.default.readFileSync(file, 'utf8');
const parsed = JSON.parse(raw);
if (!isFresh(parsed.writtenAt, DETAIL_TTL_MS, parsed.installed)) {
return undefined;
}
return parsed;
}
catch (err) {
debug.enabled && debug('readPluginDetail failed for %s: %O', name, err);
return undefined;
}
},
writePluginDetail(detail, installed) {
ensureRoot();
const dir = pluginDir(detail.name);
try {
fs_1.default.mkdirSync(dir, { recursive: true });
}
catch (err) {
debug.enabled && debug('mkdir failed for %s: %O', dir, err);
}
const record = {
writtenAt: Date.now(),
installed,
payload: { ...detail, fromCache: true }
};
try {
fs_1.default.writeFileSync(path_1.default.join(dir, 'detail.json'), JSON.stringify(record), 'utf8');
}
catch (err) {
debug.enabled &&
debug('writePluginDetail failed for %s: %O', detail.name, err);
}
},
invalidateList() {
try {
if (fs_1.default.existsSync(listFile))
fs_1.default.unlinkSync(listFile);
}
catch (err) {
debug.enabled && debug('invalidateList failed: %O', err);
}
},
invalidatePluginDetail(name) {
try {
const file = path_1.default.join(pluginDir(name), 'detail.json');
if (fs_1.default.existsSync(file))
fs_1.default.unlinkSync(file);
}
catch (err) {
debug.enabled &&
debug('invalidatePluginDetail failed for %s: %O', name, err);
}
},
invalidateAllPluginDetail() {
try {
if (fs_1.default.existsSync(pluginsDir)) {
fs_1.default.rmSync(pluginsDir, { recursive: true, force: true });
}
}
catch (err) {
debug.enabled && debug('invalidateAllPluginDetail failed: %O', err);
}
},
cacheRoot() {
return root;
}
};
}
exports.LIST_TTL = LIST_TTL_MS;
exports.DETAIL_TTL = DETAIL_TTL_MS;
//# sourceMappingURL=cache.js.map