Reactive loading state service with counter-based tracking and HTTP middleware integration.
npm install @script-development/fs-loadingPeer dependencies: vue ^3.5.33, @script-development/fs-http ^0.1.0 || ^0.2.0 || ^0.3.0
fs-loading tracks whether your application is currently loading data. It uses a counter instead of a boolean — multiple concurrent requests each increment the counter, and loading stays true until all of them complete. It integrates with fs-http via middleware so you don't have to manually track each request.
import {createLoadingService} from '@script-development/fs-loading';
const loading = createLoadingService();
loading.isLoading.value; // false
loading.startLoading();
loading.isLoading.value; // true
loading.startLoading(); // second concurrent operation
loading.isLoading.value; // still true (counter = 2)
loading.stopLoading();
loading.isLoading.value; // still true (counter = 1)
loading.stopLoading();
loading.isLoading.value; // false (counter = 0)The real power is the middleware integration. Register it once and every HTTP request automatically tracks loading state:
import {createHttpService} from '@script-development/fs-http';
import {createLoadingService, registerLoadingMiddleware} from '@script-development/fs-loading';
const http = createHttpService('https://api.example.com');
const loading = createLoadingService();
// Wire them together
registerLoadingMiddleware(http, loading);
// Now every request automatically updates loading state
await http.getRequest('/users'); // isLoading: true → false
await http.postRequest('/users', data); // isLoading: true → falseIn a Vue component:
<script setup lang="ts">
import {loading} from '@/services';
</script>
<template>
<div v-if="loading.isLoading.value" class="spinner" />
<main v-else>
<slot />
</main>
</template>Long-running or stuck requests can leave loading state active forever. The middleware supports a timeout that automatically decrements the counter:
registerLoadingMiddleware(http, loading, {
timeoutMs: 30000, // 30 seconds — auto-recover if a request hangs
});Set timeoutMs: 0 to disable the timeout entirely.
::: tip Why a counter?
A boolean isLoading flag fails when you have concurrent requests. Request A starts → isLoading = true. Request B starts. Request A finishes → isLoading = false. But Request B is still in flight! The counter prevents this: A starts (1), B starts (2), A finishes (1), B finishes (0). Loading only becomes false when everything is done.
:::
ensureLoadingFinished() returns a promise that resolves when the counter reaches zero. Useful when you need to wait for all pending operations before proceeding:
// Wait for all in-flight requests to complete
await loading.ensureLoadingFinished();
// Now safe to proceed — no pending operationsThe registerLoadingMiddleware function returns an object with an unregister method that removes all middleware and clears pending timeouts:
const {unregister} = registerLoadingMiddleware(http, loading);
// Later: clean up everything
unregister();Returns a loading service with counter-based state tracking. No parameters.
| Property | Type | Description |
|---|---|---|
isLoading |
ComputedRef<boolean> |
true when counter > 0 |
activeCount |
DeepReadonly<Ref<number>> |
Current counter value (read-only) |
startLoading() |
() => void |
Increment counter |
stopLoading() |
() => void |
Decrement counter (floor at 0) |
ensureLoadingFinished() |
() => Promise<void> |
Resolves when counter reaches 0 |
| Parameter | Type | Description |
|---|---|---|
httpService |
HttpService |
The HTTP service to hook into |
loadingService |
LoadingService |
The loading service to update |
options.timeoutMs |
number |
Auto-recovery timeout in ms (default: 30000, 0 = disabled) |
Returns: { unregister: () => void }