@humanspeak/svelte-markdown
Version:
Fast, customizable markdown renderer for Svelte with built-in caching, TypeScript support, and Svelte 5 runes
239 lines (238 loc) • 7.47 kB
JavaScript
/**
* Token Cache
*
* Efficient caching layer for parsed markdown tokens.
* Built on top of the existing MemoryCache infrastructure with
* specialized features for markdown parsing optimization.
*
* Key features:
* - Fast FNV-1a hashing for cache keys
* - LRU eviction (inherited from MemoryCache)
* - TTL support (inherited from MemoryCache)
* - Handles large markdown documents efficiently
*
* @module token-cache
*/
import { MemoryCache } from './cache.js';
/**
* Fast non-cryptographic hash function using FNV-1a algorithm.
* Optimized for speed over cryptographic security.
*
* FNV-1a (Fowler-Noll-Vo) provides:
* - Fast computation (single pass through string)
* - Good distribution (minimal collisions)
* - Small output size (base36 string)
*
* @param str - String to hash
* @returns Base36 hash string
*
* @example
* ```typescript
* const hash1 = hashString('# Hello World')
* const hash2 = hashString('# Hello World!')
* console.log(hash1 !== hash2) // true - different content = different hash
* ```
*/
function hashString(str) {
let hash = 2166136261; // FNV offset basis (32-bit)
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
// FNV prime multiply using bit shifts (faster than multiplication)
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
// Convert to unsigned 32-bit integer and base36 string
return (hash >>> 0).toString(36);
}
/**
* Generates a cache key from markdown source and parser options.
* Combines hashes of both source content and options to ensure
* different parsing configurations are cached separately.
*
* Handles non-serializable options (extensions, tokenizer, hooks, etc.) by:
* - Serializing functions by name/toString for stable keys
* - Detecting and handling circular references
* - Including ALL options that affect parsing
*
* @param source - Raw markdown string
* @param options - Svelte markdown parser options
* @returns Composite cache key
*
* @example
* ```typescript
* const key1 = getCacheKey('# Test', { gfm: true })
* const key2 = getCacheKey('# Test', { gfm: false })
* console.log(key1 !== key2) // true - different options = different key
* ```
*/
function getCacheKey(source, options) {
const sourceHash = hashString(source);
// Safely serialize all options including functions and objects
const seen = new WeakSet();
const optionsHash = hashString(JSON.stringify(options, (_, value) => {
// Serialize functions by their name or source code
if (typeof value === 'function') {
return value.name || value.toString();
}
// Handle circular references
if (value && typeof value === 'object') {
if (seen.has(value))
return '[Circular]';
seen.add(value);
}
return value;
}));
return `${sourceHash}:${optionsHash}`;
}
/**
* Specialized cache for markdown token storage.
* Extends MemoryCache with markdown-specific convenience methods.
*
* Inherits from MemoryCache:
* - Automatic LRU eviction when maxSize is reached
* - TTL-based expiration for time-sensitive content
* - Advanced deletion (deleteByPrefix, deleteByMagicString)
*
* Performance characteristics:
* - Cache hit: <1ms (vs 50-200ms parsing)
* - Memory: ~5MB for 50 cached documents (default maxSize)
* - Hash computation: ~0.05ms for 10KB, ~0.5ms for 100KB
*
* @class TokenCache
* @extends MemoryCache<Token[] | TokensList>
*
* @example
* ```typescript
* // Create cache with custom settings
* const cache = new TokenCache({
* maxSize: 100, // Cache up to 100 documents
* ttl: 5 * 60 * 1000 // Expire after 5 minutes
* })
*
* // Check cache before parsing
* const cached = cache.getTokens(markdown, options)
* if (cached) {
* return cached // Fast path - no parsing needed
* }
*
* // Parse and cache
* const tokens = lexer.lex(markdown)
* cache.setTokens(markdown, options, tokens)
* ```
*/
export class TokenCache extends MemoryCache {
/**
* Creates a new TokenCache instance.
*
* @param options - Cache configuration
* @param options.maxSize - Maximum number of documents to cache (default: 50)
* @param options.ttl - Time-to-live in milliseconds (default: 5 minutes)
*/
constructor(options) {
super({ maxSize: 50, ttl: 5 * 60 * 1000, ...options });
}
/**
* Retrieves cached tokens for given markdown source and options.
* Returns undefined if not cached or expired.
*
* @param source - Raw markdown string
* @param options - Svelte markdown parser options
* @returns Cached tokens or undefined
*
* @example
* ```typescript
* const tokens = cache.getTokens('# Hello', { gfm: true })
* if (tokens) {
* console.log('Cache hit!')
* }
* ```
*/
getTokens(source, options) {
const key = getCacheKey(source, options);
return this.get(key);
}
/**
* Stores parsed tokens in cache for given markdown source and options.
* If cache is full, oldest entry is evicted (LRU).
*
* @param source - Raw markdown string
* @param options - Svelte markdown parser options
* @param tokens - Parsed token array or token list
*
* @example
* ```typescript
* const tokens = lexer.lex(markdown)
* cache.setTokens(markdown, options, tokens)
* ```
*/
setTokens(source, options, tokens) {
const key = getCacheKey(source, options);
this.set(key, tokens);
}
/**
* Checks if tokens are cached without retrieving them.
* Useful for cache statistics or conditional logic.
*
* @param source - Raw markdown string
* @param options - Svelte markdown parser options
* @returns True if cached and not expired
*
* @example
* ```typescript
* if (cache.hasTokens(markdown, options)) {
* console.log('Cache hit - no parsing needed')
* }
* ```
*/
hasTokens(source, options) {
const key = getCacheKey(source, options);
return this.has(key);
}
/**
* Removes cached tokens for specific source and options.
*
* @param source - Raw markdown string
* @param options - Svelte markdown parser options
* @returns True if entry was removed, false if not found
*
* @example
* ```typescript
* cache.deleteTokens(markdown, options) // Remove specific cached entry
* ```
*/
deleteTokens(source, options) {
const key = getCacheKey(source, options);
return this.delete(key);
}
/**
* Removes all cached tokens from the cache.
*
* @example
* ```typescript
* cache.clearAllTokens() // Clear entire cache
* ```
*/
clearAllTokens() {
this.clear();
}
}
/**
* Global singleton instance for shared token caching.
* Use this instance across your application for maximum cache efficiency.
*
* @example
* ```typescript
* import { tokenCache } from './token-cache.js'
*
* const cached = tokenCache.getTokens(markdown, options)
* if (!cached) {
* const tokens = lexer.lex(markdown)
* tokenCache.setTokens(markdown, options, tokens)
* }
* ```
*/
export const tokenCache = new TokenCache();
/**
* Export hash function for testing purposes.
* @internal
*/
export { hashString };