how do you sync data on apple devices with service workers?

I have found an answer to this after like 2 weeks of it being on my mind and on my to do list. Now get some popcorn and strap yourself the heck in, because this is quite a chonker. In my case the sync process was pretty complex as my users could be away from any connection for such a long time that my accessTokens would expire so i had to do a check for the access token expiration as well and reFetch it. Furthermore my users could add new people to the database of people, which all had their on unique server side id-s, so i had to order my requests in a way that the person registrations are sent first then the tasks and campaigns that were completed for them, so i can receive the respective ids from the API.

Now for the fun part: Firstly you cant use a bgSyncPlugin, because you cant access the replayQueue, you have to use a normal queue, like this:

var bgSyncQueue = new workbox.backgroundSync.Queue('uploadQueue', {
    maxRetentionTime: 60 * 24 * 60,
    onSync: () => syncData(),
   });

And push the failed requests to the queue inside the fetch listener:

this.onfetch = (event) => {
    let requestClone = event.request.clone();
    if (requestClone.method === 'POST' && 'condition to match the requests you need to replay') {
        event.respondWith(
            (() => {
                const promiseChain = fetch(requestClone).catch(() => {
                    return bgSyncQueue.pushRequest(event);
                });
                event.waitUntil(promiseChain);
                return promiseChain;
            })()
        );
    } else {
        event.respondWith(fetch(event.request));
    }
};

When user has connection we trigger the “syncData()” function, on ios this is a bit complicated(more on this later), on android it happens automatically, as the service worker sees it has connection, now lets just check out what syncData does:

async function syncData() {
    if (bgSyncQueue) //is there data to sync?
        return getAccessToken() //then get the access token, if expired refresh it
            .then((token) => replayQueue(bgSyncQueue, token).then(() => showNotification({ body: 'Succsesful sync', title: 'Data synced to server' })))
            .catch(() => showNotification({ title: 'Sync unsuccessful', body: 'Please find and area with better coverage' })); //replay the requests and show a notification
    return Promise.resolve('empty');//if no requests to replay return with empty
}

For the android/desktop side of thing we are finished you can be happy with your modified data being synced, now on iOS we cant just have the users data be uploaded only when they restart the PWA, thats bad user experience, but we are playing with javascript everything is possible in a way or another.

There is a message event that can be fired every time that the client code sees that it has internet, which looks like this:

if (this.$online && this.isIOSDevice) {
                    if (window.MessageChannel) {
                        var messageChannel = new MessageChannel();
                        messageChannel.port1.onmessage = (event) => {
                            this.onMessageSuccess(event);
                        };
                    } else {
                        navigator.serviceWorker.onmessage = (event) => {
                            this.onMessageSuccess(event);
                        };
                    }
                    navigator.serviceWorker.ready.then((reg) => {
                        try {
                            reg.active.postMessage(
                                {
                                    text: 'sync',
                                    port: messageChannel && messageChannel.port2,
                                },
                                [messageChannel && messageChannel.port2]
                            );
                        } catch (e) {
                            //firefox support
                            reg.active.postMessage({
                                text: 'sync',
                            });
                        }
                    });
                }

this is inside a Vue.js watch function, which watches whether we have connection or not, if we have connection it also checks if this is a device from the apple ecosystem, like so:

isIosDevice() {
            return !!navigator.platform && /iPad|iPhone|MacIntel|iPod/.test(navigator.platform) && /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
        }

And so it tells the service worker that it has internet and it has to sync, in that case this bit of code gets activated:

this.onmessage = (event) => {
    if (event.data.text === 'sync') {
        event.waitUntil(
            syncData().then((res) => {
                if (res !== 'empty') {
                    if (event.source) {
                        event.source.postMessage('doNotification');//this is telling the client code to show a notification (i have a built in notification system into the app, that does not use push notification, just shows a little pill on the bottom of the app with the message)
                    } else if (event.data.port) {
                        event.data.port.postMessage('doNotification'); //same thing
                    }
                    return res;
                }
            })
        );
    }
};

Now the most useful part in my opinion, the replay queue function, this guy gets the queue and the token from getAccessToken, and then it does its thing like clockwork:

    const replayQueue = async (queue, token) => {
        let entry;
        while ((entry = await queue.shiftRequest())) {//while we have requests to replay
            let data = await entry.request.clone().json();
            try {
//replay the person registrations first and store them into indexed db
                if (isPersonRequest) {
                    //if new person
                    await fetchPerson(entry, data, token);
//then replay the campaign and task submissions
                } else if (isTaskOrCampaignRequest) {
                    //if task
                    await fetchCampaigns(entry, data, token);
                } 
            } catch (error) {
                showNotification({ title: 'no success', body: 'go for better internet plox' });
                await queue.unshiftRequest(entry); //put failed request back into queue, and try again later
            }
        }
        return Promise.resolve();
    };

Now this is the big picture as how to use this guy on iOS devices and make Apple mad as heck 🙂 I am open to any questions that are related, in this time i think i have become pretty good with service worker related stuff as this was not the only difficult part of this project but i digress, thats a story for another day.

(you may see that error handling is not perfect and maybe this thing is not he most secure of them all, but this project has a prettty small amount of users, with a fixed number which know how to use it and what it does, so im not really afraid of security in this case, but you may want to improve on things if you use in in a more serious project)

Hope i could help and all of you have a grea day.

CLICK HERE to find out more related problems solutions.

Leave a Comment

Your email address will not be published.

Scroll to Top