# 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](https://pwa.spomky-labs.com/the-service-worker/workbox/background-sync) - One-time background sync
* [Service Worker Configuration](https://pwa.spomky-labs.com/the-service-worker/configuration) - Service worker setup
* [Workbox](https://pwa.spomky-labs.com/the-service-worker/workbox) - Workbox features
