Simplify online/offline status management
This commit is contained in:
parent
b359d28771
commit
9aff86f02b
22 changed files with 432 additions and 335 deletions
|
@ -80,6 +80,7 @@ const noop = () => {};
|
|||
window.Whisper = window.Whisper || {};
|
||||
window.Whisper.events = {
|
||||
on: noop,
|
||||
off: noop,
|
||||
};
|
||||
|
||||
window.SignalContext = {
|
||||
|
|
163
ts/background.ts
163
ts/background.ts
|
@ -478,8 +478,7 @@ export async function startApp(): Promise<void> {
|
|||
|
||||
senderCertificateService.initialize({
|
||||
server,
|
||||
navigator,
|
||||
onlineEventTarget: window,
|
||||
events: window.Whisper.events,
|
||||
storage: window.storage,
|
||||
});
|
||||
|
||||
|
@ -1334,8 +1333,8 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
log.warn('background: remote expiration detected, disabling reconnects');
|
||||
drop(server?.onRemoteExpiration());
|
||||
remotelyExpired = true;
|
||||
onOffline();
|
||||
});
|
||||
|
||||
async function runStorageService() {
|
||||
|
@ -1417,12 +1416,18 @@ export async function startApp(): Promise<void> {
|
|||
log.info('Expiration start timestamp cleanup: complete');
|
||||
|
||||
log.info('listening for registration events');
|
||||
window.Whisper.events.on('registration_done', async () => {
|
||||
window.Whisper.events.on('registration_done', () => {
|
||||
log.info('handling registration event');
|
||||
|
||||
strictAssert(server !== undefined, 'WebAPI not ready');
|
||||
await server.authenticate(
|
||||
window.textsecure.storage.user.getWebAPICredentials()
|
||||
|
||||
// Once this resolves it will trigger `online` event and cause
|
||||
// `connect()`, but with `firstRun` set to `false`. Thus it is important
|
||||
// not to await it and let execution fall through.
|
||||
drop(
|
||||
server.authenticate(
|
||||
window.textsecure.storage.user.getWebAPICredentials()
|
||||
)
|
||||
);
|
||||
|
||||
// Cancel throttled calls to refreshRemoteConfig since our auth changed.
|
||||
|
@ -1525,52 +1530,20 @@ export async function startApp(): Promise<void> {
|
|||
return syncRequest;
|
||||
};
|
||||
|
||||
let disconnectTimer: Timers.Timeout | undefined;
|
||||
let reconnectTimer: Timers.Timeout | undefined;
|
||||
function onOffline() {
|
||||
log.info('offline');
|
||||
function onNavigatorOffline() {
|
||||
log.info('background: navigator offline');
|
||||
|
||||
window.removeEventListener('offline', onOffline);
|
||||
window.addEventListener('online', onOnline);
|
||||
|
||||
// We've received logs from Linux where we get an 'offline' event, then 30ms later
|
||||
// we get an online event. This waits a bit after getting an 'offline' event
|
||||
// before disconnecting the socket manually.
|
||||
disconnectTimer = Timers.setTimeout(disconnect, 1000);
|
||||
|
||||
if (challengeHandler) {
|
||||
void challengeHandler.onOffline();
|
||||
}
|
||||
drop(server?.onNavigatorOffline());
|
||||
}
|
||||
|
||||
function onOnline() {
|
||||
if (remotelyExpired) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('online');
|
||||
|
||||
window.removeEventListener('online', onOnline);
|
||||
window.addEventListener('offline', onOffline);
|
||||
|
||||
if (disconnectTimer && isSocketOnline()) {
|
||||
log.warn('Already online. Had a blip in online/offline status.');
|
||||
Timers.clearTimeout(disconnectTimer);
|
||||
disconnectTimer = undefined;
|
||||
|
||||
if (challengeHandler) {
|
||||
drop(challengeHandler.onOnline());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (disconnectTimer) {
|
||||
Timers.clearTimeout(disconnectTimer);
|
||||
disconnectTimer = undefined;
|
||||
}
|
||||
|
||||
void connect();
|
||||
function onNavigatorOnline() {
|
||||
log.info('background: navigator online');
|
||||
drop(server?.onNavigatorOnline());
|
||||
}
|
||||
|
||||
window.addEventListener('online', onNavigatorOnline);
|
||||
window.addEventListener('offline', onNavigatorOffline);
|
||||
|
||||
function isSocketOnline() {
|
||||
const socketStatus = window.getSocketStatus();
|
||||
return (
|
||||
|
@ -1579,34 +1552,47 @@ export async function startApp(): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
log.info('disconnect');
|
||||
|
||||
// Clear timer, since we're only called when the timer is expired
|
||||
disconnectTimer = undefined;
|
||||
|
||||
void AttachmentDownloads.stop();
|
||||
if (server !== undefined) {
|
||||
strictAssert(
|
||||
messageReceiver !== undefined,
|
||||
'WebAPI should be initialized together with MessageReceiver'
|
||||
);
|
||||
await server.onOffline();
|
||||
await messageReceiver.drain();
|
||||
window.Whisper.events.on('online', () => {
|
||||
log.info('background: online');
|
||||
if (!remotelyExpired) {
|
||||
drop(connect());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.Whisper.events.on('offline', () => {
|
||||
log.info('background: offline');
|
||||
|
||||
drop(challengeHandler?.onOffline());
|
||||
drop(AttachmentDownloads.stop());
|
||||
drop(messageReceiver?.drain());
|
||||
|
||||
if (connectCount === 0) {
|
||||
log.info('background: offline, never connected, showing inbox');
|
||||
|
||||
drop(onEmpty()); // this ensures that the loading screen is dismissed
|
||||
|
||||
// Switch to inbox view even if contact sync is still running
|
||||
if (window.reduxStore.getState().app.appView === AppViewType.Installer) {
|
||||
log.info('background: offline, opening inbox');
|
||||
window.reduxActions.app.openInbox();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let connectCount = 0;
|
||||
let connecting = false;
|
||||
let remotelyExpired = false;
|
||||
async function connect(firstRun?: boolean) {
|
||||
if (connecting) {
|
||||
log.warn('connect already running', { connectCount });
|
||||
log.warn('background: connect already running', {
|
||||
connectCount,
|
||||
firstRun,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (remotelyExpired) {
|
||||
log.warn('remotely expired, not reconnecting');
|
||||
log.warn('background: remotely expired, not reconnecting');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1618,40 +1604,13 @@ export async function startApp(): Promise<void> {
|
|||
// Reset the flag and update it below if needed
|
||||
setIsInitialSync(false);
|
||||
|
||||
log.info('connect', { firstRun, connectCount });
|
||||
|
||||
if (reconnectTimer) {
|
||||
Timers.clearTimeout(reconnectTimer);
|
||||
reconnectTimer = undefined;
|
||||
}
|
||||
|
||||
// Bootstrap our online/offline detection, only the first time we connect
|
||||
if (connectCount === 0 && navigator.onLine) {
|
||||
window.addEventListener('offline', onOffline);
|
||||
}
|
||||
if (connectCount === 0 && !navigator.onLine) {
|
||||
log.warn(
|
||||
'Starting up offline; will connect when we have network access'
|
||||
);
|
||||
window.addEventListener('online', onOnline);
|
||||
void onEmpty(); // this ensures that the loading screen is dismissed
|
||||
|
||||
// Switch to inbox view even if contact sync is still running
|
||||
if (
|
||||
window.reduxStore.getState().app.appView === AppViewType.Installer
|
||||
) {
|
||||
log.info('firstRun: offline, opening inbox');
|
||||
window.reduxActions.app.openInbox();
|
||||
} else {
|
||||
log.info('firstRun: offline, not opening inbox');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Registration.everDone()) {
|
||||
log.info('background: registration not done, not connecting');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('background: connect', { firstRun, connectCount });
|
||||
|
||||
// Update our profile key in the conversation if we just got linked.
|
||||
const profileKey = await ourProfileKeyService.get();
|
||||
if (firstRun && profileKey) {
|
||||
|
@ -1710,14 +1669,11 @@ export async function startApp(): Promise<void> {
|
|||
messageReceiver.reset();
|
||||
server.registerRequestHandler(messageReceiver);
|
||||
|
||||
// If coming here after `offline` event - connect again.
|
||||
if (!remotelyExpired) {
|
||||
await server.onOnline();
|
||||
}
|
||||
|
||||
void AttachmentDownloads.start({
|
||||
logger: log,
|
||||
});
|
||||
drop(
|
||||
AttachmentDownloads.start({
|
||||
logger: log,
|
||||
})
|
||||
);
|
||||
|
||||
if (connectCount === 1) {
|
||||
Stickers.downloadQueuedPacks();
|
||||
|
@ -2053,7 +2009,8 @@ export async function startApp(): Promise<void> {
|
|||
}
|
||||
|
||||
log.info('manualConnect: calling connect()');
|
||||
void connect();
|
||||
enqueueReconnectToWebSocket();
|
||||
drop(connect());
|
||||
}
|
||||
|
||||
async function onConfiguration(ev: ConfigurationEvent): Promise<void> {
|
||||
|
|
|
@ -86,7 +86,7 @@ function getUrlsToDownload(): Array<string> {
|
|||
}
|
||||
|
||||
async function downloadBadgeImageFile(url: string): Promise<string> {
|
||||
await waitForOnline(navigator, window, { timeout: 1 * MINUTE });
|
||||
await waitForOnline({ timeout: 1 * MINUTE });
|
||||
|
||||
const { server } = window.textsecure;
|
||||
if (!server) {
|
||||
|
|
|
@ -3,22 +3,31 @@
|
|||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
function getOnlineStatus(): boolean {
|
||||
if (window.textsecure) {
|
||||
return window.textsecure.server?.isOnline() ?? true;
|
||||
}
|
||||
|
||||
// Only for storybook
|
||||
return navigator.onLine;
|
||||
}
|
||||
|
||||
export function useIsOnline(): boolean {
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||
const [isOnline, setIsOnline] = useState(getOnlineStatus());
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
setIsOnline(navigator.onLine);
|
||||
setIsOnline(getOnlineStatus());
|
||||
};
|
||||
|
||||
update();
|
||||
|
||||
window.addEventListener('offline', update);
|
||||
window.addEventListener('online', update);
|
||||
window.Whisper.events.on('online', update);
|
||||
window.Whisper.events.on('offline', update);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('offline', update);
|
||||
window.removeEventListener('online', update);
|
||||
window.Whisper.events.off('online', update);
|
||||
window.Whisper.events.off('offline', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ export async function commonShouldJobContinue({
|
|||
}
|
||||
|
||||
try {
|
||||
await waitForOnline(window.navigator, window, { timeout: timeRemaining });
|
||||
await waitForOnline({ timeout: timeRemaining });
|
||||
} catch (err: unknown) {
|
||||
log.info("didn't come online in time, giving up");
|
||||
return false;
|
||||
|
|
|
@ -62,7 +62,7 @@ export class ReportSpamJobQueue extends JobQueue<ReportSpamJobData> {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
await waitForOnline(window.navigator, window);
|
||||
await waitForOnline();
|
||||
|
||||
const { server } = this;
|
||||
strictAssert(server !== undefined, 'ReportSpamJobQueue not initialized');
|
||||
|
|
|
@ -12,7 +12,7 @@ export class AreWeASubscriberService {
|
|||
|
||||
update(
|
||||
storage: Pick<StorageInterface, 'get' | 'put' | 'onready'>,
|
||||
server: Pick<WebAPIType, 'getHasSubscription'>
|
||||
server: Pick<WebAPIType, 'getHasSubscription' | 'isOnline'>
|
||||
): void {
|
||||
this.queue.add(async () => {
|
||||
await new Promise<void>(resolve => storage.onready(resolve));
|
||||
|
@ -23,7 +23,7 @@ export class AreWeASubscriberService {
|
|||
return;
|
||||
}
|
||||
|
||||
await waitForOnline(navigator, window);
|
||||
await waitForOnline({ server });
|
||||
|
||||
await storage.put(
|
||||
'areWeASubscriber',
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type {
|
||||
CheckNetworkStatusPayloadType,
|
||||
SetNetworkStatusPayloadType,
|
||||
NetworkActionType,
|
||||
} from '../state/ducks/network';
|
||||
import { getSocketStatus } from '../shims/socketStatus';
|
||||
|
@ -17,9 +17,16 @@ const OUTAGE_CHECK_INTERVAL = 60 * SECOND;
|
|||
const OUTAGE_HEALTY_ADDR = '127.0.0.1';
|
||||
const OUTAGE_NO_SERVICE_ADDR = '127.0.0.2';
|
||||
|
||||
enum OnlineStatus {
|
||||
Online = 'Online',
|
||||
MaybeOffline = 'MaybeOffline',
|
||||
Offline = 'Offline',
|
||||
}
|
||||
|
||||
const OFFLINE_DELAY = 5 * SECOND;
|
||||
|
||||
type NetworkActions = {
|
||||
checkNetworkStatus: (x: CheckNetworkStatusPayloadType) => NetworkActionType;
|
||||
closeConnectingGracePeriod: () => NetworkActionType;
|
||||
setNetworkStatus: (x: SetNetworkStatusPayloadType) => NetworkActionType;
|
||||
setOutage: (isOutage: boolean) => NetworkActionType;
|
||||
};
|
||||
|
||||
|
@ -28,11 +35,13 @@ export function initializeNetworkObserver(
|
|||
): void {
|
||||
log.info('Initializing network observer');
|
||||
|
||||
let onlineStatus = OnlineStatus.Online;
|
||||
|
||||
const refresh = () => {
|
||||
const socketStatus = getSocketStatus();
|
||||
|
||||
networkActions.checkNetworkStatus({
|
||||
isOnline: navigator.onLine,
|
||||
networkActions.setNetworkStatus({
|
||||
isOnline: onlineStatus !== OnlineStatus.Offline,
|
||||
socketStatus,
|
||||
});
|
||||
|
||||
|
@ -89,12 +98,27 @@ export function initializeNetworkObserver(
|
|||
networkActions.setOutage(false);
|
||||
};
|
||||
|
||||
window.Whisper.events.on('socketStatusChange', refresh);
|
||||
window.Whisper.events.on('socketConnectError', onPotentialOutage);
|
||||
let offlineTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
window.addEventListener('online', refresh);
|
||||
window.addEventListener('offline', refresh);
|
||||
window.setTimeout(() => {
|
||||
networkActions.closeConnectingGracePeriod();
|
||||
}, 5 * SECOND);
|
||||
window.Whisper.events.on('socketStatusChange', refresh);
|
||||
window.Whisper.events.on('online', () => {
|
||||
onlineStatus = OnlineStatus.Online;
|
||||
if (offlineTimer) {
|
||||
clearTimeout(offlineTimer);
|
||||
offlineTimer = undefined;
|
||||
}
|
||||
refresh();
|
||||
});
|
||||
window.Whisper.events.on('offline', () => {
|
||||
if (onlineStatus !== OnlineStatus.Online) {
|
||||
return;
|
||||
}
|
||||
|
||||
onlineStatus = OnlineStatus.MaybeOffline;
|
||||
offlineTimer = setTimeout(() => {
|
||||
onlineStatus = OnlineStatus.Offline;
|
||||
refresh();
|
||||
onPotentialOutage();
|
||||
}, OFFLINE_DELAY);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -34,28 +34,23 @@ export class SenderCertificateService {
|
|||
Promise<undefined | SerializedCertificateType>
|
||||
> = new Map();
|
||||
|
||||
private navigator?: { onLine: boolean };
|
||||
|
||||
private onlineEventTarget?: EventTarget;
|
||||
private events?: Pick<typeof window.Whisper.events, 'on' | 'off'>;
|
||||
|
||||
private storage?: StorageInterface;
|
||||
|
||||
initialize({
|
||||
server,
|
||||
navigator,
|
||||
onlineEventTarget,
|
||||
events,
|
||||
storage,
|
||||
}: {
|
||||
server: WebAPIType;
|
||||
navigator: Readonly<{ onLine: boolean }>;
|
||||
onlineEventTarget: EventTarget;
|
||||
events?: Pick<typeof window.Whisper.events, 'on' | 'off'>;
|
||||
storage: StorageInterface;
|
||||
}): void {
|
||||
log.info('Sender certificate service initialized');
|
||||
|
||||
this.server = server;
|
||||
this.navigator = navigator;
|
||||
this.onlineEventTarget = onlineEventTarget;
|
||||
this.events = events;
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
|
@ -150,9 +145,9 @@ export class SenderCertificateService {
|
|||
private async fetchAndSaveCertificate(
|
||||
mode: SenderCertificateMode
|
||||
): Promise<undefined | SerializedCertificateType> {
|
||||
const { storage, navigator, onlineEventTarget } = this;
|
||||
const { storage, server, events } = this;
|
||||
assertDev(
|
||||
storage && navigator && onlineEventTarget,
|
||||
storage && server && events,
|
||||
'Sender certificate service method was called before it was initialized'
|
||||
);
|
||||
|
||||
|
@ -162,7 +157,7 @@ export class SenderCertificateService {
|
|||
)} certificate`
|
||||
);
|
||||
|
||||
await waitForOnline(navigator, onlineEventTarget);
|
||||
await waitForOnline({ server, events });
|
||||
|
||||
let certificateString: string;
|
||||
try {
|
||||
|
|
|
@ -477,7 +477,12 @@ const doGroupCallPeek = ({
|
|||
// If we peek right after receiving the message, we may get outdated information.
|
||||
// This is most noticeable when someone leaves. We add a delay and then make sure
|
||||
// to only be peeking once.
|
||||
await Promise.all([sleep(1000), waitForOnline(navigator, window)]);
|
||||
const { server } = window.textsecure;
|
||||
if (!server) {
|
||||
log.error('doGroupCallPeek: no textsecure server');
|
||||
return;
|
||||
}
|
||||
await Promise.all([sleep(1000), waitForOnline()]);
|
||||
|
||||
let peekInfo = null;
|
||||
try {
|
||||
|
|
|
@ -14,30 +14,24 @@ export type NetworkStateType = ReadonlyDeep<{
|
|||
isOnline: boolean;
|
||||
isOutage: boolean;
|
||||
socketStatus: SocketStatus;
|
||||
withinConnectingGracePeriod: boolean;
|
||||
challengeStatus: 'required' | 'pending' | 'idle';
|
||||
}>;
|
||||
|
||||
// Actions
|
||||
|
||||
const CHECK_NETWORK_STATUS = 'network/CHECK_NETWORK_STATUS';
|
||||
const CLOSE_CONNECTING_GRACE_PERIOD = 'network/CLOSE_CONNECTING_GRACE_PERIOD';
|
||||
const SET_NETWORK_STATUS = 'network/SET_NETWORK_STATUS';
|
||||
const RELINK_DEVICE = 'network/RELINK_DEVICE';
|
||||
const SET_CHALLENGE_STATUS = 'network/SET_CHALLENGE_STATUS';
|
||||
const SET_OUTAGE = 'network/SET_OUTAGE';
|
||||
|
||||
export type CheckNetworkStatusPayloadType = ReadonlyDeep<{
|
||||
export type SetNetworkStatusPayloadType = ReadonlyDeep<{
|
||||
isOnline: boolean;
|
||||
socketStatus: SocketStatus;
|
||||
}>;
|
||||
|
||||
type CheckNetworkStatusAction = ReadonlyDeep<{
|
||||
type: 'network/CHECK_NETWORK_STATUS';
|
||||
payload: CheckNetworkStatusPayloadType;
|
||||
}>;
|
||||
|
||||
type CloseConnectingGracePeriodActionType = ReadonlyDeep<{
|
||||
type: 'network/CLOSE_CONNECTING_GRACE_PERIOD';
|
||||
type SetNetworkStatusAction = ReadonlyDeep<{
|
||||
type: 'network/SET_NETWORK_STATUS';
|
||||
payload: SetNetworkStatusPayloadType;
|
||||
}>;
|
||||
|
||||
type RelinkDeviceActionType = ReadonlyDeep<{
|
||||
|
@ -59,8 +53,7 @@ type SetOutageActionType = ReadonlyDeep<{
|
|||
}>;
|
||||
|
||||
export type NetworkActionType = ReadonlyDeep<
|
||||
| CheckNetworkStatusAction
|
||||
| CloseConnectingGracePeriodActionType
|
||||
| SetNetworkStatusAction
|
||||
| RelinkDeviceActionType
|
||||
| SetChallengeStatusActionType
|
||||
| SetOutageActionType
|
||||
|
@ -68,21 +61,15 @@ export type NetworkActionType = ReadonlyDeep<
|
|||
|
||||
// Action Creators
|
||||
|
||||
function checkNetworkStatus(
|
||||
payload: CheckNetworkStatusPayloadType
|
||||
): CheckNetworkStatusAction {
|
||||
function setNetworkStatus(
|
||||
payload: SetNetworkStatusPayloadType
|
||||
): SetNetworkStatusAction {
|
||||
return {
|
||||
type: CHECK_NETWORK_STATUS,
|
||||
type: SET_NETWORK_STATUS,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
function closeConnectingGracePeriod(): CloseConnectingGracePeriodActionType {
|
||||
return {
|
||||
type: CLOSE_CONNECTING_GRACE_PERIOD,
|
||||
};
|
||||
}
|
||||
|
||||
function relinkDevice(): RelinkDeviceActionType {
|
||||
trigger('setupAsNewDevice');
|
||||
|
||||
|
@ -108,8 +95,7 @@ function setOutage(isOutage: boolean): SetOutageActionType {
|
|||
}
|
||||
|
||||
export const actions = {
|
||||
checkNetworkStatus,
|
||||
closeConnectingGracePeriod,
|
||||
setNetworkStatus,
|
||||
relinkDevice,
|
||||
setChallengeStatus,
|
||||
setOutage,
|
||||
|
@ -123,10 +109,9 @@ export const useNetworkActions = (): BoundActionCreatorsMapObject<
|
|||
|
||||
export function getEmptyState(): NetworkStateType {
|
||||
return {
|
||||
isOnline: navigator.onLine,
|
||||
isOnline: true,
|
||||
isOutage: false,
|
||||
socketStatus: SocketStatus.OPEN,
|
||||
withinConnectingGracePeriod: true,
|
||||
challengeStatus: 'idle',
|
||||
};
|
||||
}
|
||||
|
@ -135,7 +120,7 @@ export function reducer(
|
|||
state: Readonly<NetworkStateType> = getEmptyState(),
|
||||
action: Readonly<NetworkActionType>
|
||||
): NetworkStateType {
|
||||
if (action.type === CHECK_NETWORK_STATUS) {
|
||||
if (action.type === SET_NETWORK_STATUS) {
|
||||
const { isOnline, socketStatus } = action.payload;
|
||||
|
||||
// This action is dispatched frequently. We avoid allocating a new object if nothing
|
||||
|
@ -146,13 +131,6 @@ export function reducer(
|
|||
});
|
||||
}
|
||||
|
||||
if (action.type === CLOSE_CONNECTING_GRACE_PERIOD) {
|
||||
return {
|
||||
...state,
|
||||
withinConnectingGracePeriod: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SET_CHALLENGE_STATUS) {
|
||||
return {
|
||||
...state,
|
||||
|
|
|
@ -6,7 +6,6 @@ import { createSelector } from 'reselect';
|
|||
import type { StateType } from '../reducer';
|
||||
import type { NetworkStateType } from '../ducks/network';
|
||||
import { isDone } from '../../util/registration';
|
||||
import { SocketStatus } from '../../types/SocketStatus';
|
||||
|
||||
const getNetwork = (state: StateType): NetworkStateType => state.network;
|
||||
|
||||
|
@ -29,21 +28,9 @@ export const hasNetworkDialog = createSelector(
|
|||
getNetwork,
|
||||
isDone,
|
||||
(
|
||||
{
|
||||
isOnline,
|
||||
isOutage,
|
||||
socketStatus,
|
||||
withinConnectingGracePeriod,
|
||||
}: NetworkStateType,
|
||||
{ isOnline, isOutage }: NetworkStateType,
|
||||
isRegistrationDone: boolean
|
||||
): boolean =>
|
||||
isRegistrationDone &&
|
||||
(!isOnline ||
|
||||
isOutage ||
|
||||
(socketStatus === SocketStatus.CONNECTING &&
|
||||
!withinConnectingGracePeriod) ||
|
||||
socketStatus === SocketStatus.CLOSED ||
|
||||
socketStatus === SocketStatus.CLOSING)
|
||||
): boolean => isRegistrationDone && (!isOnline || isOutage)
|
||||
);
|
||||
|
||||
export const getChallengeStatus = createSelector(
|
||||
|
|
|
@ -20,11 +20,13 @@ describe('"are we a subscriber?" service', () => {
|
|||
sandbox = sinon.createSandbox();
|
||||
|
||||
service = new AreWeASubscriberService();
|
||||
sandbox.stub(navigator, 'onLine').get(() => true);
|
||||
});
|
||||
|
||||
it("stores false if there's no local subscriber ID", done => {
|
||||
const fakeServer = { getHasSubscription: sandbox.stub() };
|
||||
const fakeServer = {
|
||||
getHasSubscription: sandbox.stub(),
|
||||
isOnline: () => true,
|
||||
};
|
||||
const fakeStorage = {
|
||||
...fakeStorageDefaults,
|
||||
get: () => undefined,
|
||||
|
@ -39,7 +41,10 @@ describe('"are we a subscriber?" service', () => {
|
|||
});
|
||||
|
||||
it("doesn't make a network request if there's no local subscriber ID", done => {
|
||||
const fakeServer = { getHasSubscription: sandbox.stub() };
|
||||
const fakeServer = {
|
||||
getHasSubscription: sandbox.stub(),
|
||||
isOnline: () => true,
|
||||
};
|
||||
const fakeStorage = {
|
||||
...fakeStorageDefaults,
|
||||
get: () => undefined,
|
||||
|
@ -53,7 +58,10 @@ describe('"are we a subscriber?" service', () => {
|
|||
});
|
||||
|
||||
it('requests the subscriber ID from the server', done => {
|
||||
const fakeServer = { getHasSubscription: sandbox.stub().resolves(false) };
|
||||
const fakeServer = {
|
||||
getHasSubscription: sandbox.stub().resolves(false),
|
||||
isOnline: () => true,
|
||||
};
|
||||
const fakeStorage = {
|
||||
...fakeStorageDefaults,
|
||||
put: sandbox
|
||||
|
@ -72,7 +80,10 @@ describe('"are we a subscriber?" service', () => {
|
|||
});
|
||||
|
||||
it("stores when we're not a subscriber", done => {
|
||||
const fakeServer = { getHasSubscription: sandbox.stub().resolves(false) };
|
||||
const fakeServer = {
|
||||
getHasSubscription: sandbox.stub().resolves(false),
|
||||
isOnline: () => true,
|
||||
};
|
||||
const fakeStorage = {
|
||||
...fakeStorageDefaults,
|
||||
put: sandbox.stub().callsFake((key, value) => {
|
||||
|
@ -86,7 +97,10 @@ describe('"are we a subscriber?" service', () => {
|
|||
});
|
||||
|
||||
it("stores when we're a subscriber", done => {
|
||||
const fakeServer = { getHasSubscription: sandbox.stub().resolves(true) };
|
||||
const fakeServer = {
|
||||
getHasSubscription: sandbox.stub().resolves(true),
|
||||
isOnline: () => true,
|
||||
};
|
||||
const fakeStorage = {
|
||||
...fakeStorageDefaults,
|
||||
put: sandbox.stub().callsFake((key, value) => {
|
||||
|
@ -103,7 +117,10 @@ describe('"are we a subscriber?" service', () => {
|
|||
const allDone = explodePromise<void>();
|
||||
let putCallCount = 0;
|
||||
|
||||
const fakeServer = { getHasSubscription: sandbox.stub().resolves(false) };
|
||||
const fakeServer = {
|
||||
getHasSubscription: sandbox.stub().resolves(false),
|
||||
isOnline: () => true,
|
||||
};
|
||||
const fakeStorage = {
|
||||
...fakeStorageDefaults,
|
||||
put: sandbox.stub().callsFake(() => {
|
||||
|
|
|
@ -25,16 +25,14 @@ describe('SenderCertificateService', () => {
|
|||
let fakeValidEncodedCertificate: Uint8Array;
|
||||
let fakeValidCertificateExpiry: number;
|
||||
let fakeServer: any;
|
||||
let fakeNavigator: { onLine: boolean };
|
||||
let fakeWindow: EventTarget;
|
||||
let fakeEvents: Pick<typeof window.Whisper.events, 'on' | 'off'>;
|
||||
let fakeStorage: any;
|
||||
|
||||
function initializeTestService(): SenderCertificateService {
|
||||
const result = new SenderCertificateService();
|
||||
result.initialize({
|
||||
server: fakeServer,
|
||||
navigator: fakeNavigator,
|
||||
onlineEventTarget: fakeWindow,
|
||||
events: fakeEvents,
|
||||
storage: fakeStorage,
|
||||
});
|
||||
return result;
|
||||
|
@ -51,18 +49,16 @@ describe('SenderCertificateService', () => {
|
|||
SenderCertificate.encode(fakeValidCertificate).finish();
|
||||
|
||||
fakeServer = {
|
||||
isOnline: () => true,
|
||||
getSenderCertificate: sinon.stub().resolves({
|
||||
certificate: Bytes.toBase64(fakeValidEncodedCertificate),
|
||||
}),
|
||||
};
|
||||
|
||||
fakeNavigator = { onLine: true };
|
||||
|
||||
fakeWindow = {
|
||||
addEventListener: sinon.stub(),
|
||||
dispatchEvent: sinon.stub(),
|
||||
removeEventListener: sinon.stub(),
|
||||
};
|
||||
fakeEvents = {
|
||||
on: sinon.stub(),
|
||||
off: sinon.stub(),
|
||||
} as unknown as typeof fakeEvents;
|
||||
|
||||
fakeStorage = {
|
||||
get: sinon.stub(),
|
||||
|
@ -221,6 +217,7 @@ describe('SenderCertificateService', () => {
|
|||
let count = 0;
|
||||
|
||||
fakeServer = {
|
||||
isOnline: () => true,
|
||||
getSenderCertificate: sinon.spy(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as sinon from 'sinon';
|
||||
|
||||
import { waitForOnline } from '../../util/waitForOnline';
|
||||
|
@ -17,28 +18,28 @@ describe('waitForOnline', () => {
|
|||
sandbox.restore();
|
||||
});
|
||||
|
||||
function getFakeWindow(): EventTarget {
|
||||
const result = new EventTarget();
|
||||
sinon.stub(result, 'addEventListener');
|
||||
sinon.stub(result, 'removeEventListener');
|
||||
function getFakeEmitter(): EventEmitter {
|
||||
const result = new EventEmitter();
|
||||
sinon.stub(result, 'on');
|
||||
sinon.stub(result, 'off');
|
||||
return result;
|
||||
}
|
||||
|
||||
it("resolves immediately if you're online", async () => {
|
||||
const fakeNavigator = { onLine: true };
|
||||
const fakeWindow = getFakeWindow();
|
||||
const fakeServer = { isOnline: () => true };
|
||||
const fakeEvents = getFakeEmitter();
|
||||
|
||||
await waitForOnline(fakeNavigator, fakeWindow);
|
||||
await waitForOnline({ server: fakeServer, events: fakeEvents });
|
||||
|
||||
sinon.assert.notCalled(fakeWindow.addEventListener as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeWindow.removeEventListener as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeEvents.on as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeEvents.off as sinon.SinonStub);
|
||||
});
|
||||
|
||||
it("if you're offline, resolves as soon as you're online (and cleans up listeners)", async () => {
|
||||
const fakeNavigator = { onLine: false };
|
||||
const fakeWindow = getFakeWindow();
|
||||
const fakeServer = { isOnline: () => false };
|
||||
const fakeEvents = getFakeEmitter();
|
||||
|
||||
(fakeWindow.addEventListener as sinon.SinonStub)
|
||||
(fakeEvents.on as sinon.SinonStub)
|
||||
.withArgs('online')
|
||||
.callsFake((_eventName: string, callback: () => void) => {
|
||||
setTimeout(callback, 0);
|
||||
|
@ -46,7 +47,7 @@ describe('waitForOnline', () => {
|
|||
|
||||
let done = false;
|
||||
const promise = (async () => {
|
||||
await waitForOnline(fakeNavigator, fakeWindow);
|
||||
await waitForOnline({ server: fakeServer, events: fakeEvents });
|
||||
done = true;
|
||||
})();
|
||||
|
||||
|
@ -55,37 +56,41 @@ describe('waitForOnline', () => {
|
|||
await promise;
|
||||
|
||||
assert.isTrue(done);
|
||||
sinon.assert.calledOnce(fakeWindow.addEventListener as sinon.SinonStub);
|
||||
sinon.assert.calledOnce(fakeWindow.removeEventListener as sinon.SinonStub);
|
||||
sinon.assert.calledOnce(fakeEvents.on as sinon.SinonStub);
|
||||
sinon.assert.calledOnce(fakeEvents.off as sinon.SinonStub);
|
||||
});
|
||||
|
||||
it("resolves immediately if you're online when passed a timeout", async () => {
|
||||
const fakeNavigator = { onLine: true };
|
||||
const fakeWindow = getFakeWindow();
|
||||
const fakeServer = { isOnline: () => true };
|
||||
const fakeEvents = getFakeEmitter();
|
||||
|
||||
await waitForOnline(fakeNavigator, fakeWindow, { timeout: 1234 });
|
||||
await waitForOnline({
|
||||
server: fakeServer,
|
||||
events: fakeEvents,
|
||||
timeout: 1234,
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(fakeWindow.addEventListener as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeWindow.removeEventListener as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeEvents.on as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeEvents.off as sinon.SinonStub);
|
||||
});
|
||||
|
||||
it("resolves immediately if you're online even if passed a timeout of 0", async () => {
|
||||
const fakeNavigator = { onLine: true };
|
||||
const fakeWindow = getFakeWindow();
|
||||
const fakeServer = { isOnline: () => true };
|
||||
const fakeEvents = getFakeEmitter();
|
||||
|
||||
await waitForOnline(fakeNavigator, fakeWindow, { timeout: 0 });
|
||||
await waitForOnline({ server: fakeServer, events: fakeEvents, timeout: 0 });
|
||||
|
||||
sinon.assert.notCalled(fakeWindow.addEventListener as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeWindow.removeEventListener as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeEvents.on as sinon.SinonStub);
|
||||
sinon.assert.notCalled(fakeEvents.off as sinon.SinonStub);
|
||||
});
|
||||
|
||||
it("if you're offline, resolves as soon as you're online if it happens before the timeout", async () => {
|
||||
const clock = sandbox.useFakeTimers();
|
||||
|
||||
const fakeNavigator = { onLine: false };
|
||||
const fakeWindow = getFakeWindow();
|
||||
const fakeServer = { isOnline: () => false };
|
||||
const fakeEvents = getFakeEmitter();
|
||||
|
||||
(fakeWindow.addEventListener as sinon.SinonStub)
|
||||
(fakeEvents.on as sinon.SinonStub)
|
||||
.withArgs('online')
|
||||
.callsFake((_eventName: string, callback: () => void) => {
|
||||
setTimeout(callback, 1000);
|
||||
|
@ -93,7 +98,11 @@ describe('waitForOnline', () => {
|
|||
|
||||
let done = false;
|
||||
void (async () => {
|
||||
await waitForOnline(fakeNavigator, fakeWindow, { timeout: 9999 });
|
||||
await waitForOnline({
|
||||
server: fakeServer,
|
||||
events: fakeEvents,
|
||||
timeout: 9999,
|
||||
});
|
||||
done = true;
|
||||
})();
|
||||
|
||||
|
@ -108,16 +117,18 @@ describe('waitForOnline', () => {
|
|||
it('rejects if too much time has passed, and cleans up listeners', async () => {
|
||||
const clock = sandbox.useFakeTimers();
|
||||
|
||||
const fakeNavigator = { onLine: false };
|
||||
const fakeWindow = getFakeWindow();
|
||||
const fakeServer = { isOnline: () => false };
|
||||
const fakeEvents = getFakeEmitter();
|
||||
|
||||
(fakeWindow.addEventListener as sinon.SinonStub)
|
||||
(fakeEvents.on as sinon.SinonStub)
|
||||
.withArgs('online')
|
||||
.callsFake((_eventName: string, callback: () => void) => {
|
||||
setTimeout(callback, 9999);
|
||||
});
|
||||
|
||||
const promise = waitForOnline(fakeNavigator, fakeWindow, {
|
||||
const promise = waitForOnline({
|
||||
server: fakeServer,
|
||||
events: fakeEvents,
|
||||
timeout: 100,
|
||||
});
|
||||
|
||||
|
@ -125,20 +136,24 @@ describe('waitForOnline', () => {
|
|||
|
||||
await assert.isRejected(promise);
|
||||
|
||||
sinon.assert.calledOnce(fakeWindow.removeEventListener as sinon.SinonStub);
|
||||
sinon.assert.calledOnce(fakeEvents.off as sinon.SinonStub);
|
||||
});
|
||||
|
||||
it('rejects if offline and passed a timeout of 0', async () => {
|
||||
const fakeNavigator = { onLine: false };
|
||||
const fakeWindow = getFakeWindow();
|
||||
const fakeServer = { isOnline: () => false };
|
||||
const fakeEvents = getFakeEmitter();
|
||||
|
||||
(fakeWindow.addEventListener as sinon.SinonStub)
|
||||
(fakeEvents.on as sinon.SinonStub)
|
||||
.withArgs('online')
|
||||
.callsFake((_eventName: string, callback: () => void) => {
|
||||
setTimeout(callback, 9999);
|
||||
});
|
||||
|
||||
const promise = waitForOnline(fakeNavigator, fakeWindow, { timeout: 0 });
|
||||
const promise = waitForOnline({
|
||||
server: fakeServer,
|
||||
events: fakeEvents,
|
||||
timeout: 100,
|
||||
});
|
||||
|
||||
await assert.isRejected(promise);
|
||||
});
|
||||
|
|
|
@ -10,9 +10,14 @@ import EventListener from 'events';
|
|||
|
||||
import { AbortableProcess } from '../util/AbortableProcess';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import { BackOff, FIBONACCI_TIMEOUTS } from '../util/BackOff';
|
||||
import {
|
||||
BackOff,
|
||||
FIBONACCI_TIMEOUTS,
|
||||
EXTENDED_FIBONACCI_TIMEOUTS,
|
||||
} from '../util/BackOff';
|
||||
import * as durations from '../util/durations';
|
||||
import { sleep } from '../util/sleep';
|
||||
import { drop } from '../util/drop';
|
||||
import { createProxyAgent } from '../util/createProxyAgent';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import * as Errors from '../types/errors';
|
||||
|
@ -38,6 +43,7 @@ const FIVE_MINUTES = 5 * durations.MINUTE;
|
|||
|
||||
const JITTER = 5 * durations.SECOND;
|
||||
|
||||
const OFFLINE_KEEPALIVE_TIMEOUT_MS = 5 * durations.SECOND;
|
||||
export const UNAUTHENTICATED_CHANNEL_NAME = 'unauthenticated';
|
||||
|
||||
export const AUTHENTICATED_CHANNEL_NAME = 'authenticated';
|
||||
|
@ -86,10 +92,16 @@ export class SocketManager extends EventListener {
|
|||
|
||||
private incomingRequestQueue = new Array<IncomingWebSocketRequest>();
|
||||
|
||||
private isOffline = false;
|
||||
private isNavigatorOffline = false;
|
||||
|
||||
private privIsOnline: boolean | undefined;
|
||||
|
||||
private isRemotelyExpired = false;
|
||||
|
||||
private hasStoriesDisabled: boolean;
|
||||
|
||||
private reconnectController: AbortController | undefined;
|
||||
|
||||
constructor(private readonly options: SocketManagerOptions) {
|
||||
super();
|
||||
|
||||
|
@ -107,8 +119,8 @@ export class SocketManager extends EventListener {
|
|||
// Update WebAPICredentials and reconnect authenticated resource if
|
||||
// credentials changed
|
||||
public async authenticate(credentials: WebAPICredentials): Promise<void> {
|
||||
if (this.isOffline) {
|
||||
throw new HTTPError('SocketManager offline', {
|
||||
if (this.isRemotelyExpired) {
|
||||
throw new HTTPError('SocketManager remotely expired', {
|
||||
code: 0,
|
||||
headers: {},
|
||||
stack: new Error().stack,
|
||||
|
@ -169,6 +181,11 @@ export class SocketManager extends EventListener {
|
|||
this.authenticated = process;
|
||||
|
||||
const reconnect = async (): Promise<void> => {
|
||||
if (this.isRemotelyExpired) {
|
||||
log.info('SocketManager: remotely expired, not reconnecting');
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = this.backOff.getAndIncrement();
|
||||
|
||||
log.info(
|
||||
|
@ -176,14 +193,22 @@ export class SocketManager extends EventListener {
|
|||
`after ${timeout}ms`
|
||||
);
|
||||
|
||||
await sleep(timeout);
|
||||
if (this.isOffline) {
|
||||
log.info('SocketManager: cancelled reconnect because we are offline');
|
||||
const reconnectController = new AbortController();
|
||||
this.reconnectController = reconnectController;
|
||||
|
||||
try {
|
||||
await sleep(timeout, reconnectController.signal);
|
||||
} catch {
|
||||
log.info('SocketManager: reconnect cancelled');
|
||||
return;
|
||||
} finally {
|
||||
if (this.reconnectController === reconnectController) {
|
||||
this.reconnectController = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.authenticated) {
|
||||
log.info('SocketManager: authenticated socket already reconnected');
|
||||
log.info('SocketManager: authenticated socket already connecting');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -230,12 +255,13 @@ export class SocketManager extends EventListener {
|
|||
return;
|
||||
}
|
||||
|
||||
if (code === -1) {
|
||||
this.emit('connectError');
|
||||
if (code === -1 && this.privIsOnline !== false) {
|
||||
this.privIsOnline = false;
|
||||
this.emit('offline');
|
||||
}
|
||||
}
|
||||
|
||||
void reconnect();
|
||||
drop(reconnect());
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -267,7 +293,7 @@ export class SocketManager extends EventListener {
|
|||
return;
|
||||
}
|
||||
|
||||
void reconnect();
|
||||
drop(reconnect());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -287,6 +313,10 @@ export class SocketManager extends EventListener {
|
|||
public async getProvisioningResource(
|
||||
handler: IRequestHandler
|
||||
): Promise<IWebSocketResource> {
|
||||
if (this.isRemotelyExpired) {
|
||||
throw new Error('Remotely expired, not connecting provisioning socket');
|
||||
}
|
||||
|
||||
return this.connectResource({
|
||||
name: 'provisioning',
|
||||
path: '/v1/websocket/provisioning/',
|
||||
|
@ -397,40 +427,6 @@ export class SocketManager extends EventListener {
|
|||
|
||||
public async reconnect(): Promise<void> {
|
||||
log.info('SocketManager.reconnect: starting...');
|
||||
this.onOffline();
|
||||
await this.onOnline();
|
||||
log.info('SocketManager.reconnect: complete.');
|
||||
}
|
||||
|
||||
// Force keep-alive checks on WebSocketResources
|
||||
public async check(): Promise<void> {
|
||||
if (this.isOffline) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('SocketManager.check');
|
||||
await Promise.all([
|
||||
SocketManager.checkResource(this.authenticated),
|
||||
SocketManager.checkResource(this.unauthenticated),
|
||||
]);
|
||||
}
|
||||
|
||||
// Puts SocketManager into "online" state and reconnects the authenticated
|
||||
// IWebSocketResource (if there are valid credentials)
|
||||
public async onOnline(): Promise<void> {
|
||||
log.info('SocketManager.onOnline');
|
||||
this.isOffline = false;
|
||||
|
||||
if (this.credentials) {
|
||||
await this.authenticate(this.credentials);
|
||||
}
|
||||
}
|
||||
|
||||
// Puts SocketManager into "offline" state and gracefully disconnects both
|
||||
// unauthenticated and authenticated resources.
|
||||
public onOffline(): void {
|
||||
log.info('SocketManager.onOffline');
|
||||
this.isOffline = true;
|
||||
|
||||
const { authenticated, unauthenticated } = this;
|
||||
if (authenticated) {
|
||||
|
@ -441,6 +437,54 @@ export class SocketManager extends EventListener {
|
|||
unauthenticated.abort();
|
||||
this.dropUnauthenticated(unauthenticated);
|
||||
}
|
||||
|
||||
if (this.credentials) {
|
||||
this.backOff.reset();
|
||||
|
||||
// Cancel old reconnect attempt
|
||||
this.reconnectController?.abort();
|
||||
|
||||
// Start the new attempt
|
||||
await this.authenticate(this.credentials);
|
||||
}
|
||||
|
||||
log.info('SocketManager.reconnect: complete.');
|
||||
}
|
||||
|
||||
// Force keep-alive checks on WebSocketResources
|
||||
public async check(): Promise<void> {
|
||||
log.info('SocketManager.check');
|
||||
await Promise.all([
|
||||
this.checkResource(this.authenticated),
|
||||
this.checkResource(this.unauthenticated),
|
||||
]);
|
||||
}
|
||||
|
||||
public async onNavigatorOnline(): Promise<void> {
|
||||
log.info('SocketManager.onNavigatorOnline');
|
||||
this.isNavigatorOffline = false;
|
||||
this.backOff.reset(FIBONACCI_TIMEOUTS);
|
||||
|
||||
// Reconnect earlier if waiting
|
||||
if (this.credentials !== undefined) {
|
||||
this.reconnectController?.abort();
|
||||
await this.authenticate(this.credentials);
|
||||
}
|
||||
}
|
||||
|
||||
public async onNavigatorOffline(): Promise<void> {
|
||||
log.info('SocketManager.onNavigatorOffline');
|
||||
this.isNavigatorOffline = true;
|
||||
this.backOff.reset(EXTENDED_FIBONACCI_TIMEOUTS);
|
||||
await this.check();
|
||||
}
|
||||
|
||||
public async onRemoteExpiration(): Promise<void> {
|
||||
log.info('SocketManager.onRemoteExpiration');
|
||||
this.isRemotelyExpired = true;
|
||||
|
||||
// Cancel reconnect attempt if any
|
||||
this.reconnectController?.abort();
|
||||
}
|
||||
|
||||
public async logout(): Promise<void> {
|
||||
|
@ -453,6 +497,10 @@ export class SocketManager extends EventListener {
|
|||
this.credentials = undefined;
|
||||
}
|
||||
|
||||
public get isOnline(): boolean {
|
||||
return this.privIsOnline !== false;
|
||||
}
|
||||
|
||||
//
|
||||
// Private
|
||||
//
|
||||
|
@ -464,6 +512,11 @@ export class SocketManager extends EventListener {
|
|||
|
||||
this.status = status;
|
||||
this.emit('statusChange');
|
||||
|
||||
if (this.status === SocketStatus.OPEN && !this.privIsOnline) {
|
||||
this.privIsOnline = true;
|
||||
this.emit('online');
|
||||
}
|
||||
}
|
||||
|
||||
private transportOption(): TransportOption {
|
||||
|
@ -522,17 +575,19 @@ export class SocketManager extends EventListener {
|
|||
}
|
||||
|
||||
private async getUnauthenticatedResource(): Promise<IWebSocketResource> {
|
||||
if (this.isOffline) {
|
||||
throw new HTTPError('SocketManager offline', {
|
||||
if (this.unauthenticated) {
|
||||
return this.unauthenticated.getResult();
|
||||
}
|
||||
|
||||
if (this.isRemotelyExpired) {
|
||||
throw new HTTPError('SocketManager remotely expired', {
|
||||
code: 0,
|
||||
headers: {},
|
||||
stack: new Error().stack,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.unauthenticated) {
|
||||
return this.unauthenticated.getResult();
|
||||
}
|
||||
log.info('SocketManager: connecting unauthenticated socket');
|
||||
|
||||
const transportOption = this.transportOption();
|
||||
log.info(
|
||||
|
@ -631,7 +686,7 @@ export class SocketManager extends EventListener {
|
|||
});
|
||||
}
|
||||
|
||||
private static async checkResource(
|
||||
private async checkResource(
|
||||
process?: AbortableProcess<IWebSocketResource>
|
||||
): Promise<void> {
|
||||
if (!process) {
|
||||
|
@ -639,7 +694,11 @@ export class SocketManager extends EventListener {
|
|||
}
|
||||
|
||||
const resource = await process.getResult();
|
||||
resource.forceKeepAlive();
|
||||
|
||||
// Force shorter timeout if we think we might be offline
|
||||
resource.forceKeepAlive(
|
||||
this.isNavigatorOffline ? OFFLINE_KEEPALIVE_TIMEOUT_MS : undefined
|
||||
);
|
||||
}
|
||||
|
||||
private dropAuthenticated(
|
||||
|
@ -770,7 +829,8 @@ export class SocketManager extends EventListener {
|
|||
callback: (error: HTTPError) => void
|
||||
): this;
|
||||
public override on(type: 'statusChange', callback: () => void): this;
|
||||
public override on(type: 'connectError', callback: () => void): this;
|
||||
public override on(type: 'online', callback: () => void): this;
|
||||
public override on(type: 'offline', callback: () => void): this;
|
||||
|
||||
public override on(
|
||||
type: string | symbol,
|
||||
|
@ -782,7 +842,8 @@ export class SocketManager extends EventListener {
|
|||
|
||||
public override emit(type: 'authError', error: HTTPError): boolean;
|
||||
public override emit(type: 'statusChange'): boolean;
|
||||
public override emit(type: 'connectError'): boolean;
|
||||
public override emit(type: 'online'): boolean;
|
||||
public override emit(type: 'offline'): boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
||||
|
|
|
@ -94,17 +94,17 @@ export class UpdateKeysListener {
|
|||
}
|
||||
|
||||
private runWhenOnline() {
|
||||
if (window.navigator.onLine) {
|
||||
if (window.textsecure.server?.isOnline()) {
|
||||
void this.run();
|
||||
} else {
|
||||
log.info(
|
||||
'UpdateKeysListener: We are offline; will update keys when we are next online'
|
||||
);
|
||||
const listener = () => {
|
||||
window.removeEventListener('online', listener);
|
||||
window.Whisper.events.off('online', listener);
|
||||
this.setTimeoutForNextRun();
|
||||
};
|
||||
window.addEventListener('online', listener);
|
||||
window.Whisper.events.on('online', listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1154,8 +1154,10 @@ export type WebAPIType = {
|
|||
unregisterRequestHandler: (handler: IRequestHandler) => void;
|
||||
onHasStoriesDisabledChange: (newValue: boolean) => void;
|
||||
checkSockets: () => void;
|
||||
onOnline: () => Promise<void>;
|
||||
onOffline: () => void;
|
||||
isOnline: () => boolean;
|
||||
onNavigatorOnline: () => Promise<void>;
|
||||
onNavigatorOffline: () => Promise<void>;
|
||||
onRemoteExpiration: () => Promise<void>;
|
||||
reconnect: () => Promise<void>;
|
||||
};
|
||||
|
||||
|
@ -1321,12 +1323,16 @@ export function initialize({
|
|||
window.Whisper.events.trigger('socketStatusChange');
|
||||
});
|
||||
|
||||
socketManager.on('authError', () => {
|
||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
||||
socketManager.on('online', () => {
|
||||
window.Whisper.events.trigger('online');
|
||||
});
|
||||
|
||||
socketManager.on('connectError', () => {
|
||||
window.Whisper.events.trigger('socketConnectError');
|
||||
socketManager.on('offline', () => {
|
||||
window.Whisper.events.trigger('offline');
|
||||
});
|
||||
|
||||
socketManager.on('authError', () => {
|
||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
||||
});
|
||||
|
||||
if (useWebSocket) {
|
||||
|
@ -1442,8 +1448,10 @@ export function initialize({
|
|||
modifyGroup,
|
||||
modifyStorageRecords,
|
||||
onHasStoriesDisabledChange,
|
||||
onOffline,
|
||||
onOnline,
|
||||
isOnline,
|
||||
onNavigatorOffline,
|
||||
onNavigatorOnline,
|
||||
onRemoteExpiration,
|
||||
postBatchIdentityCheck,
|
||||
putEncryptedAttachment,
|
||||
putProfile,
|
||||
|
@ -1620,12 +1628,20 @@ export function initialize({
|
|||
void socketManager.check();
|
||||
}
|
||||
|
||||
async function onOnline(): Promise<void> {
|
||||
await socketManager.onOnline();
|
||||
function isOnline(): boolean {
|
||||
return socketManager.isOnline;
|
||||
}
|
||||
|
||||
function onOffline(): void {
|
||||
socketManager.onOffline();
|
||||
async function onNavigatorOnline(): Promise<void> {
|
||||
await socketManager.onNavigatorOnline();
|
||||
}
|
||||
|
||||
async function onNavigatorOffline(): Promise<void> {
|
||||
await socketManager.onNavigatorOffline();
|
||||
}
|
||||
|
||||
async function onRemoteExpiration(): Promise<void> {
|
||||
await socketManager.onRemoteExpiration();
|
||||
}
|
||||
|
||||
async function reconnect(): Promise<void> {
|
||||
|
|
|
@ -42,6 +42,7 @@ import EventTarget from './EventTarget';
|
|||
|
||||
import * as durations from '../util/durations';
|
||||
import { dropNull } from '../util/dropNull';
|
||||
import { drop } from '../util/drop';
|
||||
import { isOlderThan } from '../util/timestamp';
|
||||
import { strictAssert } from '../util/assert';
|
||||
import * as Errors from '../types/errors';
|
||||
|
@ -52,7 +53,6 @@ import type { IResource } from './WebSocket';
|
|||
import { isProduction, isStaging } from '../util/version';
|
||||
|
||||
import { ToastType } from '../types/Toast';
|
||||
import { drop } from '../util/drop';
|
||||
|
||||
const THIRTY_SECONDS = 30 * durations.SECOND;
|
||||
|
||||
|
@ -251,7 +251,7 @@ export interface IWebSocketResource extends IResource {
|
|||
|
||||
addEventListener(name: 'close', handler: (ev: CloseEvent) => void): void;
|
||||
|
||||
forceKeepAlive(): void;
|
||||
forceKeepAlive(timeout?: number): void;
|
||||
|
||||
shutdown(): void;
|
||||
|
||||
|
@ -395,8 +395,8 @@ export class WebSocketResourceWithShadowing implements IWebSocketResource {
|
|||
this.shadowing.shutdown();
|
||||
}
|
||||
|
||||
public forceKeepAlive(): void {
|
||||
this.main.forceKeepAlive();
|
||||
public forceKeepAlive(timeout?: number): void {
|
||||
this.main.forceKeepAlive(timeout);
|
||||
}
|
||||
|
||||
public async sendRequest(options: SendRequestOptions): Promise<Response> {
|
||||
|
@ -627,11 +627,11 @@ export default class WebSocketResource
|
|||
return WebSocketResource.intoResponse(requestResult);
|
||||
}
|
||||
|
||||
public forceKeepAlive(): void {
|
||||
public forceKeepAlive(timeout?: number): void {
|
||||
if (!this.keepalive) {
|
||||
return;
|
||||
}
|
||||
void this.keepalive.send();
|
||||
drop(this.keepalive.send(timeout));
|
||||
}
|
||||
|
||||
public close(code = 3000, reason?: string): void {
|
||||
|
@ -853,7 +853,7 @@ class KeepAlive {
|
|||
this.clearTimers();
|
||||
}
|
||||
|
||||
public async send(): Promise<void> {
|
||||
public async send(timeout = KEEPALIVE_TIMEOUT_MS): Promise<void> {
|
||||
this.clearTimers();
|
||||
|
||||
const isStale = isOlderThan(this.lastAliveAt, STALE_THRESHOLD_MS);
|
||||
|
@ -875,7 +875,7 @@ class KeepAlive {
|
|||
verb: 'GET',
|
||||
path: this.path,
|
||||
}),
|
||||
KEEPALIVE_TIMEOUT_MS
|
||||
timeout
|
||||
);
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
|
|
|
@ -596,8 +596,13 @@ async function doDownloadStickerPack(
|
|||
return;
|
||||
}
|
||||
|
||||
const { server } = window.textsecure;
|
||||
if (!server) {
|
||||
throw new Error('server is not available!');
|
||||
}
|
||||
|
||||
// We don't count this as an attempt if we're offline
|
||||
const attemptIncrement = navigator.onLine ? 1 : 0;
|
||||
const attemptIncrement = server.isOnline() ? 1 : 0;
|
||||
const downloadAttempts =
|
||||
(existing ? existing.downloadAttempts || 0 : 0) + attemptIncrement;
|
||||
if (downloadAttempts > 3) {
|
||||
|
|
|
@ -15,6 +15,17 @@ export const FIBONACCI_TIMEOUTS: ReadonlyArray<number> = [
|
|||
55 * SECOND,
|
||||
];
|
||||
|
||||
export const EXTENDED_FIBONACCI_TIMEOUTS: ReadonlyArray<number> = [
|
||||
...FIBONACCI_TIMEOUTS,
|
||||
89 * SECOND,
|
||||
144 * SECOND,
|
||||
233 * SECOND,
|
||||
377 * SECOND,
|
||||
610 * SECOND,
|
||||
987 * SECOND,
|
||||
1597 * SECOND, // ~26 minutes
|
||||
];
|
||||
|
||||
export type BackOffOptionsType = Readonly<{
|
||||
jitter?: number;
|
||||
|
||||
|
@ -28,7 +39,7 @@ export class BackOff {
|
|||
private count = 0;
|
||||
|
||||
constructor(
|
||||
private readonly timeouts: ReadonlyArray<number>,
|
||||
private timeouts: ReadonlyArray<number>,
|
||||
private readonly options: BackOffOptionsType = {}
|
||||
) {}
|
||||
|
||||
|
@ -53,7 +64,10 @@ export class BackOff {
|
|||
return result;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
public reset(newTimeouts?: ReadonlyArray<number>): void {
|
||||
if (newTimeouts !== undefined) {
|
||||
this.timeouts = newTimeouts;
|
||||
}
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,15 +3,31 @@
|
|||
|
||||
import { clearTimeoutIfNecessary } from './clearTimeoutIfNecessary';
|
||||
|
||||
export function waitForOnline(
|
||||
navigator: Readonly<{ onLine: boolean }>,
|
||||
onlineEventTarget: EventTarget,
|
||||
options: Readonly<{ timeout?: number }> = {}
|
||||
): Promise<void> {
|
||||
const { timeout } = options;
|
||||
export type WaitForOnlineOptionsType = Readonly<{
|
||||
server?: Readonly<{ isOnline: () => boolean }>;
|
||||
events?: {
|
||||
on: (event: 'online', fn: () => void) => void;
|
||||
off: (event: 'online', fn: () => void) => void;
|
||||
};
|
||||
timeout?: number;
|
||||
}>;
|
||||
|
||||
export function waitForOnline({
|
||||
server: maybeServer,
|
||||
events = window.Whisper.events,
|
||||
timeout,
|
||||
}: WaitForOnlineOptionsType = {}): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (navigator.onLine) {
|
||||
let server = maybeServer;
|
||||
if (server === undefined) {
|
||||
({ server } = window.textsecure);
|
||||
if (!server) {
|
||||
reject(new Error('waitForOnline: no textsecure server'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (server.isOnline()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
@ -24,11 +40,11 @@ export function waitForOnline(
|
|||
};
|
||||
|
||||
const cleanup = () => {
|
||||
onlineEventTarget.removeEventListener('online', listener);
|
||||
events.off('online', listener);
|
||||
clearTimeoutIfNecessary(timeoutId);
|
||||
};
|
||||
|
||||
onlineEventTarget.addEventListener('online', listener);
|
||||
events.on('online', listener);
|
||||
|
||||
if (timeout !== undefined) {
|
||||
timeoutId = setTimeout(() => {
|
||||
|
|
Loading…
Add table
Reference in a new issue