Skip to content

Commit a220dc0

Browse files
timsuchanekhuv1k
authored andcommitted
Make schema reloading more resilient. Closes #940 (#951)
* Make schema reloading more resilient. Closes #940 * Regarding "If there’s an active introspection query, wait for it’s result before sending another one" is default behavior when schema is reloading. The changes I made to account for this are unnecessary and actually cause polling to stop if the endpoint is unreachable. * getIsPollingSchema catching regex error * add schema reference equality & printing cache * improve code readability * cleanup: remove dead code * fix build: use lru-cache. less unneeded repaints * fix lru cache usage
1 parent e0dd5b5 commit a220dc0

File tree

13 files changed

+178
-76
lines changed

13 files changed

+178
-76
lines changed

packages/graphql-playground-react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"why-did-you-update": "0.1.1"
106106
},
107107
"dependencies": {
108+
"@types/lru-cache": "^4.1.1",
108109
"apollo-link": "^1.0.7",
109110
"apollo-link-http": "^1.3.2",
110111
"apollo-link-ws": "1.0.8",

packages/graphql-playground-react/src/components/Playground.tsx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
} from '../state/sessions/actions'
2828
import { setConfigString } from '../state/general/actions'
2929
import { initState } from '../state/workspace/actions'
30-
import { GraphQLSchema, printSchema } from 'graphql'
30+
import { GraphQLSchema } from 'graphql'
3131
import { createStructuredSelector } from 'reselect'
3232
import {
3333
getIsConfigTab,
@@ -50,6 +50,7 @@ import { getWorkspaceId } from './Playground/util/getWorkspaceId'
5050
import { getSettings, getSettingsString } from '../state/workspace/reducers'
5151
import { Backoff } from './Playground/util/fibonacci-backoff'
5252
import { debounce } from 'lodash'
53+
import { cachedPrintSchema } from './util'
5354

5455
export interface Response {
5556
resultID: string
@@ -173,6 +174,7 @@ export class Playground extends React.PureComponent<Props & ReduxProps, State> {
173174
private backoff: Backoff
174175
private initialIndex: number = -1
175176
private mounted = false
177+
private initialSchemaFetch = true
176178

177179
constructor(props: Props & ReduxProps) {
178180
super(props)
@@ -270,11 +272,14 @@ export class Playground extends React.PureComponent<Props & ReduxProps, State> {
270272
})
271273
if (schema) {
272274
this.updateSchema(currentSchema, schema.schema, props)
273-
this.props.schemaFetchingSuccess(
274-
data.endpoint,
275-
schema.tracingSupported,
276-
props.isPollingSchema,
277-
)
275+
if (this.initialSchemaFetch) {
276+
this.props.schemaFetchingSuccess(
277+
data.endpoint,
278+
schema.tracingSupported,
279+
props.isPollingSchema,
280+
)
281+
this.initialSchemaFetch = false
282+
}
278283
this.backoff.stop()
279284
}
280285
} catch (e) {
@@ -367,10 +372,17 @@ export class Playground extends React.PureComponent<Props & ReduxProps, State> {
367372
props: Readonly<{ children?: React.ReactNode }> &
368373
Readonly<Props & ReduxProps>,
369374
) {
370-
const currentSchemaStr = currentSchema ? printSchema(currentSchema) : null
371-
const newSchemaStr = printSchema(newSchema)
372-
if (newSchemaStr !== currentSchemaStr || !props.isPollingSchema) {
373-
this.setState({ schema: newSchema })
375+
// first check for reference equality
376+
if (currentSchema !== newSchema) {
377+
// if references are not equal, do an equality check on the printed schema
378+
const currentSchemaStr = currentSchema
379+
? cachedPrintSchema(currentSchema)
380+
: null
381+
const newSchemaStr = cachedPrintSchema(newSchema)
382+
383+
if (newSchemaStr !== currentSchemaStr || !props.isPollingSchema) {
384+
this.setState({ schema: newSchema })
385+
}
374386
}
375387
}
376388

packages/graphql-playground-react/src/components/Playground/SchemaFetcher.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Map, set } from 'immutable'
55
import { makeOperation } from './util/makeOperation'
66
import { parseHeaders } from './util/parseHeaders'
77
import { LinkCreatorProps } from '../../state/sessions/fetchingSagas'
8+
import * as LRU from 'lru-cache'
89

910
export interface TracingSchemaTuple {
1011
schema: GraphQLSchema
@@ -18,19 +19,53 @@ export interface SchemaFetchProps {
1819

1920
export type LinkGetter = (session: LinkCreatorProps) => { link: ApolloLink }
2021

22+
/**
23+
* The SchemaFetcher class servers the purpose of providing the GraphQLSchema.
24+
* All sagas and every part of the UI is using this as a singleton to prevent
25+
* unnecessary calls to the server. We're not storing this information in Redux,
26+
* as it's a good practice to only store serializable data in Redux.
27+
* GraphQLSchema objects are serializable, but can easily exceed the localStorage
28+
* max. Another reason to keep this in a separate class is, that we have more
29+
* advanced requirements like caching.
30+
*/
2131
export class SchemaFetcher {
22-
cache: Map<string, TracingSchemaTuple>
32+
/**
33+
* The `sessionCache` property is used for UI components, that need fast access to the current schema.
34+
* If the relevant information of the session didn't change (endpoint and headers),
35+
* the cached schema will be returned.
36+
*/
37+
sessionCache: LRU.Cache<string, TracingSchemaTuple>
38+
/**
39+
* The `schemaInstanceCache` property is used to prevent unnecessary buildClientSchema calls.
40+
* It's tested by stringifying the introspection result, which is orders of magnitude
41+
* faster than rebuilding the schema.
42+
*/
43+
schemaInstanceCache: LRU.Cache<string, GraphQLSchema>
44+
/**
45+
* The `linkGetter` property is a callback that provides an ApolloLink instance.
46+
* This can be overriden by the user.
47+
*/
2348
linkGetter: LinkGetter
49+
/**
50+
* In order to prevent duplicate fetching of the same schema, we keep track
51+
* of all subsequent calls to `.fetch` with the `fetching` property.
52+
*/
2453
fetching: Map<string, Promise<any>>
54+
/**
55+
* Other parts of the application can subscribe to change of a schema for a
56+
* particular session. These subscribers are being kept track of in the
57+
* `subscriptions` property
58+
*/
2559
subscriptions: Map<string, (schema: GraphQLSchema) => void> = Map()
2660
constructor(linkGetter: LinkGetter) {
27-
this.cache = Map()
61+
this.sessionCache = new LRU<string, TracingSchemaTuple>({ max: 10 })
62+
this.schemaInstanceCache = new LRU({ max: 10 })
2863
this.fetching = Map()
2964
this.linkGetter = linkGetter
3065
}
3166
async fetch(session: SchemaFetchProps) {
3267
const hash = this.hash(session)
33-
const cachedSchema = this.cache.get(hash)
68+
const cachedSchema = this.sessionCache.get(hash)
3469
if (cachedSchema) {
3570
return cachedSchema
3671
}
@@ -52,6 +87,19 @@ export class SchemaFetcher {
5287
hash(session: SchemaFetchProps) {
5388
return `${session.endpoint}~${session.headers || ''}`
5489
}
90+
private getSchema(data: any) {
91+
const schemaString = JSON.stringify(data)
92+
const cachedSchema = this.schemaInstanceCache.get(schemaString)
93+
if (cachedSchema) {
94+
return cachedSchema
95+
}
96+
97+
const schema = buildClientSchema(data as any)
98+
99+
this.schemaInstanceCache.set(schemaString, schema)
100+
101+
return schema
102+
}
55103
private fetchSchema(
56104
session: SchemaFetchProps,
57105
): Promise<{ schema: GraphQLSchema; tracingSupported: boolean } | null> {
@@ -83,15 +131,15 @@ export class SchemaFetcher {
83131
throw new NoSchemaError(endpoint)
84132
}
85133

86-
const schema = buildClientSchema(schemaData.data as any)
134+
const schema = this.getSchema(schemaData.data as any)
87135
const tracingSupported =
88136
(schemaData.extensions && Boolean(schemaData.extensions.tracing)) ||
89137
false
90-
const result = {
138+
const result: TracingSchemaTuple = {
91139
schema,
92140
tracingSupported,
93141
}
94-
this.cache = this.cache.set(this.hash(session), result)
142+
this.sessionCache.set(this.hash(session), result)
95143
resolve(result)
96144
this.fetching = this.fetching.remove(hash)
97145
const subscription = this.subscriptions.get(hash)

packages/graphql-playground-react/src/components/Playground/TopBar/Polling.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import PollingIcon from './PollingIcon'
33

44
export interface Props {
55
interval: number
6-
isReloadingSchema: boolean
76
onReloadSchema: () => void
87
}
98

@@ -47,17 +46,15 @@ class SchemaPolling extends React.Component<Props, State> {
4746
}
4847
}
4948
componentWillReceiveProps(nextProps: Props) {
50-
if (nextProps.isReloadingSchema !== this.props.isReloadingSchema) {
51-
this.updatePolling(nextProps)
52-
}
49+
this.updatePolling(nextProps)
5350
}
5451

5552
render() {
5653
return <PollingIcon animate={this.state.windowVisible} />
5754
}
5855
private updatePolling = (props: Props = this.props) => {
5956
this.clearTimer()
60-
if (!props.isReloadingSchema && this.state.windowVisible) {
57+
if (this.state.windowVisible) {
6158
// timer starts only when introspection not in flight
6259
this.timer = setInterval(() => props.onReloadSchema(), props.interval)
6360
}

packages/graphql-playground-react/src/components/Playground/TopBar/SchemaReload.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import * as React from 'react'
22
import ReloadIcon from './Reload'
33
import Polling from './Polling'
44
import { ISettings } from '../../../types'
5+
import { createStructuredSelector } from 'reselect'
6+
import { getIsReloadingSchema } from '../../../state/sessions/selectors'
7+
import { connect } from 'react-redux'
58

69
export interface Props {
710
isPollingSchema: boolean
@@ -10,12 +13,11 @@ export interface Props {
1013
settings: ISettings
1114
}
1215

13-
export default (props: Props) => {
16+
const SchemaReload = (props: Props) => {
1417
if (props.isPollingSchema) {
1518
return (
1619
<Polling
1720
interval={props.settings['schema.polling.interval']}
18-
isReloadingSchema={props.isReloadingSchema}
1921
onReloadSchema={props.onReloadSchema}
2022
/>
2123
)
@@ -27,3 +29,9 @@ export default (props: Props) => {
2729
/>
2830
)
2931
}
32+
33+
const mapStateToProps = createStructuredSelector({
34+
isReloadingSchema: getIsReloadingSchema,
35+
})
36+
37+
export default connect(mapStateToProps)(SchemaReload)

packages/graphql-playground-react/src/components/Playground/TopBar/TopBar.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { createStructuredSelector } from 'reselect'
88
import {
99
getEndpoint,
1010
getSelectedSession,
11-
getIsReloadingSchema,
1211
getEndpointUnreachable,
1312
getIsPollingSchema,
1413
} from '../../../state/sessions/selectors'
@@ -29,7 +28,6 @@ export interface Props {
2928
endpoint: string
3029
shareEnabled?: boolean
3130
fixedEndpoint?: boolean
32-
isReloadingSchema: boolean
3331
isPollingSchema: boolean
3432
endpointUnreachable: boolean
3533

@@ -83,7 +81,6 @@ class TopBar extends React.Component<Props, {}> {
8381
<SchemaReload
8482
settings={settings}
8583
isPollingSchema={this.props.isPollingSchema}
86-
isReloadingSchema={this.props.isReloadingSchema}
8784
onReloadSchema={this.props.refetchSchema}
8885
/>
8986
</div>
@@ -157,7 +154,6 @@ class TopBar extends React.Component<Props, {}> {
157154
const mapStateToProps = createStructuredSelector({
158155
endpoint: getEndpoint,
159156
fixedEndpoint: getFixedEndpoint,
160-
isReloadingSchema: getIsReloadingSchema,
161157
isPollingSchema: getIsPollingSchema,
162158
endpointUnreachable: getEndpointUnreachable,
163159
settings: getSettings,

packages/graphql-playground-react/src/components/PlaygroundWrapper.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,10 +427,8 @@ class PlaygroundWrapper extends React.Component<
427427

428428
handleSaveConfig = () => {
429429
/* tslint:disable-next-line */
430-
console.log('handleSaveConfig called')
431430
if (typeof this.props.onSaveConfig === 'function') {
432431
/* tslint:disable-next-line */
433-
console.log('calling this.props.onSaveConfig', this.state.configString)
434432
this.props.onSaveConfig(this.state.configString!)
435433
}
436434
}

packages/graphql-playground-react/src/components/util.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { GraphQLConfig, GraphQLConfigEnpointConfig } from '../graphqlConfig'
2+
import { GraphQLSchema, printSchema } from 'graphql'
3+
import * as LRU from 'lru-cache'
24

35
export function getActiveEndpoints(
46
config: GraphQLConfig,
@@ -32,3 +34,20 @@ export function getEndpointFromEndpointConfig(
3234
}
3335
}
3436
}
37+
38+
const printSchemaCache: LRU.Cache<GraphQLSchema, string> = new LRU({ max: 10 })
39+
/**
40+
* A cached version of `printSchema`
41+
* @param schema GraphQLSchema instance
42+
*/
43+
export function cachedPrintSchema(schema: GraphQLSchema) {
44+
const cachedString = printSchemaCache.get(schema)
45+
if (cachedString) {
46+
return cachedString
47+
}
48+
49+
const schemaString = printSchema(schema)
50+
printSchemaCache.set(schema, schemaString)
51+
52+
return schemaString
53+
}

0 commit comments

Comments
 (0)