Beat Loading & Caching System Overview
Introduction
The beat loading system in Pajama provides smooth, responsive story reading experiences through a sophisticated multi-layered caching architecture. The system features a streamlined coordination pattern with clear separation of concerns, actor-based thread safety, and intelligent preloading strategies.
System Architecture
┌─────────────────────────────────────────────┐
│ UI/Experience Layer │
├─────────────────────────────────────────────┤
│ StoryReader │ CoReading │ ← UI Components │
└─────────────────────┬───────────────────────┘
│
|
│
┌─────────────────────▼─────────────────────┐
│ StoryContentManager │ ← Single content orchestrator
│ "Manage story content experience" │ (@MainActor, events, coordination)
│ • getBeat() / loadBeat() │ • Story-wide loading strategy
│ • beginLoadingContent() │ • Beat + asset coordination
│ • UI event publishing │ • Priority management
└─────────┬─────────────┬─────────┬─────────┘
│ │ │
│ │ │ immediate asset needs
│ │ └───────────────────────────────────────────────────────────┐
│ ┌─────────▼─────────┐ ┌─────────────────┐ |
│ │ BeatPreloader │ ─── invokes for assets ──▶ │ AssetPreloader │ |
│ │ "Preload beats + │ (sliding window, choice- │ "Extract & load │ |
│ │ their assets" │ aware preloading) │ beat assets" │ |
│ │ │ │ │ |
│ └─────────┬─────────┘ └─────────┬───────┘ |
│ │ │ |
│ │ uses for beat loading │ delegates |
│ │ │ |
│ delegates │ ▼ |
│ beat | ┌───────────────────┬────────────────┐ |
│ loading │ │ ImageLoading │ AudioManager │ |
│ │ │ Service │ │ |
┌─────────▼─────────────▼──────────────────┐ │ │ │◀--┘
│ BeatStorageCoordinator │ │ • preloadImages │ • preloadSounds│
│ "How to get beats efficiently" │ │ • cancelLoading │ • cancelSounds │
│ • getBeat() / loadBeat() │ └───────────────────┴────────────────┘
│ • storeBeat() │
│ • clearCache() │
└─────────┬────────────────────────────────┘
│
│ orchestrates hierarchy access
│
│
┌─────────▼───────────────────────────────────────────────────────┐
│ Storage Vehicle Layer │
├─────────────────┬─────────────────┬─────────────────────────────┤
│ InMemoryBeat │ BeatDiskCache │ BeatRepository │
│ Cache │ Adapter │ (Network) │
│ (Actor) │ (Actor) │ (Firestore) │
│ │ │ │
│ • get() │ • get() │ • fetchBeat() │
│ • store() │ • store() │ • fetchInitialBeats() │
│ • clear() │ • clear() │ • Firestore integration │
│ • evict() │ • cleanup() │ │
└─────────────────┴─────────────────┴─────────────────────────────┘
Core Design Principles
Actor-Based Concurrency
All storage components use Swift actors for thread safety, eliminating data races and ensuring reliable concurrent access patterns.
Layered Caching Strategy
Three-tier hierarchy optimizes for different access patterns:
- Memory Cache: Instant access (0ms latency) via
InMemoryBeatCache - Disk Cache: Fast persistent storage (1-5ms latency) via
BeatDiskCacheAdapter - Network: Authoritative source (50-500ms latency) via
PajamaScriptBeatRepository
Intelligent Preloading
Sliding window algorithm preloads content based on reading position and story structure, including choice-aware prediction for branching narratives.
Clean Architecture
Clear separation between orchestration (StoryContentManager), coordination (BeatStorageCoordinator), specialized caching (InMemoryBeatCache, BeatDiskCacheAdapter), and preloading intelligence (BeatPreloader, AssetPreloader).
Component Responsibilities
StoryContentManager
Location: Pajama/Features/StoryReader/StoryContentManager.swift
Central orchestrator implementing the StoryBeatManaging protocol. Runs on @MainActor for UI integration and manages the complete beat lifecycle.
Key Interfaces:
getBeat(_:)- Synchronous cache-only accessloadBeat(_:)- Async loading with full hierarchy checkbeginLoadingContent()- Initiates content loading for specific beatsbeginLoadingAllStoryContent()- Bulk loading for story initialization- Event publishing via
storyBeatLoadedPublisher
Integration Points:
- Coordinates with
BeatStorageCoordinatorfor beat data - Manages
BeatPreloaderfor intelligent preloading - Coordinates
AssetPreloaderfor asset management - Publishes loading events for UI reactivity
BeatStorageCoordinator
Location: Pajama/Features/StoryReader/Storage/Coordination/BeatStorageCoordinator.swift
Actor-based storage coordinator that implements the complete Memory→Disk→Network hierarchy. Serves as the single source of truth for beat data access.
Storage Hierarchy:
- First:
InMemoryBeatCache- 50MB/100 beats with LRU eviction - Second:
BeatDiskCacheAdapter- Persistent storage with corruption detection - Third:
PajamaScriptBeatRepository- Firestore network access
Key Capabilities:
- Task deduplication to prevent redundant network calls
- Detailed performance metrics and cache hit tracking
- Runtime cache configuration (enable/disable layers)
- Automatic promotion from disk to memory
- Comprehensive error handling and recovery
BeatPreloader
Location: Pajama/Features/StoryReader/Storage/Coordination/BeatPreloader.swift
Intelligent preloading system using sliding window strategy to optimize reading experience.
Preloading Strategy:
- Configurable window size (default: 2 before, 5 after current position)
- Choice-aware preloading for story branching via
choiceTargets - Concurrent loading limits (max 3) to avoid resource exhaustion
- Task deduplication with automatic cancellation
Window Management:
- Updates automatically as user progresses through story
- Analyzes story structure for predictive loading
- Coordinates with
AssetPreloaderfor holistic preloading
AssetPreloader
Location: Pajama/Features/StoryReader/Storage/Coordination/AssetPreloader.swift
Asset coordination system that extracts and preloads images, audio, and other assets from story beats.
Coordination Responsibilities:
- Extracts asset URLs from beat content using regex patterns
- Coordinates image preloading via
ImageLoadingServiceProtocol - Manages audio asset coordination with
AudioManager - Priority-based loading for current story position
- Cancellation support for memory management
Storage Vehicle Layer
InMemoryBeatCache
Location: Pajama/Features/StoryReader/Storage/Cache/InMemoryBeatCache.swift
Actor-based in-memory cache with sophisticated eviction and memory management.
Features:
- LRU eviction policy with 50MB size limit and 100 beat count limit
- Memory pressure handling with automatic cleanup
- Size estimation for beats including metadata
- Thread-safe operations through actor isolation
BeatDiskCacheAdapter
Location: Pajama/Features/StoryReader/Storage/Cache/BeatDiskCacheAdapter.swift
Actor-based adapter wrapping TerryKit's DiskCache for persistent storage.
Features:
- Async operations with proper error handling
- Metadata validation and corruption detection
- Prefix-based cache clearing for story-specific cleanup
- Integration with unified cache management
PajamaScriptBeatRepository
Location: Pajama/Core/Storage/Firestore/Repositories/PajamaScriptBeatRepository.swift
Firestore integration layer handling all network operations for story beats.
Network Operations:
- Single beat fetching with direct document access
- Concurrent batch loading using TaskGroup
- Intelligent initial story loading with configurable depth
- Choice target prefetching for story branching
File Organization
The storage system is organized under Features/StoryReader/Storage/ with logical subdirectories:
Cache/
Memory and disk cache implementations:
InMemoryBeatCache.swift- Actor-based memory storage with LRU evictionBeatDiskCacheAdapter.swift- Persistent storage adapter
Coordination/
Orchestration and preloading logic:
BeatStorageCoordinator.swift- Main storage coordination (Memory→Disk→Network)BeatPreloader.swift- Intelligent sliding window preloadingAssetPreloader.swift- Asset coordination for beats
Root Storage/
Core interfaces:
StoryBeatManagingProtocols.swift- Protocol definitionsBeatStorageError.swift- Error types and handling
Data Flow Patterns
Primary User Flow
User Tap/Swipe → StoryPageController → StoryContentManager →
BeatStorageCoordinator → [Memory → Disk → Network] → Content Display
Background Intelligence Flow
Current Position Update → BeatPreloader → Sliding Window Calculation →
Concurrent Preloading Tasks → BeatStorageCoordinator → Storage Population
Asset Loading Flow
The system uses different patterns for beat vs. asset loading due to their different access characteristics:
Immediate Asset Loading (UI-driven)
UI Component (CachedAsyncImage) → ImageLoadingService →
Check Cache → Load if Needed → Display
Predictive Asset Preloading
BeatPreloader → AssetPreloader → Asset Extraction →
[ImageLoadingService + AudioManager] → Bulk Preload
Why Different Patterns for Beats vs. Assets?
Beats require explicit availability checking:
- Navigation depends on beat availability
- UI needs synchronous
getBeat()to decide whether to show content or loading state - One beat at a time access pattern
Assets work well with async loading:
- Images are visual enhancements that can load progressively
CachedAsyncImagehandles the check-and-load pattern internally- Many images per page access pattern
- Standard iOS async image loading patterns
Performance Characteristics
Loading Performance
- Critical Path: Beat loading through
BeatStorageCoordinatorblocks navigation - Background: Asset loading via
AssetPreloaderis non-blocking - Preloading:
BeatPreloaderreduces network requests by 60-80%
Memory Management
- Automatic cache eviction under memory pressure in
InMemoryBeatCache - Task deduplication prevents redundant network calls in
BeatStorageCoordinator - Concurrent loading limits (max 3) in
BeatPreloader
Integration Architecture
App-Level Integration
The system integrates at the AppServices level with shared cache instances used across all story reading experiences.
Story Reader Integration
StoryReaderViewModel creates StoryContentManager instances that coordinate beat loading with asset preloading, ensuring smooth user experience.
Co-Reading Integration
Shared storage ensures consistent state across solo and collaborative reading modes, with the same caching benefits for all participants.
Error Handling Strategy
Error Classification
Location: Pajama/Features/StoryReader/Storage/BeatStorageError.swift
Comprehensive error types covering disk failures, network issues, corruption detection, and memory pressure scenarios.
Recovery Strategies
- Retry from Network: For transient failures
- Cache Layer Fallback: Memory→Disk→Network graceful degradation
- Corrupted Data Cleanup: Automatic bad data removal
- Task Deduplication: Prevents error amplification
Configuration & Monitoring
Runtime Configuration
- Memory/disk cache enable/disable controls in
BeatStorageCoordinator - Cache size limits and eviction policies in
InMemoryBeatCache - Preloading window size adjustment in
BeatPreloader
Performance Monitoring
- Real-time cache hit rate tracking in
BeatStorageCoordinator - Detailed loading metrics and timing information
- Task deduplication statistics and concurrent operation tracking
Best Practices
Memory Management
- Always use weak references in async Task closures
- Leverage actor isolation for thread safety
- Implement proper cleanup in deinit methods
- Respect system memory pressure warnings
Error Handling
- Provide comprehensive error classification
- Implement meaningful recovery strategies
- Log errors with sufficient context for debugging
- Design for graceful degradation scenarios
Performance Optimization
- Use TaskGroup for concurrent network operations
- Implement task deduplication to avoid redundant work
- Monitor cache hit rates for optimization opportunities
- Balance preloading aggressiveness with resource usage
Conclusion
The beat loading system provides a robust, performant foundation for story content delivery in Pajama. The architecture features a clean separation of concerns with:
- Single orchestrator (
StoryContentManager) for UI interaction - Dedicated coordination (
BeatStorageCoordinator) for storage hierarchy management - Intelligent preloading (
BeatPreloader,AssetPreloader) for performance optimization - Actor-based thread safety throughout the storage layer
- Clear file organization under the StoryReader feature module
This architecture balances performance, reliability, and resource efficiency while maintaining flexibility for various reading scenarios and providing comprehensive monitoring and configuration capabilities for both development and production needs.