File size: 3,520 Bytes
6111b2b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | import fs from "node:fs";
import path from "node:path";
import { SQLITE_FILE } from "@/lib/db/core";
import { getSettings } from "@/lib/db/settings";
import { applyRuntimeSettings, type RuntimeReloadChange } from "./runtimeSettings";
const DEFAULT_POLL_INTERVAL_MS = 5_000;
const MIN_POLL_INTERVAL_MS = 1_000;
let activeCheck: Promise<void> | null = null;
let queuedSource: string | null = null;
let pollTimer: ReturnType<typeof setInterval> | null = null;
let sqliteWatcher: fs.FSWatcher | null = null;
function getPollIntervalMs() {
const parsed = Number.parseInt(process.env.OMNIROUTE_CONFIG_HOT_RELOAD_MS || "", 10);
if (!Number.isFinite(parsed) || parsed < MIN_POLL_INTERVAL_MS) {
return DEFAULT_POLL_INTERVAL_MS;
}
return parsed;
}
function isRelevantSqliteChange(filename: string | null): boolean {
if (!filename || !SQLITE_FILE) return false;
const baseName = path.basename(SQLITE_FILE);
return filename === baseName || filename.startsWith(`${baseName}-`);
}
function logChanges(source: string, changes: RuntimeReloadChange[]) {
if (changes.length === 0) return;
console.log(
`[HOT_RELOAD] source=${source} reloaded sections: ${changes
.map((entry) => entry.section)
.join(", ")}`
);
}
async function runHotReloadCheck(source: string) {
const settings = await getSettings();
const changes = await applyRuntimeSettings(settings, { source });
logChanges(source, changes);
}
function queueHotReloadCheck(source: string) {
if (activeCheck) {
queuedSource = source;
return;
}
activeCheck = runHotReloadCheck(source)
.catch((error) => {
console.warn(
`[HOT_RELOAD] Runtime config reload failed for ${source}:`,
error instanceof Error ? error.message : error
);
})
.finally(() => {
activeCheck = null;
if (!queuedSource) return;
const nextSource = queuedSource;
queuedSource = null;
queueHotReloadCheck(nextSource);
});
}
export function startRuntimeConfigHotReload(options: { pollIntervalMs?: number } = {}) {
if (pollTimer || sqliteWatcher) return;
const pollIntervalMs = Math.max(
options.pollIntervalMs || getPollIntervalMs(),
MIN_POLL_INTERVAL_MS
);
pollTimer = setInterval(() => {
queueHotReloadCheck("hot-reload:poll");
}, pollIntervalMs);
if (typeof pollTimer === "object" && "unref" in pollTimer) {
(pollTimer as { unref?: () => void }).unref?.();
}
if (SQLITE_FILE) {
try {
sqliteWatcher = fs.watch(path.dirname(SQLITE_FILE), (_eventType, filename) => {
const normalizedFilename = typeof filename === "string" ? filename : filename?.toString();
if (isRelevantSqliteChange(normalizedFilename || null)) {
queueHotReloadCheck("hot-reload:fs-watch");
}
});
} catch (error) {
console.warn(
"[HOT_RELOAD] SQLite file watch unavailable, polling only:",
error instanceof Error ? error.message : error
);
}
}
console.log(
`[HOT_RELOAD] Runtime config hot-reload started (poll=${pollIntervalMs}ms, fsWatch=${
sqliteWatcher ? "on" : "off"
})`
);
queueHotReloadCheck("hot-reload:start");
}
export function stopRuntimeConfigHotReloadForTests() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
sqliteWatcher?.close();
sqliteWatcher = null;
queuedSource = null;
activeCheck = null;
}
|