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