SlickExperience
SlickExperience

Reputation: 51

Safari dropping Web Socket connection due to idle/inactivity when page not in focus

We are facing this issues with our app, only in safari browser, especially on iOS devices.

Current behavior

Not sure if this is a known issue (I tried searching but found nothing). Safari for Mac appears to be silently dropping web socket connections due to inactivity/idle if the page/tab is not in focus. The biggest issue is that in mobile iOS X is very persistent.

Steps to reproduce

Open Safari > Website loads > Put Safari in Idle and open any application or lock the device. On wake up, Safari is closing the connection and the data is not displayed anymore, we get infinite loading of the modules where we request the data.

Expected behavior Websockets should be kept alive via the heartbeat functionality. Not seeing this behavior in other browsers so unlikely to be the code.

Is this possibly some sort of power-saving feature that is overriding/ignoring the heartbeats?

import 'whatwg-fetch';
import Config from "../config/main";
import WS from "./websocket";
import Helpers from "./helperFunctions";

var Zergling = (function (WS, Config) {
    'use strict';

    var Zergling = {};

    var subscriptions = {}, useWebSocket = false, sessionRequestIsInProgress = false, loginInProgress = false,
        uiLogggedIn = false, // uiLogggedIn is the login state displayed in UI (sometimes it differs from real one, see delayedLogoutIfNotRestored func)
        authData, session, connectionAvailable, isLoggedIn, longPollUrl;

    Zergling.loginStates = {
        LOGGED_OUT: 0,
        LOGGED_IN: 1,
        IN_PROGRESS: 2
    };

    Zergling.codes = { // Swarm response codes
        OK: 0,
        SESSION_LOST: 5,
        NEED_TO_LOGIN: 12
    };

    function getLanguageCode (lng) {
        if (Config.swarm.languageMap && Config.swarm.languageMap[lng]) {
            return Config.swarm.languageMap[lng];
        }
        return lng;
    }

    //helper func for fetch
    function checkStatus (response) {
        if (response.status >= 200 && response.status < 300) {
            return response;
        } else {
            var error = new Error(response.statusText);
            error.response = response;
            throw error;
        }
    }

    //helper func for fetch
    function parseJSON (response) {
        return response.json();
    }

    /**
     * @description returns randomly selected(taking weight into consideration) long poll url
     * @returns {String} long polling URL
     */
    function getLongPollUrl () {
        if (!longPollUrl) {
            longPollUrl = Helpers.getWeightedRandom(Config.swarm.url).url;
            console.debug('long Polling URL selected:', longPollUrl);
        }
        return longPollUrl;
    }

    /**
     * @description
     * Applies the diff on object
     * properties having null values in diff are removed from  object, others' values are replaced.
     *
     * Also checks the 'price' field for changes and adds new field 'price_change' as sibling
     * which indicates the change direction (1 - up, -1 down, null - unchanged)
     *
     * @param {Object} current current object
     * @param {Object} diff    received diff
     */
    function destructivelyUpdateObject (current, diff) {
        if (current === undefined || !(current instanceof Object)) {
            throw new Error('wrong call');
        }

        for (var key in diff) {
            if (!diff.hasOwnProperty(key)) continue;
            var val = diff[key];
            if (val === null) {
                delete current[key];
            } else if (typeof val !== 'object') {
                current[key] = val;
            } else { // diff[key] is Object
                if (typeof current[key] !== 'object' || current[key] === null) {
                    current[key] = val;
                } else {
                    var hasPrice = (current[key].price !== undefined);
                    var oldPrice;
                    if (hasPrice) {
                        oldPrice = current[key].price;
                    }
                    destructivelyUpdateObject(current[key], val);
                    if (hasPrice) {
                        current[key].price_change = (val.price === oldPrice) ? null : (oldPrice < val.price) * 2 - 1;
                    }
                }
            }
        }
    }

Upvotes: 5

Views: 3612

Answers (1)

Myst
Myst

Reputation: 19221

This is and iOS feature that protects users against code draining their battery...

Push notifications for background applications should be performed using iOS's push notification system rather than by keeping an open connection alive.

There are hacks around this limitation, but the truth is that the limitation is good for the users and shouldn't be circumvented.

Read the technical note in the link for more details.

Upvotes: 2

Related Questions