mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
perf(bootstrap): tier slow/fast data for ~46% CDN egress reduction (#838)
Split bootstrap endpoint into slow-changing (1h TTL: BIS rates, minerals, sectors, etc.) and fast-changing (10min TTL: earthquakes, outages, macro signals, etc.) tiers via ?tier=slow|fast query param. Frontend fetches both tiers in parallel with shared 800ms timeout. Partial failure is graceful — panels fall through to individual RPCs. Backward compatible: no ?tier= param returns all keys at s-maxage=600. Also removes orphaned ucdpEvents key (no getHydratedData consumer).
This commit is contained in:
@@ -12,17 +12,21 @@ describe('Bootstrap cache key registry', () => {
|
||||
const cacheKeysSrc = readFileSync(cacheKeysPath, 'utf-8');
|
||||
const bootstrapSrc = readFileSync(join(root, 'api', 'bootstrap.js'), 'utf-8');
|
||||
|
||||
const cacheKeysBlock = cacheKeysSrc.match(/BOOTSTRAP_CACHE_KEYS[^{]*\{([^}]+)\}/)?.[1] ?? '';
|
||||
|
||||
it('exports BOOTSTRAP_CACHE_KEYS with at least 10 entries', () => {
|
||||
const matches = cacheKeysSrc.match(/^\s+\w+:\s+'[^']+'/gm);
|
||||
const matches = cacheKeysBlock.match(/^\s+\w+:\s+'[^']+'/gm);
|
||||
assert.ok(matches && matches.length >= 10, `Expected ≥10 keys, found ${matches?.length ?? 0}`);
|
||||
});
|
||||
|
||||
it('api/bootstrap.js inlined keys match server/_shared/cache-keys.ts', () => {
|
||||
const extractKeys = (src) => {
|
||||
const block = src.match(/BOOTSTRAP_CACHE_KEYS[^=]*=\s*\{([^}]+)\}/);
|
||||
if (!block) return {};
|
||||
const re = /(\w+):\s+'([a-z_]+(?::[a-z_-]+)+:v\d+)'/g;
|
||||
const keys = {};
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) keys[m[1]] = m[2];
|
||||
while ((m = re.exec(block[1])) !== null) keys[m[1]] = m[2];
|
||||
return keys;
|
||||
};
|
||||
const canonical = extractKeys(cacheKeysSrc);
|
||||
@@ -40,7 +44,7 @@ describe('Bootstrap cache key registry', () => {
|
||||
const keyRe = /:\s+'([^']+)'/g;
|
||||
let m;
|
||||
const keys = [];
|
||||
while ((m = keyRe.exec(cacheKeysSrc)) !== null) {
|
||||
while ((m = keyRe.exec(cacheKeysBlock)) !== null) {
|
||||
keys.push(m[1]);
|
||||
}
|
||||
for (const key of keys) {
|
||||
@@ -52,7 +56,7 @@ describe('Bootstrap cache key registry', () => {
|
||||
const keyRe = /:\s+'([^']+)'/g;
|
||||
let m;
|
||||
const keys = [];
|
||||
while ((m = keyRe.exec(cacheKeysSrc)) !== null) {
|
||||
while ((m = keyRe.exec(cacheKeysBlock)) !== null) {
|
||||
keys.push(m[1]);
|
||||
}
|
||||
const unique = new Set(keys);
|
||||
@@ -63,7 +67,7 @@ describe('Bootstrap cache key registry', () => {
|
||||
const nameRe = /^\s+(\w+):/gm;
|
||||
let m;
|
||||
const names = [];
|
||||
while ((m = nameRe.exec(cacheKeysSrc)) !== null) {
|
||||
while ((m = nameRe.exec(cacheKeysBlock)) !== null) {
|
||||
names.push(m[1]);
|
||||
}
|
||||
const unique = new Set(names);
|
||||
@@ -71,10 +75,11 @@ describe('Bootstrap cache key registry', () => {
|
||||
});
|
||||
|
||||
it('every cache key maps to a handler file with a matching cache key string', () => {
|
||||
const block = cacheKeysSrc.match(/BOOTSTRAP_CACHE_KEYS[^{]*\{([^}]+)\}/);
|
||||
const keyRe = /:\s+'([^']+)'/g;
|
||||
let m;
|
||||
const keys = [];
|
||||
while ((m = keyRe.exec(cacheKeysSrc)) !== null) {
|
||||
while ((m = keyRe.exec(block[1])) !== null) {
|
||||
keys.push(m[1]);
|
||||
}
|
||||
|
||||
@@ -127,8 +132,9 @@ describe('Bootstrap endpoint (api/bootstrap.js)', () => {
|
||||
assert.ok(src.includes('missing'), 'Missing missing field in response');
|
||||
});
|
||||
|
||||
it('sets Cache-Control header with s-maxage', () => {
|
||||
assert.ok(src.includes('s-maxage=60'), 'Missing s-maxage=60 Cache-Control');
|
||||
it('sets Cache-Control header with s-maxage for both tiers', () => {
|
||||
assert.ok(src.includes('s-maxage=3600'), 'Missing s-maxage=3600 for slow tier');
|
||||
assert.ok(src.includes('s-maxage=600'), 'Missing s-maxage=600 for fast tier');
|
||||
assert.ok(src.includes('stale-while-revalidate'), 'Missing stale-while-revalidate');
|
||||
});
|
||||
|
||||
@@ -140,6 +146,13 @@ describe('Bootstrap endpoint (api/bootstrap.js)', () => {
|
||||
assert.ok(src.includes("'OPTIONS'"), 'Missing OPTIONS method handling');
|
||||
assert.ok(src.includes('getCorsHeaders'), 'Missing CORS headers');
|
||||
});
|
||||
|
||||
it('supports ?tier= query param for tiered fetching', () => {
|
||||
assert.ok(src.includes("'tier'"), 'Missing tier query param handling');
|
||||
assert.ok(src.includes('SLOW_KEYS'), 'Missing SLOW_KEYS set');
|
||||
assert.ok(src.includes('FAST_KEYS'), 'Missing FAST_KEYS set');
|
||||
assert.ok(src.includes('TIER_CACHE'), 'Missing TIER_CACHE map');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Frontend hydration (src/services/bootstrap.ts)', () => {
|
||||
@@ -159,19 +172,25 @@ describe('Frontend hydration (src/services/bootstrap.ts)', () => {
|
||||
});
|
||||
|
||||
it('has a fast timeout cap to avoid regressing startup', () => {
|
||||
const timeoutMatch = src.match(/AbortSignal\.timeout\((\d+)\)/);
|
||||
assert.ok(timeoutMatch, 'Missing AbortSignal.timeout');
|
||||
const timeoutMatch = src.match(/(?:AbortSignal\.timeout|setTimeout)\D+(\d+)\)/);
|
||||
assert.ok(timeoutMatch, 'Missing timeout');
|
||||
const ms = parseInt(timeoutMatch[1], 10);
|
||||
assert.ok(ms <= 2000, `Timeout ${ms}ms too high — should be ≤2000ms to avoid regressing startup`);
|
||||
});
|
||||
|
||||
it('fetches from /api/bootstrap', () => {
|
||||
assert.ok(src.includes('/api/bootstrap'), 'Missing /api/bootstrap fetch URL');
|
||||
it('fetches tiered bootstrap URLs', () => {
|
||||
assert.ok(src.includes('/api/bootstrap?tier='), 'Missing tiered bootstrap fetch URLs');
|
||||
});
|
||||
|
||||
it('handles fetch failure silently', () => {
|
||||
assert.ok(src.includes('catch'), 'Missing error handling — panels should fall through to individual calls');
|
||||
});
|
||||
|
||||
it('fetches both tiers in parallel', () => {
|
||||
assert.ok(src.includes('Promise.all'), 'Missing Promise.all for parallel tier fetches');
|
||||
assert.ok(src.includes("'slow'"), 'Missing slow tier fetch');
|
||||
assert.ok(src.includes("'fast'"), 'Missing fast tier fetch');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel hydration consumers', () => {
|
||||
@@ -194,12 +213,12 @@ describe('Panel hydration consumers', () => {
|
||||
describe('Bootstrap key hydration coverage', () => {
|
||||
it('every bootstrap key has a getHydratedData consumer in src/', () => {
|
||||
const bootstrapSrc = readFileSync(join(root, 'api', 'bootstrap.js'), 'utf-8');
|
||||
const block = bootstrapSrc.match(/BOOTSTRAP_CACHE_KEYS\s*=\s*\{([^}]+)\}/);
|
||||
const keyRe = /(\w+):\s+'[a-z_]+(?::[a-z_-]+)+:v\d+'/g;
|
||||
const keys = [];
|
||||
let m;
|
||||
while ((m = keyRe.exec(bootstrapSrc)) !== null) keys.push(m[1]);
|
||||
while ((m = keyRe.exec(block[1])) !== null) keys.push(m[1]);
|
||||
|
||||
// Gather all src/ .ts files
|
||||
const srcFiles = [];
|
||||
function walk(dir) {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
@@ -220,6 +239,62 @@ describe('Bootstrap key hydration coverage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bootstrap tier definitions', () => {
|
||||
const bootstrapSrc = readFileSync(join(root, 'api', 'bootstrap.js'), 'utf-8');
|
||||
const cacheKeysSrc = readFileSync(join(root, 'server', '_shared', 'cache-keys.ts'), 'utf-8');
|
||||
|
||||
function extractSetKeys(src, varName) {
|
||||
const re = new RegExp(`${varName}\\s*=\\s*new Set\\(\\[([^\\]]+)\\]`, 's');
|
||||
const m = src.match(re);
|
||||
if (!m) return new Set();
|
||||
return new Set([...m[1].matchAll(/'(\w+)'/g)].map(x => x[1]));
|
||||
}
|
||||
|
||||
function extractBootstrapKeys(src) {
|
||||
const block = src.match(/BOOTSTRAP_CACHE_KEYS\s*=\s*\{([^}]+)\}/);
|
||||
if (!block) return new Set();
|
||||
return new Set([...block[1].matchAll(/(\w+):\s+'/g)].map(x => x[1]));
|
||||
}
|
||||
|
||||
function extractTierKeys(src) {
|
||||
const block = src.match(/BOOTSTRAP_TIERS[^{]*\{([^}]+)\}/);
|
||||
if (!block) return {};
|
||||
const result = {};
|
||||
for (const m of block[1].matchAll(/(\w+):\s+'(slow|fast)'/g)) {
|
||||
result[m[1]] = m[2];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
it('SLOW_KEYS + FAST_KEYS cover all BOOTSTRAP_CACHE_KEYS with no overlap', () => {
|
||||
const slow = extractSetKeys(bootstrapSrc, 'SLOW_KEYS');
|
||||
const fast = extractSetKeys(bootstrapSrc, 'FAST_KEYS');
|
||||
const all = extractBootstrapKeys(bootstrapSrc);
|
||||
|
||||
const union = new Set([...slow, ...fast]);
|
||||
assert.deepEqual([...union].sort(), [...all].sort(), 'SLOW_KEYS ∪ FAST_KEYS must equal BOOTSTRAP_CACHE_KEYS');
|
||||
|
||||
const intersection = [...slow].filter(k => fast.has(k));
|
||||
assert.equal(intersection.length, 0, `Overlap between tiers: ${intersection.join(', ')}`);
|
||||
});
|
||||
|
||||
it('tier sets in bootstrap.js match BOOTSTRAP_TIERS in cache-keys.ts', () => {
|
||||
const slow = extractSetKeys(bootstrapSrc, 'SLOW_KEYS');
|
||||
const fast = extractSetKeys(bootstrapSrc, 'FAST_KEYS');
|
||||
const tiers = extractTierKeys(cacheKeysSrc);
|
||||
|
||||
for (const k of slow) {
|
||||
assert.equal(tiers[k], 'slow', `SLOW_KEYS has '${k}' but BOOTSTRAP_TIERS says '${tiers[k]}'`);
|
||||
}
|
||||
for (const k of fast) {
|
||||
assert.equal(tiers[k], 'fast', `FAST_KEYS has '${k}' but BOOTSTRAP_TIERS says '${tiers[k]}'`);
|
||||
}
|
||||
const tierKeys = new Set(Object.keys(tiers));
|
||||
const setKeys = new Set([...slow, ...fast]);
|
||||
assert.deepEqual([...tierKeys].sort(), [...setKeys].sort(), 'BOOTSTRAP_TIERS keys must match SLOW_KEYS ∪ FAST_KEYS');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Adaptive backoff adopters', () => {
|
||||
it('ServiceStatusPanel.fetchStatus returns Promise<boolean>', () => {
|
||||
const src = readFileSync(join(root, 'src/components/ServiceStatusPanel.ts'), 'utf-8');
|
||||
|
||||
Reference in New Issue
Block a user