diff --git a/packages/rempl-apollo-devtools/src/publisher/helpers/apollo-client-compat.ts b/packages/rempl-apollo-devtools/src/publisher/helpers/apollo-client-compat.ts new file mode 100644 index 000000000..afbd3a7a7 --- /dev/null +++ b/packages/rempl-apollo-devtools/src/publisher/helpers/apollo-client-compat.ts @@ -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, +): 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 = {}; + } +} diff --git a/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-operations-tracker-publisher.ts b/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-operations-tracker-publisher.ts index 511a49791..3321118b4 100644 --- a/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-operations-tracker-publisher.ts +++ b/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-operations-tracker-publisher.ts @@ -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; @@ -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, + ); + return { client: ac.client as ApolloClient, clientId: ac.clientId, - } as unknown as IApolloClientObject), + } as unknown as IApolloClientObject; + }, ); const inspector = new ApolloInspector(apolloClients); diff --git a/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-recent-activity-publisher.ts b/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-recent-activity-publisher.ts index 8badf57e3..792e7b394 100644 --- a/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-recent-activity-publisher.ts +++ b/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-recent-activity-publisher.ts @@ -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 || {}; } } diff --git a/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-tracker-publisher.ts b/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-tracker-publisher.ts index 08767a9ba..2d278f50e 100644 --- a/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-tracker-publisher.ts +++ b/packages/rempl-apollo-devtools/src/publisher/publishers/apollo-tracker-publisher.ts @@ -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 || {}; } } diff --git a/packages/rempl-apollo-devtools/src/publisher/rempl-wrapper.ts b/packages/rempl-apollo-devtools/src/publisher/rempl-wrapper.ts index 842d1481c..fb895c5f1 100644 --- a/packages/rempl-apollo-devtools/src/publisher/rempl-wrapper.ts +++ b/packages/rempl-apollo-devtools/src/publisher/rempl-wrapper.ts @@ -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 = { @@ -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;