0.2.6-dev: merge claude/bw-l01 — host bandwidth layers 0 and 1 (no BGM download while the music is silent, WebSocket permessage-deflate, lossless WebP 玩法说明 pages, traffic counters, content-hashed asset URLs with immutable caching, brotli q5) on top of the 0.2.5 release; its note becomes DESIGN §31 in docs/history/0.2.6.md, CHANGELOG 0.2.6-dev, version 0.2.6-dev; golden: unchanged
This commit is contained in:
commit
3d5d6b380d
47 files changed
+2325
-136
No files matched your search
@@ -0,0 +1,78 @@
|
||||
// Asset URL paths shared by the server and the browser (host bandwidth, docs/ASSETS.md "The served manifests"):
|
||||
//
|
||||
// * the content-hash segment. The server hands out data/assets.json and data/local-assets.json with every file URL
|
||||
// carrying the first 16 hex digits of the file's SHA-256 (of the model's files for a Spine model) as its first
|
||||
// segment — `/assets/char/avatar/x.png` → `/assets/~0123456789abcdef/char/avatar/x.png`, audio at
|
||||
// `/media/~<hash>/…` (shared/media.js) — so a browser may keep it for a year. The server strips the segment and
|
||||
// serves the file at the plain path (server/http/static.js, media.js). A path segment, never `?v=`: several
|
||||
// loaders look at the URL's ending (`.skel`, `.mp3`) and pixi-spine finds the atlas and its pages next to the
|
||||
// skeleton, which a query string would not survive.
|
||||
// * trafficCategory: the category a request path counts under in the traffic counters (server/http/traffic.js,
|
||||
// /healthz?traffic=1; public/js/diag.js for the browser's own totals).
|
||||
|
||||
/** Hex digits of a content hash in a URL. */
|
||||
export const ASSET_HASH_HEX = 16;
|
||||
|
||||
/** The content-hash segment: `~` + ASSET_HASH_HEX lower-case hex digits. */
|
||||
export const HASH_SEGMENT_RE = /^~([0-9a-f]{16})$/;
|
||||
|
||||
/** URL roots whose first segment may be a content hash. */
|
||||
export const HASHED_ROOTS = Object.freeze(['/assets/', '/fonts/', '/media/']);
|
||||
|
||||
/**
|
||||
* Split the content-hash segment off a URL path.
|
||||
* '/assets/~0123456789abcdef/x.png' → { hash: '0123456789abcdef', path: '/assets/x.png' }; any other path (no segment,
|
||||
* a malformed one, another root) → { hash: null, path } unchanged.
|
||||
* @param {string} path a URL path (no query)
|
||||
* @returns {{ hash: string | null, path: string }}
|
||||
*/
|
||||
export function splitHashedPath(path) {
|
||||
const p = String(path ?? '');
|
||||
for (const root of HASHED_ROOTS) {
|
||||
if (!p.startsWith(root)) continue;
|
||||
const slash = p.indexOf('/', root.length);
|
||||
if (slash < 0) break;
|
||||
const m = HASH_SEGMENT_RE.exec(p.slice(root.length, slash));
|
||||
return m ? { hash: m[1], path: root + p.slice(slash + 1) } : { hash: null, path: p };
|
||||
}
|
||||
return { hash: null, path: p };
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL path with the content-hash segment after its root ('/assets/x.png', 'abc…' → '/assets/~abc…/x.png'). A
|
||||
* path outside HASHED_ROOTS, one that already has a segment, or no valid hash → the path unchanged.
|
||||
* @param {string} path
|
||||
* @param {string | null | undefined} hash
|
||||
*/
|
||||
export function hashedPath(path, hash) {
|
||||
const p = String(path ?? '');
|
||||
if (typeof hash !== 'string' || !HASH_SEGMENT_RE.test(`~${hash}`)) return p;
|
||||
const root = HASHED_ROOTS.find((r) => p.startsWith(r));
|
||||
if (!root || p.length === root.length || splitHashedPath(p).hash) return p;
|
||||
return `${root}~${hash}/${p.slice(root.length)}`;
|
||||
}
|
||||
|
||||
/** The URL with its content-hash segment removed (a URL derived from a hashed one names another file). */
|
||||
export const unhashedUrl = (url) => (typeof url === 'string' ? splitHashedPath(url).path : url);
|
||||
|
||||
/** Traffic categories, in report order. */
|
||||
export const TRAFFIC_CATEGORIES = Object.freeze(['assets', 'audio', 'code', 'data', 'other']);
|
||||
|
||||
/**
|
||||
* The traffic category of a request path (the content-hash segment does not matter): `audio` (/media/,
|
||||
* /assets/audio/), `assets` (/assets/, /fonts/, /icons/, the art of a content pack), `data` (/data/*.json, the UI
|
||||
* language packs /i18n/, the pack index, other JSON), `code` (the page, scripts, styles: /, *.js, *.css, *.html,
|
||||
* /vendor/, /sim/, /shared/), `other` (/healthz, error pages for anything else).
|
||||
* @param {string} path a URL path (no query)
|
||||
* @returns {'assets'|'audio'|'code'|'data'|'other'}
|
||||
*/
|
||||
export function trafficCategory(path) {
|
||||
const p = splitHashedPath(path).path;
|
||||
if (p.startsWith('/media/') || p.startsWith('/assets/audio/')) return 'audio';
|
||||
if (p.startsWith('/assets/') || p.startsWith('/fonts/') || p.startsWith('/icons/')) return 'assets';
|
||||
if (p.startsWith('/packs/')) return /\.json$/i.test(p) ? 'data' : 'assets';
|
||||
if (p.startsWith('/data/') || p.startsWith('/i18n/') || /\.json$/i.test(p)) return 'data';
|
||||
if (p === '/' || p.endsWith('/') || /\.(?:m?js|css|html?|map|webmanifest)$/i.test(p)
|
||||
|| p.startsWith('/vendor/') || p.startsWith('/sim/') || p.startsWith('/shared/')) return 'code';
|
||||
return 'other';
|
||||
}
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { N_ } from './i18n.js';
|
||||
export const PROTOCOL_VERSION = 1;
|
||||
/** Release version shown to players (title screen, server banner, /healthz). Kept equal to package.json "version"
|
||||
* (test/version.test.js); PROTOCOL_VERSION above is the separate wire-format number. */
|
||||
export const APP_VERSION = '0.2.5';
|
||||
export const APP_VERSION = '0.2.6-dev';
|
||||
/** A development build (the public `dev` branch): the title screen, the boot banner and the README say so, and
|
||||
* tools/package.mjs refuses to build a release zip from it without --allow-dev. */
|
||||
export const DEV_BUILD = /-dev$/.test(APP_VERSION);
|
||||
|
||||
+3
-1
@@ -7,7 +7,9 @@
|
||||
// /assets/audio/bgm/act1.mp3 → /media/bgm/act1
|
||||
//
|
||||
// — and `server/http/media.js` resolves it back to the real file under public/assets/audio, still answering with
|
||||
// `Content-Type: audio/mpeg`, Range support and the ETag of the file it resolved.
|
||||
// `Content-Type: audio/mpeg`, Range support and the ETag of the file it resolved. The content-hash segment of a served
|
||||
// manifest's URL (shared/assetPaths.js) comes along: `/assets/~<hash>/audio/bgm/act1.mp3` → `/media/~<hash>/bgm/act1`,
|
||||
// cached by the browser for a year while the hash names the file.
|
||||
//
|
||||
// Both sides import this module: the browser to decide which URLs to rewrite (`public/js/media.js`), the server to
|
||||
// decide which extensions a `/media/…` request may resolve to. A second copy of the list on one side would let them
|
||||
|
||||
Reference in new issue
Block a user