UNPKG

@skybolt/server-adapter

Version:

Skybolt server adapter for Node.js/Bun - High-performance asset caching for multi-page applications

177 lines (154 loc) 4.54 kB
/** * Cache Digest - Cuckoo Filter implementation for Skybolt * * This is a read-only parser for digests created by the JavaScript client. * It's used to determine which assets the client already has cached. * * @module cache-digest */ export const FINGERPRINT_BITS = 12 export const BUCKET_SIZE = 4 /** * Compute FNV-1a hash of a string (32-bit). * * Uses BigInt for precise integer arithmetic to ensure cross-language * compatibility with PHP, Python, and Ruby implementations. * * @param {string} str - Input string * @returns {number} 32-bit hash value */ export function fnv1a(str) { let hash = 2166136261n const prime = 16777619n const mask = 0xFFFFFFFFn for (let i = 0; i < str.length; i++) { hash ^= BigInt(str.charCodeAt(i)) hash = (hash * prime) & mask } return Number(hash) } /** * Compute fingerprint for Cuckoo filter. * * @param {string} str - Input string * @returns {number} Fingerprint in range [1, 4095] */ export function fingerprint(str) { const hash = fnv1a(str) return (hash & ((1 << FINGERPRINT_BITS) - 1)) || 1 } /** * Compute primary bucket index for Cuckoo filter. * * @param {string} str - Input string * @param {number} numBuckets - Total number of buckets * @returns {number} Bucket index */ export function primaryBucket(str, numBuckets) { return fnv1a(str) % numBuckets } /** * Compute alternate bucket index for Cuckoo filter. * * @param {number} bucket - Current bucket index * @param {number} fp - Fingerprint value * @param {number} numBuckets - Total number of buckets * @returns {number} Alternate bucket index */ export function computeAlternateBucket(bucket, fp, numBuckets) { const fpHash = fnv1a(String(fp)) const bucketMask = numBuckets - 1 const offset = (fpHash | 1) & bucketMask return (bucket ^ offset) & bucketMask } /** * Cache Digest class for server-side lookup. * * Parses a base64-encoded Cuckoo filter digest from the sb_digest cookie * and provides lookup functionality. */ export class CacheDigest { #numBuckets = 0 #buckets = null #valid = false /** * Create a CacheDigest from a base64-encoded string. * * @param {string} digest - URL-safe base64-encoded digest * @returns {CacheDigest} A CacheDigest instance */ static fromBase64(digest) { return new CacheDigest(digest) } /** * @param {string} digest - Base64-encoded digest string */ constructor(digest) { this.#parseDigest(digest) } /** * Check if this is a valid digest. * * @returns {boolean} True if the digest was parsed successfully */ isValid() { return this.#valid } /** * Check if an item exists in the digest. * * @param {string} item - Item to look up (e.g., "src/css/main.css:hash123") * @returns {boolean} True if item might be in the filter (may have false positives) */ lookup(item) { if (!this.#valid) return false const fp = fingerprint(item) const i1 = primaryBucket(item, this.#numBuckets) const i2 = computeAlternateBucket(i1, fp, this.#numBuckets) return this.#bucketContains(i1, fp) || this.#bucketContains(i2, fp) } /** * Parse a base64-encoded digest. * * @param {string} digest - Base64-encoded digest */ #parseDigest(digest) { if (!digest) return try { // Handle URL-safe base64 let normalized = digest.replace(/-/g, '+').replace(/_/g, '/') while (normalized.length % 4) normalized += '=' const bytes = Buffer.from(normalized, 'base64') if (bytes.length < 5) return // Check version (must be 1) if (bytes[0] !== 1) return this.#numBuckets = (bytes[1] << 8) | bytes[2] const numFingerprints = this.#numBuckets * BUCKET_SIZE this.#buckets = new Uint16Array(numFingerprints) for (let i = 0; i < numFingerprints; i++) { const offset = 5 + i * 2 if (offset + 1 < bytes.length) { this.#buckets[i] = (bytes[offset] << 8) | bytes[offset + 1] } } this.#valid = true } catch { // Invalid digest, leave as invalid } } /** * Check if a bucket contains a fingerprint. * * @param {number} bucketIndex - Bucket index * @param {number} fp - Fingerprint to search for * @returns {boolean} True if found */ #bucketContains(bucketIndex, fp) { const offset = bucketIndex * BUCKET_SIZE for (let i = 0; i < BUCKET_SIZE; i++) { if (this.#buckets[offset + i] === fp) return true } return false } } export default CacheDigest