@dimensional-innovations/electron-asset-cache
v1.0.0
Published
Transparent asset caching for Electron apps with automatic garbage collection
Downloads
9
Keywords
Readme
Electron Asset Cache
⚠️ Pre-release (v0.0.0): This package is under active development. API may change.
Transparent asset caching for Electron apps with automatic garbage collection. Cache remote assets locally with zero code changes in your renderer process.
Features
- Zero Renderer Changes - Enable caching in main process, renderer code works unchanged
- Automatic Cleanup - Mark-and-sweep garbage collection based on API responses
- Custom Protocol - Serves cached assets through
assets://protocol - Priority Queue - Downloads managed with priority-based queue (high/normal/low)
- Progress Tracking - Real-time progress events for downloads
- Smart Detection - Automatically detects JSON API responses and cleans up orphaned assets
- Type-Safe - Full TypeScript with strict mode
Installation
yarn add @dimensional-innovations/electron-asset-cacheOr with npm:
npm install @dimensional-innovations/electron-asset-cacheQuick Start
One-Line Setup
// main.ts
import { enableAssetCache } from '@dimensional-innovations/electron-asset-cache'
import { app } from 'electron'
app.whenReady().then(async () => {
// Enable transparent caching - that's it!
await enableAssetCache({ baseUrls: 'https://api.myapp.com' })
createWindow()
})With Configuration
import { enableAssetCache } from '@dimensional-innovations/electron-asset-cache'
app.whenReady().then(async () => {
await enableAssetCache({
baseUrls: ['https://api.myapp.com', 'https://cdn.myapp.com'],
patterns: ['*.jpg', '*.png', '*.mp4'], // Optional: specific file patterns
sweepDelay: 3000, // Optional: cleanup delay (default: 2000ms)
scheme: 'my-assets', // Optional: custom protocol (default: 'assets')
assetDir: 'custom-cache' // Optional: cache location (default: 'assets')
})
createWindow()
})Your Renderer Code Works Unchanged
That's it! Your renderer code needs no changes:
// renderer.ts - Works unchanged
const img = document.createElement('img')
img.src = 'https://api.myapp.com/assets/image.jpg'
// First request: downloads and caches
// Subsequent requests: served from cache as assets://image.jpgHow It Works
The asset loader uses Electron's webRequest API to transparently intercept and cache asset requests:
- Intercept - Catches requests to configured domains
- Check Cache - Returns cached asset if available
- Download - Downloads uncached assets to local storage
- Redirect - Serves cached assets through
assets://protocol - Monitor - Watches for API responses (Content-Type: application/json)
- Mark - When API response detected, marks all cached assets for deletion
- Access Window - Waits 2 seconds for your app to access current assets
- Unmark - Assets accessed during window are kept
- Sweep - Deletes unmarked assets from disk and memory
This ensures your cache mirrors exactly what the API returns - no orphaned files.
Automatic Garbage Collection
When the asset loader detects an API response (Content-Type: application/json), it:
- Marks all cached assets for potential deletion
- Waits
sweepDelaymilliseconds (default: 2000ms) for your app to access current assets - Any asset accessed during this window is unmarked and kept
- Remaining marked assets are deleted from both memory and disk
- The cycle repeats on the next API response
Your API is the single source of truth - only assets referenced in the latest API response remain cached.
Advanced Usage
Using Lower-Level APIs
For more control over initialization, use the lower-level APIs:
import { AssetLoader } from '@dimensional-innovations/electron-asset-cache'
import { enableWebRequestCache } from '@dimensional-innovations/electron-asset-cache/auto'
import { app, session } from 'electron'
app.whenReady().then(async () => {
// Create and configure the asset loader
const loader = new AssetLoader({
scheme: 'my-assets',
assetDir: 'custom/cache',
maxConcurrentDownloads: 5,
logger: customLogger
})
// Initialize the loader
await loader.initialize()
// Enable web request caching with full control
const interceptor = enableWebRequestCache(
loader,
{
baseUrls: ['https://api.myapp.com', 'https://cdn.myapp.com'],
patterns: ['*.jpg', '*.png', '*.gif', '*.mp4'],
sweepDelay: 3000
},
session.defaultSession,
customLogger
)
// Later: stop intercepting
// interceptor.stop()
createWindow()
})Manual Asset Downloads
// Download with high priority
const assetUrl = await loader.downloadAsset('https://example.com/critical.jpg', {
priority: 'high'
})
// Returns: 'assets://critical.jpg'
// Download with custom cache key
await loader.downloadAsset('https://example.com/logo.png', {
key: 'app-logo',
priority: 'high'
})
// Accessible at: assets://app-logoProgress Tracking
// Listen to download progress
loader.on('progress', (progress) => {
console.log(`${progress.url}: ${progress.percent}%`)
})
loader.on('complete', (result) => {
console.log(`Downloaded: ${result.url}`)
})
loader.on('error', (error) => {
console.error(`Failed: ${error.url}`, error.message)
})
// Get progress for specific download
const progress = loader.getDownloadProgress('https://example.com/video.mp4')
if (progress) {
console.log(`Progress: ${progress.percent}%`)
console.log(`Downloaded: ${progress.downloaded} / ${progress.total} bytes`)
}API Reference
enableAssetCache(config)
Primary "set it and forget it" API for enabling transparent asset caching.
Parameters:
interface AssetCacheConfig {
baseUrls: string | string[] // Required - API domain(s) to cache from
session?: Session // Optional - Electron session (default: defaultSession)
scheme?: string // Optional - Protocol scheme (default: 'assets')
assetDir?: string // Optional - Cache directory (default: 'assets')
patterns?: string[] // Optional - File patterns to cache
sweepDelay?: number // Optional - Cleanup delay in ms (default: 2000)
maxConcurrentDownloads?: number // Optional - Max parallel downloads (default: 3)
logger?: Logger // Optional - Custom logger
}Example:
await enableAssetCache({
baseUrls: ['https://api.myapp.com', 'https://cdn.myapp.com'],
patterns: ['*.jpg', '*.png', '*.mp4'],
sweepDelay: 3000
})AssetLoader
Main class for asset management.
Constructor
const loader = new AssetLoader({
scheme?: string // Custom protocol (default: 'assets')
assetDir?: string // Cache directory (default: 'assets')
maxConcurrentDownloads?: number // Max parallel downloads (default: 3)
logger?: Logger // Optional custom logger
})Methods
initialize(): Promise<void>
Initialize the asset loader. Must be called after app.whenReady().
await loader.initialize()downloadAsset(url, options?): Promise<string>
Manually download and cache an asset.
const assetUrl = await loader.downloadAsset('https://example.com/image.jpg', {
priority: 'high' | 'normal' | 'low', // Default: 'normal'
key: 'custom-key' // Optional: custom cache key
})
// Returns: 'assets://image.jpg' or 'assets://custom-key'getCachedAsset(urlOrKey): CachedAsset | undefined
Get information about a cached asset.
const asset = loader.getCachedAsset('https://example.com/image.jpg')
if (asset) {
console.log(asset.localPath, asset.size, asset.createdAt)
}isCached(urlOrKey): boolean
Check if an asset is cached.
if (loader.isCached('https://example.com/image.jpg')) {
console.log('Asset is cached')
}getAssetUrl(urlOrKey): string | null
Get the assets:// URL for a cached asset.
const assetUrl = loader.getAssetUrl('https://example.com/image.jpg')
// Returns: 'assets://image.jpg' or null if not cachedgetCacheInfo(): { totalSize: number, assets: CachedAsset[] }
Get cache statistics.
const info = loader.getCacheInfo()
console.log(`Total cached: ${info.totalSize} bytes`)
console.log(`Cached assets: ${info.assets.length}`)
info.assets.forEach(asset => {
console.log(`- ${asset.url}: ${asset.size} bytes`)
})getDownloadProgress(urlOrKey): DownloadProgress | undefined
Get download progress for a specific asset.
const progress = loader.getDownloadProgress('https://example.com/video.mp4')
if (progress) {
console.log(`${progress.percent}% (${progress.downloaded}/${progress.total})`)
}getQueueStatus(): QueueStatus
Get download queue status.
const status = loader.getQueueStatus()
console.log(`Queued: ${status.queueDepth}`)
console.log(`Active: ${status.activeDownloads}`)cancelDownload(urlOrKey): boolean
Cancel a pending or active download.
loader.cancelDownload('https://example.com/large-file.mp4')clearCache(): void
Clear in-memory cache. Files remain on disk.
loader.clearCache()pruneCache(maxAge): number
Remove cached assets older than maxAge milliseconds.
// Remove assets older than 24 hours
const removed = loader.pruneCache(24 * 60 * 60 * 1000)
console.log(`Removed ${removed} old assets`)shutdown(): Promise<void>
Shutdown the loader and cleanup resources.
await loader.shutdown()Events
The AssetLoader extends EventEmitter and emits the following events:
loader.on('progress', (progress: DownloadProgress) => {
// Emitted during download progress
})
loader.on('complete', (result: DownloadResult) => {
// Emitted when download completes successfully
})
loader.on('error', (error: DownloadError) => {
// Emitted when download fails
})enableWebRequestCache(loader, config, session, logger?)
Enable web request interception for automatic caching.
Parameters:
enableWebRequestCache(
loader: AssetLoader,
config: string | string[] | {
baseUrls: string[]
patterns?: string[]
sweepDelay?: number
},
session: Session,
logger?: Logger
)Examples:
// Simple - single URL
enableWebRequestCache(loader, 'https://api.myapp.com', session.defaultSession)
// Multiple URLs
enableWebRequestCache(
loader,
['https://api.myapp.com', 'https://cdn.myapp.com'],
session.defaultSession
)
// Full configuration
const interceptor = enableWebRequestCache(
loader,
{
baseUrls: ['https://api.myapp.com'],
patterns: ['*.jpg', '*.png', '*.gif', '*.mp4'],
sweepDelay: 3000
},
session.defaultSession,
customLogger
)
// Stop intercepting
interceptor.stop()Configuration Options
Default File Patterns
If you don't specify patterns, these are cached by default:
['*.jpg', '*.jpeg', '*.png', '*.gif', '*.svg',
'*.mp4', '*.webm', '*.mp3', '*.wav']Download Configuration
Default download settings:
{
maxConcurrentDownloads: 3,
chunkSize: 1048576, // 1MB
timeoutMs: 120000, // 120 seconds
retryAttempts: 3,
retryDelayMs: 1000
}Priority Queue
Downloads are processed by priority:
high- Processed firstnormal- Default prioritylow- Processed last
Within the same priority, downloads are FIFO (first in, first out).
Examples
React Integration
import { useState, useEffect } from 'react'
function ImageComponent({ url }: { url: string }) {
const [src, setSrc] = useState<string>(url)
useEffect(() => {
// Just use the URL - caching happens automatically
setSrc(url)
}, [url])
return <img src={src} alt="Cached image" />
}Vue Integration
<template>
<img :src="imageUrl" alt="Cached image" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
// Just use the URL - caching is automatic
const imageUrl = ref('https://api.myapp.com/image.jpg')
</script>Preloading Critical Assets
app.whenReady().then(async () => {
await enableAssetCache({ baseUrls: 'https://api.myapp.com' })
// Preload critical assets during startup
const loader = new AssetLoader()
await loader.initialize()
await Promise.all([
loader.downloadAsset('https://api.myapp.com/logo.png', { priority: 'high' }),
loader.downloadAsset('https://api.myapp.com/hero-bg.jpg', { priority: 'high' })
])
createWindow()
})Custom Logger
const loader = new AssetLoader({
logger: {
info: (msg, ...args) => console.log('[INFO]', msg, ...args),
warn: (msg, ...args) => console.warn('[WARN]', msg, ...args),
error: (msg, ...args) => console.error('[ERROR]', msg, ...args)
}
})Progress Bar Example
let lastUpdate = 0
loader.on('progress', (progress) => {
const now = Date.now()
// Throttle updates to max 10 per second
if (now - lastUpdate > 100) {
updateProgressBar(progress.percent)
lastUpdate = now
}
})Cache Storage
Assets are stored in your app's user data directory:
- macOS:
~/Library/Application Support/<app-name>/assets/ - Windows:
%APPDATA%/<app-name>/assets/ - Linux:
~/.config/<app-name>/assets/
Files are named using a sanitized version of the URL or custom key.
Troubleshooting
Assets Not Caching
Problem: All requests go to network, nothing is cached.
Solutions:
- Ensure
baseUrlsexactly matches request URLs (including protocol and port) - Try using pattern
['*']to catch all file types - Enable logging to see what's being intercepted:
const loader = new AssetLoader({
logger: {
info: console.log,
warn: console.warn,
error: console.error
}
})Protocol Registration Fails
Problem: Error when registering custom protocol.
Solutions:
- Ensure you call
enableAssetCache()insideapp.whenReady() - Check if protocol is already registered:
protocol.isProtocolRegistered('assets')
Downloads Stuck
Problem: Downloads stop progressing.
Solutions:
- Check network connectivity
- Increase timeout if needed (modify
DOWNLOAD_CONFIG) - Listen to error events:
loader.on('error', (error) => {
console.error('Download error:', error)
})Sweep Deleting Assets Too Quickly
Problem: Assets deleted while still in use.
Solution: Increase sweep delay:
await enableAssetCache({
baseUrls: 'https://api.myapp.com',
sweepDelay: 5000 // Wait 5 seconds instead of 2
})Performance Considerations
Memory Usage
- In-memory cache stores metadata for each cached asset
- Clear cache periodically if needed:
loader.clearCache() - Disk files remain after clearing memory cache
- Use
pruneCache(maxAge)to remove old assets
Download Concurrency
Default is 3 concurrent downloads. Increase for faster networks:
const loader = new AssetLoader({
maxConcurrentDownloads: 5
})Be careful not to overwhelm your network or the remote server.
Event Throttling
Progress events fire for every chunk (1MB). Throttle updates in your UI:
let lastUpdate = 0
loader.on('progress', (event) => {
const now = Date.now()
if (now - lastUpdate > 100) { // Max 10 updates/second
updateUI(event.percent)
lastUpdate = now
}
})Requirements
- Electron: 38.0.0 or higher
- Node.js: 22.21.0 or higher
Contributing
This is a private package for Dimensional Innovations. For issues or questions, contact the maintainers.
License
MIT
Development
Building
yarn buildTesting
yarn test # Run all tests
yarn test:unit # Unit tests only
yarn test:integration # Integration tests only
yarn test:coverage # With coverageCode Quality
yarn lint # Run ESLint
yarn typecheck # Run TypeScript compiler
yarn check-all # Run all checksNeed Help? See CLAUDE.md for comprehensive developer documentation.
