Analytics: Skripte ausliefern
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
da8bba2a1c
commit
28c7a90d8b
3 files changed
+168
-1
No files matched your search
@@ -0,0 +1,60 @@
|
||||
// GET/HEAD /a/w.js und /a/h.js (öffentlicher Listener): generierte Client-Skripte aus client/dist/.
|
||||
// Einmal beim Start in den Speicher geladen (roh + gzip); fehlt eine Datei, antwortet der Pfad mit 503.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const DEFAULT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'client', 'dist');
|
||||
const FILES = { '/a/w.js': 'w.js', '/a/h.js': 'h.js' };
|
||||
|
||||
export function createAssets({ distDir = DEFAULT_DIR, log = console } = {}) {
|
||||
const assets = new Map();
|
||||
for (const [path, file] of Object.entries(FILES)) {
|
||||
try {
|
||||
const raw = readFileSync(join(distDir, file));
|
||||
assets.set(path, { raw, gz: gzipSync(raw, { level: 9 }), etag: `"${crypto.createHash('sha256').update(raw).digest('hex')}"` });
|
||||
} catch (e) {
|
||||
log.error(`Analytics: Skript ${file} fehlt in ${distDir} (${e.code || e.message}); ${path} antwortet mit 503`);
|
||||
}
|
||||
}
|
||||
|
||||
// true, wenn die Anfrage ein Skript betraf.
|
||||
return function handle(req, res, pathname) {
|
||||
if (!(pathname in FILES)) return false;
|
||||
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
||||
res.writeHead(405, { Allow: 'GET, HEAD' });
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
const a = assets.get(pathname);
|
||||
if (!a) {
|
||||
res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
||||
res.end(req.method === 'HEAD' ? '' : JSON.stringify({ error: 'Skript nicht verfügbar' }));
|
||||
return true;
|
||||
}
|
||||
const headers = {
|
||||
'Content-Type': 'text/javascript; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
ETag: a.etag,
|
||||
Vary: 'Accept-Encoding',
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Cross-Origin-Resource-Policy': 'cross-origin',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
};
|
||||
const inm = req.headers['if-none-match'];
|
||||
if (inm && inm.split(',').some((t) => t.trim().replace(/^W\//, '') === a.etag)) {
|
||||
res.writeHead(304, headers);
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
const gzip = /\bgzip\b/i.test(req.headers['accept-encoding'] || '');
|
||||
const body = gzip ? a.gz : a.raw;
|
||||
if (gzip) headers['Content-Encoding'] = 'gzip';
|
||||
headers['Content-Length'] = body.length;
|
||||
res.writeHead(200, headers);
|
||||
res.end(req.method === 'HEAD' ? undefined : body);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { createIngest } from './ingest.js';
|
||||
import { createSalt } from './salt.js';
|
||||
import { createHeat } from './heat.js';
|
||||
import { computeStats } from './stats.js';
|
||||
import { createAssets } from './assets.js';
|
||||
|
||||
const MAX_ADMIN_BODY = 64 * 1024;
|
||||
const SITE_KEY = /^[a-z0-9]{16,40}$/;
|
||||
@@ -60,6 +61,7 @@ function validateSite(b) {
|
||||
function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged }) {
|
||||
const ingest = role === 'public' ? createIngest({ db, geo, salt: salt || createSalt({ now }), now }) : null;
|
||||
const heat = role === 'public' ? createHeat({ db, config, now }) : null;
|
||||
const assets = role === 'public' ? createAssets({ distDir: config.distDir }) : null;
|
||||
const clock = now || Date.now;
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
@@ -69,6 +71,7 @@ function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged
|
||||
|
||||
if (ingest && await ingest(req, res, url.pathname)) return;
|
||||
if (heat && heat(req, res, url)) return;
|
||||
if (assets && assets(req, res, url.pathname)) return;
|
||||
|
||||
if (method === 'GET' && url.pathname === '/a/health') return send(res, 200, { ok: true });
|
||||
|
||||
@@ -127,7 +130,7 @@ function createHandlerServer({ config, db, role, geo, salt, now, onSitesChanged
|
||||
return server;
|
||||
}
|
||||
|
||||
// Öffentlicher Listener: /a/health und /a/e (Erfassung); Admin-Routen gibt es hier nicht.
|
||||
// Öffentlicher Listener: /a/health, /a/e (Erfassung), /a/heat, /a/w.js und /a/h.js; Admin-Routen gibt es hier nicht.
|
||||
export const createPublicServer = ({ config, db, geo, salt, now }) => createHandlerServer({ config, db, role: 'public', geo, salt, now });
|
||||
// Interner Listener für die Website-Verwaltung.
|
||||
// onSitesChanged: wird nach jeder Änderung an Websites gerufen (public.invalidateSites()).
|
||||
|
||||
Reference in new issue
Block a user