npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@dimensional-innovations/electron-asset-cache

v1.0.0

Published

Transparent asset caching for Electron apps with automatic garbage collection

Downloads

9

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-cache

Or with npm:

npm install @dimensional-innovations/electron-asset-cache

Quick 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.jpg

How It Works

The asset loader uses Electron's webRequest API to transparently intercept and cache asset requests:

  1. Intercept - Catches requests to configured domains
  2. Check Cache - Returns cached asset if available
  3. Download - Downloads uncached assets to local storage
  4. Redirect - Serves cached assets through assets:// protocol
  5. Monitor - Watches for API responses (Content-Type: application/json)
  6. Mark - When API response detected, marks all cached assets for deletion
  7. Access Window - Waits 2 seconds for your app to access current assets
  8. Unmark - Assets accessed during window are kept
  9. 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:

  1. Marks all cached assets for potential deletion
  2. Waits sweepDelay milliseconds (default: 2000ms) for your app to access current assets
  3. Any asset accessed during this window is unmarked and kept
  4. Remaining marked assets are deleted from both memory and disk
  5. 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-logo

Progress 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 cached
getCacheInfo(): { 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 first
  • normal - Default priority
  • low - 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 baseUrls exactly 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() inside app.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 build

Testing

yarn test              # Run all tests
yarn test:unit         # Unit tests only
yarn test:integration  # Integration tests only
yarn test:coverage     # With coverage

Code Quality

yarn lint              # Run ESLint
yarn typecheck         # Run TypeScript compiler
yarn check-all         # Run all checks

Need Help? See CLAUDE.md for comprehensive developer documentation.