quasarframework / quasar

Quasar Framework - Build high-performance VueJS user interfaces in record time
https://quasar.dev
MIT License
26.02k stars 3.54k forks source link

[BEX] Events triggered in background from within another event's handler never resolve. #16081

Open esindger opened 1 year ago

esindger commented 1 year ago

What happened?

I am trying to dispatch an event test to my-content-script from a popup, using background as a proxy, and sending the event test.bg to it.

async function emitBexEvent () {
  const res = await $q.bex.send('test.bg', {
    foo: 'bar'
  })

  // NEVER REACHED
  console.log('[index-page] test.bg RESOLVED', res)
}

https://github.com/esindger/quasar-bex-bridge/blob/4681cffab581194baeb201964a5b5d2aa54006f7/src/pages/IndexPage.vue#L47-L56

The problem lies in that if I call bridge.send('test') from the handler of another event (e.g., test.bg), the promise never gets resolved. The respond() function does not seem to work.

export default bexBackground((bridge) => {
  bridge.on('test.bg', async ({
    data,
    eventResponseKey,
    respond
  }) => {
    const res = await bridge.send('test', data)

    // NEVER REACHED
    console.log('[background] test RESOLVED', res)
    respond()
  })
})

https://github.com/esindger/quasar-bex-bridge/blob/4681cffab581194baeb201964a5b5d2aa54006f7/src-bex/background.ts#L32-L45

my-content-script.ts

  bridge.on('test', ({
    // data,
    eventResponseKey,
    respond
  }) => {
    console.log(`[my-content-script] ${eventResponseKey}, eventNames: ${JSON.stringify(bridge.eventNames())}`)
    respond()
  })

https://github.com/esindger/quasar-bex-bridge/blob/65a94c9c4c1e4bdee30d6cca343c5dcc7d7852c0/src-bex/my-content-script.ts#L17-L25

Could you please help in identifying the root cause of this issue and provide a suitable fix or workaround?

What did you expect to happen?

I expect that after the call to respond(), the promise will resolve.

Reproduction URL

https://github.com/esindger/quasar-bex-bridge

How to reproduce?

  1. Clone the repository
  2. Install the dependencies
  3. Run the application in BEX mode
  4. Install the browser extension
  5. Open the popup and click anywhere

Flavour

Quasar CLI with Vite (@quasar/cli | @quasar/app-vite)

Areas

BEX Mode

Platforms/Browsers

Chrome

Quasar info output

Operating System - Darwin(22.1.0) - darwin/arm64
NodeJs - 18.13.0

Global packages
  NPM - 8.19.3
  yarn - Not installed
  @quasar/cli - undefined
  @quasar/icongenie - Not installed
  cordova - Not installed

Important local packages
  quasar - 2.12.2 -- Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
  @quasar/app-vite - 1.4.3 -- Quasar Framework App CLI with Vite
  @quasar/extras - 1.16.5 -- Quasar Framework fonts, icons and animations
  eslint-plugin-quasar - Not installed
  vue - 3.3.4 -- The progressive JavaScript framework for building modern web UI.
  vue-router - 4.2.4
  pinia - 2.1.4 -- Intuitive, type safe and flexible Store for Vue
  vuex - Not installed
  vite - 2.9.16 -- Native-ESM powered web dev build tool
  eslint - 8.45.0 -- An AST-based pattern checker for JavaScript.
  electron - Not installed
  electron-packager - Not installed
  electron-builder - Not installed
  register-service-worker - 1.7.2 -- Script for registering service worker, with hooks
  @capacitor/core - Not installed
  @capacitor/cli - Not installed
  @capacitor/android - Not installed
  @capacitor/ios - Not installed

Quasar App Extensions
  *None installed*

Networking
  Host - me
  en0 - 192.168.0.101

Relevant log output

No response

Additional context

No response

esindger commented 1 year ago

Upon thorough analysis, I have determined that the root cause of this issue lies within the behavior of the Bridge in Quasar BEX context. Bridge creates a new instance for each new connection, and each Bridge instance operates independently from the others.

This implies that an event handler I've set for test in one Bridge instance is not visible to other Bridge instances. As a result, when I'm sending the test event from the test.bg event handler, the new Bridge instance that gets created for this connection does not have a handler for the test event. This means that the promise that I expect to get resolved upon calling respond() never gets resolved, as the test event handler in the new Bridge instance is missing.

What is the recommended solution?

Danny2462 commented 1 year ago

I ran into the same situation. Responses seem to not work, and I'm not sure they are supposed to?

The only way I could get data back to my popup from a content script, is by having another set of proxy events for the "upstream": on event, content script send -> background script listen & re-send -> listener in popup

This caused every active content script to emit such an event, because obviously, there's got to be multicasting when sending "downstream" from one background script to multiple tabs' content scripts.

And I'm not sure how respond functions would handle this, since they resolve single Promises, so I guess currently they just don't do anything and that's what we're encountering?

Logically, responding to a multicast event would necessitate either a callback function or an Observable stream from the sender's side to handle multiple async value pushes properly, not a Promise which is only viable for single async value push scenarios, so it seems a different bridge API would be needed for content-background script communication to not be misleading?

For me, the documentation implied that a "downstream" proxy would be enough, nothing suggested that responses would not work, nor did the API of the bridge imply that responses are sometimes unusable. They don't even get into a race condition, they just never resolve.

At least a note on this in the docs would've saved me quite a bit of fiddling around, unless I missed that / this is actually a bug somehow?

Flamenco commented 4 months ago

The source of this issue is that the response is emitted on the content script's bridge, but the response callback is registered on the background script's bridge. The bridges are not ...bridged.

This actually results in a leak as well. The uncalled callbacks pile up.

There is also another issue that if multiple content scripts are active, only one of them will end up resolving the promise, although all of them will be invoked. This is because there is a one-to-many relationship between the background script and the content-scripts.

I think a good solution may be for the allActiveConnections parameter to expose the other active bridges. Then the client could choose the bridge to dispatch to. This would resolve the multiple content-script issue while also ensuring the callback was invoked.

export default bexBackground((bridge, allActiveConnections) => {
  bridge.on('go', ({data, respond}) => {
    const contentBridge = allActiveConnections.findBridge(someCondition)
    contentBridge?.sendOnlyTo('go', data).then(it=> respond(it,data); 
  });
danhumphrey commented 4 months ago

I've also just come across this issue... I added event listeners (eg. chrome.contextMenus.onClicked) within the bexBackground function so that I could communicate with the rest of the extension, however, my event listeners are being called twice.

It seems that bexBackground and the bridge are not reliable and that I may have to resort to native messaging instead, unless anyone has a solution?