export type AssetInput = { name: string type: string bytes: Uint8Array } export type AssetRef = { originalName: string mimeType: string bytes: Uint8Array hash8: string ext: string path: string // assets/. } export type AssetManagerOptions = { maxBytes: number allowedExts: string[] // e.g., ['png','jpg','jpeg','webp','svg'] } function extOf(name: string): string { const i = name.lastIndexOf('.') return i >= 0 ? name.slice(i + 1).toLowerCase() : '' } // Simple FNV-1a 32-bit hash -> 8-hex chars function hash8(bytes: Uint8Array): string { let h = 0x811c9dc5 >>> 0 for (let i = 0; i < bytes.length; i++) { h ^= bytes[i] h = Math.imul(h, 0x01000193) >>> 0 } return ('00000000' + h.toString(16)).slice(-8) } export class AssetManager { private readonly maxBytes: number private readonly allowed: Set private byHash = new Map() private byOriginal = new Map() // originalName -> path private referenced = new Set() // originalName referenced via rewrite constructor(opts: AssetManagerOptions) { this.maxBytes = opts.maxBytes this.allowed = new Set(opts.allowedExts.map((e) => e.toLowerCase())) } async add(file: AssetInput): Promise { const ext = extOf(file.name) if (!this.allowed.has(ext)) { throw new Error('Invalid extension') } if (file.bytes.length > this.maxBytes) { throw new Error('File size exceeded') } const h = hash8(file.bytes) const existing = this.byHash.get(h) if (existing) { // map original name for rewrite convenience if (!this.byOriginal.has(file.name)) this.byOriginal.set(file.name, existing.path) return existing } const path = `assets/${h}.${ext}` const ref: AssetRef = { originalName: file.name, mimeType: file.type, bytes: file.bytes, hash8: h, ext, path, } this.byHash.set(h, ref) if (!this.byOriginal.has(file.name)) this.byOriginal.set(file.name, path) return ref } list(): AssetRef[] { return Array.from(this.byHash.values()) } toZipStructure(): Record { const out: Record = {} for (const ref of this.byHash.values()) { out[ref.path] = ref.bytes } return out } rewriteUrl(urlOrLocalRef: string): string { if (/^https?:\/\//i.test(urlOrLocalRef)) return urlOrLocalRef if (urlOrLocalRef.startsWith('local:')) { const key = urlOrLocalRef.slice('local:'.length) const p = this.byOriginal.get(key) if (p) this.referenced.add(key) return p ?? urlOrLocalRef } return urlOrLocalRef } referencedList(): string[] { return Array.from(this.referenced) } }