signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
125 lines • 4.74 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.createNpmMetadataClient = createNpmMetadataClient;
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:npm-metadata');
const FETCH_TIMEOUT_MS = 15_000;
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6h — matches the plugin detail TTL
async function fetchPackageMetadata(name, version) {
const url = `https://registry.npmjs.org/${encodeURIComponent(name).replace('%40', '@')}/${encodeURIComponent(version)}`;
try {
const res = await fetch(url, {
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
});
if (!res.ok) {
debug.enabled && debug('GET %s returned %d', url, res.status);
// Only treat true not-found / forbidden / gone as definitive. 429
// (rate-limited) is transient and must keep the stale-on-disk
// fallback path open so a throttled npm response doesn't blow away
// hours of cached metadata.
if (res.status === 404 || res.status === 403 || res.status === 410) {
return 'gone';
}
return undefined;
}
return (await res.json());
}
catch (err) {
debug.enabled && debug('GET %s failed: %O', url, err);
return undefined;
}
}
function createNpmMetadataClient(cacheDir) {
const dir = path_1.default.join(cacheDir, 'npm-metadata');
function ensureDir() {
try {
fs_1.default.mkdirSync(dir, { recursive: true });
}
catch (err) {
debug.enabled && debug('mkdir %s failed: %O', dir, err);
}
}
function fileFor(name, version) {
return path_1.default.join(dir, `${(0, safe_name_1.safeName)(name)}@${version}.json`);
}
function readDiskRaw(name, version) {
try {
const file = fileFor(name, version);
if (!fs_1.default.existsSync(file))
return undefined;
return JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
}
catch (err) {
debug.enabled && debug('readDisk failed: %O', err);
return undefined;
}
}
function readDisk(name, version) {
const entry = readDiskRaw(name, version);
if (!entry)
return undefined;
if (Date.now() - entry.writtenAt > CACHE_TTL_MS)
return undefined;
return entry;
}
function writeDisk(name, version, payload) {
ensureDir();
try {
fs_1.default.writeFileSync(fileFor(name, version), JSON.stringify({
writtenAt: Date.now(),
payload
}), 'utf8');
}
catch (err) {
debug.enabled && debug('writeDisk failed: %O', err);
}
}
return {
async get(name, version) {
const disk = readDisk(name, version);
if (disk)
return disk.payload;
const fresh = await fetchPackageMetadata(name, version);
if (fresh === 'gone') {
// Registry definitively says this version isn't there. Don't
// serve stale data — keeping a removed package's screenshots
// and dependencies visible would mislead users.
return undefined;
}
if (fresh) {
writeDisk(name, version, fresh);
return fresh;
}
// Transient failure (network, 5xx). A stale cache entry is
// strictly more useful than nothing — the alternative is the
// page degrading to "no signalk metadata" until npm comes back.
const stale = readDiskRaw(name, version);
return stale?.payload;
},
invalidate() {
try {
if (fs_1.default.existsSync(dir))
fs_1.default.rmSync(dir, { recursive: true, force: true });
}
catch (err) {
debug.enabled && debug('invalidate failed: %O', err);
}
}
};
}
//# sourceMappingURL=npm-metadata.js.map