@urql/devtools
Version:
The official exchange for use with Urql Devtools
162 lines (142 loc) • 3.95 kB
text/typescript
import { Exchange, Client, Operation, OperationResult } from '@urql/core';
import { parse } from 'graphql';
import { pipe, tap, take, toPromise } from 'wonka';
import { DevtoolsExecuteQueryMessage } from './types';
import {
getDisplayName,
hash,
createDebugMessage,
createNativeMessenger,
createBrowserMessenger,
Messenger,
} from './utils';
interface HandlerArgs {
sendMessage: Messenger['sendMessage'];
}
const curriedDevtoolsExchange: (a: Messenger) => Exchange = ({
sendMessage,
addMessageListener,
}) => ({ client, forward }) => {
// Listen for messages from devtools
addMessageListener((message) => {
if (message.source !== 'devtools' || !(message.type in messageHandlers)) {
return;
}
messageHandlers[message.type]({ client, sendMessage })(message as any);
});
// Forward debug events to content script
client.subscribeToDebugTarget &&
client.subscribeToDebugTarget((event) =>
sendMessage({
type: 'debug-event',
source: 'exchange',
data: event,
})
);
return (ops$) =>
pipe(
ops$,
tap(handleOperation({ client, sendMessage })),
forward,
tap(handleResult({ client, sendMessage }))
);
};
interface HandlerArgs {
client: Client;
sendMessage: Messenger['sendMessage'];
}
/** Handle outgoing operations */
const handleOperation = ({ sendMessage }: HandlerArgs) => (
operation: Operation
) => {
if (operation.kind === 'teardown') {
const msg = createDebugMessage({
type: 'teardown',
message: 'The operation has been torn down',
operation,
data: undefined,
});
return sendMessage(msg);
}
const msg = createDebugMessage({
type: 'execution',
message: 'The client has received an execute command.',
operation,
data: {
sourceComponent: getDisplayName(),
},
});
return sendMessage(msg);
};
/** Handle new value or error */
const handleResult = ({ sendMessage }: HandlerArgs) => ({
operation,
data,
error,
}: OperationResult) => {
if (error) {
const msg = createDebugMessage({
type: 'error',
message: 'The operation has returned a new error.',
operation,
data: {
value: error,
},
});
return sendMessage(msg);
}
const msg = createDebugMessage({
type: 'update',
message: 'The operation has returned a new response.',
operation,
data: {
value: data,
},
});
sendMessage(msg);
};
/** Handle execute request message. */
const handleExecuteQueryMessage = ({ client }: HandlerArgs) => (
message: DevtoolsExecuteQueryMessage
) => {
const isMutation = /(^|\W)+mutation\W/.test(message.query);
const requestType = isMutation ? 'mutation' : 'query';
const op = client.createRequestOperation(
requestType,
{
key: hash(JSON.stringify(message.query)),
query: parse(message.query),
},
{
meta: {
source: 'Devtools',
},
}
);
pipe(client.executeRequestOperation(op), take(1), toPromise);
};
/** Handle connection initiated by devtools. */
const handleConnectionInitMessage = ({ sendMessage }: HandlerArgs) => () =>
sendMessage({
type: 'connection-acknowledge',
source: 'exchange',
version: __pkg_version__,
});
/** Map of handlers for incoming messages. */
const messageHandlers = {
'execute-query': handleExecuteQueryMessage,
'connection-init': handleConnectionInitMessage,
} as const;
export const devtoolsExchange = ((): Exchange => {
const isNative =
typeof navigator !== 'undefined' && navigator?.product === 'ReactNative';
const isSSR = !isNative && typeof window === 'undefined';
// Prod or SSR
if (process.env.NODE_ENV === 'production' || isSSR) {
return ({ forward }) => (ops$) => pipe(ops$, forward);
}
if (isNative) {
return curriedDevtoolsExchange(createNativeMessenger());
}
return curriedDevtoolsExchange(createBrowserMessenger());
})();