Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { ApolloClient, NormalizedCacheObject } from "@apollo/client";

/**
* Ensures an Apollo Client instance is compatible with apollo-inspector
* by shimming internal APIs that were renamed/removed in newer versions.
*
* Apollo Client 3.8+ replaced `queryManager.fetchQueryObservable` with
* the private `queryManager.fetchConcastWithInfo` and renamed `getQuery`
* to `getOrCreateQuery`. apollo-inspector monkey-patches
* `fetchQueryObservable` to track operations, so we must:
*
* 1. Provide a `fetchQueryObservable` shim backed by `fetchConcastWithInfo`.
* 2. Wrap `fetchConcastWithInfo` so that every call routes through
* `fetchQueryObservable` — this ensures apollo-inspector's hook is
* triggered by Apollo Client's own internal calls.
* 3. Alias `getQuery` → `getOrCreateQuery` (used by apollo-inspector
* inside its `fetchQueryObservable` hook).
*/
export function ensureApolloClientCompat(
client: ApolloClient<NormalizedCacheObject>,
): void {
const qm = (client as any).queryManager;
if (!qm) return;

// Guard against repeated calls on the same client.
if (qm.__apolloCompatApplied) return;
qm.__apolloCompatApplied = true;

// 1. Alias getQuery → getOrCreateQuery (renamed in Apollo Client 3.8+).
// apollo-inspector calls queryManager.getQuery(queryId) in its hooks.
if (!qm.getQuery && typeof qm.getOrCreateQuery === "function") {
qm.getQuery = qm.getOrCreateQuery;
}

// 2. Shim fetchQueryObservable and wrap fetchConcastWithInfo.
// apollo-inspector hooks fetchQueryObservable, but Apollo Client 3.8+
// only calls fetchConcastWithInfo internally — so we must bridge them.
if (!qm.fetchQueryObservable && qm.fetchConcastWithInfo) {
const getQueryInfo =
typeof qm.getOrCreateQuery === "function"
? qm.getOrCreateQuery.bind(qm)
: typeof qm.getQuery === "function"
? qm.getQuery.bind(qm)
: undefined;

if (getQueryInfo) {
const originalFetchConcastWithInfo = qm.fetchConcastWithInfo;

// Closure variable to pass fromLink from the shim back to the
// fetchConcastWithInfo wrapper (safe: single-threaded execution).
let lastFromLink = true;

// The shim that apollo-inspector will hook into.
// It delegates to the *original* fetchConcastWithInfo.
const shimFetchQueryObservable = function fetchQueryObservableShim(
this: any,
queryId: string,
options: any,
networkStatus?: any,
) {
const queryInfo = getQueryInfo(queryId);
const result = originalFetchConcastWithInfo.call(
this,
queryInfo,
options,
networkStatus,
);
lastFromLink = result.fromLink;
return result.concast;
};
qm.fetchQueryObservable = shimFetchQueryObservable;

// Wrap fetchConcastWithInfo so that Apollo Client's internal calls
// are routed through fetchQueryObservable (which may be replaced by
// apollo-inspector's hook at tracking time).
qm.fetchConcastWithInfo = function fetchConcastWithInfoWrapper(
this: any,
queryInfo: any,
options: any,
networkStatus?: any,
query?: any,
) {
// If apollo-inspector has replaced fetchQueryObservable with its
// own hook, route through it so operation tracking triggers.
if (this.fetchQueryObservable !== shimFetchQueryObservable) {
const concast = this.fetchQueryObservable(
queryInfo.queryId,
options,
networkStatus,
);
return { concast, fromLink: lastFromLink };
}

// No hook installed — call original directly (avoids overhead).
return originalFetchConcastWithInfo.call(
this,
queryInfo,
options,
networkStatus,
query,
);
};
}
}

// 3. Ensure mutationStore exists — in Apollo Client 3.13+,
// mutationStore is only initialized when onBroadcast is provided.
// Devtools code iterates mutationStore, so it must be non-null.
if (!qm.mutationStore) {
qm.mutationStore = {};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { ClientObject, WrapperCallbackParams } from "../../types";
import { Subscription } from "rxjs";
import { ICopyData } from "apollo-inspector-ui";
import { ensureApolloClientCompat } from "../helpers/apollo-client-compat";

export class ApolloOperationsTrackerPublisher {
private remplWrapper: RemplWrapper;
Expand All @@ -34,11 +35,15 @@ export class ApolloOperationsTrackerPublisher {
this.apolloPublisher.provide("startOperationsTracker", (options: any) => {
this.trackingSubscription?.unsubscribe();
const apolloClients: IApolloClientObject[] = this.apolloClients.map(
(ac) =>
({
(ac) => {
ensureApolloClientCompat(
ac.client as ApolloClient<NormalizedCacheObject>,
);
return {
client: ac.client as ApolloClient<NormalizedCacheObject>,
clientId: ac.clientId,
} as unknown as IApolloClientObject),
} as unknown as IApolloClientObject;
},
);
const inspector = new ApolloInspector(apolloClients);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export class ApolloRecentActivityPublisher {
if (client.queryManager.mutationStore?.getStore) {
return client.queryManager.mutationStore.getStore();
} else {
// Apollo Client 3.3+
return client.queryManager.mutationStore;
// Apollo Client 3.3+ (mutationStore may be undefined in 3.13+)
return client.queryManager.mutationStore || {};
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ export class ApolloTrackerPublisher {
if (client.queryManager.mutationStore?.getStore) {
return client.queryManager.mutationStore.getStore();
} else {
// Apollo Client 3.3+
return client.queryManager.mutationStore;
// Apollo Client 3.3+ (mutationStore may be undefined in 3.13+)
return client.queryManager.mutationStore || {};
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/rempl-apollo-devtools/src/publisher/rempl-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createPublisher, getHost } from "rempl";
import hotkeys from "hotkeys-js";
import { ClientObject, WrapperCallbackParams, Publisher } from "../types";
import { ensureApolloClientCompat } from "./helpers/apollo-client-compat";

declare let __APOLLO_DEVTOOLS_SUBSCRIBER__: string;
type RemplStatusHook = {
Expand Down Expand Up @@ -94,6 +95,12 @@ export class RemplWrapper {
return;
}

// Apply compatibility shims for newer Apollo Client versions (3.8+).
// This must run before any publisher accesses client internals.
for (const clientObj of browserWindow.__APOLLO_CLIENTS__) {
ensureApolloClientCompat(clientObj.client);
}

for (const { id, callback, timeout } of this.remplStatusHooks) {
if (this.intervalExists(id)) {
return;
Expand Down
Loading