Skip to content

Documentation

Global Loading Store

Enterprise-grade loading state management with SSR safety, auto-timeout protection, and contextual loading operations for SveltyCMS

3/27/2026
8 min read Edit on GitHub

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:

  1. loading-store.svelte.ts - Core state manager with reactive store
  2. global-loading.svelte - UI component displaying full-screen loading overlay
flowchart TB subgraph Consumers["Loading Triggers"] NAV[Navigation] FETCH[Data Fetch] AUTH[Authentication] CONFIG[Config Save] UPLOAD[Image Upload] ROLE[Role Management] INIT[Initialization] end subgraph Store["loading-store.svelte.ts"] MAP["SvelteMap<string, LoadingEntry>"] IS_LOADING["isLoading ($state)"] TIMEOUT["Auto-Timeout (30s)"] SSR_GUARD["Browser Guard"] end subgraph UI["global-loading.svelte"] OVERLAY["Full-Screen Overlay"] RINGS["4 Animated Loader Rings"] LOGO["SveltyCMSLogo"] TEXT["Contextual Paraglide Messages"] PROGRESS["Progress Bar"] ELAPSED["Elapsed Time"] end subgraph Lifecycle["When This Runs"] POST_BOOT["Post-Boot Operations
App is running, user is active"] end Consumers --> Store Store --> UI UI --> POST_BOOT

Zero-tax initialization: The handleSystemState middleware 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:

  1. Component-specific initialization (widget loading)
  2. Background/silent operations (autosave, polling)
  3. Form validation feedback (inline error states)
  4. 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 SvelteMap for efficient reactive updates
  • Derived State: isLoading computed from loadingEntries.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.

Related Documentation

architectureloadingstate-managementssrsvelte5
Was this page helpful?