15 KiB
15 KiB
UI Element Processing & Neighbor Preloading Analysis
Executive Summary
Deep analysis of how UI elements are processed throughout the Tour Builder Platform - from creation in the Constructor, to rendering in Runtime Presentations, across Online and Offline modes. This document traces each preloading thread step-by-step to verify robustness.
1. PRELOAD CONFIGURATION (Single Source of Truth)
File: src/config/preload.config.ts
1.1 Asset URL Fields Configuration
assetFields: {
// All 18 URL fields for preloading extraction
all: [
'iconUrl', // Base element icon
'imageUrl', // Generic image reference
'mediaUrl', // Video/audio player source
'videoUrl', // Video element
'audioUrl', // Audio element
'transitionVideoUrl', // Navigation transition video
'backgroundImageUrl', // Element background
'reverseVideoUrl', // Reverse transition video
'carouselPrevIconUrl', // Carousel prev button
'carouselNextIconUrl', // Carousel next button
'galleryHeaderImageUrl', // Gallery header background
'galleryCarouselPrevIconUrl', // Gallery carousel prev
'galleryCarouselNextIconUrl', // Gallery carousel next
'galleryCarouselBackIconUrl', // Gallery carousel back
'src', 'url', 'poster', 'thumbnail', // Generic fallbacks
],
// 10 Image-only fields for pre-decode optimization
images: [
'iconUrl', 'imageUrl', 'backgroundImageUrl',
'carouselPrevIconUrl', 'carouselNextIconUrl',
'galleryHeaderImageUrl', 'galleryCarouselPrevIconUrl',
'galleryCarouselNextIconUrl', 'galleryCarouselBackIconUrl', 'src',
],
// 3 Nested array fields containing assets
nested: ['galleryCards', 'carouselSlides', 'galleryInfoSpans'],
// 3 URL fields within nested items
nestedUrlFields: ['imageUrl', 'videoUrl', 'iconUrl'],
}
1.2 Configuration Consumers (10 files)
| File | Uses | Purpose |
|---|---|---|
extractPageLinks.ts |
all, nested, nestedUrlFields | Explicit field extraction |
useNeighborGraph.ts |
all | Recursive traversal (depth 5) |
usePreloadOrchestrator.ts |
all | Asset initialization |
imagePreDecode.ts |
images, nested, nestedUrlFields | Image-only pre-decode |
StorageManager.ts |
storage config | Storage thresholds |
DownloadManager.ts |
queue settings | Download concurrency |
useStorageQuota.ts |
storage config | Quota warnings |
usePreloadProgress.ts |
autoRemove | Progress cleanup |
DownloadContext.tsx |
various | Download UI state |
2. COMPLETE PRELOADING FLOW (Step-by-Step)
2.1 Entry Point: RuntimePresentation Mount
RuntimePresentation.tsx mounts
↓
extractPageLinksAndElements(pages) [lib/extractPageLinks.ts:105-182]
├── Parse ui_schema_json for each page
├── extractAssetFields(element) → uses PRELOAD_CONFIG.assetFields
│ ├── Extract top-level fields from assetFields.all
│ └── Extract nested arrays (galleryCards, carouselSlides, galleryInfoSpans)
│ └── Within each item, extract nestedUrlFields (imageUrl, videoUrl, iconUrl)
├── Build pageLinks[] for navigation graph
└── Build preloadElements[] with content_json containing asset URLs
↓
usePreloadOrchestrator({ pages, pageLinks, elements, currentPageId })
2.2 Thread 1: Neighbor Graph Building
useNeighborGraph({ pages, pageLinks, elements, maxDepth: 1 })
↓
Build adjacencyList (Map<pageId, neighborPageIds[]>) [line 119-141]
├── Initialize all pages in map
└── Add edges from active pageLinks (from_pageId → to_pageId)
↓
getNeighbors(currentPageId, depth) [line 144-177]
├── BFS traversal from current page
├── Track visited pages to avoid cycles
└── Return PreloadNeighborInfo[] sorted by distance
↓
getAssetsForPages(pageIds) [line 180-224]
├── For each page: filter elements by pageId
├── extractAssetsFromContent(content_json) [line 56-106]
│ ├── Parse JSON content
│ ├── checkObject() recursive traversal (depth ≤ 5)
│ │ └── For each field in PRELOAD_CONFIG.assetFields.all:
│ │ └── If string value found, classify asset type and add to assets[]
│ └── Return PreloadAssetInfo[] with { url, pageId, assetType, priority }
└── Also extract transition videos from pageLinks
2.3 Thread 2: Priority Assignment
getPrioritizedAssets(currentPageId, maxDepth) [line 228-274]
↓
Current page assets:
priority = PRELOAD_CONFIG.priority.currentPage (1000)
+ PRELOAD_CONFIG.priority.assetType[type]
Asset type priorities:
├── transition: +150 (highest - needed on navigation click)
├── image: +100 (backgrounds load during transition)
├── audio: +50
└── video: +30
↓
Neighbor page assets:
basePriority = PRELOAD_CONFIG.priority.neighborBase (500) / distance
priority = basePriority + assetType priority
↓
Deduplicate by URL, keep highest priority
Sort descending by priority
2.4 Thread 3: URL Resolution (Presigned URLs)
usePreloadOrchestrator effect on currentPageId change [line 669-924]
↓
Collect storage paths needing presigning:
├── Current page: background_image_url, background_video_url, background_audio_url
├── Element assets from neighborGraph.getPrioritizedAssets()
└── Neighbor pages: background URLs
↓
queuePresignedUrls(storagePaths) [lib/assetUrl.ts]
├── POST /api/file/presign { urls: storagePaths[] }
├── Response: { [storageKey]: presignedUrl }
└── Cache presigned URLs (1-hour expiry)
↓
resolveUrl(storageKey, presignedUrls) [line 761-771]
├── If presignedUrls[storageKey] exists → use presigned URL
└── Fallback → resolveAssetPlaybackUrl (proxy URL)
2.5 Thread 4: Download Queue Processing
addToQueue(item: PreloadQueueItem) [line 494-530]
├── Skip if already in queue or preloaded
├── Insert in priority order (binary search insertion point)
└── Trigger processQueue()
↓
processQueue() [line 355-491]
├── Check: isOnline, queue not empty, not already processing
├── maxConcurrent = recommendedConcurrency (network-aware)
↓
While queue has items AND activeDownloads < maxConcurrent:
├── Shift item from queue
├── Skip if already preloaded (preloadedUrls.has(url))
├── Skip if already cached (isUrlCached → StorageManager.hasAsset)
│ └── If cached: createReadyBlobUrl() for instant display
↓
preloadWithProgress(url, jobId, assetId) [line 140-234]
├── Emit downloadEventBus.emitPreloadStart
├── fetch(url) with streaming progress
├── Collect chunks, emit progress events
├── Store in Cache API: caches.open(assets).put(url, response)
└── Emit downloadEventBus.emitPreloadComplete
↓
On success:
├── createReadyBlobUrl(url, storageKey)
│ ├── StorageManager.getAsset(url) → blob
│ ├── URL.createObjectURL(blob) → blobUrl
│ ├── If image: decodeImage(blobUrl) → pre-paint ready
│ └── readyBlobUrlsRef.set(url, blobUrl)
└── Also cache under storageKey for post-refresh lookup
↓
On failure (presigned URL CORS):
├── markPresignedUrlFailed(storageKey)
├── Build proxy URL: /api/file/download?privateUrl=...
└── Retry with proxy URL
2.6 Thread 5: Ready State & Instant Lookup
getReadyBlobUrl(url): string | null [line 582-584]
└── readyBlobUrlsRef.current.get(url) → O(1) Map lookup
Usage in RuntimePresentation:
const resolveUrlWithBlob = (url) => {
const blobUrl = preloadOrchestrator.getReadyBlobUrl(url);
return blobUrl || resolveAssetPlaybackUrl(url);
};
<RuntimeElement resolveUrl={resolveUrlWithBlob} ... />
3. ELEMENT TYPE → URL FIELD MAPPING (Complete Audit)
3.1 All Element Types (11 total)
| Element Type | URL Fields | Nested Arrays |
|---|---|---|
navigation_next/prev |
iconUrl, transitionVideoUrl, reverseVideoUrl | - |
gallery |
iconUrl, galleryHeaderImageUrl, galleryCarouselPrevIconUrl, galleryCarouselNextIconUrl, galleryCarouselBackIconUrl | galleryCards[].imageUrl, galleryInfoSpans[].iconUrl |
carousel |
iconUrl, carouselPrevIconUrl, carouselNextIconUrl | carouselSlides[].imageUrl |
video_player |
iconUrl, mediaUrl | - |
audio_player |
iconUrl, mediaUrl | - |
tooltip |
iconUrl | - |
description |
iconUrl | - |
spot |
iconUrl | - |
logo |
iconUrl | - |
popup |
iconUrl | - |
3.2 Cross-Reference Verification
constructor.ts URL Fields vs preload.config.ts assetFields.all:
| Field in constructor.ts | In assetFields.all? | Status |
|---|---|---|
| iconUrl | Yes | OK |
| mediaUrl | Yes | OK |
| backgroundImageUrl | Yes | OK |
| videoUrl | Yes | OK |
| audioUrl | Yes | OK |
| transitionVideoUrl | Yes | OK |
| reverseVideoUrl | Yes | OK |
| galleryHeaderImageUrl | Yes | OK |
| carouselPrevIconUrl | Yes | OK |
| carouselNextIconUrl | Yes | OK |
| galleryCarouselPrevIconUrl | Yes | OK |
| galleryCarouselNextIconUrl | Yes | OK |
| galleryCarouselBackIconUrl | Yes | OK |
Nested Arrays vs nested config:
| Nested Array | In nested config? | Status |
|---|---|---|
| galleryCards | Yes | OK |
| carouselSlides | Yes | OK |
| galleryInfoSpans | Yes | OK |
Nested URL Fields vs nestedUrlFields:
| Field in Nested Items | In nestedUrlFields? | Status |
|---|---|---|
| galleryCards[].imageUrl | Yes | OK |
| carouselSlides[].imageUrl | Yes | OK |
| galleryInfoSpans[].iconUrl | Yes | OK |
4. OFFLINE MODE PROCESSING
4.1 Storage Architecture
StorageManager [lib/offline/StorageManager.ts]
├── Threshold: OFFLINE_CONFIG.storage.indexedDbMinSize (5MB)
├── Small files (< 5MB): Cache API
│ └── caches.open('vm-assets').put(url, response)
└── Large files (≥ 5MB): IndexedDB via Dexie
└── OfflineDbManager.storeAsset(asset)
hasAsset(url): Promise<boolean>
1. Check IndexedDB: OfflineDbManager.hasAssetByUrl(url)
2. Check Cache API: caches.open().match(url)
getAsset(url): Promise<Blob | null>
1. Try IndexedDB: OfflineDbManager.getAssetByUrl(url)
2. Try Cache API: caches.open().match(url).blob()
4.2 Service Worker Integration
sw.ts (Serwist-generated)
├── Precache: static assets (JS, CSS, fonts)
├── Runtime caching strategies:
│ ├── API requests: NetworkFirst
│ └── Assets: CacheFirst
└── Offline fallback handling
5. IMAGE PRE-DECODE FLOW
5.1 extractPageImageUrls
extractPageImageUrls(page) [lib/imagePreDecode.ts:110-175]
↓
Extract background_image_url
↓
Parse ui_schema_json → elements[]
↓
For each element:
├── Direct image fields (assetFields.images):
│ └── iconUrl, imageUrl, backgroundImageUrl, carousel*IconUrl, gallery*IconUrl, src
└── Nested arrays (assetFields.nested):
└── Filter nestedUrlFields to images only:
├── imageUrl (in images array)
├── iconUrl (in images array)
└── videoUrl (not in images - correctly excluded)
5.2 waitForPageImages
waitForPageImages(page, timeoutMs, cacheProvider)
↓
extractPageImageUrls(page) → imageUrls[]
↓
decodeImages(imageUrls, timeoutMs, cacheProvider)
├── For each URL: decodeImage()
│ ├── If cacheProvider: try getCachedBlobUrl() → blob URL (local, fast)
│ └── new Image().decode() or onload fallback
└── Promise.race([all decoded, timeout])
6. ROBUSTNESS VERIFICATION
6.1 All Asset Types Covered
| Asset Type | Extracted? | Preloaded? | Pre-decoded? | Cached? |
|---|---|---|---|---|
| Background images | Yes | Yes | Yes | Yes |
| Background videos | Yes | Yes | N/A | Yes |
| Background audio | Yes | Yes | N/A | Yes |
| Element icons | Yes | Yes | Yes | Yes |
| Transition videos | Yes | Yes | N/A | Yes |
| Reverse videos | Yes | Yes | N/A | Yes |
| Gallery cards images | Yes | Yes | Yes | Yes |
| Gallery info span icons | Yes | Yes | Yes | Yes |
| Gallery header image | Yes | Yes | Yes | Yes |
| Gallery carousel icons | Yes | Yes | Yes | Yes |
| Carousel slide images | Yes | Yes | Yes | Yes |
| Carousel nav icons | Yes | Yes | Yes | Yes |
| Media player URLs | Yes | Yes | N/A | Yes |
6.2 Extraction Algorithm Robustness
-
extractPageLinks.ts - Explicit field extraction
- Uses
assetFields.allfor top-level fields - Uses
nested+nestedUrlFieldsfor nested arrays - Handles all configured fields
- Uses
-
useNeighborGraph.ts - Recursive traversal
checkObject()recursively traverses to depth 5- Matches any field from
assetFields.allat any nesting level - Handles deeply nested structures
-
imagePreDecode.ts - Image-only extraction
- Filters
nestedUrlFieldsagainstassetFields.images - Only pre-decodes actual images (not videos/audio)
- Correctly filters non-image URLs
- Filters
6.3 Edge Cases Handled
| Edge Case | Handling |
|---|---|
| Empty ui_schema_json | Returns empty arrays safely |
| Missing nested arrays | Skipped gracefully |
| Null/undefined values | Filtered out |
| Already cached assets | Skipped download, creates blob URL |
| Presigned URL CORS failure | Retries with proxy URL |
| Network offline | Skips preloading, uses cache |
| Large files (>=5MB) | Stored in IndexedDB instead of Cache API |
7. CONCLUSION
The neighbor preloading system is robust and comprehensive:
- Central Configuration - All asset URL fields defined in
preload.config.ts - Consistent Extraction - All consumers use the same config
- Complete Coverage - All 11 element types and their URL fields are covered
- Nested Arrays -
galleryCards,carouselSlides,galleryInfoSpansall handled - Dual Storage - Cache API for small files, IndexedDB for large files
- Fallback Handling - Presigned URLs with proxy fallback
- Network Awareness - Adaptive concurrency based on connection
No gaps identified - all asset types are properly extracted, preloaded, and cached.
Related Documentation
- assets-preloading.md - E2E preloading feature documentation
- runtime-presentation.md - Runtime presentation viewer
- constructor-page-editor.md - Constructor page editor
- hooks-module.md - Custom hooks reference
- lib-module.md - Library utilities reference