diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index cf834a2c34..c36e5dd1fc 100755 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -22,7 +22,7 @@ import { import { composeExchanges, defaultExchanges, - fallbackExchangeIO, + fallbackExchange, } from './exchanges'; import { @@ -147,7 +147,7 @@ export class Client { this.results$ = share( this.exchange({ client: this, - forward: fallbackExchangeIO, + forward: fallbackExchange({ client: this }), })(this.operations$) ); diff --git a/packages/core/src/exchanges/fallback.test.ts b/packages/core/src/exchanges/fallback.test.ts index 6819729651..79a54d4d58 100644 --- a/packages/core/src/exchanges/fallback.test.ts +++ b/packages/core/src/exchanges/fallback.test.ts @@ -1,9 +1,15 @@ import { forEach, fromValue, pipe } from 'wonka'; import { queryOperation, teardownOperation } from '../test-utils'; -import { fallbackExchangeIO } from './fallback'; +import { fallbackExchange } from './fallback'; const consoleWarn = console.warn; +const client = { + debugTarget: { + dispatchEvent: jest.fn(), + }, +} as any; + beforeEach(() => { console.warn = jest.fn(); }); @@ -16,7 +22,7 @@ it('filters all results and warns about input', () => { const res: any[] = []; pipe( - fallbackExchangeIO(fromValue(queryOperation)), + fallbackExchange({ client })(fromValue(queryOperation)), forEach(x => res.push(x)) ); @@ -28,7 +34,7 @@ it('filters all results and warns about input', () => { const res: any[] = []; pipe( - fallbackExchangeIO(fromValue(teardownOperation)), + fallbackExchange({ client })(fromValue(teardownOperation)), forEach(x => res.push(x)) ); diff --git a/packages/core/src/exchanges/fallback.ts b/packages/core/src/exchanges/fallback.ts index 7aa790cd5d..ca15daa679 100644 --- a/packages/core/src/exchanges/fallback.ts +++ b/packages/core/src/exchanges/fallback.ts @@ -1,18 +1,25 @@ import { filter, pipe, tap } from 'wonka'; -import { ExchangeIO, Operation } from '../types'; +import { Operation, ExchangeIO } from '../types'; /** This is always the last exchange in the chain; No operation should ever reach it */ -export const fallbackExchangeIO: ExchangeIO = ops$ => +export const fallbackExchange: ({ client: Client }) => ExchangeIO = ({ + client, +}) => ops$ => pipe( ops$, - tap(({ operationName }) => { + tap(operation => { if ( - operationName !== 'teardown' && + operation.operationName !== 'teardown' && process.env.NODE_ENV !== 'production' ) { - console.warn( - `No exchange has handled operations of type "${operationName}". Check whether you've added an exchange responsible for these operations.` - ); + const message = `No exchange has handled operations of type "${operation.operationName}". Check whether you've added an exchange responsible for these operations.`; + + client.debugTarget!.dispatchEvent({ + type: 'fallbackCatch', + message, + operation, + }); + console.warn(message); } }), /* All operations that skipped through the entire exchange chain should be filtered from the output */ diff --git a/packages/core/src/exchanges/fetch.test.ts b/packages/core/src/exchanges/fetch.test.ts index 2eac8215d3..768be2e94f 100755 --- a/packages/core/src/exchanges/fetch.test.ts +++ b/packages/core/src/exchanges/fetch.test.ts @@ -40,7 +40,11 @@ const response = { const exchangeArgs = { forward: () => empty as Source, - client: {} as Client, + client: ({ + debugTarget: { + dispatchEvent: jest.fn(), + }, + } as any) as Client, }; describe('on success', () => { diff --git a/packages/core/src/exchanges/fetch.ts b/packages/core/src/exchanges/fetch.ts index aca8dd8cd7..50c65bbfb3 100755 --- a/packages/core/src/exchanges/fetch.ts +++ b/packages/core/src/exchanges/fetch.ts @@ -3,6 +3,7 @@ import { Kind, DocumentNode, OperationDefinitionNode, print } from 'graphql'; import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka'; import { Exchange, Operation, OperationResult } from '../types'; import { makeResult, makeErrorResult } from '../utils'; +import { Client } from '../client'; interface Body { query: string; @@ -11,7 +12,7 @@ interface Body { } /** A default exchange for fetching GraphQL requests. */ -export const fetchExchange: Exchange = ({ forward }) => { +export const fetchExchange: Exchange = ({ forward, client }) => { const isOperationFetchable = (operation: Operation) => { const { operationName } = operation; return operationName === 'query' || operationName === 'mutation'; @@ -31,6 +32,7 @@ export const fetchExchange: Exchange = ({ forward }) => { return pipe( createFetchSource( + client, operation, operation.operationName === 'query' && !!operation.context.preferGetMethod @@ -60,7 +62,11 @@ const getOperationName = (query: DocumentNode): string | null => { return node !== undefined && node.name ? node.name.value : null; }; -const createFetchSource = (operation: Operation, shouldUseGet: boolean) => { +const createFetchSource = ( + client: Client, + operation: Operation, + shouldUseGet: boolean +) => { if ( process.env.NODE_ENV !== 'production' && operation.operationName === 'subscription' @@ -113,7 +119,9 @@ const createFetchSource = (operation: Operation, shouldUseGet: boolean) => { let ended = false; Promise.resolve() - .then(() => (ended ? undefined : executeFetch(operation, fetchOptions))) + .then(() => + ended ? undefined : executeFetch(client, operation, fetchOptions) + ) .then((result: OperationResult | undefined) => { if (!ended) { ended = true; @@ -132,12 +140,23 @@ const createFetchSource = (operation: Operation, shouldUseGet: boolean) => { }; const executeFetch = ( + client: Client, operation: Operation, opts: RequestInit ): Promise => { const { url, fetch: fetcher } = operation.context; let response: Response | undefined; + client.debugTarget!.dispatchEvent({ + type: 'fetchRequest', + message: 'A fetch request is being executed.', + operation, + data: { + url, + fetchOptions: opts, + }, + }); + return (fetcher || fetch)(url, opts) .then(res => { const { status } = res; @@ -150,11 +169,37 @@ const executeFetch = ( return res.json(); } }) - .then(result => makeResult(operation, result, response)) + .then(result => { + client.debugTarget!.dispatchEvent({ + type: 'fetchSuccess', + message: 'A successful fetch response has been returned.', + operation, + data: { + url, + fetchOptions: opts, + value: result, + }, + }); + + return makeResult(operation, result, response); + }) .catch(err => { - if (err.name !== 'AbortError') { - return makeErrorResult(operation, err, response); + if (err.name === 'AbortError') { + return; } + + client.debugTarget!.dispatchEvent({ + type: 'fetchError', + message: err.name, + operation, + data: { + url, + fetchOptions: opts, + value: err, + }, + }); + + return makeErrorResult(operation, err, response); }); }; diff --git a/packages/core/src/exchanges/index.ts b/packages/core/src/exchanges/index.ts index 798c4f481a..213e8cc095 100644 --- a/packages/core/src/exchanges/index.ts +++ b/packages/core/src/exchanges/index.ts @@ -4,7 +4,7 @@ export { subscriptionExchange } from './subscription'; export { debugExchange } from './debug'; export { dedupExchange } from './dedup'; export { fetchExchange } from './fetch'; -export { fallbackExchangeIO } from './fallback'; +export { fallbackExchange } from './fallback'; export { composeExchanges } from './compose'; import { cacheExchange } from './cache'; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2b5e2ff8db..8ccf51e069 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -88,11 +88,27 @@ export type ExchangeIO = (ops$: Source) => Source; /** Debug event types (interfaced for declaration merging). */ export interface DebugEventTypes { + // Cache exchange cacheHit: { value: any }; cacheInvalidation: { typenames: string[]; response: OperationResult; }; + // Fetch exchange + fetchRequest: { + url: string; + fetchOptions: RequestInit; + }; + fetchSuccess: { + url: string; + fetchOptions: RequestInit; + value: object; + }; + fetchError: { + url: string; + fetchOptions: RequestInit; + value: Error; + }; } export type DebugEvent = { @@ -100,5 +116,5 @@ export type DebugEvent = { message: string; operation: Operation; } & (T extends keyof DebugEventTypes - ? { data: T extends keyof DebugEventTypes ? DebugEventTypes[T] : never } + ? { data: DebugEventTypes[T] } : { data?: any });