diff --git a/packages/extension-base/src/background/handlers/State.ts b/packages/extension-base/src/background/handlers/State.ts index 74ee6de3..3fc5dc38 100644 --- a/packages/extension-base/src/background/handlers/State.ts +++ b/packages/extension-base/src/background/handlers/State.ts @@ -247,7 +247,7 @@ export default class State { public rpcSend (request: RequestRpcSend, port: chrome.runtime.Port): Promise { const provider = this.#injectedProviders.get(port); - assert(provider, 'Cannot call pub(rpc.subscribe) before provider has been set'); + assert(provider, 'Cannot call pub(rpc.subscribe) before provider is set'); return provider.send(request.method, request.params); } @@ -280,15 +280,25 @@ export default class State { public rpcSubscribe ({ method, params, type }: RequestRpcSubscribe, cb: ProviderInterfaceCallback, port: chrome.runtime.Port): Promise { const provider = this.#injectedProviders.get(port); - assert(provider, 'Cannot call pub(rpc.subscribe) before provider has been set'); + assert(provider, 'Cannot call pub(rpc.subscribe) before provider is set'); return provider.subscribe(type, method, params, cb); } + public rpcSubscribeConnected (_request: null, cb: ProviderInterfaceCallback, port: chrome.runtime.Port): void { + const provider = this.#injectedProviders.get(port); + + assert(provider, 'Cannot call pub(rpc.subscribeConnected) before provider is set'); + + cb(null, provider.isConnected()); // Immediately send back current isConnected + provider.on('connected', () => cb(null, true)); + provider.on('disconnected', () => cb(null, false)); + } + public rpcUnsubscribe (request: RequestRpcUnsubscribe, port: chrome.runtime.Port): Promise { const provider = this.#injectedProviders.get(port); - assert(provider, 'Cannot call pub(rpc.unsubscribe) before provider has been set'); + assert(provider, 'Cannot call pub(rpc.unsubscribe) before provider is set'); return provider.unsubscribe(request.type, request.method, request.subscriptionId); } diff --git a/packages/extension-base/src/background/handlers/Tabs.ts b/packages/extension-base/src/background/handlers/Tabs.ts index 53a3fb76..e298464b 100644 --- a/packages/extension-base/src/background/handlers/Tabs.ts +++ b/packages/extension-base/src/background/handlers/Tabs.ts @@ -98,6 +98,18 @@ export default class Tabs { return true; } + private rpcSubscribeConnected (request: null, id: string, port: chrome.runtime.Port): Promise { + const innerCb = createSubscription<'pub(rpc.subscribeConnected)'>(id, port); + const cb = (_error: Error | null, data: SubscriptionMessageTypes['pub(rpc.subscribeConnected)']): void => innerCb(data); + this.#state.rpcSubscribeConnected(request, cb, port); + + port.onDisconnect.addListener((): void => { + unsubscribe(id); + }); + + return Promise.resolve(true); + } + private async rpcUnsubscribe (request: RequestRpcUnsubscribe, port: chrome.runtime.Port): Promise { return this.#state.rpcUnsubscribe(request, port); } @@ -135,6 +147,9 @@ export default class Tabs { case 'pub(rpc.subscribe)': return this.rpcSubscribe(request as RequestRpcSubscribe, id, port); + case 'pub(rpc.subscribeConnected)': + return this.rpcSubscribeConnected(request as null, id, port); + case 'pub(rpc.unsubscribe)': return this.rpcUnsubscribe(request as RequestRpcUnsubscribe, port); diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index 4a934997..77e56f40 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -72,6 +72,7 @@ export interface RequestSignatures { 'pub(rpc.send)': [RequestRpcSend, JsonRpcResponse]; 'pub(rpc.startProvider)': [string, ProviderMeta]; 'pub(rpc.subscribe)': [RequestRpcSubscribe, number, JsonRpcResponse]; + 'pub(rpc.subscribeConnected)': [null, boolean, boolean]; 'pub(rpc.unsubscribe)': [RequestRpcUnsubscribe, boolean]; } diff --git a/packages/extension-base/src/page/PostMessageProvider.ts b/packages/extension-base/src/page/PostMessageProvider.ts index 8f129c35..7dffbf9a 100644 --- a/packages/extension-base/src/page/PostMessageProvider.ts +++ b/packages/extension-base/src/page/PostMessageProvider.ts @@ -29,6 +29,9 @@ interface SubscriptionHandler { export default class PostMessageProvider implements InjectedProvider { readonly #eventemitter: EventEmitter; + // Whether or not the actual extension background provider is connected + #isConnected = false + readonly #sendRequest: SendRequest; // Subscription IDs are (historically) not guaranteed to be globally unique; @@ -43,11 +46,6 @@ export default class PostMessageProvider implements InjectedProvider { public constructor (sendRequest: SendRequest) { this.#eventemitter = new EventEmitter(); this.#sendRequest = sendRequest; - - // Give subscribers time to subscribe - setTimeout((): void => { - this.#eventemitter.emit('connected'); - }); } /** @@ -78,8 +76,7 @@ export default class PostMessageProvider implements InjectedProvider { * @return {boolean} true if connected */ public isConnected (): boolean { - // FIXME This should see if the extension's state's provider is connected - return true; + return this.#isConnected; } public listProviders (): Promise { @@ -121,8 +118,29 @@ export default class PostMessageProvider implements InjectedProvider { } } - public startProvider (key: string): Promise { - return this.#sendRequest('pub(rpc.startProvider)', key); + /** + * @summary Spawn a provider on the extension background. + */ + public async startProvider (key: string): Promise { + // Disconnect from the previous provider + this.#isConnected = false; + this.#eventemitter.emit('disconnected'); + + const meta = await this.#sendRequest('pub(rpc.startProvider)', key); + + this.#sendRequest('pub(rpc.subscribeConnected)', null, (connected) => { + this.#isConnected = connected; + + if (connected) { + this.#eventemitter.emit('connected'); + } else { + this.#eventemitter.emit('disconnected'); + } + + return true; + }); + + return meta; } // eslint-disable-next-line @typescript-eslint/no-explicit-any