MV3: Re-activate service worker and reconnect UI streams (#14781)

feature/default_network_editable
Jyoti Puri 2 years ago committed by GitHub
parent 5d828be611
commit bca9a61d6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      app/scripts/app-init.js
  2. 107
      app/scripts/ui.js
  3. 36
      ui/index.js

@ -1,8 +1,9 @@
/* global chrome */
// This file is used only for manifest version 3 // This file is used only for manifest version 3
// Represents if importAllScripts has been run // Represents if importAllScripts has been run
// eslint-disable-next-line // eslint-disable-next-line
let scriptsLoaded = false; let scriptsLoadInitiated = false;
// Variable testMode is set to true when preparing test build. // Variable testMode is set to true when preparing test build.
// This helps in changing service worker execution in test environment. // This helps in changing service worker execution in test environment.
@ -35,10 +36,10 @@ function tryImport(...fileNames) {
function importAllScripts() { function importAllScripts() {
// Bail if we've already imported scripts // Bail if we've already imported scripts
if (scriptsLoaded) { if (scriptsLoadInitiated) {
return; return;
} }
scriptsLoadInitiated = true;
const files = []; const files = [];
// In testMode individual files are imported, this is to help capture load time stats // In testMode individual files are imported, this is to help capture load time stats
@ -69,9 +70,6 @@ function importAllScripts() {
loadFile('./runtime-cjs.js'); loadFile('./runtime-cjs.js');
} }
// Mark scripts as loaded
scriptsLoaded = true;
const fileList = [ const fileList = [
// The list of files is injected at build time by replacing comment below with comma separated strings of file names // The list of files is injected at build time by replacing comment below with comma separated strings of file names
// https://github.com/MetaMask/metamask-extension/blob/496d9d81c3367931031edc11402552690c771acf/development/build/scripts.js#L406 // https://github.com/MetaMask/metamask-extension/blob/496d9d81c3367931031edc11402552690c771acf/development/build/scripts.js#L406
@ -111,14 +109,20 @@ function importAllScripts() {
} }
} }
// Ref: https://stackoverflow.com/questions/66406672/chrome-extension-mv3-modularize-service-worker-js-file
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
self.addEventListener('install', importAllScripts); self.addEventListener('install', importAllScripts);
/* /*
* Message event listener below loads script if they are no longer available. * A keepalive message listener to prevent Service Worker getting shut down due to inactivity.
* UI sends the message periodically, in a setInterval.
* Chrome will revive the service worker if it was shut down, whenever a new message is sent, but only if a listener was defined here.
*
* chrome below needs to be replaced by cross-browser object, * chrome below needs to be replaced by cross-browser object,
* but there is issue in importing webextension-polyfill into service worker. * but there is issue in importing webextension-polyfill into service worker.
* chrome does seems to work in at-least all chromium based browsers * chrome does seems to work in at-least all chromium based browsers
*/ */
// eslint-disable-next-line no-undef chrome.runtime.onMessage.addListener(() => {
chrome.runtime.onMessage.addListener(importAllScripts); importAllScripts();
return false;
});

@ -11,7 +11,7 @@ import Eth from 'ethjs';
import EthQuery from 'eth-query'; import EthQuery from 'eth-query';
import StreamProvider from 'web3-stream-provider'; import StreamProvider from 'web3-stream-provider';
import log from 'loglevel'; import log from 'loglevel';
import launchMetaMaskUi from '../../ui'; import launchMetaMaskUi, { updateBackgroundConnection } from '../../ui';
import { import {
ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_FULLSCREEN,
ENVIRONMENT_TYPE_POPUP, ENVIRONMENT_TYPE_POPUP,
@ -24,34 +24,37 @@ import { setupMultiplex } from './lib/stream-utils';
import { getEnvironmentType } from './lib/util'; import { getEnvironmentType } from './lib/util';
import metaRPCClientFactory from './lib/metaRPCClientFactory'; import metaRPCClientFactory from './lib/metaRPCClientFactory';
start().catch(log.error); const container = document.getElementById('app-content');
async function start() {
async function displayCriticalError(container, err, metamaskState) {
const html = await getErrorHtml(SUPPORT_LINK, metamaskState);
container.innerHTML = html;
const button = document.getElementById('critical-error-button'); const WORKER_KEEP_ALIVE_INTERVAL = 1000;
const WORKER_KEEP_ALIVE_MESSAGE = 'WORKER_KEEP_ALIVE_MESSAGE';
button.addEventListener('click', (_) => { /*
browser.runtime.reload(); * As long as UI is open it will keep sending messages to service worker
}); * In service worker as this message is received
* if service worker is inactive it is reactivated and script re-loaded
* Time has been kept to 1000ms but can be reduced for even faster re-activation of service worker
*/
if (isManifestV3()) {
setInterval(() => {
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
}, WORKER_KEEP_ALIVE_INTERVAL);
}
log.error(err.stack); start().catch(log.error);
throw err;
}
async function start() {
// create platform global // create platform global
global.platform = new ExtensionPlatform(); global.platform = new ExtensionPlatform();
// identify window type (popup, notification) // identify window type (popup, notification)
const windowType = getEnvironmentType(); const windowType = getEnvironmentType();
// setup stream to background let isUIInitialised = false;
const extensionPort = browser.runtime.connect({ name: windowType });
const connectionStream = new PortStream(extensionPort); // setup stream to background
let extensionPort = browser.runtime.connect({ name: windowType });
let connectionStream = new PortStream(extensionPort);
const activeTab = await queryCurrentActiveTab(windowType); const activeTab = await queryCurrentActiveTab(windowType);
@ -60,23 +63,52 @@ async function start() {
* Code below ensures that UI is rendered only after background is ready. * Code below ensures that UI is rendered only after background is ready.
*/ */
if (isManifestV3()) { if (isManifestV3()) {
extensionPort.onMessage.addListener((message) => { /*
* In case of MV3 the issue of blank screen was very frequent, it is caused by UI initialising before background is ready to send state.
* Code below ensures that UI is rendered only after CONNECTION_READY message is received thus background is ready.
* In case the UI is already rendered, only update the streams.
*/
const messageListener = (message) => {
if (message?.name === 'CONNECTION_READY') { if (message?.name === 'CONNECTION_READY') {
if (isUIInitialised) {
updateUiStreams();
} else {
initializeUiWithTab(activeTab); initializeUiWithTab(activeTab);
} }
}); }
};
// resetExtensionStreamAndListeners takes care to remove listeners from closed streams
// it also creates new streams and attach event listeners to them
const resetExtensionStreamAndListeners = () => {
extensionPort.onMessage.removeListener(messageListener);
extensionPort.onDisconnect.removeListener(
resetExtensionStreamAndListeners,
);
// message below will try to activate service worker
// in MV3 is likely that reason of stream closing is service worker going in-active
browser.runtime.sendMessage({ name: WORKER_KEEP_ALIVE_MESSAGE });
extensionPort = browser.runtime.connect({ name: windowType });
connectionStream = new PortStream(extensionPort);
extensionPort.onMessage.addListener(messageListener);
extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners);
};
extensionPort.onMessage.addListener(messageListener);
extensionPort.onDisconnect.addListener(resetExtensionStreamAndListeners);
} else { } else {
initializeUiWithTab(activeTab); initializeUiWithTab(activeTab);
} }
function initializeUiWithTab(tab) { function initializeUiWithTab(tab) {
const container = document.getElementById('app-content'); initializeUi(tab, connectionStream, (err, store) => {
initializeUi(tab, container, connectionStream, (err, store) => {
if (err) { if (err) {
// if there's an error, store will be = metamaskState // if there's an error, store will be = metamaskState
displayCriticalError(container, err, store); displayCriticalError(err, store);
return; return;
} }
isUIInitialised = true;
const state = store.getState(); const state = store.getState();
const { metamask: { completedOnboarding } = {} } = state; const { metamask: { completedOnboarding } = {} } = state;
@ -86,6 +118,18 @@ async function start() {
} }
}); });
} }
// Function to update new backgroundConnection in the UI
function updateUiStreams() {
connectToAccountManager(connectionStream, (err, backgroundConnection) => {
if (err) {
displayCriticalError(err);
return;
}
updateBackgroundConnection(backgroundConnection);
});
}
} }
async function queryCurrentActiveTab(windowType) { async function queryCurrentActiveTab(windowType) {
@ -112,7 +156,7 @@ async function queryCurrentActiveTab(windowType) {
}); });
} }
function initializeUi(activeTab, container, connectionStream, cb) { function initializeUi(activeTab, connectionStream, cb) {
connectToAccountManager(connectionStream, (err, backgroundConnection) => { connectToAccountManager(connectionStream, (err, backgroundConnection) => {
if (err) { if (err) {
cb(err, null); cb(err, null);
@ -130,6 +174,21 @@ function initializeUi(activeTab, container, connectionStream, cb) {
}); });
} }
async function displayCriticalError(err, metamaskState) {
const html = await getErrorHtml(SUPPORT_LINK, metamaskState);
container.innerHTML = html;
const button = document.getElementById('critical-error-button');
button.addEventListener('click', (_) => {
browser.runtime.reload();
});
log.error(err.stack);
throw err;
}
/** /**
* Establishes a connection to the background and a Web3 provider * Establishes a connection to the background and a Web3 provider
* *

@ -31,9 +31,30 @@ import txHelper from './helpers/utils/tx-helper';
log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn'); log.setLevel(global.METAMASK_DEBUG ? 'debug' : 'warn');
let reduxStore;
/**
* Method to update backgroundConnection object use by UI
*
* @param backgroundConnection - connection object to background
*/
export const updateBackgroundConnection = (backgroundConnection) => {
actions._setBackgroundConnection(backgroundConnection);
backgroundConnection.onNotification((data) => {
if (data.method === 'sendUpdate') {
reduxStore.dispatch(actions.updateMetamaskState(data.params[0]));
} else {
throw new Error(
`Internal JSON-RPC Notification Not Handled:\n\n ${JSON.stringify(
data,
)}`,
);
}
});
};
export default function launchMetamaskUi(opts, cb) { export default function launchMetamaskUi(opts, cb) {
const { backgroundConnection } = opts; const { backgroundConnection } = opts;
actions._setBackgroundConnection(backgroundConnection);
// check if we are unlocked first // check if we are unlocked first
backgroundConnection.getState(function (err, metamaskState) { backgroundConnection.getState(function (err, metamaskState) {
if (err) { if (err) {
@ -117,6 +138,7 @@ async function startApp(metamaskState, backgroundConnection, opts) {
} }
const store = configureStore(draftInitialState); const store = configureStore(draftInitialState);
reduxStore = store;
// if unconfirmed txs, start on txConf page // if unconfirmed txs, start on txConf page
const unapprovedTxsAll = txHelper( const unapprovedTxsAll = txHelper(
@ -138,17 +160,7 @@ async function startApp(metamaskState, backgroundConnection, opts) {
); );
} }
backgroundConnection.onNotification((data) => { updateBackgroundConnection(backgroundConnection);
if (data.method === 'sendUpdate') {
store.dispatch(actions.updateMetamaskState(data.params[0]));
} else {
throw new Error(
`Internal JSON-RPC Notification Not Handled:\n\n ${JSON.stringify(
data,
)}`,
);
}
});
// global metamask api - used by tooling // global metamask api - used by tooling
global.metamask = { global.metamask = {

Loading…
Cancel
Save