Skip to content

Commit 8a33b6c

Browse files
authored
feat: reload failed IPFS tabs when API becomes available (#1092)
1 parent d842c57 commit 8a33b6c

File tree

11 files changed

+372
-221
lines changed

11 files changed

+372
-221
lines changed

.editorconfig

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
root=true
2+
3+
[*]
4+
end_of_line = lf
5+
insert_final_newline = true
6+
charset = utf-8
7+
indent_style = space
8+
indent_size = 2

add-on/src/lib/ipfs-client/index.js

+39-24
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const external = require('./external')
1010
const embedded = require('./embedded')
1111
const brave = require('./brave')
1212
const { precache } = require('../precache')
13+
const { prepareReloadExtensions, WebUiReloader, LocalGatewayReloader, InternalTabReloader } = require('./reloaders')
1314

1415
// ensure single client at all times, and no overlap between init and destroy
1516
let client
@@ -61,40 +62,54 @@ async function destroyIpfsClient (browser) {
6162
}
6263
}
6364

64-
function _isWebuiTab (url) {
65-
const bundled = !url.startsWith('http') && url.includes('/webui/index.html#/')
66-
const ipns = url.includes('/webui.ipfs.io/#/')
67-
return bundled || ipns
68-
}
69-
70-
function _isInternalTab (url, extensionOrigin) {
71-
return url.startsWith(extensionOrigin)
72-
}
73-
74-
async function _reloadIpfsClientDependents (browser, instance, opts) {
65+
/**
66+
* Reloads pages dependant on ipfs to be online
67+
*
68+
* @typedef {embedded|brave|external} Browser
69+
* @param {Browser} browser
70+
* @param {import('ipfs-http-client').default} instance
71+
* @param {Object} opts
72+
* @param {Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]=} reloadExtensions
73+
* @returns {void}
74+
*/
75+
async function _reloadIpfsClientDependents (
76+
browser, instance, opts, reloadExtensions = [WebUiReloader, LocalGatewayReloader, InternalTabReloader]) {
7577
// online || offline
7678
if (browser.tabs && browser.tabs.query) {
7779
const tabs = await browser.tabs.query({})
7880
if (tabs) {
79-
const extensionOrigin = browser.runtime.getURL('/')
80-
tabs.forEach((tab) => {
81-
// detect bundled webui in any of open tabs
82-
if (_isWebuiTab(tab.url)) {
83-
log(`reloading webui at ${tab.url}`)
84-
browser.tabs.reload(tab.id)
85-
} else if (_isInternalTab(tab.url, extensionOrigin)) {
86-
log(`reloading internal extension page at ${tab.url}`)
87-
browser.tabs.reload(tab.id)
88-
}
89-
})
81+
try {
82+
const reloadExtensionInstances = await prepareReloadExtensions(reloadExtensions, browser, log)
83+
// the reload process is async, fire and forget.
84+
reloadExtensionInstances.forEach(ext => ext.reload(tabs))
85+
} catch (e) {
86+
log('Failed to trigger reloaders')
87+
}
9088
}
9189
}
90+
9291
// online only
9392
if (client && instance && opts) {
9493
// add important data to local ipfs repo for instant load
9594
setTimeout(() => precache(instance, opts), 5000)
9695
}
9796
}
9897

99-
exports.initIpfsClient = initIpfsClient
100-
exports.destroyIpfsClient = destroyIpfsClient
98+
/**
99+
* Reloads local gateway pages dependant on ipfs to be online
100+
*
101+
* @typedef {embedded|brave|external} Browser
102+
* @param {Browser} browser
103+
* @param {import('ipfs-http-client').default} instance
104+
* @param {Object} opts
105+
* @returns {void}
106+
*/
107+
function reloadIpfsClientOfflinePages (browser, instance, opts) {
108+
_reloadIpfsClientDependents(browser, instance, opts, [LocalGatewayReloader])
109+
}
110+
111+
module.exports = {
112+
initIpfsClient,
113+
destroyIpfsClient,
114+
reloadIpfsClientOfflinePages
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const { InternalTabReloader } = require('./internalTabReloader')
2+
const { LocalGatewayReloader } = require('./localGatewayReloader')
3+
const { WebUiReloader } = require('./webUiReloader')
4+
5+
/**
6+
* Prepares extension by creating an instance and awaiting for init.
7+
*
8+
* @param {Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]} extensions
9+
* @param {Browser} browserInstance
10+
* @param {Logger} loggerInstance
11+
* @returns {Promise<Array.[InternalTabReloader|LocalGatewayReloader|WebUiReloader]>}
12+
*/
13+
function prepareReloadExtensions (extensions, browserInstance, loggerInstance) {
14+
const reloadExtensions = Array.isArray(extensions) ? extensions : [extensions]
15+
return Promise.all(reloadExtensions
16+
.map(async Ext => {
17+
try {
18+
const ext = new Ext(browserInstance, loggerInstance)
19+
await ext.init()
20+
return ext
21+
} catch (e) {
22+
loggerInstance(`Extension Instance Failed to Initialize with error: ${e}. Extension: ${Ext}`)
23+
}
24+
})
25+
)
26+
}
27+
28+
module.exports = {
29+
InternalTabReloader,
30+
LocalGatewayReloader,
31+
WebUiReloader,
32+
prepareReloadExtensions
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const { ReloaderBase } = require('./reloaderBase')
2+
3+
class InternalTabReloader extends ReloaderBase {
4+
/**
5+
* Setting up the extension origin.
6+
*/
7+
init () {
8+
this.extensionOrigin = this._browserInstance.runtime.getURL('/')
9+
this._log('InternalTabReloader Ready for use.')
10+
}
11+
12+
/**
13+
* Performs url validation for the tab. If tab is a WebUI tab.
14+
*
15+
* @param {Object} tab
16+
* @param {string} tab.url
17+
* @returns {boolean}
18+
*/
19+
validation ({ url }) {
20+
return url.startsWith(this.extensionOrigin)
21+
}
22+
23+
/**
24+
* Returns message when reloading the tab.
25+
*
26+
* @param {Object} tab
27+
* @param {string} tab.url
28+
* @returns {string} message.
29+
*/
30+
message ({ url }) {
31+
return `reloading internal extension page at ${url}`
32+
}
33+
}
34+
35+
module.exports = {
36+
InternalTabReloader
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const { ReloaderBase } = require('./reloaderBase')
2+
const isIPFS = require('is-ipfs')
3+
4+
class LocalGatewayReloader extends ReloaderBase {
5+
/**
6+
* Performs url validation for the tab. If tab is loaded via local gateway.
7+
*
8+
* @param {Object} tab
9+
* @param {string} tab.url
10+
* @param {string} tab.url
11+
* @returns {boolean}
12+
*/
13+
validation ({ url, title }) {
14+
/**
15+
* Check if the url is the local gateway url and if the title is contained within the url then it was not loaded.
16+
* - This assumes that the title of most pages on the web will be set and hence when not reachable, the browser
17+
* will set title to the url/host (both chrome and brave) and 'problem loading page' for firefox.
18+
* - There is probability that this might be true in case the <title> tag is omitted, but worst case it only reloads
19+
* those pages.
20+
* - The benefit we get from this approach is the static nature of just observing the tabs in their current state
21+
* which reduces the overhead of injecting content scripts to track urls that were loaded after the connection
22+
* was offline, it may also need extra permissions to inject code on error pages.
23+
*/
24+
return (isIPFS.url(url) || isIPFS.subdomain(url)) &&
25+
(url.includes(title) || title.toLowerCase() === 'problem loading page')
26+
}
27+
28+
/**
29+
* Returns message when reloading the tab.
30+
*
31+
* @param {Object} tab
32+
* @param {string} tab.url
33+
* @returns {string} message.
34+
*/
35+
message ({ url }) {
36+
return `reloading local gateway at ${url}`
37+
}
38+
}
39+
40+
module.exports = {
41+
LocalGatewayReloader
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
class ReloaderBase {
2+
/**
3+
* Constructor for reloader base class.
4+
*
5+
* @param {Browser} browser
6+
* @param {Logger} log
7+
*/
8+
constructor (browser, log) {
9+
if (!browser || !log) {
10+
throw new Error('Instances of browser and logger are needed!')
11+
}
12+
this._browserInstance = browser
13+
this._log = log
14+
};
15+
16+
/**
17+
* Initializes the instance.
18+
*/
19+
init () {
20+
this._log('Initialized without additional config.')
21+
}
22+
23+
/**
24+
* To be implemented in child class.
25+
*/
26+
validation () {
27+
throw new Error('Validation: Method Not Implemented')
28+
}
29+
30+
/**
31+
* To be implemented in child class.
32+
*/
33+
message () {
34+
throw new Error('Message: Method Not Implemented')
35+
}
36+
37+
/**
38+
* Handles reload for all tabs.
39+
* params {Array<tabs>}
40+
*/
41+
reload (tabs) {
42+
tabs
43+
.filter(tab => this.validation(tab))
44+
.forEach(tab => {
45+
this._log(this.message(tab))
46+
this._browserInstance.tabs.reload(tab.id)
47+
})
48+
}
49+
}
50+
51+
module.exports = {
52+
ReloaderBase
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const { ReloaderBase } = require('./reloaderBase')
2+
3+
class WebUiReloader extends ReloaderBase {
4+
/**
5+
* Performs url validation for the tab. If tab is a WebUI tab.
6+
*
7+
* @param {Object} tab
8+
* @returns {boolean}
9+
*/
10+
validation ({ url }) {
11+
const bundled = !url.startsWith('http') && url.includes('/webui/index.html#/')
12+
const ipns = url.includes('/webui.ipfs.io/#/')
13+
return bundled || ipns
14+
}
15+
16+
/**
17+
* Returns message when reloading the tab.
18+
*
19+
* @param {Object} tab
20+
* @param {string} tab.url
21+
* @returns {string} message.
22+
*/
23+
message ({ url }) {
24+
return `reloading webui at ${url}`
25+
}
26+
}
27+
28+
module.exports = {
29+
WebUiReloader
30+
}

add-on/src/lib/ipfs-companion.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const { initState, offlinePeerCount } = require('./state')
1515
const { createIpfsPathValidator, sameGateway, safeHostname } = require('./ipfs-path')
1616
const createDnslinkResolver = require('./dnslink')
1717
const { createRequestModifier } = require('./ipfs-request')
18-
const { initIpfsClient, destroyIpfsClient } = require('./ipfs-client')
18+
const { initIpfsClient, destroyIpfsClient, reloadIpfsClientOfflinePages } = require('./ipfs-client')
1919
const { braveNodeType, useBraveEndpoint, releaseBraveEndpoint } = require('./ipfs-client/brave')
2020
const { createIpfsImportHandler, formatImportDirectory, browserActionFilesCpImportCurrentTab } = require('./ipfs-import')
2121
const createNotifier = require('./notifier')
@@ -595,6 +595,7 @@ module.exports = async function init () {
595595
if (oldPeerCount === offlinePeerCount && newPeerCount > offlinePeerCount && !state.redirect) {
596596
await browser.storage.local.set({ useCustomGateway: true })
597597
await notify('notify_apiOnlineTitle', 'notify_apiOnlineAutomaticModeMsg')
598+
reloadIpfsClientOfflinePages(browser, ipfs, state)
598599
} else if (newPeerCount === offlinePeerCount && state.redirect) {
599600
await browser.storage.local.set({ useCustomGateway: false })
600601
await notify('notify_apiOfflineTitle', 'notify_apiOfflineAutomaticModeMsg')

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@
141141
"webextension-polyfill": "0.7.0"
142142
},
143143
"engines": {
144-
"node": ">=14.15.0",
144+
"node": ">=14.15.0 <17",
145145
"npm": ">=6.14.0"
146146
}
147147
}

0 commit comments

Comments
 (0)