UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

125 lines 4.74 kB
"use strict"; /* * 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