[next] Relay GraphQL Batching (#1965)

* feat: added graphqlBatch

* refactor: cleanup of json serial

* refactor: cleanup of json serilization

* feat: use react network layer

* fix: adjusted broken user

* fix: temporarily disable cache for profile query

* test: temporarily use precompiled modules

* fix: bug when updating comment count on an asset

* fix: compile modules to commonjs for jest

* test: add react-relay-network-layer to transform whitelist

* fix: use react-relay-network-layer/es

* types: add react-relay-network-modern typescript types

* feat: integrate custom error middleware

* review: add todo
This commit is contained in:
Wyatt Johnson
2018-10-15 22:46:21 +00:00
committed by GitHub
parent 9347815276
commit e9c80fc02a
27 changed files with 646 additions and 186 deletions
@@ -4,7 +4,7 @@ import { noop } from "lodash";
import { Child as PymChild } from "pym.js";
import React, { Component, ComponentType } from "react";
import { Formatter } from "react-timeago";
import { Environment, Network, RecordSource, Store } from "relay-runtime";
import { Environment, RecordSource, Store } from "relay-runtime";
import uuid from "uuid/v4";
import { getBrowserInfo } from "talk-framework/lib/browserInfo";
@@ -21,7 +21,7 @@ import { RestClient } from "talk-framework/lib/rest";
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
import { generateBundles, LocalesData, negotiateLanguages } from "../i18n";
import { createFetch, TokenGetter } from "../network";
import { createNetwork, TokenGetter } from "../network";
import { PostMessageService } from "../postMessage";
import { TalkContext, TalkContextProvider } from "./TalkContext";
@@ -97,7 +97,7 @@ function createRelayEnvironment() {
return "";
};
const environment = new Environment({
network: Network.create(createFetch(tokenGetter)),
network: createNetwork(tokenGetter),
store: new Store(source),
});
return { environment, tokenGetter };
@@ -1,25 +0,0 @@
export interface GraphQLErrorItem {
message: string;
locations: Array<{
line: number;
column: number;
}>;
}
/**
* Graphql wraps graphql errors at the network layer.
*/
export default class GraphQLError extends Error {
// Original error.
public readonly origin: GraphQLErrorItem[];
constructor(origin: GraphQLErrorItem[]) {
super(origin.map(o => o.message).join(" "));
// Maintains proper stack trace for where our error was thrown.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, GraphQLError);
}
this.origin = origin;
}
}
@@ -1,6 +1,2 @@
export { default as NetworkError } from "./networkError";
export { default as UnknownServerError } from "./unknownServerError";
export { default as BadUserInputError } from "./badUserInputError";
export { default as GraphQLError } from "./graphqlError";
export * from "./graphqlError";
@@ -1,18 +0,0 @@
/**
* NetworkError wraps errors at the network layer.
*/
export default class NetworkError extends Error {
// Original error.
public readonly origin: Error;
constructor(origin: Error) {
// Pass remaining arguments (including vendor specific ones) to parent constructor.
super(origin.message);
// Maintains proper stack trace for where our error was thrown.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, NetworkError);
}
this.origin = origin;
}
}
@@ -0,0 +1,40 @@
import {
authMiddleware,
batchMiddleware,
cacheMiddleware,
RelayNetworkLayer,
retryMiddleware,
urlMiddleware,
} from "react-relay-network-modern/es";
import customErrorMiddleware from "./customErrorMiddleware";
export type TokenGetter = () => string;
const graphqlURL = "/api/tenant/graphql";
export default function createNetwork(tokenGetter: TokenGetter) {
return new RelayNetworkLayer([
customErrorMiddleware,
cacheMiddleware({
size: 100, // max 100 requests
ttl: 900000, // 15 minutes
}),
urlMiddleware({
url: req => Promise.resolve(graphqlURL),
}),
batchMiddleware({
batchUrl: (requestMap: any) => Promise.resolve(graphqlURL),
batchTimeout: 10,
}),
retryMiddleware({
fetchTimeout: 15000,
retryDelays: (attempt: number) => Math.pow(2, attempt + 4) * 100,
// or simple array [3200, 6400, 12800, 25600, 51200, 102400, 204800, 409600],
statusCodes: [500, 503, 504],
}),
authMiddleware({
token: tokenGetter,
}),
]);
}
@@ -0,0 +1,31 @@
import { Middleware } from "react-relay-network-modern/es";
import { BadUserInputError, UnknownServerError } from "../errors";
function getError(errors: Error[]): Error | null {
if (errors.length > 1 || !(errors[0] as any).extensions) {
// Multiple errors are GraphQL errors.
// TODO: (cvle) Is this assumption correct?
// No extensions == GraphQL error.
// TODO: (cvle) harmonize with server.
return null;
}
const err = errors[0];
if ((err as any).code === "BAD_USER_INPUT") {
return new BadUserInputError((err as any).extensions);
}
return new UnknownServerError(err.message, (err as any).extensions);
}
const customErrorMiddleware: Middleware = next => async req => {
const res = await next(req);
if (req.isMutation() && res.errors) {
// Extract custom error.
const error = getError(res.errors);
if (error) {
throw error;
}
}
return res;
};
export default customErrorMiddleware;
@@ -1,72 +0,0 @@
import { FetchFunction } from "relay-runtime";
import {
BadUserInputError,
GraphQLError,
NetworkError,
UnknownServerError,
} from "../errors";
// Normalize errors.
function getError(errors: Error[]): Error {
if (errors.length > 1) {
// Multiple errors are GraphQL errors.
// TODO: (cvle) Is this assumption correct?
return new GraphQLError(errors as any);
}
const err = errors[0] as Error;
if ((err as any).extensions) {
if ((err as any).code === "BAD_USER_INPUT") {
return new BadUserInputError((err as any).extensions);
}
return new UnknownServerError(err.message, (err as any).extensions);
}
// No extensions == GraphQL error.
// TODO: (cvle) harmonize with server.
return new GraphQLError(errors as any);
}
export type TokenGetter = () => string;
type CreateFetch = (token?: TokenGetter) => FetchFunction;
/**
* createFetch returns a simple implementation of the `FetchFunction`
* required by Relay. It'll return a `NetworkError` on failure.
*/
const createFetch: CreateFetch = tokenGetter => async (
operation,
variables
) => {
const token = tokenGetter && tokenGetter();
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
try {
const response = await fetch("/api/tenant/graphql", {
method: "POST",
headers,
body: JSON.stringify({
query: operation.text,
variables,
}),
});
if (response.status >= 500) {
throw new Error(`${response.status} ${response.statusText}`);
}
const data = await response.json();
if (data.errors) {
throw getError(data.errors);
}
return data;
} catch (err) {
if (err instanceof TypeError) {
throw new NetworkError(err);
}
throw err;
}
};
export default createFetch;
@@ -1 +1 @@
export { default as createFetch, TokenGetter } from "./fetchQuery";
export { default as createNetwork, TokenGetter } from "./createNetwork";