Defend against stale skill-tree JSON cache
A friend on Chrome hit "C.subtrees is not iterable" — Chrome was serving
the previous skill-tree JSON shape ({nodes, edges} at top level) out of
the disk cache while running the new bundle that expects {subtrees}.
Hard reload fixed his session but other users could hit the same.
Three layers of defense:
1. backend/data.controller.ts: Cache-Control switches from
'public, max-age=3600' to 'no-cache, must-revalidate' so the browser
always revalidates JSON files against the server.
2. frontend/data.ts: every data fetch gets ?v=2 + cache:'no-cache' to
bust any existing browser/CDN entry.
3. frontend/data.ts loadSkillTree(): if the response still has the legacy
shape (no subtrees, but nodes array), wrap it in a single anonymous
subtree so the UI doesn't crash on iteration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9f53a58701
commit
25083d5572
2 changed files with 28 additions and 3 deletions
|
|
@ -35,7 +35,10 @@ const ALLOWED = new Set<string>([
|
||||||
export class DataController {
|
export class DataController {
|
||||||
@Get(':file')
|
@Get(':file')
|
||||||
@Header('Content-Type', 'application/json; charset=utf-8')
|
@Header('Content-Type', 'application/json; charset=utf-8')
|
||||||
@Header('Cache-Control', 'public, max-age=3600')
|
// These files change with the data extractor and schema. A 3600s cache
|
||||||
|
// bit us once — a browser held an older shape after a schema migration
|
||||||
|
// and crashed the UI. Force revalidation per request.
|
||||||
|
@Header('Cache-Control', 'no-cache, must-revalidate')
|
||||||
one(@Param('file') file: string, @Res() res: Response) {
|
one(@Param('file') file: string, @Res() res: Response) {
|
||||||
if (!ALLOWED.has(file)) {
|
if (!ALLOWED.has(file)) {
|
||||||
throw new BadRequestException('unknown data file');
|
throw new BadRequestException('unknown data file');
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,11 @@ import type {
|
||||||
const API = '/api/data';
|
const API = '/api/data';
|
||||||
|
|
||||||
async function getJSON<T>(file: string): Promise<T> {
|
async function getJSON<T>(file: string): Promise<T> {
|
||||||
const res = await fetch(`${API}/${file}`);
|
// cache-bust against stale browser caches that hold an older schema for
|
||||||
|
// these files. The query string is ignored server-side but defeats any
|
||||||
|
// long-lived Cache-Control we (or a CDN) may have previously sent.
|
||||||
|
const url = `${API}/${file}?v=2`;
|
||||||
|
const res = await fetch(url, { cache: 'no-cache' });
|
||||||
if (!res.ok) throw new Error(`fetch ${file}: ${res.status}`);
|
if (!res.ok) throw new Error(`fetch ${file}: ${res.status}`);
|
||||||
return res.json() as Promise<T>;
|
return res.json() as Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
@ -49,5 +53,23 @@ export async function loadFaction(house: House): Promise<FactionTable> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSkillTree(id: ClassId): Promise<SkillTree> {
|
export async function loadSkillTree(id: ClassId): Promise<SkillTree> {
|
||||||
return getJSON<SkillTree>(`skills-${id}.json`);
|
const raw = await getJSON<any>(`skills-${id}.json`);
|
||||||
|
// Backwards-compat: an older snapshot of this file had {nodes, edges} at
|
||||||
|
// the top level instead of {subtrees}. Treat it as a single anonymous
|
||||||
|
// subtree so the UI doesn't blow up on iteration.
|
||||||
|
if (raw && !raw.subtrees && Array.isArray(raw.nodes)) {
|
||||||
|
return {
|
||||||
|
id: raw.id,
|
||||||
|
name: raw.name,
|
||||||
|
subtrees: [
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
cols: Math.max(1, ...raw.nodes.map((n: any) => n.col || 1)),
|
||||||
|
nodes: raw.nodes,
|
||||||
|
edges: raw.edges || [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as SkillTree;
|
||||||
|
}
|
||||||
|
return raw as SkillTree;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue