Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/zotero/zotero-connectors/llms.txt

Use this file to discover all available pages before exploring further.

The background process is the persistent engine of the Zotero Connector. Unlike injected content scripts, which live and die with the tab they are injected into, the background context outlives any individual page and maintains shared state: the translator cache, per-tab translation results, connector preferences, and the online/offline status of the Zotero desktop application. It acts as the middle-layer between the translation framework running in inject scripts and either the Zotero desktop HTTP server or the zotero.org cloud API.

MV2 vs MV3: Event Page vs Service Worker

In Manifest V2 the background context is a persistent event page loaded from a list of scripts. The backgroundIncludeBrowserExt array in gulpfile.js (plus background.js itself) defines which scripts are concatenated. The page persists for the lifetime of the browser session and has full access to DOM APIs.
In Manifest V3 the background becomes a service worker, which Chrome can terminate at any time to save memory. The entry point is src/browserExt/background-worker.js:
// This is the Chrome MV3 background worker entrypoint script
try {
  var scriptsToImport = [
    /*BACKGROUND SCRIPTS*/,
    "keep-mv3-alive.js",
    "background.js"
  ];

  for (let script of scriptsToImport) {
    self.importScripts('./' + script);
  }
} catch (e) {
  console.error(e);
}
The /*BACKGROUND SCRIPTS*/ placeholder is filled by the build pipeline with the same backgroundIncludeBrowserExt script list. keep-mv3-alive.js and background.js are appended after the shared scripts.
Service workers do not have a window object and cannot use browser APIs that require a live DOM (e.g. DOMParser). Those operations are delegated to an offscreen document instead — see the Offscreen Document section below.

What the Background Process Does

Translator Cache (Zotero.Translators)

Zotero.Translators (src/common/translators.js) loads translator metadata from extension preferences on startup and maintains four in-memory caches keyed by type (import, export, web, search):
this.init = async function() {
  if (_initializedPromise) return _initializedPromise;
  _cache = {"import":[], "export":[], "web":[], "search":[]};
  _translators = {};
  _initializedPromise = new Promise(async (resolve, reject) => {
    try {
      let translators = Zotero.Prefs.get("translatorMetadata");
      // No stored translators — fetch everything from the repo
      if (typeof translators !== "object" || !translators.length) {
        Zotero.debug(`Translators: First time launch, getting all translators.`);
        await this.updateFromRemote(true);
      }
      else {
        this._load(translators);
      }
      this.keepTranslatorsUpdated();
      resolve();
    }
    catch (e) { /* … */ }
  });
}
keepTranslatorsUpdated() runs a recursive timer loop that calls updateFromRemote() every 24 hours (ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL). Metadata is first requested from the Zotero desktop client; if unavailable, it falls back to the zotero.org translator repository.

URL-Match Translator Detection

When a new page URL is reported (Zotero.Translators.getWebTranslatorsForLocation(url, rootURL)), the background iterates over the web cache and tests each translator’s target regexp against the URL. Only translators that match are returned to the injected script for the heavier detectWeb() phase.

Extension Toolbar Icon & Badge

Zotero.Connector_Browser._updateExtensionUI(tab) in src/browserExt/background.js calls browser.action.setIcon() and browser.action.setTitle() to reflect the current tab state:
ConditionIcon shown
Translators foundItem-type icon (e.g. attachment-pdf.png, treeitem-journalArticle.png)
PDF in frame, no translatorimages/pdf.png
No translatorGray webpage icon (treeitem-webpage-gray.png)
Disabled URL (file://, extension page)Zotero-Z icon, button disabled

Connector Preferences (Zotero.Prefs)

Zotero.Prefs wraps browser.storage to provide synchronous get/set and asynchronous getAsync access to extension preferences. Preferences include translatorMetadata, firstUse, automaticSnapshots, and per-translator code caches.

Routing Translated Items

When a save is triggered, Zotero.Connector.callMethod(endpoint, data) (src/common/connector.js) POSTs to http://127.0.0.1:23119/connector/<endpoint>. If the request fails with HTTP status 0 (connection refused), isOnline is set to false and Zotero.Connector.onStateChange(false) fires, which disables Zotero.ContentTypeHandler and causes subsequent saves to fall back to the zotero.org API.

Background Script Bundle

The full background bundle (from gulpfile.js) is:
var backgroundInclude = [
  'zotero_config.js',
  'zotero.js',
  'i18n.js',
  'translate/promise.js',
  // … utilities …
  'prefs.js',
  'api.js',
  'http.js',
  'oauthsimple.js',
  'proxy.js',
  'connector.js',
  'updaterFix.js',
  'repo.js',
  'translate/debug.js',
  'translate/tlds.js',
  'translate/translator.js',
  'itemSaver_background.js',
  'translators.js',
  'cachedTypes.js',
  'errors_webkit.js',
  'zotero-google-docs-integration/api.js',
  'messages.js',
  'messaging.js',
];

var backgroundIncludeBrowserExt = ['browser-polyfill.js'].concat(backgroundInclude, [
  'webRequestIntercept.js',
  'contentTypeHandler.js',
  'saveWithoutProgressWindow.js',
  'messagingGeneric.js',
  'browserAttachmentMonitor/browserAttachmentMonitor.js',
  'offscreen/offscreenFunctionOverrides.js',
  'background/offscreenManager.js',
]);

Offscreen Document (MV3 Only)

Chrome MV3 service workers lack a DOM, so any code that needs DOMParser (used by translators) must run in an offscreen document — a hidden, tab-less HTML page that Chrome allows a service worker to create. Zotero.OffscreenManager (src/browserExt/background/offscreenManager.js) owns the lifecycle:
Zotero.OffscreenManager = {
  offscreenPageInitialized: false,
  messagingDeferred: Zotero.Promise.defer(),
  offscreenUrl: 'offscreen/offscreen.html',

  async init() {
    const offscreenPage = await this.getOffscreenPage();
    if (!offscreenPage) {
      // Make sure we're waiting for a new deferred
      this.messagingDeferred = Zotero.Promise.defer();
      // Create offscreen document
      await browser.offscreen.createDocument({
        url: this.offscreenUrl,
        reasons: ['DOM_PARSER'],
        justification: 'Scraping the document with Zotero Translators',
      });
    }
    else {
      // Service worker restarted; notify offscreen page to reinit messaging
      offscreenPage.postMessage('service-worker-restarted');
    }
    await this.messagingDeferred.promise;
    // …
  },
  // …
};
The offscreen page (offscreen/offscreen.html + offscreen/offscreen.js) hosts the full translation sandbox. When a translation is requested, the background’s OffscreenManager.sendMessage(message, payload, tab, frameId) forwards the work to the offscreen page via MessagingGeneric, receives the result, and relays it back to the inject script.
The offscreen permission must be declared in manifest-v3.json to use browser.offscreen.createDocument(). The offscreen document is subject to Chrome’s idle-cleanup policy and may be destroyed; offscreenManager handles re-creation transparently.

Keeping the MV3 Service Worker Alive

Chrome terminates idle service workers after roughly 30 seconds. src/browserExt/keep-mv3-alive.js runs a polling loop to prevent this during active operations:
const LET_DIE_AFTER = 10*60e3; // 1 hour (comment in source; actual value is 10 minutes)

const startedOn = Date.now();
let interval;

function keepAlive() {
  if (startedOn + LET_DIE_AFTER < Date.now()
      && !Zotero.Connector_Browser.shouldKeepServiceWorkerAlive()) {
    clearInterval(interval);
  }
  chrome.runtime.getPlatformInfo();
}
interval = setInterval(keepAlive, 20e3);
Zotero.Connector_Browser.shouldKeepServiceWorkerAlive() returns truthy whenever long-running tasks are active (e.g. a Google Docs integration HTTP round-trip). The internal counter _keepServiceWorkerAlive is mutated by a reference-counted setter:
this._keepServiceWorkerAlive = 0;

this.shouldKeepServiceWorkerAlive = () => this._keepServiceWorkerAlive;
// Parallel async functions may call this, so we use a counter to make sure
// one keep-alive function finishing does not kill the service worker for other
// still-running functions
this.setKeepServiceWorkerAlive = (val) => this._keepServiceWorkerAlive += val ? 1 : -1;
Without keep-mv3-alive.js a service-worker termination mid-translation would drop the entire session and require the user to restart Zotero.

Message Listener Registration

Zotero.Messaging.init() in src/common/messaging.js registers the central browser.runtime.onMessage listener that handles all inject-to-background RPC calls:
this.init = function() {
  if (Zotero.isBrowserExt) {
    browser.runtime.onMessage.addListener(function(request, sender) {
      // All Zotero messages are arrays — ignore SingleFile objects, etc.
      if (!Array.isArray(request)) {
        return;
      }

      return Zotero.Messaging.receiveMessage(
        request[0],   // message name  e.g. "Translators.getWebTranslatorsForLocation"
        request[1],   // args array
        sender.tab,
        sender.frameId
      )
      .catch(function(err) {
        err = JSON.stringify(Object.assign({
          name: err.name,
          message: err.message,
          stack: err.stack
        }, err));
        return ['error', err];
      });
    });
  }
  Zotero.Messaging.initialized = true;
};
receiveMessage() splits the message name on ".", resolves Zotero[NAMESPACE][METHOD], applies any configured background.postReceive / background.preSend hooks from MESSAGES, and returns the result to the inject side.

Background Initialization Sequence

When Zotero.initGlobal() is called at the bottom of background.js:
1

Core namespaces initialized

Zotero.Prefs, Zotero.HTTP, Zotero.Connector, and other shared modules are set up.
2

Messaging initialized

Zotero.Messaging.init() registers the browser.runtime.onMessage listener.
3

Translators initialized

Zotero.Translators.init() loads cached metadata from preferences or fetches from the remote repo.
4

Offscreen document created (MV3/Chromium only)

Zotero.Connector_Browser.init() calls Zotero.OffscreenManager.init(), which creates the offscreen document and waits for it to signal readiness.
5

Tab info persistence (MV3)

_tabInfo is hydrated from Zotero.Utilities.Connector.createMV3PersistentObject('tabInfo', …) so per-tab translator state survives service-worker restarts.
6

Web navigation listeners attached

browser.webNavigation.onCommitted, onDOMContentLoaded, and onHistoryStateUpdated are registered to track page navigations and trigger translator detection.

Build docs developers (and LLMs) love