Global Loading Store
Enterprise-grade loading state management with SSR safety, auto-timeout protection, and contextual loading operations for SveltyCMS
On this page
The Global Loading Store is an enterprise-grade loading state management system built for SveltyCMS. It provides centralized, stack-based loading state with SSR safety, automatic timeout protection, and contextual loading messages.
Architecture Overview
The loading system consists of two core components:
loading-store.svelte.ts- Core state manager with reactive storeglobal-loading.svelte- UI component displaying full-screen loading overlay
App is running, user is active"] end Consumers --> Store Store --> UI UI --> POST_BOOT
Zero-tax initialization: The
handleSystemStatemiddleware waits synchronously for DB initialization on the first request. No intermediate warming-up page or polling — the hooks and state machine work together to resolve requests inline, saving system resources.
Key Features
- Stack-based Concurrent Operations - Handles multiple simultaneous loading states
- SSR Safety - Browser guards prevent hydration mismatches
- Auto-timeout Protection - Prevents stuck loading states (30s default)
- Contextual Messages - Operation-specific translated messages
- Error-safe Cleanup - Guaranteed cleanup via try-finally patterns
- Debug Capabilities -
getStats()method for monitoring active operations
Loading Operations Catalog
export const loadingOperations = {
navigation: "navigation", // Page navigation
dataFetch: "dataFetch", // General data fetching
authentication: "authentication", // Login/logout operations
initialization: "initialization", // App/component initialization
imageUpload: "imageUpload", // Media upload operations
formSubmission: "formSubmission", // Form POST operations
configSave: "configSave", // System configuration updates
roleManagement: "roleManagement", // Role CRUD operations
permissionUpdate: "permissionUpdate", // Permission matrix updates
tokenGeneration: "tokenGeneration", // API token generation
collectionLoad: "collectionLoad", // Collection data loading
widgetInit: "widgetInit", // Widget initialization
} as const;
Core API
Starting/Stopping Loading
import { globalLoadingStore, loadingOperations } from "$stores/loading-store.svelte";
// Start loading with context
globalLoadingStore.startLoading(
loadingOperations.dataFetch,
"admin-area.fetchData", // Optional context for debugging
5000, // Optional custom timeout (ms)
);
// Stop loading
globalLoadingStore.stopLoading(loadingOperations.dataFetch);
// Check if any loading operation is active
const isLoading = globalLoadingStore.isLoading;
Async Wrapper (Recommended)
The withLoading() wrapper provides automatic cleanup with error safety:
import { globalLoadingStore, loadingOperations } from "$stores/loading-store.svelte";
async function fetchRoles() {
return await globalLoadingStore.withLoading(
async () => {
const response = await fetch("/api/roles");
if (!response.ok) throw new Error("Failed to fetch roles");
return await response.json();
},
loadingOperations.roleManagement,
"Roles.loadData", // Optional context
);
}
Benefits:
- Automatic
startLoading()before async operation - Guaranteed
stopLoading()in finally block - Error propagation without cleanup issues
- Optional context for debugging
Debugging
// Get current loading state statistics
const stats = globalLoadingStore.getStats();
console.log(stats);
// Output: { activeCount: 2, reasons: ['dataFetch', 'roleManagement'] }
SSR Safety Implementation
The store uses browser guards to prevent SSR execution:
import { browser } from "$app/environment";
export const globalLoadingStore = createLoadingStore();
function createLoadingStore() {
let loadingEntries = new SvelteMap<string, LoadingEntry>();
let isLoading = $state(false);
return {
get isLoading() {
return isLoading;
},
startLoading(reason: LoadingReason, context?: string, timeout?: number) {
if (!browser) return; // SSR guard
// ... implementation
},
// ...
};
}
Why this matters:
- Prevents hydration mismatches
- Avoids server-side timeout creation
- Ensures client-only reactivity
Auto-Timeout Protection
Every loading operation has automatic timeout protection:
startLoading(reason: LoadingReason, context?: string, timeout: number = 30000) {
if (!browser) return;
const timeoutId = setTimeout(() => {
console.warn(`Loading operation "${reason}" timed out after ${timeout}ms`);
this.stopLoading(reason);
}, timeout);
loadingEntries.set(reason, { reason, context, timeoutId });
isLoading = true;
}
Default timeout: 30 seconds
Custom timeout: Pass third parameter to startLoading() or withLoading()
Concurrent Operations
The store uses a SvelteMap to track multiple simultaneous operations:
let loadingEntries = new SvelteMap<string, LoadingEntry>();
// Multiple operations can be active simultaneously
globalLoadingStore.startLoading(loadingOperations.dataFetch, "Users");
globalLoadingStore.startLoading(loadingOperations.roleManagement, "Roles");
console.log(globalLoadingStore.getStats());
// { activeCount: 2, reasons: ['dataFetch', 'roleManagement'] }
// isLoading remains true until ALL operations complete
globalLoadingStore.stopLoading(loadingOperations.dataFetch);
console.log(globalLoadingStore.isLoading); // true
globalLoadingStore.stopLoading(loadingOperations.roleManagement);
console.log(globalLoadingStore.isLoading); // false
global-loading UI Component
The global-loading.svelte component displays operation-specific messages:
<script lang="ts">
import {
loading_dataFetch_bottom,
loading_dataFetch_top,
loading_loading,
loading_navigation_bottom,
loading_navigation_top,
loading_pleasewait
} from '@src/paraglide/messages';
import { globalLoadingStore, loadingOperations } from '@src/stores/loading-store.svelte';
const loadingTextMap = {
[loadingOperations.navigation]: {
top: loading_navigation_top(),
bottom: loading_navigation_bottom()
},
[loadingOperations.dataFetch]: {
top: loading_dataFetch_top(),
bottom: loading_dataFetch_bottom()
}
// ... additional operations
};
const currentText = $derived(() => {
const stats = globalLoadingStore.getStats();
if (stats.activeCount === 0) return null;
const firstReason = stats.reasons[0];
return (
loadingTextMap[firstReason] || {
top: loading_pleasewait(),
bottom: loading_loading()
}
);
});
</script>
{#if globalLoadingStore.isLoading}
<div class="loading-overlay">
<div class="loading-content">
<div class="loaders">
<!-- 4 animated circles with different speeds/directions -->
</div>
{#if currentText}
<h2>{currentText.top}</h2>
<p>{currentText.bottom}</p>
{/if}
</div>
</div>
{/if}
Translation Integration
All loading messages use ParaglideJS for i18n:
src/messages/en.json:
{
"loading_navigation_top": "Navigating",
"loading_navigation_bottom": "Loading page...",
"loading_configSave_top": "Saving Configuration",
"loading_configSave_bottom": "Updating system settings..."
}
src/messages/de.json:
{
"loading_navigation_top": "Navigiere",
"loading_navigation_bottom": "Seite wird geladen...",
"loading_configSave_top": "Konfiguration wird gespeichert",
"loading_configSave_bottom": "Systemeinstellungen werden aktualisiert..."
}
Real-World Examples
Role Management (admin-role.svelte)
async function loadRoles() {
roles = await globalLoadingStore.withLoading(
async () => {
const res = await fetch("/api/roles");
if (!res.ok) throw new Error("Failed to fetch roles");
return await res.json();
},
loadingOperations.roleManagement,
"admin-role.loadRoles",
);
}
Configuration Save (accessManagement/+page.svelte)
async function saveAllChanges() {
await globalLoadingStore.withLoading(
async () => {
const response = await fetch("/api/config/access-management", {
method: "POST",
body: JSON.stringify(allChanges),
});
if (!response.ok) throw new Error("Save failed");
invalidateAll(); // SvelteKit cache invalidation
},
loadingOperations.configSave,
"AccessManagement.saveAll",
);
}
Token Generation (website-tokens.svelte)
async function fetchTokens() {
const data = await globalLoadingStore.withLoading(
async () => {
const res = await fetch("/api/tokens");
return await res.json();
},
loadingOperations.tokenGeneration,
"website-tokens.fetch",
);
tokens = data.tokens;
}
When NOT to Use Global Loading
The global loading store is designed for user-initiated actions and page-level operations. Use local state for:
- Component-specific initialization (widget loading)
- Background/silent operations (autosave, polling)
- Form validation feedback (inline error states)
- Partial UI updates (single component refresh)
Example - OAuth Form (Keep Local State):
<script lang="ts">
// Form submission needs immediate, component-specific feedback
let isSubmitting = $state(false);
async function handleSubmit() {
isSubmitting = true;
try {
await fetch('/oauth/callback', { method: 'POST' });
} finally {
isSubmitting = false;
}
}
</script>
<button disabled={isSubmitting}>
{isSubmitting ? 'Connecting...' : 'Connect OAuth'}
</button>
Performance Considerations
- SvelteMap Reactivity: Uses Svelte 5’s
SvelteMapfor efficient reactive updates - Derived State:
isLoadingcomputed fromloadingEntries.size > 0 - Minimal Re-renders: Only updates when entries added/removed
- Browser-only: Zero SSR overhead
Troubleshooting
Loading State Stuck
// Check what's active
const stats = globalLoadingStore.getStats();
console.log("Active operations:", stats);
// Manual cleanup (emergency only)
globalLoadingStore.stopLoading(loadingOperations.dataFetch);
SSR Hydration Mismatch
Ensure all loading operations are inside onMount or event handlers:
import { onMount } from "svelte";
onMount(() => {
// Safe - runs client-side only
globalLoadingStore.startLoading(loadingOperations.initialization);
});
Custom Timeout Not Working
Remember timeout is the third parameter:
// ✅ Correct
await globalLoadingStore.withLoading(
asyncFn,
loadingOperations.dataFetch,
"context", // Optional but must be provided if using timeout
);
) ;
// With custom timeout
globalLoadingStore.startLoading(
loadingOperations.dataFetch,
"MyComponent.load",
60000, // 60 seconds
);
Progress Tracking
Report granular progress for long-running operations using withProgress. The operation receives an updateProgress callback that accepts values 0–100:
await globalLoadingStore.withProgress(
loadingOperations.imageUpload,
async (updateProgress) => {
for (const batch of batches) {
await uploadBatch(batch);
updateProgress((processedCount / totalCount) * 100);
}
return result;
},
"Uploading media files",
);
The global-loading overlay displays a progress bar when progress data is available. The setProgress() method can also update progress directly for manual loading control.
Priority Queue Management
Operations can be assigned priorities (high, normal, low) via startLoadingWith. When multiple operations are active, the UI displays context from the highest-priority operation:
// High-priority operation (displays immediately in UI)
globalLoadingStore.startLoadingWith(loadingOperations.authentication, {
priority: "high",
context: "Verifying credentials",
});
// Low-priority background sync
globalLoadingStore.startLoadingWith(loadingOperations.collectionLoad, {
priority: "low",
context: "Syncing collections",
});
Cancellation Support
Cancel long-running operations with AbortController integration. The Cancel button in the loading overlay is automatically wired:
const result = await globalLoadingStore.withCancellable(
loadingOperations.dataFetch,
async (signal) => {
const res = await fetch("/api/large-dataset", { signal });
if (signal.aborted) throw new DOMException("Cancelled", "AbortError");
return res.json();
},
{ context: "Exporting report", cancellable: true },
);
Analytics
Track loading performance over time with getAnalytics():
const stats = globalLoadingStore.getAnalytics();
// {
// totalOperations: 1423,
// avgDuration: 245,
// byOperation: {
// "data-fetch": { count: 890, avgDuration: 180, failures: 3 },
// "image-upload": { count: 45, avgDuration: 3200, failures: 0 }
// },
// recentOperations: [{ operation, duration, success, timestamp }, ...]
// }
The store maintains a rolling window of 200 metrics entries. Failures are tracked separately from successes for accurate reliability monitoring.
Connection Awareness
src/utils/connection-detector.ts provides reactive network quality detection. The loading store integrates with it to adapt behavior based on connection speed:
import { shouldSkipPreload, shouldDisableRealtime } from "@utils/connection-detector";
if (shouldSkipPreload()) return; // Don't preload on 2G or Save-Data
if (shouldDisableRealtime()) return; // Don't stream on degraded connections
The global-loading component handles post-boot operations. For system initialization, the middleware pipeline (handleSystemState) waits synchronously for the database to become ready on the first request, then proceeds directly to the target page without an intermediate loading screen.