UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

324 lines (323 loc) • 12.4 kB
/** * Update State Persistence * Manages persistent state for the proxy auto-update feature. * Tracks check timestamps, suppressed versions, and update history. * * State file location: ~/.neurolink/update-state.json * Suppressed versions expire after 24 hours. */ import fs from "fs"; import os from "os"; import path from "path"; import { randomUUID } from "node:crypto"; // ============================================ // Constants // ============================================ const STATE_FILENAME = "update-state.json"; const SUPPRESSION_TTL_MS = 86_400_000; // 24 hours const UPDATE_FAILURE_STAGES = new Set([ "check", "install", "validation", "restart", "health", ]); // ============================================ // Internal Helpers // ============================================ /** * Resolve the path to the update state file. * Accepts an override for testing; defaults to ~/.neurolink/update-state.json. */ function resolveStatePath(overridePath) { if (overridePath) { return overridePath; } return path.join(os.homedir(), ".neurolink", STATE_FILENAME); } /** * Ensure the parent directory of the given file path exists. */ function ensureParentDir(filePath) { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } /** Reject malformed persisted failure metadata before it reaches status output. */ function isValidLastFailure(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } const candidate = value; return (typeof candidate.at === "string" && typeof candidate.version === "string" && typeof candidate.stage === "string" && UPDATE_FAILURE_STAGES.has(candidate.stage) && typeof candidate.message === "string"); } function isValidDeferredUpdate(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } const candidate = value; return (typeof candidate.version === "string" && typeof candidate.since === "string" && typeof candidate.updatedAt === "string" && [ "waiting_for_quiet", "draining", "drain_timeout", "drain_unavailable", "activity_unavailable", ].includes(String(candidate.reason)) && (candidate.activeRequests === null || (typeof candidate.activeRequests === "number" && Number.isInteger(candidate.activeRequests) && candidate.activeRequests >= 0))); } // ============================================ // Exported Functions // ============================================ /** * Return an empty/initial UpdateState. */ export function getDefaultUpdateState() { return { lastCheckAt: new Date(0).toISOString(), lastCheckVersion: "", suppressedVersions: {}, installedVersion: null, lastUpdateAt: null, lastUpdateVersion: null, pendingRestartVersion: null, deferredUpdate: null, lastFailure: null, }; } /** * Load the update state from disk. * Returns null if the file does not exist. * Returns the default state if the file contains corrupt JSON. * * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json) */ export function loadUpdateState(stateFilePath) { const filePath = resolveStatePath(stateFilePath); try { if (!fs.existsSync(filePath)) { return null; } const content = fs.readFileSync(filePath, "utf8"); const parsed = JSON.parse(content); // Minimal shape check — reject valid JSON that isn't an UpdateState // Note: typeof null === "object", so we check both if (typeof parsed !== "object" || parsed === null || typeof parsed.suppressedVersions !== "object" || parsed.suppressedVersions === null || Array.isArray(parsed.suppressedVersions) || typeof parsed.lastCheckAt !== "string") { return getDefaultUpdateState(); } const candidate = parsed; return { ...getDefaultUpdateState(), ...candidate, lastCheckAttemptAt: typeof candidate.lastCheckAttemptAt === "string" ? candidate.lastCheckAttemptAt : undefined, lastCheckError: typeof candidate.lastCheckError === "string" ? candidate.lastCheckError.slice(0, 1_000) : null, suppressedVersions: candidate.suppressedVersions ?? {}, // Backfill order matters for state files written before `installedVersion` // existed. Back then `recordUpdateInstalled()` set ONLY // `pendingRestartVersion`, leaving `lastUpdateVersion` on the previously // activated build — so a validated-but-not-yet-running update lives in // `pendingRestartVersion` and is the newer of the two. Reading // `lastUpdateVersion` first would report the superseded version as // installed and re-offer an update that is already on disk. installedVersion: typeof candidate.installedVersion === "string" ? candidate.installedVersion : typeof candidate.pendingRestartVersion === "string" ? candidate.pendingRestartVersion : typeof candidate.lastUpdateVersion === "string" ? candidate.lastUpdateVersion : null, pendingRestartVersion: typeof candidate.pendingRestartVersion === "string" ? candidate.pendingRestartVersion : null, deferredUpdate: isValidDeferredUpdate(candidate.deferredUpdate) ? candidate.deferredUpdate : null, lastFailure: isValidLastFailure(candidate.lastFailure) ? candidate.lastFailure : null, }; } catch { // Corrupt or unreadable JSON — return default state return getDefaultUpdateState(); } } /** * Save the update state to disk. * * @param state - The UpdateState to persist * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json) */ export function saveUpdateState(state, stateFilePath) { const filePath = resolveStatePath(stateFilePath); ensureParentDir(filePath); // A process-unique temp path prevents concurrent update checks from renaming // each other's file. The guard is single-owner now, but this also keeps // explicit update commands safe when they overlap. const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`; try { fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2), { mode: 0o600 }); fs.renameSync(tmpPath, filePath); } finally { try { fs.rmSync(tmpPath, { force: true }); } catch { // Best-effort cleanup after a successful rename or interrupted write. } } } /** * Check whether a version is currently suppressed (i.e., suppressed AND within the 24-hour window). * * @param version - Semver version string to check * @param stateFilePath - Override path for testing */ export function isVersionSuppressed(version, stateFilePath) { const state = loadUpdateState(stateFilePath); if (!state) { return false; } const entry = state.suppressedVersions[version]; if (!entry) { return false; } return Date.now() - Date.parse(entry.suppressedAt) < SUPPRESSION_TTL_MS; } /** * Add a version to the suppressed list and persist. * * @param version - Semver version string to suppress * @param reason - Human-readable reason for suppression * @param stateFilePath - Override path for testing */ export function suppressVersion(version, reason, stateFilePath) { const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState(); state.suppressedVersions[version] = { suppressedAt: new Date().toISOString(), reason, }; saveUpdateState(state, stateFilePath); } /** * Record a successful update: set lastUpdateAt and lastUpdateVersion, then persist. * * @param version - The version that was successfully installed * @param stateFilePath - Override path for testing */ export function recordSuccessfulUpdate(version, stateFilePath) { const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState(); state.lastUpdateAt = new Date().toISOString(); state.lastUpdateVersion = version; state.installedVersion = version; state.pendingRestartVersion = null; state.deferredUpdate = null; state.lastFailure = null; delete state.suppressedVersions[version]; saveUpdateState(state, stateFilePath); } /** Record that the package was validated but live activation is still pending. */ export function recordUpdateInstalled(version, stateFilePath) { const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState(); state.installedVersion = version; state.pendingRestartVersion = version; state.lastFailure = null; saveUpdateState(state, stateFilePath); } /** Abandon a matching installed version so the next cycle may reinstall it. */ export function abandonPendingUpdate(version, stateFilePath) { const state = loadUpdateState(stateFilePath); if (!state || state.pendingRestartVersion !== version) { return false; } state.pendingRestartVersion = null; saveUpdateState(state, stateFilePath); return true; } /** Persist a stage-specific updater failure so status remains actionable. */ export function recordUpdateFailure(version, stage, message, stateFilePath) { const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState(); state.lastFailure = { at: new Date().toISOString(), version, stage, message: message.trim().slice(0, 1_000), }; saveUpdateState(state, stateFilePath); } /** Persist why a discovered update is waiting without treating it as a failure. */ export function recordUpdateDeferred(version, reason, activeRequests, stateFilePath) { const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState(); const now = new Date().toISOString(); state.deferredUpdate = { version, since: state.deferredUpdate?.version === version ? state.deferredUpdate.since : now, updatedAt: now, reason, activeRequests, }; saveUpdateState(state, stateFilePath); } /** Clear matching updater deferral metadata once work can proceed. */ export function clearUpdateDeferral(version, stateFilePath) { const state = loadUpdateState(stateFilePath); if (!state?.deferredUpdate || (version !== undefined && state.deferredUpdate.version !== version)) { return false; } state.deferredUpdate = null; saveUpdateState(state, stateFilePath); return true; } /** Complete an updater-managed install after a manual or automatic restart. */ export function reconcileRunningUpdate(runningVersion, stateFilePath) { const state = loadUpdateState(stateFilePath); if (!state || state.pendingRestartVersion !== runningVersion) { return false; } recordSuccessfulUpdate(runningVersion, stateFilePath); return true; } /** * Record an update check: set lastCheckAt and lastCheckVersion, then persist. * * @param latestVersion - The latest version found during the check * @param stateFilePath - Override path for testing */ export function recordCheck(latestVersion, stateFilePath) { const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState(); state.lastCheckAt = new Date().toISOString(); state.lastCheckAttemptAt = state.lastCheckAt; state.lastCheckError = null; state.lastCheckVersion = latestVersion; saveUpdateState(state, stateFilePath); } /** Preserve the last successful registry version when discovery fails. */ export function recordCheckFailure(message, stateFilePath) { const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState(); state.lastCheckAttemptAt = new Date().toISOString(); state.lastCheckError = message.slice(0, 1_000); saveUpdateState(state, stateFilePath); }