# Periodic Sync

The Periodic Background Sync API allows your PWA to fetch fresh content periodically in the background, even when the app is closed. This ensures users always have up-to-date data when they open your app.

## Overview

Periodic sync enables:

* **Background updates**: Fetch fresh content while app is closed
* **Improved UX**: Users see latest content immediately
* **Reduced data usage**: Sync only when on WiFi/charging
* **Battery efficiency**: Browser controls execution based on engagement

{% hint style="info" %}
**Important**: Unlike other PWA Bundle features, Periodic Sync requires writing JavaScript code in both the service worker and your application.
{% endhint %}

## How It Works

1. **Client registration**: App requests periodic sync with a tag and interval
2. **Browser scheduling**: Browser decides when to actually run sync (based on engagement, battery, etc.)
3. **Service worker execution**: Service worker performs background task
4. **Client notification**: Service worker notifies clients of updates

## Browser Support

{% hint style="warning" %}
**Limited Support**: Periodic Background Sync is currently only supported in Chromium-based browsers (Chrome, Edge) on Android and Desktop. Not available in Safari or Firefox.
{% endhint %}

## Service Worker Implementation

### Basic Task Registration

Create periodic tasks in your service worker:

{% code title="assets/sw\.js" overflow="wrap" lineNumbers="true" %}

```javascript
const ping = async () => {
    const cache = await openCache('ping-cache');
    const res = await fetch('/ping');
    await cache.put('/ping', res.clone());

    notifyPeriodicSyncClients('ping', { updated: true });
};

registerPeriodicSyncTask('ping', ping);
```

{% endcode %}

{% hint style="success" %}
**Helper Functions**: The bundle provides `openCache`, `notifyPeriodicSyncClients`, and `registerPeriodicSyncTask` helpers in your service worker. These are automatically generated by the bundle.
{% endhint %}

### Helper Functions Reference

The bundle generates the following helper functions in the service worker:

| Function                                            | Description                                                                                               |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `registerPeriodicSyncTask(tag, callback, priority)` | Register a callback for a periodic sync tag. Optional `priority` (default: 100) controls execution order. |
| `notifyPeriodicSyncClients(tag, payload)`           | Send a message to all clients via BroadcastChannel when sync completes.                                   |
| `openCache(name)`                                   | Open (or reuse) a named cache instance.                                                                   |

### Multiple Tasks Under Same Tag

Register multiple related tasks with priorities:

{% code title="assets/sw\.js" lineNumbers="true" %}

```javascript
// Task 1: Update news cache (higher priority)
const updateNews = async () => {
    const cache = await openCache('news-cache');
    const response = await fetch('/api/news/latest');
    await cache.put('/api/news/latest', response.clone());
};

// Task 2: Clean old news (lower priority)
const cleanOldNews = async () => {
    const cache = await openCache('news-cache');
    const keys = await cache.keys();
    const now = Date.now();
    const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days

    for (const request of keys) {
        const response = await cache.match(request);
        const date = new Date(response.headers.get('date'));
        if (now - date.getTime() > maxAge) {
            await cache.delete(request);
        }
    }
};

// Register both under 'news-sync' tag
registerPeriodicSyncTask('news-sync', updateNews, 50);   // runs first
registerPeriodicSyncTask('news-sync', cleanOldNews, 200); // runs after
```

{% endcode %}

### Complete Example: Blog Post Sync

{% code title="assets/sw\.js" lineNumbers="true" %}

```javascript
const syncBlogPosts = async () => {
    try {
        const response = await fetch('/api/blog/latest');
        if (!response.ok) {
            throw new Error('Failed to fetch blog posts');
        }

        const posts = await response.json();
        const cache = await openCache('blog-cache');
        await cache.put('/api/blog/latest', response.clone());

        for (const post of posts) {
            const postResponse = new Response(JSON.stringify(post));
            await cache.put(`/api/blog/post/${post.id}`, postResponse);
        }

        notifyPeriodicSyncClients('blog-sync', {
            updated: true,
            postCount: posts.length,
            timestamp: Date.now()
        });
    } catch (error) {
        console.error('Blog sync failed:', error);
        notifyPeriodicSyncClients('blog-sync', {
            updated: false,
            error: error.message
        });
    }
};

registerPeriodicSyncTask('blog-sync', syncBlogPosts);
```

{% endcode %}

## Client-Side Registration

### Basic Registration

Request periodic sync from your application:

{% code title="assets/app.js" lineNumbers="true" %}

```javascript
async function setupPeriodicSync() {
    // Check if supported
    if (!('periodicSync' in ServiceWorkerRegistration.prototype)) {
        console.warn('Periodic Sync not supported');
        return;
    }

    const registration = await navigator.serviceWorker.ready;

    // Register sync every 12 hours (minimum interval)
    await registration.periodicSync.register('content-sync', {
        minInterval: 12 * 60 * 60 * 1000,
    });
}

setupPeriodicSync();
```

{% endcode %}

### Listening for Sync Updates

Receive notifications when sync completes via BroadcastChannel:

{% code title="assets/app.js" lineNumbers="true" %}

```javascript
const channel = new BroadcastChannel('periodic-sync');
channel.addEventListener('message', (event) => {
    const { tag, timestamp, ...data } = event.data;

    console.log(`Periodic sync '${tag}' completed at ${new Date(timestamp)}:`, data);

    if (data.updated) {
        // Update UI with fresh data
        updateUIWithNewContent();
    }
});
```

{% endcode %}

## Common Use Cases

### News/Content Updates

{% code title="assets/sw\.js" lineNumbers="true" %}

```javascript
const syncNews = async () => {
    const cache = await openCache('news-cache');
    const response = await fetch('/api/news/latest?limit=20');
    await cache.put('/api/news/latest', response.clone());

    const articles = await response.json();
    notifyPeriodicSyncClients('news-sync', {
        updated: true,
        count: articles.length
    });
};

registerPeriodicSyncTask('news-sync', syncNews);
```

{% endcode %}

### User Data Prefetch

{% code title="assets/sw\.js" lineNumbers="true" %}

```javascript
const prefetchUserData = async () => {
    const endpoints = [
        '/api/user/profile',
        '/api/user/settings',
        '/api/user/notifications'
    ];

    const cache = await openCache('user-cache');

    for (const endpoint of endpoints) {
        try {
            const response = await fetch(endpoint);
            if (response.ok) {
                await cache.put(endpoint, response.clone());
            }
        } catch (error) {
            console.warn(`Failed to prefetch ${endpoint}:`, error);
        }
    }

    notifyPeriodicSyncClients('user-sync', { updated: true });
};

registerPeriodicSyncTask('user-sync', prefetchUserData);
```

{% endcode %}

## Browser Execution Control

{% hint style="warning" %}
**Important**: The browser controls when periodic sync actually runs. The interval you specify is a **minimum**, not a guarantee.
{% endhint %}

### Factors Affecting Execution

* **Site Engagement Score**: High engagement = more frequent sync
* **PWA Installation**: Installed PWAs get higher priority
* **Device State**: Battery level, charging status, data saver mode
* **Network**: WiFi preferred over cellular
* **Interval Duration**: Very short intervals (< 12 hours) often ignored

### Recommended Intervals

```javascript
// Too short - likely ignored
registration.periodicSync.register('tag', { minInterval: 30 * 60 * 1000 }); // 30 min

// Reasonable
registration.periodicSync.register('tag', { minInterval: 12 * 60 * 60 * 1000 }); // 12h

// Ideal - most likely respected
registration.periodicSync.register('tag', { minInterval: 24 * 60 * 60 * 1000 }); // 24h
```

## Managing Periodic Sync

### Check Registered Tags

```javascript
const registration = await navigator.serviceWorker.ready;
const tags = await registration.periodicSync.getTags();
console.log('Registered periodic sync tags:', tags);
```

### Unregister Periodic Sync

```javascript
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.unregister('news-sync');
```

## Testing

### Force Sync in DevTools

1. Open DevTools (F12)
2. Go to **Application** → **Service Workers**
3. Find your service worker
4. Look for "Periodic Sync" section
5. Enter tag name and click "Start"

## Graceful Degradation

Handle unsupported browsers:

{% code title="assets/app.js" lineNumbers="true" %}

```javascript
async function setupBackgroundSync() {
    if ('periodicSync' in ServiceWorkerRegistration.prototype) {
        const registration = await navigator.serviceWorker.ready;
        await registration.periodicSync.register('content-sync', {
            minInterval: 12 * 60 * 60 * 1000,
        });
    } else {
        // Fallback: manual polling when app is open
        setInterval(async () => {
            if (document.visibilityState === 'visible') {
                await fetchLatestContent();
            }
        }, 5 * 60 * 1000); // Check every 5 minutes
    }
}
```

{% endcode %}

## Limitations

* **Supported**: Chrome, Edge (Chromium-based) only
* **Not Supported**: Safari, Firefox, older browsers
* **Platform**: Android and Desktop only (not iOS)
* Limited execution time (typically < 30 seconds)
* Browser decides actual execution time

## Permissions

```javascript
const status = await navigator.permissions.query({
    name: 'periodic-background-sync'
});
console.log('Permission status:', status.state);
// 'granted', 'denied', or 'prompt'
```

## Best Practices

1. **Use reasonable intervals**: 12+ hours recommended
2. **Minimize network requests**: Batch operations into a single sync
3. **Provide user control**: Allow users to configure sync frequency
4. **Handle failures gracefully**: Return Promises, use try/catch
5. **Check network conditions**: Skip heavy sync on poor connections

## Related Documentation

* [Background Sync](/the-service-worker/workbox/background-sync.md) - One-time background sync
* [Service Worker Configuration](/the-service-worker/configuration.md) - Service worker setup
* [Workbox](/the-service-worker/workbox.md) - Workbox features


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://pwa.spomky-labs.com/the-service-worker/periodic-sync.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
