1

This is related to my previous question and the response I got about using the chrome.offscreen API.

I am working to adapt the example from this repository into my React / Vite app.

I created an offscreen.html page and added it as an entry point in my vite.config.js:

import { defineConfig } from 'vite';
import { fileURLToPath } from 'url';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  optimizeDeps: {
    include: ['@supabase/supabase-js'],
  },
  build: {
    outDir: 'dist',
    rollupOptions: {
      input: {
        index: './index.html',
        offscreen: './offscreen.html',
        background: fileURLToPath(new URL('./public/background.js', import.meta.url)),
      },
    },
  },
  define: {
    'process.env': {},
  },
});

I added the permission in my manifest.json:

{
  "name": "Supabase Chrome Extension",
  "version": "0.0.1",
  "description": "",
  "manifest_version": 3,
  "author": "cpppatrick",
  "icons": {
    "16": "extension_icon16.png",
    "32": "extension_icon32.png",
    "48": "extension_icon48.png",
    "128": "extension_icon128.png"
  },
  "action": {
    "default_popup": "index.html",
    "default_title": "Open extension"
  },
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "permissions": ["tabs", "storage", "offscreen"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["contentScript.js"]
    }
  ],
  "externally_connectable": {
    "matches": ["http://127.0.0.1:5173/*", "https://127.0.0.1:5173/*"]
  }
}

I updated my background.js to create the offscreen document and send a message to it when a user signs in to my web app:

const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html';

async function sendMessageToOffscreenDocument(type, data) {
  if (!(await hasDocument())) {
    try {
      await chrome.offscreen.createDocument({
        url: OFFSCREEN_DOCUMENT_PATH,
        reasons: [chrome.offscreen.Reason.LOCAL_STORAGE],
        justification: 'Use localStorage',
      });
    } catch (error) {
      console.error(error);
    }
  }

  setTimeout(() => {
    console.log('in setTimeout', type, data);
    chrome.runtime.sendMessage({
      type,
      target: 'offscreen',
      data,
    });
  }, 2000);
}

async function closeOffscreenDocument() {
  if (!(await hasDocument())) {
    return;
  }
  await chrome.offscreen.closeDocument();
}

async function hasDocument() {
  const matchedClients = await clients.matchAll();
  for (const client of matchedClients) {
    if (client.url.endsWith(OFFSCREEN_DOCUMENT_PATH)) {
      return true;
    }
  }
  return false;
}

chrome.runtime.onMessageExternal.addListener(async ({ message, session }) => {
  if (message === 'SIGN_IN') {
    sendMessageToOffscreenDocument('setUserSession', { message, session });
  } else if (message === 'SIGNED_OUT') {
    console.log('Handle sign out');
  }
});

I created an offscreen.js file that is loaded by the offscreen.html page:

import { createClient } from '../node_modules/@supabase/supabase-js';

// This console.log never runs.
console.log('offscreen is loaded');

chrome.runtime.onMessage.addListener(handleMessages);

async function handleMessages(message) {
  console.log('handleMessages message', message);
  // Return early if this message isn't meant for the offscreen document.
  if (message.target !== 'offscreen') {
    return false;
  }

  // Dispatch the message to an appropriate handler.
  switch (message.type) {
    case 'setUserSession':
      setUserSession(message.data);
      break;
    default:
      console.warn(`Unexpected message type received: '${message.type}'.`);
      return false;
  }
}

async function setUserSession(sessionData) {
  console.log('sessionData', sessionData);
}

const options = {
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true,
    storage: {
      async getItem(key) {
        const storage = await chrome.storage.local.get(key);
        return storage?.[key];
      },
      async setItem(key, value) {
        await chrome.storage.local.set({
          [key]: JSON.parse(value),
        });
      },
      async removeItem(key) {
        await chrome.storage.local.remove(key);
      },
    },
  },
};

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY,
  options
);

I get the following error when I send a message to my extension after logging into my web app:

error message

Uncaught (in promise) Error: Could not establish connection. Receiving end does not exist.

I don't know how to proceed with debugging this error. I can see that the offscreen document is being created, I have it logged in the above image.

In the extension details page, I see that there are two different views. But when I click on offscreen.html, it takes me to the background.js Service Worker inspector, and there is no other option in the dropdown menu on Chrome DevTools. enter image description here

Is there something wrong in my setup that is causing the connection to not be made? I've been researching other SO answers related to this error message but haven't found anything that solves my problem.

Update: I tried to rework my code to match the example here, since the code is different from what is in the GitHub repo, and I'm still getting the same error.

cpppatrick
  • 609
  • 3
  • 12
  • 29
  • Maybe createDocument is bugged and returns before the document is created *and loaded*. Try adding a setTimeout before sendMessage. – wOxxOm Jul 06 '23 at 05:34
  • @wOxxOm I gave that a shot and unfortunately I'm still getting the same error. I updated my `background.js` file to reflect the setTimeout addition. Are there any other paths of exploration that you might be able to suggest? – cpppatrick Jul 07 '23 at 03:17
  • 1) Use navigator.serviceWorker + self.onmessage instead of chrome.runtime messaging. 2) If you ever fix the original problem note that you can't use `async` listener for onMessage, [more info](/a/53024910). – wOxxOm Jul 07 '23 at 07:16
  • @wOxxOm Thank you for all of your help so far! I have a clarifying question. In my `offscreen.js` file, I can use `self.onmessage.addListener(handleMessage)`. But I'm having trouble figuring out how to send the messages to the offscreen document. Because I don't see any instance methods for sending messages on the `ServiceWorkerContainer` on MDN. I see a `client.postMessage` but that doesn't seem applicable here. – cpppatrick Jul 09 '23 at 04:44
  • You can modify hasDocument as to return `client` instead of `true` and then use client.postMessage, but it'd be probably simpler to do the other way around: the service worker uses self.onmessage, the document uses navigator.serviceWorker. – wOxxOm Jul 09 '23 at 06:04

0 Answers0