From c97d833cc1aa4788cddb253f8de52b8c390554f1 Mon Sep 17 00:00:00 2001 From: IsaccoSordo Date: Tue, 24 Sep 2024 14:48:12 +0200 Subject: [PATCH] fix: broadcast channel only on desktop --- package-lock.json | 51 +++++- packages/beacon-core/package.json | 1 + .../src/utils/multi-tab-channel.ts | 148 ++++++------------ .../beacon-dapp/src/dapp-client/DAppClient.ts | 12 +- .../src/types/storage/StorageKey.ts | 1 - .../types/storage/StorageKeyReturnDefaults.ts | 1 - .../src/types/storage/StorageKeyReturnType.ts | 1 - 7 files changed, 100 insertions(+), 115 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43a25d4f9..7782f3832 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7460,6 +7460,31 @@ "node": ">=8" } }, + "node_modules/broadcast-channel": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-7.0.0.tgz", + "integrity": "sha512-a2tW0Ia1pajcPBOGUF2jXlDnvE9d5/dg6BG9h60OmRUcZVr/veUrU8vEQFwwQIhwG3KVzYwSk3v2nRRGFgQDXQ==", + "dependencies": { + "@babel/runtime": "7.23.4", + "oblivious-set": "1.4.0", + "p-queue": "6.6.2", + "unload": "2.4.1" + }, + "funding": { + "url": "https://github.com/sponsors/pubkey" + } + }, + "node_modules/broadcast-channel/node_modules/@babel/runtime": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.4.tgz", + "integrity": "sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -10897,8 +10922,7 @@ "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "node_modules/events": { "version": "3.3.0", @@ -17428,6 +17452,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oblivious-set": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.4.0.tgz", + "integrity": "sha512-szyd0ou0T8nsAqHtprRcP3WidfsN1TnAR5yWXf2mFCEr5ek3LEOkT6EZ/92Xfs74HIdyhG5WkGxIssMU0jBaeg==", + "engines": { + "node": ">=16" + } + }, "node_modules/ofetch": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.3.4.tgz", @@ -17644,7 +17676,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, "engines": { "node": ">=4" } @@ -17719,7 +17750,6 @@ "version": "6.6.2", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "dev": true, "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" @@ -17744,7 +17774,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "dev": true, "dependencies": { "p-finally": "^1.0.0" }, @@ -20608,8 +20637,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -23801,6 +23829,14 @@ "node": ">= 10.0.0" } }, + "node_modules/unload": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/unload/-/unload-2.4.1.tgz", + "integrity": "sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw==", + "funding": { + "url": "https://github.com/sponsors/pubkey" + } + }, "node_modules/unstorage": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.10.2.tgz", @@ -25085,6 +25121,7 @@ "@stablelib/nacl": "^1.0.4", "@stablelib/utf8": "^1.0.1", "@stablelib/x25519-session": "^1.0.4", + "broadcast-channel": "^7.0.0", "bs58check": "2.1.2" } }, diff --git a/packages/beacon-core/package.json b/packages/beacon-core/package.json index 9f5535a77..e355216e9 100644 --- a/packages/beacon-core/package.json +++ b/packages/beacon-core/package.json @@ -40,6 +40,7 @@ "@stablelib/nacl": "^1.0.4", "@stablelib/utf8": "^1.0.1", "@stablelib/x25519-session": "^1.0.4", + "broadcast-channel": "^7.0.0", "bs58check": "2.1.2" } } diff --git a/packages/beacon-core/src/utils/multi-tab-channel.ts b/packages/beacon-core/src/utils/multi-tab-channel.ts index 2f37c2dcb..af50eb9b3 100644 --- a/packages/beacon-core/src/utils/multi-tab-channel.ts +++ b/packages/beacon-core/src/utils/multi-tab-channel.ts @@ -1,10 +1,9 @@ -import { BeaconMessageType, StorageKey } from '@airgap/beacon-types' -import { Logger } from './Logger' -import { LocalStorage } from '../storage/LocalStorage' +import { Logger } from '@airgap/beacon-core' +import { BeaconMessageType } from '@airgap/beacon-types' +import { createLeaderElection, BroadcastChannel, LeaderElector } from 'broadcast-channel' type BCMessageType = - | 'HEARTBEAT' - | 'HEARTBEAT_ACK' + | 'LEADER_DEAD' | 'RESPONSE' | 'DISCONNECT' | 'REQUEST_PAIRING' @@ -21,141 +20,92 @@ type BCMessage = { data?: any } -const timeout = 1000 // ms const logger = new Logger('MultiTabChannel') +const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent +) export class MultiTabChannel { private id: string = String(Date.now()) - private leaderID: string = '' - private neighborhood: Set = new Set() private channel: BroadcastChannel + private elector: LeaderElector private eventListeners = [ () => this.onBeforeUnloadHandler(), (message: any) => this.onMessageHandler(message) ] private onBCMessageHandler: Function private onElectedLeaderHandler: Function - private pendingACKs: Map = new Map() - private storage: LocalStorage = new LocalStorage() - - isLeader: boolean = false - - private messageHandlers: { - [key in BCMessageType]?: (data: BCMessage) => void - } = { - HEARTBEAT: this.heartbeatHandler.bind(this), - HEARTBEAT_ACK: this.heartbeatACKHandler.bind(this) - } + // Auxiliary variable needed for handling beforeUnload. + // Closing a tab causes the elector to be killed immediately + private wasLeader: boolean = false constructor(name: string, onBCMessageHandler: Function, onElectedLeaderHandler: Function) { this.onBCMessageHandler = onBCMessageHandler this.onElectedLeaderHandler = onElectedLeaderHandler this.channel = new BroadcastChannel(name) + this.elector = createLeaderElection(this.channel) this.init() + .then(() => logger.debug('MultiTabChannel', 'constructor', 'init', 'done')) + .catch((err) => logger.warn(err.message)) } private async init() { - this.storage.subscribeToStorageChanged(async (event) => { - if ( - event.eventType === 'entryModified' && - event.key === this.storage.getPrefixedKey(StorageKey.BC_NEIGHBORHOOD) - ) { - const newNeighborhood = !event.newValue ? this.neighborhood : JSON.parse(event.newValue) - - if (newNeighborhood[0] !== this.leaderID) { - this.leaderElection() - } else { - clearTimeout(this.pendingACKs.get(this.leaderID)) - } - - this.neighborhood = newNeighborhood - } - }) - await this.requestLeadership() - } + if (isMobile) { + throw new Error('BroadcastChannel is not fully supported on mobile.') + } - private async requestLeadership() { - const neighborhood = await this.storage.get(StorageKey.BC_NEIGHBORHOOD) + const hasLeader = await this.elector.hasLeader() - if (!neighborhood.length) { - this.isLeader = true - logger.log('The current tab is the leader.') + if (!hasLeader) { + await this.elector.awaitLeadership() + this.wasLeader = this.isLeader() + this.wasLeader && logger.log('The current tab is the leader.') } - neighborhood.push(this.id) - this.leaderID = neighborhood[0] - this.neighborhood = new Set(neighborhood) - this.storage.set(StorageKey.BC_NEIGHBORHOOD, neighborhood) - - window?.addEventListener('beforeunload', this.eventListeners[0]) this.channel.onmessage = this.eventListeners[1] - - this.initHeartbeat() + window?.addEventListener('beforeunload', this.eventListeners[0]) } - private initHeartbeat() { - if ( - this.isLeader || - !/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) - ) { - return + private async onBeforeUnloadHandler() { + if (this.wasLeader) { + await this.elector.die() + this.postMessage({ type: 'LEADER_DEAD' }) } - setInterval(() => { - this.leaderElection(Array.from(this.neighborhood).filter((id) => id !== this.leaderID)) - this.postMessage({ type: 'HEARTBEAT' }) - }, timeout * 2) + window?.removeEventListener('beforeunload', this.eventListeners[0]) + this.channel.removeEventListener('message', this.eventListeners[1]) } - private heartbeatHandler() { - if (!this.isLeader) { + private async onMessageHandler(message: BCMessage) { + if (message.recipient && message.recipient !== this.id) { return } - this.postMessage({ type: 'HEARTBEAT_ACK' }) - } - private heartbeatACKHandler() { - this.pendingACKs.delete(this.leaderID) - } + if (message.type === 'LEADER_DEAD') { + await this.elector.awaitLeadership() + + this.wasLeader = this.isLeader() - private leaderElection(neighborhood = Array.from(this.neighborhood)) { - this.pendingACKs.set( - this.leaderID, - setTimeout(() => { - this.leaderID = neighborhood[0] - if (neighborhood[0] !== this.id) { - return - } - this.isLeader = true + if (this.isLeader()) { this.onElectedLeaderHandler() logger.log('The current tab is the leader.') - }, timeout) - ) + } + return + } + + this.onBCMessageHandler(message) } - private async onBeforeUnloadHandler() { - const oldNeighborhood = this.neighborhood - const newNeighborhood = new Set(this.neighborhood) - newNeighborhood.delete(this.id) - - await this.storage.set(StorageKey.BC_NEIGHBORHOOD, Array.from(newNeighborhood)) - this.neighborhood = newNeighborhood - - // We can't immediately say that a child or the leader is dead - // beacause, on mobile a browser tab gets unloaded every time it no longer has focus - setTimeout(() => { - this.storage.set(StorageKey.BC_NEIGHBORHOOD, Array.from(oldNeighborhood)) - this.neighborhood = oldNeighborhood - }, timeout / 2) + isLeader(): boolean { + return this.elector.isLeader } - private onMessageHandler({ data }: { data: BCMessage }) { - const handler = this.messageHandlers[data.type] - if (handler) { - handler(data) - } else { - this.onBCMessageHandler(data) - } + async getLeadership() { + return this.elector.awaitLeadership() + } + + async hasLeader(): Promise { + return this.elector.hasLeader() } postMessage(message: Omit): void { diff --git a/packages/beacon-dapp/src/dapp-client/DAppClient.ts b/packages/beacon-dapp/src/dapp-client/DAppClient.ts index efca76738..fb58bf7c6 100644 --- a/packages/beacon-dapp/src/dapp-client/DAppClient.ts +++ b/packages/beacon-dapp/src/dapp-client/DAppClient.ts @@ -517,7 +517,7 @@ export class DAppClient extends Client { } private async handlePairingRequest(recipient: string) { - if (!this.multiTabChannel.isLeader) { + if (!this.multiTabChannel.isLeader()) { return } @@ -571,7 +571,7 @@ export class DAppClient extends Client { } private async prepareRequest({ data }: any, isV3 = false) { - if (!this.multiTabChannel.isLeader) { + if (!this.multiTabChannel.isLeader()) { return } @@ -655,7 +655,7 @@ export class DAppClient extends Client { network: this.network.type, opts: wcOptions }, - () => this.multiTabChannel.isLeader + () => this.multiTabChannel.isLeader() ) this.initEvents() @@ -730,7 +730,7 @@ export class DAppClient extends Client { } private async getPairingRequestInfo(transport: DappWalletConnectTransport) { - if (this.multiTabChannel.isLeader) { + if (this.multiTabChannel.isLeader()) { return transport.getPairingRequestInfo() } @@ -960,7 +960,7 @@ export class DAppClient extends Client { this.debounceSetActiveAccount = true this._initPromise = undefined this.postMessageTransport = this.p2pTransport = this.walletConnectTransport = undefined - if (this.multiTabChannel.isLeader) { + if (this.multiTabChannel.isLeader()) { await transport.disconnect() this.openRequestsOtherTabs.clear() } else { @@ -1149,7 +1149,7 @@ export class DAppClient extends Client { private async checkMakeRequest() { const isResolved = this._transport.isResolved() const isWCInstance = isResolved && (await this.transport) instanceof WalletConnectTransport - const isLeader = this.multiTabChannel.isLeader + const isLeader = this.multiTabChannel.isLeader() return !isResolved || !isWCInstance || isLeader } diff --git a/packages/beacon-types/src/types/storage/StorageKey.ts b/packages/beacon-types/src/types/storage/StorageKey.ts index 1814ab5fb..5bf47b614 100644 --- a/packages/beacon-types/src/types/storage/StorageKey.ts +++ b/packages/beacon-types/src/types/storage/StorageKey.ts @@ -23,7 +23,6 @@ export enum StorageKey { USER_ID = 'beacon:user-id', ENABLE_METRICS = 'beacon:enable_metrics', WC_INIT_ERROR = 'beacon:wc-init-error', - BC_NEIGHBORHOOD = 'beacon:bc-neighborhood', WC_2_CORE_PAIRING = 'wc@2:core:0.3:pairing', WC_2_CLIENT_SESSION = 'wc@2:client:0.3:session', WC_2_CORE_KEYCHAIN = 'wc@2:core:0.3:keychain', diff --git a/packages/beacon-types/src/types/storage/StorageKeyReturnDefaults.ts b/packages/beacon-types/src/types/storage/StorageKeyReturnDefaults.ts index c97972e5e..946e23fb5 100644 --- a/packages/beacon-types/src/types/storage/StorageKeyReturnDefaults.ts +++ b/packages/beacon-types/src/types/storage/StorageKeyReturnDefaults.ts @@ -30,7 +30,6 @@ export const defaultValues: StorageKeyReturnDefaults = { [StorageKey.WC_2_CLIENT_SESSION]: undefined, [StorageKey.USER_ID]: undefined, [StorageKey.ENABLE_METRICS]: undefined, - [StorageKey.BC_NEIGHBORHOOD]: [], [StorageKey.WC_INIT_ERROR]: undefined, [StorageKey.WC_2_CORE_PAIRING]: undefined, [StorageKey.WC_2_CORE_KEYCHAIN]: undefined, diff --git a/packages/beacon-types/src/types/storage/StorageKeyReturnType.ts b/packages/beacon-types/src/types/storage/StorageKeyReturnType.ts index 825718554..45406dd23 100644 --- a/packages/beacon-types/src/types/storage/StorageKeyReturnType.ts +++ b/packages/beacon-types/src/types/storage/StorageKeyReturnType.ts @@ -50,7 +50,6 @@ export interface StorageKeyReturnType { [StorageKey.USER_ID]: string | undefined [StorageKey.ENABLE_METRICS]: boolean | undefined [StorageKey.WC_INIT_ERROR]: string | undefined - [StorageKey.BC_NEIGHBORHOOD]: string[] [StorageKey.WC_2_CLIENT_SESSION]: string | undefined [StorageKey.WC_2_CORE_PAIRING]: string | undefined [StorageKey.WC_2_CORE_KEYCHAIN]: string | undefined