diff --git a/apps/client/src/manifest.ts b/apps/client/src/manifest.ts index 203c068..0970435 100644 --- a/apps/client/src/manifest.ts +++ b/apps/client/src/manifest.ts @@ -2,15 +2,30 @@ import { updateFiles } from "./files"; import { MANIFEST } from "./server"; export const updateManifest = async (dir: string) => { - MANIFEST.version = await getVersion(); MANIFEST.files = []; await updateFiles(dir); + MANIFEST.version = await getVersion(dir); }; -const getVersion = async () => { +const getVersion = async (dir: string) => { try { return await Bun.file("public/version").text(); } catch { - return "0.0.0"; + return await fingerprintFiles(dir); } }; + +const fingerprintFiles = async (dir: string): Promise => { + const stamps = await Promise.all( + MANIFEST.files.map(async ({ path }) => { + try { + const stat = await Bun.file(`${dir}${path}`).stat(); + return `${path}:${stat.size}:${stat.mtimeMs}`; + } catch { + return `${path}:missing`; + } + }), + ); + stamps.sort(); + return Bun.hash(stamps.join("|")).toString(16); +}; diff --git a/apps/website/src/cache/cache.ts b/apps/website/src/cache/cache.ts index f5d0236..969dbeb 100644 --- a/apps/website/src/cache/cache.ts +++ b/apps/website/src/cache/cache.ts @@ -10,18 +10,41 @@ import { Logger } from "../utils/logger.utils"; import { setVersion } from "../version"; import { setLoadingStatus, setLoadingTotalFiles } from "../window"; +const CACHE_LOCK_NAME = "nanoforge-game-cache"; + +async function withCacheLock(fn: () => Promise): Promise { + if (typeof navigator === "undefined" || !navigator.locks) return fn(); + return await navigator.locks.request(CACHE_LOCK_NAME, fn); +} + export class GameCache { private readonly logger: Logger = new Logger("Cache"); private readonly fs: FileSystemManager = new FileSystemManager("game"); async updateCache(manifest: IManifest, force = false): Promise { this.logger.info("Starting cache game files"); - let extendedManifest: IExtendedManifest | undefined = await this._parseCache(manifest); - if (force || !isManifestUpToDate(manifest) || !extendedManifest) - extendedManifest = await this._updateCacheProcess(manifest); + + let extendedManifest = await this._tryReuseCache(manifest, force); + + if (!extendedManifest) { + extendedManifest = await withCacheLock(async () => { + return ( + (await this._tryReuseCache(manifest, force)) ?? (await this._updateCacheProcess(manifest)) + ); + }); + } + setVersion(manifest.version); this.logger.info("Game files cached"); - return extendedManifest as IExtendedManifest; + return extendedManifest; + } + + private async _tryReuseCache( + manifest: IManifest, + force: boolean, + ): Promise { + if (force || !isManifestUpToDate(manifest)) return undefined; + return this._parseCache(manifest); } private async _updateCacheProcess(manifest: IManifest): Promise { diff --git a/apps/website/src/index.ts b/apps/website/src/index.ts index 991b999..f5a58e3 100644 --- a/apps/website/src/index.ts +++ b/apps/website/src/index.ts @@ -20,7 +20,7 @@ const runLoad = async () => { const manifest = await getManifest(); runWatcher(manifest.watch); const cache = new GameCache(); - const extendedManifest = await cache.updateCache(manifest, true); + const extendedManifest = await cache.updateCache(manifest); const [files, mainModule] = await loadGameFiles(extendedManifest); const env = await getEnv(); setLoadingStatus("Starting game"); diff --git a/apps/website/src/manifest.ts b/apps/website/src/manifest.ts index 8fa739c..9c3931a 100644 --- a/apps/website/src/manifest.ts +++ b/apps/website/src/manifest.ts @@ -15,5 +15,5 @@ export const isManifestUpToDate = (manifest: IManifest): boolean => { setLoadingStatus("Verifying manifest"); const currentVersion = getVersion(); if (!currentVersion || currentVersion !== manifest.version) return false; - return false; + return true; };