Skip to content

Commit

Permalink
move server status monitor into background
Browse files Browse the repository at this point in the history
  • Loading branch information
yushih committed Jul 15, 2024
1 parent c446fe3 commit 647cdbf
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ export class RemoteFetcher implements IFetcher {
this.getPlatform = getPlatform;
}

checkServerStatus: ServerStatusRequest => Promise<ServerStatusResponse> = (_body) => (
checkServerStatus: ServerStatusRequest => Promise<ServerStatusResponse> = (param) => (
axios(
`${getEndpoint()}/api/status`,
`${param.backend || getEndpoint()}/api/status`,
{
method: 'get',
timeout: 2 * CONFIG.app.walletRefreshInterval,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ export type BackendNetworkInfo = {|

// checkServer

export type ServerStatusRequest = void;
export type ServerStatusRequest = {|
backend: string,
|};
export type ServerStatusResponse = {|
isServerOk: boolean,
isMaintenance: boolean,
Expand Down
32 changes: 31 additions & 1 deletion packages/yoroi-extension/app/api/thunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { schema } from 'lovefield';
import { loadLovefieldDBFromDump } from './ada/lib/storage/database';
import type { lf$Database, } from 'lovefield';
import type { WalletState } from '../../chrome/extension/background/types';
import type { WalletState, ServerStatus } from '../../chrome/extension/background/types';
import { HaskellShelleyTxSignRequest } from './ada/transactions/shelley/HaskellShelleyTxSignRequest';
import type { NetworkRow, TokenRow } from './ada/lib/storage/database/primitives/tables';
import type { TxMemoLookupKey, } from './ada/lib/storage/bridge/memos';
Expand All @@ -27,6 +27,8 @@ import type { ExplorerRow, PreferredExplorerRow } from './ada/lib/storage/databa
declare var chrome;
*/

// UI -> background queries:

function callBackground<T, R>(message: T): Promise<R> {
return new Promise((resolve, reject) => {
window.chrome.runtime.sendMessage(message, response => {
Expand Down Expand Up @@ -305,3 +307,31 @@ export type SignTransactionRequestType = {|
export async function signTransaction(request: SignTransactionRequestType): Promise<string> {
return await callBackground({ type: 'sign-tx', request });
}

// Background -> UI notifications:
const callbacks = Object.freeze({
walletStateUpdate: [],
serverStatusUpdate: [],
});
chrome.runtime.onMessage.addListener(async (message, sender) => {
if (message.type === 'wallet-state-update') {
callbacks.walletStateUpdate.forEach(callback => callback({
publicDeriverId: message.publicDeriverId,
isRefreshing: message.isRefreshing,
}));
} else if (message.type === 'server-status-update') {
callbacks.serverStatusUpdate.forEach(callback => callback(message.params));
}
});

export type WalletStateUpdateParams = {|
publicDeriverId: number,
isRefreshing: boolean,
|};
export function listenForWalletStateUpdate(callback: (WalletStateUpdateParams) => Promise<void>) {
callbacks.walletStateUpdate.push(callback);
}

export function listenForServerStatusUpdate(callback: (Array<ServerStatus>) => Promise<void>) {
callbacks.serverStatusUpdate.push(callback);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,59 @@ import { action, observable, computed, runInAction } from 'mobx';
import Store from '../base/Store';
import type { ServerStatusErrorType } from '../../types/serverStatusErrorType';
import { ServerStatusErrors } from '../../types/serverStatusErrorType';
import environment from '../../environment';
import type { ServerStatusResponse } from '../../api/common/lib/state-fetch/types';
import type { ActionsMap } from '../../actions/index';
import type { StoresMap } from '../index';
import { listenForServerStatusUpdate } from '../../api/thunk';
import type { ServerStatus } from '../../../chrome/extension/background/types';
import { networks } from '../../api/ada/lib/storage/database/prepackaged/networks';

export default class ServerConnectionStore extends Store<StoresMap, ActionsMap> {
SERVER_STATUS_REFRESH_INTERVAL: number = environment.getServerStatusRefreshInterval();

@observable serverStatus: ServerStatusErrorType = ServerStatusErrors.Healthy;
@observable isMaintenance: boolean = false;
parallelSync: boolean = false;

// set to undefined as a starting value
// to detect if we've never managed to connect to the server (Yoroi running in offline mode)
@observable serverTime: void | Date = undefined;
@observable serverStatus: Array<ServerStatus>;

setup(): void {
super.setup();

// do not await on purpose -- it's okay if this is async
this._checkServerStatus();
setInterval(this._checkServerStatus, this.SERVER_STATUS_REFRESH_INTERVAL);
listenForServerStatusUpdate(async (serverStatus) => {
runInAction(() => {
this.serverStatus.splice(0, this.serverStatus.length, ...serverStatus);
});
});
}

get serverTime(): void | Date {
const serverStatus = this._getServerStatus();
if (serverStatus) {
return new Date(serverStatus.clockSkew + Date.now());
}
return undefined;
}

get isMaintenance(): boolean {
const serverStatus = this._getServerStatus();
if (serverStatus) {
return serverStatus.isMaintenance;
}
return false;
}

@computed get checkAdaServerStatus(): ServerStatusErrorType {
return this.serverStatus;
const serverStatus = this._getServerStatus();
if (serverStatus) {
return serverStatus.isServerOk ? ServerStatusErrors.Healthy : ServerStatusErrors.Server;
}
// this is a temporary condition, we'll soon get an update
return ServerStatusErrors.Healthy;
}

@action _checkServerStatus: void => Promise<void> = async () => {
const stateFetcher = this.stores.stateFetchStore.fetcher;
const checkServerStatusFunc = stateFetcher.checkServerStatus;
try {
const response: ServerStatusResponse = await checkServerStatusFunc();
runInAction('refresh server status', () => {
this.serverStatus = response.isServerOk === true
? ServerStatusErrors.Healthy
: ServerStatusErrors.Server;
this.isMaintenance = response.isMaintenance || false;
const parallelSync = response.parallelSync || false;
if (parallelSync !== this.parallelSync) {
this.parallelSync = parallelSync;
this.actions.serverConnection.parallelSyncStateChange.trigger();
}
this.serverTime = new Date(response.serverTime);
});
} catch (err) {
runInAction('refresh server status', () => {
this.serverStatus = ServerStatusErrors.Network;
});
_getServerStatus(): ServerStatus | void {
let networkId;
const { selected } = this.stores.wallets;
if (selected) {
networkId = selected.networkId;
} else {
networkId = networks.CardanoMainnet.NetworkId;
}
return this.serverStatus.find(s => s.networkId === networkId);
}
}
28 changes: 13 additions & 15 deletions packages/yoroi-extension/app/stores/toplevel/WalletStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type { StoresMap } from '../index';
import { getWalletChecksum } from '../../api/export/utils';
import { getNetworkById } from '../../api/ada/lib/storage/database/prepackaged/networks';
import type { WalletState } from '../../../chrome/extension/background/types';
import { getWallets, subscribe } from '../../api/thunk';
import { getWallets, subscribe, listenForWalletStateUpdate } from '../../api/thunk';
/*::
declare var chrome;
*/
Expand Down Expand Up @@ -167,20 +167,18 @@ export default class WalletStore extends Store<StoresMap, ActionsMap> {
this.wallets.push(...result);
});

chrome.runtime.onMessage.addListener(async (message, sender) => {
if (message.type === 'wallet-state-update') {
const index = this.wallets.findIndex(wallet => wallet.publicDeriverId === message.publicDeriverId);
if (index === -1) {
return;
}
if (message.isRefreshing) {
runInAction(() => this.wallets[index].isRefreshing = true);
} else {
const newWalletState = await getWallets(message.publicDeriverId);
runInAction(() => {
this.wallets.splice(index, 1, newWalletState[0]);
});
}
listenForWalletStateUpdate(async ({ publicDeriverId, isRefreshing }) => {
const index = this.wallets.findIndex(wallet => wallet.publicDeriverId === publicDeriverId);
if (index === -1) {
return;
}
if (isRefreshing) {
runInAction(() => this.wallets[index].isRefreshing = true);
} else {
const newWalletState = await getWallets(publicDeriverId);
runInAction(() => {
this.wallets.splice(index, 1, newWalletState[0]);
});
}
});
};
Expand Down
2 changes: 2 additions & 0 deletions packages/yoroi-extension/chrome/extension/background/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import debounce from 'lodash/debounce';
import { handleInjectorMessage } from './handlers/content';
import { isYoroiMessage, yoroiMessageHandler } from './handlers/yoroi';
import { init } from './state';
import { startMonitorServerStatus } from './serverStatus';

/*::
declare var chrome;
Expand Down Expand Up @@ -34,3 +35,4 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
});

init().catch(console.error);
startMonitorServerStatus();
101 changes: 101 additions & 0 deletions packages/yoroi-extension/chrome/extension/background/serverStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// @flow
import { ConceptualWalletSchema } from '../../../app/api/ada/lib/storage/database/walletTypes/core/tables';
import { raii, getAll } from '../../../app/api/ada/lib/storage/database/utils';
import { getDb } from './state';
import { networks } from '../../../app/api/ada/lib/storage/database/prepackaged/networks';
import type { NetworkRow } from '../../../app/api/ada/lib/storage/database/primitives/tables';
import type { ConceptualWalletRow } from '../../../app/api/ada/lib/storage/database/walletTypes/core/tables';
import { getCommonStateFetcher } from './utils';
import environment from '../../../app/environment';
import type { ServerStatus } from './types';
import {
getSubscriptions,
registerCallback,
emitUpdateToSubscriptions,
} from './subscriptionManager';

const serverStatusByNetworkId: Map<number, ServerStatus> = new Map();

async function getAllNetworks(): Promise<$ReadOnlyArray<NetworkRow>> {
const db = await getDb();
const allConceptualWallets = await raii<$ReadOnlyArray<ConceptualWalletRow>>(
db,
[],
tx => getAll(
db, tx,
ConceptualWalletSchema.name,
)
);
const allNetworkIdSet = new Set(allConceptualWallets.map(w => w.NetworkId));
return Object.keys(networks).map(n => networks[n]).filter(
({ NetworkId }) => allNetworkIdSet.has(NetworkId)
);
}

async function updateServerStatus() {
const networks = await getAllNetworks();
const fetcher = await getCommonStateFetcher();

for (const network of networks) {
const backend = network.Backend.BackendServiceZero;
if (!backend) {
throw new Error('unexpectedly missing backend zero');
}
const startTime = Date.now();
let resp;
try {
resp = await fetcher.checkServerStatus({ backend });
} catch {
continue;
}
const endTime = Date.now();
const roundtripTime = endTime - startTime;

serverStatusByNetworkId.set(
network.NetworkId,
{
networkId: network.NetworkId,
isServerOk: resp.isServerOk,
isMaintenance: resp.isMaintenance || false,
// server time = local time + clock skew
clockSkew: resp.serverTime + roundtripTime / 2 - endTime,
lastUpdateTimestamp: startTime + roundtripTime / 2,
}
);
}
}

let lastUpdateTimestamp: number = 0;
let updatePromise: null | Promise<void> = null;

async function updateServerStatusTick() {
const refreshInterval = environment.getServerStatusRefreshInterval();

if (getSubscriptions().length === 0) {
return;
}

if (Date.now() - lastUpdateTimestamp > refreshInterval) {
if (!updatePromise) {
updatePromise = updateServerStatus();
}
await updatePromise;
updatePromise = null;
lastUpdateTimestamp = Date.now();
}

emitUpdateToSubscriptions({
type: 'server-status-update',
params: [...serverStatusByNetworkId.values()],
});

setTimeout(updateServerStatusTick, refreshInterval);
}

export function startMonitorServerStatus() {
registerCallback(params => {
if (params.type === 'subscriptionChange') {
updateServerStatusTick();
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import { getDb } from './databaseManager';
export { getDb };
export { refreshingWalletIdSet, syncWallet } from './refreshScheduler';
export { subscribe } from './subscriptionManager';

export async function init(): Promise<void> {
// eagerly cache
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import type { WalletState } from '../types';
import { asHasLevels } from '../../../../app/api/ada/lib/storage/models/PublicDeriver/traits';
import environment from '../../../../app/environment';
import { getDb } from './databaseManager';
import { getSubscriptions, registerCallback } from './subscriptionManager';
import {
getSubscriptions,
registerCallback,
emitUpdateToSubscriptions,
} from '../subscriptionManager';
import LocalStorageApi, {
loadSubmittedTransactions,
persistSubmittedTransactions,
Expand Down Expand Up @@ -42,8 +46,6 @@ export async function refreshMain(): Promise<void> {
// return delay in ms for next run
// this function should not have unhandled exception
async function refreshAllParallel(): Promise<void> {
const subscriptions = getSubscriptions();

const db = await getDb();
const publicDerivers = await getWallets({ db });

Expand Down Expand Up @@ -132,18 +134,11 @@ export async function syncWallet(publicDeriver: PublicDeriver<>): Promise<void>
}
}

/*::
declare var chrome;
*/
function emitUpdate(publicDeriverId: number, isRefreshing: boolean): void {
for (const { tabId } of getSubscriptions()) {
chrome.tabs.sendMessage(
tabId,
{
type: 'wallet-state-update',
publicDeriverId,
isRefreshing,
}
);
}
emitUpdateToSubscriptions({
type: 'wallet-state-update',
publicDeriverId,
isRefreshing,
});
}

Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,11 @@ export function registerCallback(callback: (Callback) => void) {
callbacks.push(callback);
}

/*::
declare var chrome;
*/
export function emitUpdateToSubscriptions(data: Object): void {
for (const { tabId } of getSubscriptions()) {
chrome.tabs.sendMessage(tabId, Object);
}
}
Loading

0 comments on commit 647cdbf

Please sign in to comment.