Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No handler registered in 'background' error, but background handler still being found and invoked #69

Open
VnUgE opened this issue Oct 16, 2023 · 3 comments

Comments

@VnUgE
Copy link

VnUgE commented Oct 16, 2023

So, I recently ran into a fun new issue I am having a hard time debugging. I recently switched up from using "hard-coded" message handlers to generating them dynamically at runtime. Not a single issue passing messages until that change. (Ill explain)

webex-err-image

The background onMessage handler for that messageId is correctly registered and is actually being called despite that error in the foreground code.

Error conditions

  • Only If one or more contexts are open such as options + popup (works fine if only one is open)
  • Only if an onMessage handler actually yields asynchronously such as network request or setTimeout in a promise
  • It happens if the promise is accepted or rejected.
  • The last context to open fails; if the options page is open, async popup requests will fail

I was having issues tracing with the debugger from the background side (couldn't repro), however I could trace with console messages to determine that the functions were still getting called despite the error and failure. However the sendMessage console error appears after the method is called, but before the promise resolves.

Example Source Code

import { runtime } from "webextension-polyfill";
import { onMessage, sendMessage } from 'webext-bridge/background'
import { BridgeMessage, RuntimeContext, isInternalEndpoint } from "webext-bridge";
import { JsonObject } from "type-fest";
import { cloneDeep } from "lodash";
import { debugLog } from "@vnuge/vnlib.browser";
export interface BgRuntime<T> {
    readonly state: T;
    readonly onInstalled: (callback: () => Promise<void>) => void;
    readonly onConnected: (callback: () => Promise<void>) => void;
}
export type ApiExport = {
    [key: string]: Function
};
export type IFeatureApi<T> = (bgApi?: BgRuntime<T>) => ApiExport
export type SendMessageHandler = <T extends JsonObject | JsonObject[]>(action: string, data: any) => Promise<T>
export type VarArgsFunction<T> = (...args: any[]) => T
export interface IFeatureExport<TState, TFeature extends ApiExport> {
    background: (bgApi: BgRuntime<TState>) => TFeature
    foreground: () => TFeature
}
export interface IForegroundUnwrapper {
    use: <T extends ApiExport>(api:() => IFeatureExport<any, T>) => T
}
export interface IBackgroundWrapper<TState> {
    register: <T extends ApiExport>(features: (() => IFeatureExport<TState, T>)[]) => void
}
export const useBackgroundFeatures = <TState>(state: TState): IBackgroundWrapper<TState> => {
    const rt = { state,
        onConnected: runtime.onConnect.addListener,
        onInstalled: runtime.onInstalled.addListener,
    }   as BgRuntime<TState>
    return{
        register: <TFeature extends ApiExport>(features:(() => IFeatureExport<TState, TFeature>)[]) => {
            for (const feature of features) {
                const f = feature().background(rt)
                for (const externFuncName in f) {
                    const func = f[externFuncName] as Function
                    const onMessageFuncName = `${feature.name}-${externFuncName}`
                    onMessage(onMessageFuncName, async (msg: BridgeMessage<any>) => {
                        try {
                            if (!isInternalEndpoint(msg.sender)) {
                                throw new Error(`Unauthorized external call to ${onMessageFuncName}`)
                            }
                            const result = func(...msg.data)
                            // ---> Foreground error is raised here if pending promise is awaited
                            const data = await result;
                            return { ...data }
                        }
                        catch (e: any) {
                            return { bridgeMessageException: JSON.stringify(e),}
                        }
                    });
  }}}}
}
export const useForegoundFeatures = (sendMessage: SendMessageHandler): IForegroundUnwrapper => {
    return{
        use: <T extends ApiExport>(feature:() => IFeatureExport<any, T>): T => {
            const api = feature().foreground()
            const proxied : T = {} as T
            for(const funcName in api){
                //Create proxy for each method
                proxied[funcName] = (async (...args:any) => {
                    const result = await sendMessage(`${feature.name}-${funcName}`, cloneDeep(args)) as any
                    if(result.bridgeMessageException){
                        const err = JSON.parse(result.bridgeMessageException)
                        if(result.errResponse){
                            err.response = JSON.parse(result.errResponse)
                        }
                        throw err
                    }
                    return result;
                }) as any
            }
            return proxied;
   }}
}
const exampleFeature = () : IFeatureExport<any, {exampleMethod:() => Promise<void>}> => ({
    foreground: () => ({
        exampleMethod: () => Promise.resolve() //stub method never actually called 
    }),
    background: (state:any) => ({
        exampleMethod: () => new Promise((resolve) => setTimeout(resolve, 0)) //actual background method called
    })
})
//In background main.ts
const { register } = useBackgroundFeatures({})
register([ exampleFeature ])

//In foreground
const { use } = useForegoundFeatures(sendMessage)
const { exampleMethod } = use(exampleFeature)
await exampleMethod() //Mapped directly to background handler

Each script, popup/options/conent-script pass the correct sendMessage function to the useForegoundFeatures method. The senMessage function uses the default context argument (tried explicitly setting 'background' and doesnt change).

Extra steps I have taken

  • Confirmed that the offending methods are correctly returning pending promises
  • Confirmed the issue still exists after a release build (not just during debugging)
  • Tried loading in FireFox on another machine, not using the debug remote-control
  • Rewrite using promises instead of async/await syntax
  • Confirmed no other messages are being handled when this happens

After debugging, it seems fairly obvious that a promise is not being properly awaited, I just need to figure out where, and why it would cause that type of exception. Big apologies if this a bonehead mistake, I have just been pulling my hair out all weekend and was hoping someone might be able to help. I have not pushed these latest changes to my repo yet (I wanted to debug first) but I can create a buggy branch if it would help seeing the entire project.

@DonTsipa
Copy link

DonTsipa commented Oct 18, 2023

Hey, just ran into this issue myself, it seems like in bundle there are both .js and .cjs files from this package. It can be a problem because of webpack / babel configuration, that forces to use commonjs instead of es6 modules.
I added "sideEffects": false to my package.json and it worked

But sideEffects can be tricky
By default Typescript uses CommonJs module resolution, so you can also set

"module": "ES2020",
 "target":"ES2020",
 "moduleResolution": "Bundler",

in your tsconfig.ts. It also will force to use only es6 module resolution
Related issue on ts-loader:
TypeStrong/ts-loader#886 (comment)

@VnUgE
Copy link
Author

VnUgE commented Oct 25, 2023

Thank you for your response, I have been waiting to get some time to learn and play around. Tonight I have spent a few hours playing around with my tsconfig and Vite bundler settings, and nothing has changed unfortunately. I also tested setting "sideEffects":"false" in my package.json file. I always use "type":"module"

Here are my latest configurations maybe you might have some more insight

{
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "compilerOptions": {
    "target": "es2020",
    "useDefineForClassFields": true,
    "module": "es2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  }
}

And my vite.config.js build settings (I removed the server configs for brevity) I just added the build target value to vite for testing, but did not originally set it.

  plugins: [ vue() ],
  build: {
    cssCodeSplit: true,
    rollupOptions: {
      plugins: [],
    },
    target: "es2020",
  },
  optimizeDeps: {
    exclude: ['']
  },
  css: {
    postcss: postcss
  },

@VnUgE
Copy link
Author

VnUgE commented Nov 4, 2023

So I have been messing around a bit more. I downloaded the latest master branch into my local source tree and built it locally.

I was able to do some more tracing.

Only when tracing, I can catch the following exceptions, and see the promise being rejected with the screenshot below. Without the debugger attached, the promises returned from sendMessage() never actually complete (resolve or reject). This type of JS behavior is new to me. I have to assume this is a bundler or browser related issue. I haven't found a silver bullet bundler/tsconfig setting yet. I removed all intervals/timers that may have been blocking the event loop somehow, no change.

trace message

Error message

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants