Skip to content

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

@chriscors

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions