Skip to content

@proofkit/webviewer: duplicate package copies silently break fmFetch ("Callback is missing for fetchId") #283

Description

@chriscors

Summary

In a pnpm monorepo, @proofkit/webviewer can be installed as two physical copies via peer-dependency fan-out. Because the fetch-callback registry (cbs) and window.handleFmWVFetchCallback are module-level state in main.js, two copies split the registry: one copy registers the callback while the other owns the global handler. Every fmFetch/WebViewerAdapter response then logs Callback is missing for fetchId: … and the promise never resolves (eternal spinner). The failure is silent and points the developer at FileMaker/the bridge rather than the bundler.

Root cause

window.handleFmWVFetchCallback is overwritten on each module load (last loaded wins), and it closes over that copy's cbs. With ≥2 copies, the callback fired by FileMaker lands in the wrong registry. In our repo the fork came from a varlock peer mismatch (1.3.0 vs 1.4.0); next's optional @babel/core peer is a second fan-out axis.

Repro

  1. Monorepo where one workspace pins a peer (e.g. varlock) at a different version than another.
  2. App A imports @proofkit/webviewer directly; App A also consumes a workspace package that imports @proofkit/webviewer/adapter.
  3. find node_modules/.pnpm -path "*@proofkit+webviewer*/dist/esm/main.js" → more than one copy.
  4. Any fmFetchCallback is missing for fetchId, promise hangs.

Example: the only difference between the two resolved instances in our lockfile was the varlock peer:

@proofkit/webviewer@3.1.0(next@16.2.6(@babel/core@7.29.7)...(varlock@1.3.0))(react@19.2.6)
@proofkit/webviewer@3.1.0(next@16.2.6...(varlock@1.4.0))(react@19.2.6)

Suggested fixes (library side)

  • Hoist the registry + global handler to a shared realm-global (e.g. globalThis.__proofkitWV ??= { cbs: {}, … }) so all copies share one registry instead of clobbering each other.
  • And/or emit a one-time console.warn when window.handleFmWVFetchCallback is reassigned by a different module instance, to surface the dupe loudly.
  • Document the resolve.dedupe / single-funnel workaround.

Workaround (consumer side)

Funnel all webviewer access through one workspace package and add resolve.dedupe: ["@proofkit/webviewer"] to the Vite config.

Versions: @proofkit/webviewer@3.1.0, pnpm 9.12.3, Vite (single-file build), React 19.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions