|
| 1 | +/** |
| 2 | + * Client-safe types + pure helpers for the reference catalogue. |
| 3 | + * |
| 4 | + * Split out of `./reference` so client components (use-client files |
| 5 | + * like `EntityLink`, `EntityHoverCard`, `event-summary-react`) can |
| 6 | + * import the type surface without dragging in the server-only |
| 7 | + * fetchers — `./reference` imports `apiBase` from `./api`, which is |
| 8 | + * marked `import 'server-only'` for token-handling safety. Without |
| 9 | + * the split, importing any type from `./reference` taints the |
| 10 | + * client bundle and `next build` refuses to compile. |
| 11 | + * |
| 12 | + * The server-side wrapper `./reference` re-exports every symbol |
| 13 | + * from this module, so server callers keep working unchanged. |
| 14 | + */ |
| 15 | + |
| 16 | +import { toFriendlyName } from './heuristic-name'; |
| 17 | + |
| 18 | +export type ReferenceCategory = 'vehicle' | 'weapon' | 'item' | 'location'; |
| 19 | + |
| 20 | +export const CATEGORIES: ReadonlyArray<ReferenceCategory> = [ |
| 21 | + 'vehicle', |
| 22 | + 'weapon', |
| 23 | + 'item', |
| 24 | + 'location', |
| 25 | +]; |
| 26 | + |
| 27 | +/** Per-category curated summary, internally tagged by `category` |
| 28 | + * for discriminated narrowing. Mirrors the Rust `Summary` enum in |
| 29 | + * `crates/starstats-server/src/reference_data.rs`. Each variant's |
| 30 | + * fields are optional — empty / missing fields are stripped on |
| 31 | + * the wire (`skip_serializing_if = "Option::is_none"`). */ |
| 32 | +export type Summary = |
| 33 | + | VehicleSummary |
| 34 | + | WeaponSummary |
| 35 | + | ItemSummary |
| 36 | + | LocationSummary; |
| 37 | + |
| 38 | +export interface VehicleSummary { |
| 39 | + category: 'vehicle'; |
| 40 | + manufacturer?: string; |
| 41 | + role?: string; |
| 42 | + hull_size?: string; |
| 43 | + focus?: string; |
| 44 | +} |
| 45 | + |
| 46 | +export interface WeaponSummary { |
| 47 | + category: 'weapon'; |
| 48 | + manufacturer?: string; |
| 49 | + size?: string; |
| 50 | + damage_type?: string; |
| 51 | + weapon_type?: string; |
| 52 | +} |
| 53 | + |
| 54 | +export interface ItemSummary { |
| 55 | + category: 'item'; |
| 56 | + manufacturer?: string; |
| 57 | + item_type?: string; |
| 58 | + grade?: string; |
| 59 | +} |
| 60 | + |
| 61 | +export interface LocationSummary { |
| 62 | + category: 'location'; |
| 63 | + system?: string; |
| 64 | + parent?: string; |
| 65 | + tag?: string; |
| 66 | + classification?: string; |
| 67 | +} |
| 68 | + |
| 69 | +/** Empty/default summary for a given category — used when the |
| 70 | + * server returns nothing (or hasn't synced yet). Lets consumers |
| 71 | + * rely on `summary.category` being present even on a freshly- |
| 72 | + * defaulted entry. */ |
| 73 | +export function emptySummary(category: ReferenceCategory): Summary { |
| 74 | + switch (category) { |
| 75 | + case 'vehicle': |
| 76 | + return { category: 'vehicle' }; |
| 77 | + case 'weapon': |
| 78 | + return { category: 'weapon' }; |
| 79 | + case 'item': |
| 80 | + return { category: 'item' }; |
| 81 | + case 'location': |
| 82 | + return { category: 'location' }; |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | +/** Slim per-entry shape returned by `/v1/reference/{category}`. */ |
| 87 | +export interface ReferenceEntry { |
| 88 | + category: ReferenceCategory; |
| 89 | + class_name: string; |
| 90 | + display_name: string; |
| 91 | + /** URL-safe canonical identifier. Null on legacy rows pre-dating |
| 92 | + * the KB-v1 backfill — callers should fall back to a |
| 93 | + * `class_name` URL when null. */ |
| 94 | + slug?: string | null; |
| 95 | + /** Per-category curated fields as a discriminated union. */ |
| 96 | + summary: Summary; |
| 97 | +} |
| 98 | + |
| 99 | +/** Detail shape returned by per-entry endpoints, with the full |
| 100 | + * `metadata` blob retained for the detail page. The listing |
| 101 | + * endpoint never returns this shape — only the slim |
| 102 | + * `ReferenceEntry`. */ |
| 103 | +export interface ReferenceEntryDetail extends ReferenceEntry { |
| 104 | + metadata: Record<string, unknown>; |
| 105 | +} |
| 106 | + |
| 107 | +/** Map keyed by lowercased class_name → display_name. Retained for |
| 108 | + * the legacy `prettyClass` callers in `event-summary.ts` and the |
| 109 | + * journey/dashboard pages; new code should prefer |
| 110 | + * [`ReferenceCatalog`] which keeps slug + summary attached. */ |
| 111 | +export type ReferenceMap = ReadonlyMap<string, string>; |
| 112 | + |
| 113 | +/** Rich map keyed by lowercased class_name → full slim entry. */ |
| 114 | +export type ReferenceCatalog = ReadonlyMap<string, ReferenceEntry>; |
| 115 | + |
| 116 | +/** One Map per category. Each Map is empty (not absent) on fetch |
| 117 | + * failure so callers don't need a per-category presence check. */ |
| 118 | +export interface ReferenceLookup { |
| 119 | + vehicles: ReferenceMap; |
| 120 | + weapons: ReferenceMap; |
| 121 | + items: ReferenceMap; |
| 122 | + locations: ReferenceMap; |
| 123 | +} |
| 124 | + |
| 125 | +/** Empty lookup — safe default for callers before the fetch |
| 126 | + * resolves. */ |
| 127 | +export const EMPTY_REFERENCE_LOOKUP: ReferenceLookup = { |
| 128 | + vehicles: new Map(), |
| 129 | + weapons: new Map(), |
| 130 | + items: new Map(), |
| 131 | + locations: new Map(), |
| 132 | +}; |
| 133 | + |
| 134 | +/** Catalog form keyed by class_name → full slim entry, one per |
| 135 | + * category. Populated alongside `ReferenceLookup` from the same |
| 136 | + * fetch, so passing both around stays cheap. */ |
| 137 | +export interface ReferenceCatalogs { |
| 138 | + vehicles: ReferenceCatalog; |
| 139 | + weapons: ReferenceCatalog; |
| 140 | + items: ReferenceCatalog; |
| 141 | + locations: ReferenceCatalog; |
| 142 | +} |
| 143 | + |
| 144 | +/** Empty catalog set — safe default before the fetch resolves. */ |
| 145 | +export const EMPTY_REFERENCE_CATALOGS: ReferenceCatalogs = { |
| 146 | + vehicles: new Map(), |
| 147 | + weapons: new Map(), |
| 148 | + items: new Map(), |
| 149 | + locations: new Map(), |
| 150 | +}; |
| 151 | + |
| 152 | +/** Bundle of both views produced by a single category fetch. */ |
| 153 | +export interface CategoryBundle { |
| 154 | + map: ReferenceMap; |
| 155 | + catalog: ReferenceCatalog; |
| 156 | +} |
| 157 | + |
| 158 | +export const EMPTY_CATEGORY_BUNDLE: CategoryBundle = { |
| 159 | + map: new Map(), |
| 160 | + catalog: new Map(), |
| 161 | +}; |
| 162 | + |
| 163 | +/** Outcome of a `getEntityDetail` call. `not_found` lets the |
| 164 | + * detail page render the dedicated 404 path; `error` lets the |
| 165 | + * page distinguish transient backend trouble from a genuinely |
| 166 | + * missing entity (the old single-`null` collapse rendered a |
| 167 | + * permanent "not found" on a transient 503, which is misleading). */ |
| 168 | +export type EntityDetailOutcome = |
| 169 | + | { kind: 'ok'; entry: ReferenceEntryDetail } |
| 170 | + | { kind: 'not_found' } |
| 171 | + | { kind: 'error'; reason: string }; |
| 172 | + |
| 173 | +/** |
| 174 | + * Resolve a raw class identifier through a category Map; on miss, |
| 175 | + * fall through to the heuristic prettifier so the UI never renders |
| 176 | + * a bare underscored identifier. Pure — no fetches, no I/O. |
| 177 | + */ |
| 178 | +export function prettyClass( |
| 179 | + raw: string | null | undefined, |
| 180 | + map: ReferenceMap, |
| 181 | +): string { |
| 182 | + if (!raw) return ''; |
| 183 | + return map.get(raw.toLowerCase()) ?? toFriendlyName(raw); |
| 184 | +} |
| 185 | + |
| 186 | +// -- Location catalog types (Wave 1: catalog-driven hierarchy) --------- |
| 187 | + |
| 188 | +/** Trimmed shape of a wiki location entry — only the fields we use |
| 189 | + * for hierarchy resolution. Sourced from the raw wiki JSON which |
| 190 | + * the server persists verbatim into `reference_registry.metadata`. */ |
| 191 | +export interface LocationEntry { |
| 192 | + /** Engine join key as the server stores it. Usually the wiki |
| 193 | + * `slug` (e.g. `aberdeen-2`) since the wiki has no |
| 194 | + * `class_name` field for locations. */ |
| 195 | + classKey: string; |
| 196 | + /** Canonical display name (`"Aberdeen"`). */ |
| 197 | + displayName: string; |
| 198 | + /** Parent system display from `star.name` (`"Stanton"`). */ |
| 199 | + system: string | null; |
| 200 | + /** Parent body from `parent.name`. Null when the entry IS a |
| 201 | + * planet or has no parent. */ |
| 202 | + parent: string | null; |
| 203 | + /** Engine-internal joined short form from `tag.name` |
| 204 | + * (`"Stanton1b"`). Primary match candidate against event |
| 205 | + * payloads. */ |
| 206 | + tag: string | null; |
| 207 | + /** URL slug (`"aberdeen-2"`). Match fallback. */ |
| 208 | + slug: string | null; |
| 209 | + /** `type.classification` — `"Star"` / `"Planet"` / `"Moon"` / |
| 210 | + * `"City"` / `"Station"` / `"Outpost"`. Drives display |
| 211 | + * decisions (e.g. don't render `parent` for a planet). */ |
| 212 | + classification: string | null; |
| 213 | +} |
| 214 | + |
| 215 | +/** Multi-index lookup over the location catalog. Several keys per |
| 216 | + * entry so the parser can match by name, by engine tag, or by slug |
| 217 | + * without knowing which form an event payload uses. */ |
| 218 | +export interface LocationCatalog { |
| 219 | + byName: ReadonlyMap<string, LocationEntry>; |
| 220 | + byTag: ReadonlyMap<string, LocationEntry>; |
| 221 | + bySlug: ReadonlyMap<string, LocationEntry>; |
| 222 | + display: ReferenceMap; |
| 223 | + /** Lookup of `class_name (lowercased) → slug`. Used by the |
| 224 | + * HierarchicalBucketList to turn aggregate leaves into |
| 225 | + * `/kb/location/{slug}` links when the leaf represents exactly |
| 226 | + * one wiki-known location. Empty when slug backfill hasn't run. */ |
| 227 | + slugByClass: ReadonlyMap<string, string>; |
| 228 | +} |
| 229 | + |
| 230 | +/** Empty catalog — safe default when the fetch fails or hasn't |
| 231 | + * resolved yet. */ |
| 232 | +export const EMPTY_LOCATION_CATALOG: LocationCatalog = { |
| 233 | + byName: new Map(), |
| 234 | + byTag: new Map(), |
| 235 | + bySlug: new Map(), |
| 236 | + display: new Map(), |
| 237 | + slugByClass: new Map(), |
| 238 | +}; |
| 239 | + |
| 240 | +/** Internal: listing-endpoint response shape used by the fetchers |
| 241 | + * in `./reference`. Exported here so the server-side wrapper can |
| 242 | + * type its JSON deserialisations against the same union the rest |
| 243 | + * of the codebase reads. */ |
| 244 | +export interface ReferenceListResponse { |
| 245 | + entries: Array<{ |
| 246 | + class_name: string; |
| 247 | + display_name: string; |
| 248 | + slug?: string | null; |
| 249 | + summary?: Summary; |
| 250 | + }>; |
| 251 | +} |
0 commit comments