All files / src/composables useOpenStackResourceCache.ts

0% Statements 0/117
100% Branches 1/1
100% Functions 1/1
0% Lines 0/117

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
/**
 * Display-Cache für OpenStack-Resourcen.
 *
 * Hintergrund: Der Picker speichert je nach ``osMode`` entweder die UUID
 * oder den Namen einer Resource. Der User soll im Trigger-Button und in
 * der Summary-View aber IMMER den schönen Namen sehen — auch wenn der
 * gespeicherte Wert eine UUID ist.
 *
 * Lösung: Modul-globaler Cache, gefüllt von jedem Picker, der lädt.
 * - Mehrere Picker für denselben ``osType`` teilen den Cache (kein
 *   Re-Fetch).
 * - Summary-View kann via ``getDisplayName(osType, mode, value)``
 *   synchron auf den Cache zugreifen, ODER ``ensureLoaded(osType)``
 *   einmalig im Mount triggern.
 * - Cache-TTL: gleichgesetzt mit Backend-Cache (60s) — danach wird
 *   beim nächsten Picker-Open neu geladen.
 *
 * REAKTIVITÄT: Der Cache selbst ist eine plain ``Map``, aber ein Vue-
 * ``ref`` (``cacheVersion``) wird bei jedem ``prime``/``invalidate``
 * incrementiert. Konsumenten lesen den Cache via ``getDisplayName`` /
 * ``getItems``, die ``cacheVersion.value`` mitlesen — damit hängen die
 * computeds, die diese Funktionen aufrufen, an der Vue-Reactivity und
 * laufen neu, sobald andere Komponenten den Cache aktualisieren.
 */
import { ref } from 'vue'
import {
  openstackResourcesApi,
  type OsResourceType,
  type OsResourceBase,
} from '@/api/openstack-resources.api'
 
interface CachedItem {
  id: string
  name: string
  // Sekundäre Display-Info, falls vom Picker mitgegeben (z.B.
  // Flavor-Specs). Optional — Summary-View nutzt nur ``name``.
  secondary?: string
  raw: any
}
 
interface CacheEntry {
  items: CachedItem[]
  loadedAt: number
  // Pending-Promise — verhindert Donnerherden, wenn mehrere Picker
  // gleichzeitig dieselbe Liste anfordern.
  loading?: Promise<void>
}
 
// In-Memory-Cache, prozesslokal pro Browser-Tab. Reload löscht.
const cache = new Map<OsResourceType, CacheEntry>()
 
// Reaktiver Versions-Counter. JEDE Mutation des Caches incrementiert
// den. Konsumenten (Picker, Summary-View) lesen ``cacheVersion.value``
// in ihren computeds — Vue trackt die Abhängigkeit und re-läuft
// computeds, wenn der Cache sich ändert.
const cacheVersion = ref(0)
 
const TTL_MS = 60_000
 
/**
 * Holt einen Eintrag aus dem Cache, NICHT älter als TTL. Liefert
 * ``null``, wenn nicht vorhanden / abgelaufen.
 */
function getFresh(osType: OsResourceType): CacheEntry | null {
  const entry = cache.get(osType)
  if (!entry) return null
  if (Date.now() - entry.loadedAt > TTL_MS) return null
  return entry
}
 
/**
 * Stellt sicher, dass die Liste für ``osType`` geladen ist (oder
 * gerade lädt). Dedupliziert parallele Aufrufe.
 *
 * Optionale Filter (z.B. ``network_id`` für Subnets) werden NICHT
 * gecached — Filter-spezifische Listen sind die Verantwortung des
 * jeweiligen Pickers, nicht dieses zentralen Display-Caches.
 *
 * Wirft NICHT — Fehler werden geschluckt; Cache bleibt einfach leer.
 * Picker-Komponenten haben eigene Fehler-Anzeige.
 */
export async function ensureLoaded(osType: OsResourceType): Promise<void> {
  const fresh = getFresh(osType)
  if (fresh && !fresh.loading) return
  if (fresh?.loading) {
    await fresh.loading
    return
  }
 
  const loading = (async () => {
    try {
      const res = await fetchList(osType)
      cache.set(osType, {
        items: res.map(toCachedItem),
        loadedAt: Date.now(),
      })
      cacheVersion.value += 1
    } catch {
      // Bewusst kein Throw — Cache bleibt leer, Picker zeigt seine
      // eigene Error-UI.
    }
  })()
 
  // Pending-Marker setzen, damit parallele Aufrufe warten statt
  // selbst neu zu fetchen. ``cacheVersion`` NICHT incrementieren —
  // solange noch geladen wird, hat sich die sichtbare Cache-Sicht
  // nicht geändert.
  cache.set(osType, {
    items: cache.get(osType)?.items || [],
    loadedAt: cache.get(osType)?.loadedAt || 0,
    loading,
  })
 
  await loading
  // ``loading`` aus dem Eintrag wieder entfernen.
  const final = cache.get(osType)
  if (final) {
    delete final.loading
  }
}
 
/**
 * Schreibt eine bereits gefetchte Liste in den Cache. Wird vom Picker
 * aufgerufen, wenn er aus eigenen Gründen schon Items in der Hand hat
 * (z.B. mit Filter geladen, oder gerade refresht). Verhindert
 * Doppel-Fetch durch andere Picker oder die Summary.
 */
export function prime(osType: OsResourceType, items: ReadonlyArray<OsResourceBase & Record<string, any>>): void {
  cache.set(osType, {
    items: items.map(toCachedItem),
    loadedAt: Date.now(),
  })
  cacheVersion.value += 1
}
 
/**
 * Cache-Bust für genau einen ``osType`` (Refresh-Button).
 */
export function invalidate(osType: OsResourceType): void {
  if (cache.delete(osType)) {
    cacheVersion.value += 1
  }
}
 
/**
 * Komplette Cache-Invalidierung. Wird beim Logout aufgerufen — der
 * neue User hat seine eigenen OpenStack-Credentials, der Cache der
 * vorherigen Session darf nicht stehenbleiben.
 */
export function invalidateAll(): void {
  if (cache.size === 0) return
  cache.clear()
  cacheVersion.value += 1
}
 
/**
 * Synchroner Lookup. Liefert den Display-Namen für einen Roh-Wert
 * (UUID oder Name), abhängig vom Mode der Variable.
 *
 * - Mode='id', value=UUID → Lookup über ``items.id``, gibt ``name`` zurück
 * - Mode='name', value=name → bestätigt Existenz, gibt name zurück
 * - Cache-Miss / unbekannter Wert → gibt den Roh-Wert zurück mit
 *   ``known: false``
 * - Cross-Mode-Fallback: Trifft der primäre Mode-Lookup nicht zu, wird
 *   der ANDERE Mode probiert. Beispiel: Variable speichert UUIDs, aber
 *   der HCL-Default ist ein Name (Bug #4-Paar) — dann findet der
 *   id-Lookup nichts, der name-Lookup aber schon. In dem Fall liefern
 *   wir ``known: true`` mit ``modeMismatch: true``, damit der Caller
 *   einen subtilen Hinweis rendern kann ("falscher Mode-Default").
 *
 * Liefert ``null``, wenn ``value`` leer ist.
 *
 * Liest ``cacheVersion.value``, damit jede computed, die diese Funktion
 * aufruft, automatisch neu läuft, sobald sich der Cache ändert.
 */
export function getDisplayName(
  osType: OsResourceType,
  mode: 'id' | 'name',
  value: string | null | undefined,
): { name: string; known: boolean; modeMismatch?: boolean } | null {
  // Reaktive Abhängigkeit registrieren — siehe Modul-Docstring.
  void cacheVersion.value
  if (!value) return null
  const entry = cache.get(osType)
  if (!entry) {
    return { name: value, known: false }
  }
  const found = mode === 'id'
    ? entry.items.find((it) => it.id === value)
    : entry.items.find((it) => it.name === value)
  if (found) {
    return { name: found.name, known: true }
  }
  // Cross-Mode-Fallback: primärer Mode-Lookup ist gescheitert. Wir
  // probieren den jeweils anderen Mode — typischerweise speichert die
  // Variable UUIDs, der hinterlegte Default ist aber ein Name (oder
  // umgekehrt). Findet der Fallback ein Match, geben wir den Namen
  // zurück und setzen ``modeMismatch``, damit das UI dem Nutzer den
  // Mismatch sichtbar machen kann ohne den Wert zu blockieren.
  const fallback = mode === 'id'
    ? entry.items.find((it) => it.name === value)
    : entry.items.find((it) => it.id === value)
  if (fallback) {
    return { name: fallback.name, known: true, modeMismatch: true }
  }
  return { name: value, known: false }
}
 
/**
 * Alle Items eines Cache-Eintrags. Liest ebenfalls ``cacheVersion``,
 * damit Konsumenten reaktiv sind.
 */
export function getItems(osType: OsResourceType): CachedItem[] {
  void cacheVersion.value
  return cache.get(osType)?.items || []
}
 
// ----------------------------------------------------------------
// Internal: per-type Fetcher
// ----------------------------------------------------------------
async function fetchList(osType: OsResourceType): Promise<any[]> {
  switch (osType) {
    case 'network':
      return (await openstackResourcesApi.listNetworks()).data
    case 'subnet':
      return (await openstackResourcesApi.listSubnets()).data
    case 'flavor':
      return (await openstackResourcesApi.listFlavors()).data
    case 'image':
      return (await openstackResourcesApi.listImages('active')).data
    case 'keypair':
      return (await openstackResourcesApi.listKeypairs()).data
    case 'security_group':
      return (await openstackResourcesApi.listSecurityGroups()).data
    case 'floating_ip_pool':
      return (await openstackResourcesApi.listFloatingIpPools()).data
    case 'volume':
      return (await openstackResourcesApi.listVolumes()).data
    case 'router':
      return (await openstackResourcesApi.listRouters()).data
    case 'availability_zone':
      return (await openstackResourcesApi.listAvailabilityZones('compute')).data
  }
}
 
function toCachedItem(raw: any): CachedItem {
  return {
    id: raw.id ?? raw.name ?? '',
    name: raw.name ?? '',
    raw,
  }
}