const path = require('path'); const { createJsonFile } = require('./store'); const { fetchRecentNames, checkRequestAccess } = require('./tickets'); const REFRESH_HOURS = 1; // SDP is asked for fresh names at most this often, whoever asks const LOOKBACK_DAYS = 30; const PAGES = 5; // 500 tickets is plenty to find the active technicians and sites const MAX_NAMES = 500; // per list, so the file can't grow without end const FIELDS = ['sites', 'groups', 'technicians']; // Names seen in SDP, shared by all users and kept across restarts, so suggestions build up // over time. Typing a name by hand always works; this only saves people from typos. function createNameCache({ dataDir, seed }) { const file = createJsonFile(path.join(dataDir, 'names.json'), { sites: [], groups: [], technicians: [], fetchedAt: 0 }); let pending = null; function merge(found) { const value = file.get(); for (const field of FIELDS) { const byLower = new Map(value[field].map(n => [n.toLowerCase(), n])); for (const name of found[field] || []) { if (!byLower.has(name.toLowerCase())) byLower.set(name.toLowerCase(), name); } value[field] = [...byLower.values()].sort((a, b) => a.localeCompare(b)).slice(0, MAX_NAMES); } } // The cache is only handed out once the caller's own token has read from SDP, so the names // never reach someone SDP turns away. That token also refreshes the cache when it's due; a // failed refresh still returns the cached names, since the caller has already proved access. async function list(accessToken) { await checkRequestAccess(accessToken); if (Date.now() - file.get().fetchedAt > REFRESH_HOURS * 60 * 60 * 1000) { pending = pending || fetchRecentNames({ accessToken, days: LOOKBACK_DAYS, pages: PAGES }) .then(found => { merge(found); file.get().fetchedAt = Date.now(); return file.save(); }) .catch(err => { if (err.response?.status === 401) throw err; console.error('Could not refresh name suggestions:', err.response?.data || err.message); }) .finally(() => { pending = null; }); await pending; } const value = file.get(); merge(seed()); return Object.fromEntries(FIELDS.map(f => [f, value[f]])); } // a typed name that differs from SDP only in case is stored with SDP's spelling, since the // technician filters compare exactly function canonical(field, names) { const byLower = new Map(file.get()[field].map(n => [n.toLowerCase(), n])); return names.map(n => byLower.get(n.toLowerCase()) || n); } return { list, canonical }; } module.exports = { createNameCache };