Search code examples
javascriptes6-promiseservice-workerrace-condition

Trying to run fetch function before register a service worker.


I am trying to run a fetch function which will get me some values from my server. But for some reason the registration of a service worker fires first and its leaving "app_key" undefined causing it to thrown exception. It seems like this is a race condition that I do not know how to solve.

I am not sure how should I assign the values to the variables to make sure they are visible by the other function. Any suggestions would be appreciated.

project_url = window.location.hostname;

var vapi_key;
var app_key;
var pushSub;
var end_point = "https://" + project_url + "/push/subscribe/create";

(() =>  {
    return fetch ("https://" + project_url + "/push/get_public_key"
    ).then((response) => {
        return response.json();
    })
    .then((responseJSON) => {
        vapi_key = responseJSON;
        urlBase64ToUint8Array(vapi_key);
        console.log("VAPI_KEY: ", vapi_key);
        console.log("APP_KEY: ", app_key)
    }).catch((err) => {
        console.log('Fetch Error : ', err);
    });
})();


(() => {
    return new Promise((resolve, reject) => {
        const permissionResult = Notification.requestPermission((result) => {
            resolve(result);
        });

        if (permissionResult) {
            permissionResult.then(resolve, reject);
        }
    })
        .then((permissionResult) => {
            if (permissionResult !== 'granted') {
                throw new Error('We weren\'t granted permission.');
            }
        });
})();

(() => {
    if ('serviceWorker' in navigator && 'PushManager' in window) {
        navigator.serviceWorker.register('/sw_bundle.js')
            .then((registration) => {
                let subscribeOptions = {
                    userVisibleOnly: true,
                    applicationServerKey: app_key,
                };

                return registration.pushManager.subscribe(subscribeOptions);
            })
            .then((pushSubscription) => {
                console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
                const subscriptionObject = {
                    endpoint: pushSubscription.endpoint,
                    keys: {
                        p256dh: pushSubscription.keys['p256dh'],
                        auth: pushSubscription.keys['auth']
                    }
                };
                sendSubscriptionToBackEnd(subscriptionObject);
                return pushSubscription;
            });
    } else {
        console.warn('Push messaging is not supported');
    }
})();

function urlBase64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');
    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);
    for (var i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    app_key = outputArray;
}

Solution

  • I'm unsure why you expect this to work. Especially why you use IIFEs to create the promises. Anyway, if you want one promise to be chained to another promise, you need to make this explicit by creating the second promise in a .then call to the first promise. Here's an example:

    const createPromiseA = () => new Promise((resolve) => {
    window.setTimeout(() => { console.log('promiseA'); resolve(); }, 2000);
    });
    
    const createPromiseB = () => new Promise((resolve) => {
    window.setTimeout(() => { console.log('promiseB'); resolve(); }, 3000);
    });
    
    createPromiseA().then(createPromiseB);

    In your case, you need to do something like:

    const fetchKeys = () => fetch ("https://" + project_url + "/push/get_public_key"
        ).then((response) => {
            return response.json();
        })
        .then((responseJSON) => {
            vapi_key = responseJSON;
            urlBase64ToUint8Array(vapi_key);
            console.log("VAPI_KEY: ", vapi_key);
            console.log("APP_KEY: ", app_key)
        }).catch((err) => {
            console.log('Fetch Error : ', err);
        });
    
    const registerWorker = () => {
        if ('serviceWorker' in navigator && 'PushManager' in window) {
            navigator.serviceWorker.register('/sw_bundle.js')
                .then((registration) => {
                    let subscribeOptions = {
                        userVisibleOnly: true,
                        applicationServerKey: app_key,
                    };
    
                    return registration.pushManager.subscribe(subscribeOptions);
                })
                .then((pushSubscription) => {
                    console.log('Received PushSubscription: ', JSON.stringify(pushSubscription));
                    const subscriptionObject = {
                        endpoint: pushSubscription.endpoint,
                        keys: {
                            p256dh: pushSubscription.keys['p256dh'],
                            auth: pushSubscription.keys['auth']
                        }
                    };
                    sendSubscriptionToBackEnd(subscriptionObject);
                    return pushSubscription;
                });
        } else {
            return Promise.reject('Push messaging is not supported');
        }
    };
    
    // …
    
    fetchKeys().then(registerWorker)
    .then(() => { console.log('(!) Success!'); });
    

    NOTE: An—imo—better way to utilize IIFEs would be to wrap your entire script in a function and execute it. That would bind the variables to the local scope of the function and not expose them. I.e. do something like:

    (() => {
        project_url = window.location.hostname;
    
        var vapi_key;
        var app_key;
        var pushSub;
        var end_point = "https://" + project_url + "/push/subscribe/create";
    
        // ...
    
        const fetchKeys = () => { ... } ;
        const registerWorker = () => { ... } ;
    
        // ...
    
        fetchKeys().then(registerWorker)
        .then(() => { console.log('(!) Success!'); });
    
    })();