BackgroundSync Queue
The BackgroundSync Queue component provides real-time monitoring and manual control of background synchronization queues in your Progressive Web App. When network requests fail (due to being offline or network errors), they are automatically queued by the service worker and retried when connectivity is restored. This component displays the number of pending requests and allows users to manually trigger queue replay.
This component is particularly useful for:
Showing users how many pending actions are waiting to sync
Providing manual retry buttons for failed operations
Building offline-first forms and data entry applications
Creating reliable e-commerce checkout flows that work offline
Implementing offline task management and todo applications
Monitoring background sync status in real-time
Giving users control over when to retry failed operations
Building dashboards that display sync status across multiple queues
Browser Support
The BackgroundSync Queue component relies on the Broadcast Channel API for communication with the service worker, which is widely supported in modern browsers.
Support level: Excellent - Works on all modern browsers that support service workers and the Broadcast Channel API (Chrome, Firefox, Safari 15.4+, Edge).
Background Sync automatically retries failed requests when the device regains connectivity. This component simply provides visibility and manual control over that process.
How It Works
Service Worker Queue: When requests fail, Workbox queues them in a background sync queue
Broadcast Channel: The service worker broadcasts queue status updates via a named channel
Component Listening: The BackgroundSync Queue component listens to the channel
Status Display: Component receives and displays the number of pending requests
Manual Replay: Users can trigger immediate queue replay via a button action
Configuration
First, configure background sync with a broadcast channel in your PWA configuration:
pwa:
serviceworker:
enabled: true
workbox:
enabled: true
background_sync:
- queue_name: 'form-submissions'
broadcast_channel: 'form-queue-status'
max_retention_time: 10080 # 7 days in minutes
- queue_name: 'api-requests'
broadcast_channel: 'api-queue-status'
max_retention_time: 4320 # 3 days in minutesThe broadcast_channel name must match the channel parameter in your component.
Usage
Basic Queue Status Display
<div {{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'form-queue-status'}) }}>
<p>Pending form submissions: <strong id="queue-count">--</strong></p>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}>
Retry Now
</button>
</div>
<script type="module">
const host = document.querySelector('[data-controller="pwa__backgroundsync-queue"]');
const countEl = document.getElementById('queue-count');
host.addEventListener('backgroundsync-queue:status', (e) => {
if (Number.isInteger(e.detail.remaining)) {
countEl.textContent = e.detail.remaining;
if (e.detail.remaining === 0) {
countEl.parentElement.classList.add('text-green-600');
countEl.parentElement.classList.remove('text-yellow-600');
} else {
countEl.parentElement.classList.add('text-yellow-600');
countEl.parentElement.classList.remove('text-green-600');
}
}
});
host.addEventListener('backgroundsync-queue:error', (e) => {
console.error('Queue error:', e.detail.reason);
});
</script>Visual Queue Status Indicator
<div {{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'form-queue-status'}) }}>
<div id="sync-status" class="status-indicator hidden">
<div class="status-icon">⏳</div>
<div class="status-text">
<strong id="queue-count">0</strong> item(s) waiting to sync
</div>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}
class="retry-btn">
Sync Now
</button>
</div>
</div>
<script type="module">
const host = document.querySelector('[data-controller="pwa__backgroundsync-queue"]');
const statusEl = document.getElementById('sync-status');
const countEl = document.getElementById('queue-count');
host.addEventListener('backgroundsync-queue:status', (e) => {
const remaining = e.detail.remaining;
if (Number.isInteger(remaining)) {
countEl.textContent = remaining;
if (remaining > 0) {
statusEl.classList.remove('hidden');
} else {
statusEl.classList.add('hidden');
}
}
});
</script>
<style>
.status-indicator {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
margin: 1rem 0;
}
.status-icon {
font-size: 2rem;
}
.retry-btn {
margin-left: auto;
padding: 0.5rem 1rem;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.retry-btn:hover {
background: #0056b3;
}
</style>Multiple Queue Monitoring
<div class="queue-dashboard">
<h2>Sync Status</h2>
{# Form submissions queue #}
<div class="queue-item"
{{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'form-queue-status'}) }}
data-queue="forms">
<span class="queue-name">Form Submissions:</span>
<span class="queue-count" data-target="count">--</span>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}>Retry</button>
</div>
{# API requests queue #}
<div class="queue-item"
{{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'api-queue-status'}) }}
data-queue="api">
<span class="queue-name">API Requests:</span>
<span class="queue-count" data-target="count">--</span>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}>Retry</button>
</div>
{# File uploads queue #}
<div class="queue-item"
{{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'upload-queue-status'}) }}
data-queue="uploads">
<span class="queue-name">File Uploads:</span>
<span class="queue-count" data-target="count">--</span>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}>Retry</button>
</div>
</div>
<script type="module">
document.querySelectorAll('.queue-item').forEach(queueEl => {
const countEl = queueEl.querySelector('[data-target="count"]');
queueEl.addEventListener('backgroundsync-queue:status', (e) => {
const remaining = e.detail.remaining;
if (Number.isInteger(remaining)) {
countEl.textContent = remaining;
if (remaining > 0) {
queueEl.classList.add('has-pending');
} else {
queueEl.classList.remove('has-pending');
}
}
});
});
</script>
<style>
.queue-dashboard {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1.5rem;
}
.queue-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
margin: 0.5rem 0;
background: #f8f9fa;
border-radius: 4px;
}
.queue-item.has-pending {
background: #fff3cd;
border-left: 4px solid #ffc107;
}
.queue-name {
flex: 1;
font-weight: 500;
}
.queue-count {
font-weight: bold;
min-width: 3ch;
text-align: center;
}
</style>Automatic Polling for Status Updates
<div {{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'form-queue-status'}) }}
id="sync-monitor">
<div class="sync-header">
<h3>Sync Queue</h3>
<span id="last-update">Never</span>
</div>
<div class="sync-body">
<p>Pending items: <strong id="pending-count">--</strong></p>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}>
Sync Now
</button>
</div>
</div>
<script type="module">
const host = document.querySelector('[data-controller="pwa__backgroundsync-queue"]');
const countEl = document.getElementById('pending-count');
const updateEl = document.getElementById('last-update');
// Request status update every 30 seconds
setInterval(() => {
// The component automatically requests status on connect
// For manual refresh, we can reconnect the controller
if (host.controller && host.controller.bc) {
host.controller.bc.postMessage({ type: 'status-request' });
}
}, 30000);
host.addEventListener('backgroundsync-queue:status', (e) => {
if (Number.isInteger(e.detail.remaining)) {
countEl.textContent = e.detail.remaining;
updateEl.textContent = new Date().toLocaleTimeString();
}
});
</script>Integration with Form Submission
<div {{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'form-queue-status'}) }}>
{# Show queue status banner if items are pending #}
<div id="sync-banner" class="hidden alert alert-warning">
<p>You have <strong id="queue-size">0</strong> form(s) waiting to sync.</p>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}>
Try Syncing Now
</button>
</div>
{# Regular form #}
<form action="/submit" method="POST">
<input type="text" name="title" required>
<textarea name="description" required></textarea>
<button type="submit">Submit</button>
</form>
</div>
<script type="module">
const host = document.querySelector('[data-controller="pwa__backgroundsync-queue"]');
const banner = document.getElementById('sync-banner');
const sizeEl = document.getElementById('queue-size');
host.addEventListener('backgroundsync-queue:status', (e) => {
const remaining = e.detail.remaining;
if (Number.isInteger(remaining)) {
sizeEl.textContent = remaining;
if (remaining > 0) {
banner.classList.remove('hidden');
} else {
banner.classList.add('hidden');
}
}
});
// Show success message when queue is emptied
let previousCount = null;
host.addEventListener('backgroundsync-queue:status', (e) => {
const current = e.detail.remaining;
if (previousCount > 0 && current === 0) {
alert('All pending forms have been synced successfully!');
}
previousCount = current;
});
</script>Best Practices
Always Configure Broadcast Channel
The broadcast channel must be configured in both the PWA config file and the component. Mismatched channel names will prevent communication between the service worker and the component.
# config/packages/pwa.yaml
pwa:
serviceworker:
workbox:
background_sync:
- queue_name: 'my-queue'
broadcast_channel: 'my-queue-status' # Must match component{# Channel name must match configuration #}
<div {{ stimulus_controller('@pwa/backgroundsync-queue', {
channel: 'my-queue-status'
}) }}>Handle Edge Cases
host.addEventListener('backgroundsync-queue:status', (e) => {
// Always validate the data type
if (Number.isInteger(e.detail.remaining)) {
updateDisplay(e.detail.remaining);
} else {
// Show loading state
updateDisplay('--');
}
});Provide User Feedback
Always give users feedback when they trigger a manual replay. Even if the sync fails, users should know something happened.
const replayBtn = document.querySelector('button');
let isReplaying = false;
replayBtn.addEventListener('click', () => {
if (isReplaying) return;
isReplaying = true;
replayBtn.disabled = true;
replayBtn.textContent = 'Syncing...';
// Reset after a few seconds (service worker will handle the actual sync)
setTimeout(() => {
isReplaying = false;
replayBtn.disabled = false;
replayBtn.textContent = 'Sync Now';
}, 3000);
});Monitor Multiple Queues Separately
Common Use Cases
1. Offline Form Submission Tracker
Track pending form submissions and show users what's waiting to sync:
<div {{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'form-submissions'}) }}>
<div class="submission-tracker">
<div id="offline-notice" class="hidden">
<h4>⚠️ You're currently offline</h4>
<p>Your form submissions will be sent automatically when you're back online.</p>
<p>Pending submissions: <strong id="pending">0</strong></p>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}>
Try Sending Now
</button>
</div>
</div>
</div>
<script type="module">
const host = document.querySelector('[data-controller="pwa__backgroundsync-queue"]');
const notice = document.getElementById('offline-notice');
const pending = document.getElementById('pending');
host.addEventListener('backgroundsync-queue:status', (e) => {
const count = e.detail.remaining;
if (Number.isInteger(count) && count > 0) {
pending.textContent = count;
notice.classList.remove('hidden');
} else {
notice.classList.add('hidden');
}
});
// Also check online status
window.addEventListener('online', () => {
notice.querySelector('h4').textContent = '✅ Back online!';
setTimeout(() => {
if (host.controller && host.controller.bc) {
host.controller.bc.postMessage({ type: 'status-request' });
}
}, 1000);
});
</script>2. E-Commerce Offline Checkout
Show customers their pending orders waiting to process:
<div class="checkout-status"
{{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'order-queue'}) }}>
<div id="pending-orders" class="hidden">
<div class="alert alert-info">
<h5>Pending Orders</h5>
<p>You have <strong id="order-count">0</strong> order(s) waiting to be processed.</p>
<p class="text-sm">Orders will be submitted automatically when connection is restored.</p>
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}
class="btn btn-primary btn-sm">
Submit Orders Now
</button>
</div>
</div>
</div>
<script type="module">
const host = document.querySelector('[data-controller="pwa__backgroundsync-queue"]');
const container = document.getElementById('pending-orders');
const count = document.getElementById('order-count');
host.addEventListener('backgroundsync-queue:status', (e) => {
if (Number.isInteger(e.detail.remaining)) {
count.textContent = e.detail.remaining;
if (e.detail.remaining > 0) {
container.classList.remove('hidden');
} else {
container.classList.add('hidden');
}
}
});
</script>3. Todo/Task Management Offline Sync
Display pending task changes waiting to sync with the server:
<div class="task-manager">
<div {{ stimulus_controller('@pwa/backgroundsync-queue', {channel: 'task-sync'}) }}
class="sync-indicator">
<span id="sync-status" class="badge badge-secondary">Synced</span>
</div>
<div class="task-list">
{# Task items here #}
</div>
</div>
<script type="module">
const host = document.querySelector('[data-controller="pwa__backgroundsync-queue"]');
const status = document.getElementById('sync-status');
host.addEventListener('backgroundsync-queue:status', (e) => {
const pending = e.detail.remaining;
if (Number.isInteger(pending)) {
if (pending > 0) {
status.textContent = `Syncing... (${pending} pending)`;
status.className = 'badge badge-warning';
} else {
status.textContent = 'Synced';
status.className = 'badge badge-success';
}
}
});
</script>API Reference
Values
channel
channelType: String Required: Yes
The name of the broadcast channel to listen to. Must match the broadcast_channel configured in your PWA configuration file.
<div {{ stimulus_controller('@pwa/backgroundsync-queue', {
channel: 'form-queue-status'
}) }}>Actions
replay()
replay()Manually triggers replay of all pending requests in the queue. Sends a replay-request message to the service worker via the broadcast channel.
<button {{ stimulus_action('@pwa/backgroundsync-queue', 'replay') }}>
Retry Now
</button>Targets
None
Events
status
statusFired when a status update is received from the service worker containing the current queue state.
Event detail:
name(string): Name of the queueremaining(number): Number of pending requests in the queue
host.addEventListener('backgroundsync-queue:status', (e) => {
console.log(`Queue ${e.detail.name} has ${e.detail.remaining} pending requests`);
});error
errorFired when an error occurs (e.g., no channel provided).
Event detail:
reason(string): Description of the error
host.addEventListener('backgroundsync-queue:error', (e) => {
console.error('Queue error:', e.detail.reason);
});How Queue Updates Work
The component automatically:
On Connect: Opens a BroadcastChannel connection with the specified channel name
Status Request: Immediately sends a
status-requestmessage to the service workerListen for Updates: Listens for status messages from the service worker
Dispatch Events: Emits
statusevents with queue informationOn Disconnect: Closes the broadcast channel connection
The service worker broadcasts status updates:
When requests are added to the queue
When requests are successfully synced
When a
status-requestis receivedWhen a
replay-requestis processed
Related Components
BackgroundSync Form - Automatically queue form submissions when offline
Service Worker - Service worker configuration and setup
Connection Status - Monitor online/offline status
Sync Broadcast - Generic broadcast channel communication
Resources
Last updated
Was this helpful?