diff --git a/README.md b/README.md index a142028..3bb0bde 100644 --- a/README.md +++ b/README.md @@ -209,45 +209,46 @@ const environment = new Environment({ network, store }, persistOfflineOptions); ```ts import { Store } from "react-relay-offline"; import { CacheOptions } from "@wora/cache-persist"; -import { CacheOptionsStore } from "@wora/relay-store"; +import { StoreOptions } from "@wora/relay-store"; -const persistOptionsStore: CacheOptionsStore = { defaultTTL: 10 * 60 * 1000 }; // default -const persistOptionsRecords: CacheOptions = {}; // default +const persistOptionsStore: CacheOptions = { }; +const persistOptionsRecords: CacheOptions = {}; +const relayStoreOptions: StoreOptions = { queryCacheExpirationTime: 10 * 60 * 1000 }; // default const recordSource = new RecordSource(persistOptionsRecords); -const store = new Store(recordSource, persistOptionsStore); +const store = new Store(recordSource, persistOptionsStore, relayStoreOptions); const environment = new Environment({ network, store }); ``` -## QueryRenderer -- Add "cached" property in render function -- Add "ttl" property in order to change default ttl in store -- `fetchPolicy` determine whether it should use data cached in the Relay store and whether to send a network request. The options are: - - `store-or-network` (default): Reuse data cached in the store; if the whole query is cached, skip the network request - - `store-and-network`: Reuse data cached in the store; always send a network request. - - `network-only`: Don't reuse data cached in the store; always send a network request. (This is the default behavior of Relay's existing `QueryRenderer`.) - - `store-only`: Reuse data cached in the store; never send a network request. +## useQuery -```ts -import { QueryRenderer } from 'react-relay-offline'; - - { -``` +`useQuery` does not take an environment as an argument. Instead, it reads the environment set in the context; this also implies that it does not set any React context. +In addition to `query` (first argument) and `variables` (second argument), `useQuery` accepts a third argument `options`. -## useQuery +**options** + +`fetchPolicy`: determine whether it should use data cached in the Relay store and whether to send a network request. The options are: + * `store-or-network` (default): Reuse data cached in the store; if the whole query is cached, skip the network request + * `store-and-network`: Reuse data cached in the store; always send a network request. + * `network-only`: Don't reuse data cached in the store; always send a network request. (This is the default behavior of Relay's existing `QueryRenderer`.) + * `store-only`: Reuse data cached in the store; never send a network request. + +`fetchKey`: [Optional] A fetchKey can be passed to force a refetch of the current query and variables when the component re-renders, even if the variables didn't change, or even if the component isn't remounted (similarly to how passing a different key to a React component will cause it to remount). If the fetchKey is different from the one used in the previous render, the current query and variables will be refetched. + +`networkCacheConfig`: [Optional] Object containing cache config options for the network layer. Note the the network layer may contain an additional query response cache which will reuse network responses for identical queries. If you want to bypass this cache completely, pass {force: true} as the value for this option. **Added the TTL property to configure a specific ttl for the query.** + +`skip`: [Optional] If skip is true, the query will be skipped entirely. + +`onComplete`: [Optional] Function that will be called whenever the fetch request has completed ```ts import { useQuery } from "react-relay-offline"; +const networkCacheConfig = { + ttl: 1000 +} const hooksProps = useQuery(query, variables, { - networkCacheConfig: cacheConfig, + networkCacheConfig, fetchPolicy, - ttl }); ``` @@ -255,10 +256,12 @@ const hooksProps = useQuery(query, variables, { ```ts import { useQuery } from "react-relay-offline"; +const networkCacheConfig = { + ttl: 1000 +} const hooksProps = useLazyLoadQuery(query, variables, { - networkCacheConfig: cacheConfig, + networkCacheConfig, fetchPolicy, - ttl }); ``` @@ -283,11 +286,6 @@ const isRehydrated = useRestore(environment); import { fetchQuery } from "react-relay-offline"; ``` -## Mutation - -```ts -import { commitMutation, graphql } from "react-relay-offline"; -``` ## Detect Network @@ -366,7 +364,7 @@ to notify any updated data in the store. ## Requirement -- Version >=8.0.0 of the react-relay library +- Version >=10.1.0 of the relay-runtime library - When a new node is created by mutation the id must be generated in the browser to use it in the optimistic response ## License diff --git a/__tests__/ReactRelayQueryRenderer-test.tsx b/__tests__/ReactRelayQueryRenderer-test.tsx index 91a1328..33710c0 100644 --- a/__tests__/ReactRelayQueryRenderer-test.tsx +++ b/__tests__/ReactRelayQueryRenderer-test.tsx @@ -1,3 +1,8 @@ +/* eslint-disable max-len */ +/* eslint-disable @typescript-eslint/camelcase */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /** * Copyright (c) Facebook, Inc. and its affiliates. * @@ -19,7 +24,8 @@ const Scheduler = require('scheduler'); //import { ReactRelayContext } from "react-relay"; -import { useQuery, RelayEnvironmentProvider, NETWORK_ONLY, Store, RecordSource, Environment } from '../src'; +import { useQuery, Store, RecordSource, Environment } from '../src'; +import { RelayEnvironmentProvider, NETWORK_ONLY } from 'relay-hooks'; import * as ReactTestRenderer from 'react-test-renderer'; @@ -28,41 +34,108 @@ import * as ReactTestRenderer from 'react-test-renderer'; import { createOperationDescriptor, Network, Observable, ROOT_ID } from 'relay-runtime'; import { ROOT_TYPE } from 'relay-runtime/lib/store/RelayStoreUtils'; -import { generateAndCompile, simpleClone } from 'relay-test-utils-internal'; +import { generateAndCompile } from 'relay-test-utils-internal'; import { createMockEnvironment } from './RelayModernEnvironmentMock'; import { createPersistedStorage } from './Utils'; -/* -function expectToBeRendered(renderFn, readyState) { - // Ensure useEffect is called before other timers - ReactTestRenderer.act(() => { - jest.runAllImmediates(); - }); - expect(renderFn).toBeCalledTimes(1); - expect(renderFn.mock.calls[0][0]).toEqual(readyState); - renderFn.mockClear(); -}*/ -const QueryRendererHook = (props) => { +function expectToBeRendered( + renderSpy, + readyState: { + data: any; + error: Error | null; + }, +): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(2); + + expect(renderSpy.mock.calls[0][0].isLoading).toEqual(true); + expect(renderSpy.mock.calls[0][0].error).toEqual(null); + + const actualResult = renderSpy.mock.calls[1][0]; + expect(renderSpy.mock.calls[1][0].isLoading).toEqual(false); + + expect(actualResult.data).toEqual(readyState.data); + expect(actualResult.error).toEqual(readyState.error); + expect(actualResult.retry).toEqual(expect.any(Function)); +} + +function expectToBeLoading(renderSpy): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(true); + expect(actualResult.error).toEqual(null); + expect(actualResult.data).toEqual(null); +} + +function expectToBeError(renderSpy, error): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(false); + expect(actualResult.error).toEqual(error); + expect(actualResult.data).toEqual(null); +} + +function expectToBeRenderedFirst( + renderSpy, + readyState: { + data: any; + error: Error | null; + isLoading?: boolean; + }, +): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + const { isLoading = false } = readyState; + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(isLoading); + + expect(actualResult.data).toEqual(readyState.data); + expect(actualResult.error).toEqual(readyState.error); + expect(actualResult.retry).toEqual(expect.any(Function)); +} + +const QueryRendererHook = (props: any): JSX.Element => { const { render, fetchPolicy = NETWORK_ONLY, query, variables, cacheConfig, fetchKey, skip } = props; - const { cached, ...relays } = useQuery(query, variables, { + const queryData = useQuery(query, variables, { networkCacheConfig: cacheConfig, fetchPolicy, fetchKey, skip, }); - return {render(relays)}; + return {render(queryData)}; }; -const ReactRelayQueryRenderer = (props) => ( +const ReactRelayQueryRenderer = (props: any): JSX.Element => ( ); -function sleep(ms) { +/*function sleep(ms): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); -} +}*/ describe('ReactRelayQueryRenderer', () => { let TestQuery; @@ -90,9 +163,11 @@ describe('ReactRelayQueryRenderer', () => { props: null, }; } + setProps(props) { this.setState({ props }); } + render() { const child: any = React.Children.only(this.props.children); if (this.state.props) { @@ -105,14 +180,6 @@ describe('ReactRelayQueryRenderer', () => { beforeEach(async () => { Scheduler.unstable_clearYields(); jest.resetModules(); - expect.extend({ - toBeRendered(readyState) { - const calls = render.mock.calls; - expect(calls.length).toBe(1); - expect(calls[0][0]).toEqual(readyState); - return { message: '', pass: true }; - }, - }); environment = createMockEnvironment(); store = environment.getStore(); @@ -169,9 +236,9 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); environment.mock.resolve(TestQuery, response); const owner = createOperationDescriptor(TestQuery, variables); - expect({ + expectToBeRendered(render, { error: null, - props: { + data: { node: { id: '4', @@ -183,11 +250,7 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + }); }); it('fetches the query', () => { @@ -240,13 +303,7 @@ describe('ReactRelayQueryRenderer', () => { Scheduler.unstable_flushNumberOfYields(2); expect(Scheduler.unstable_clearYields()).toEqual(['A', 'B']); expect(renderer.toJSON()).toEqual(null); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added - }).toBeRendered(); + expectToBeLoading(render); expect(environment.execute.mock.calls.length).toBe(1); render.mockClear(); @@ -255,13 +312,7 @@ describe('ReactRelayQueryRenderer', () => { renderer.update(); }); expect(environment.execute.mock.calls.length).toBe(1); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added - }).toBeRendered(); + expectToBeLoading(render); }); }); @@ -315,9 +366,9 @@ describe('ReactRelayQueryRenderer', () => { Scheduler.unstable_flushNumberOfYields(2); expect(Scheduler.unstable_clearYields()).toEqual(['A', 'B']); expect(renderer.toJSON()).toEqual(null); - expect({ - error: null, - props: { + + expectToBeRenderedFirst(render, { + data: { node: { id: '4', @@ -329,10 +380,9 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + error: null, + isLoading: true, + }); expect(environment.execute.mock.calls.length).toBe(1); render.mockClear(); @@ -341,9 +391,9 @@ describe('ReactRelayQueryRenderer', () => { renderer.update(); }); expect(environment.execute.mock.calls.length).toBe(1); - expect({ + expectToBeRenderedFirst(render, { error: null, - props: { + data: { node: { id: '4', @@ -355,19 +405,20 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + isLoading: true, + }); }); }); describe('when fetch returns a response synchronously first time', () => { it('fetches the query once, always renders snapshot returned by fetch', async () => { const fetch = jest.fn().mockReturnValueOnce(response); - store = new Store(new RecordSource({ storage: createPersistedStorage() }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage() }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = new Environment({ network: Network.create(fetch), store, @@ -407,9 +458,9 @@ describe('ReactRelayQueryRenderer', () => { Scheduler.unstable_flushNumberOfYields(2); expect(Scheduler.unstable_clearYields()).toEqual(['A', 'B']); expect(renderer.toJSON()).toEqual(null); - expect({ + expectToBeRenderedFirst(render, { error: null, - props: { + data: { node: { id: '4', @@ -421,10 +472,7 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + }); expect(fetch.mock.calls.length).toBe(1); render.mockClear(); @@ -433,9 +481,9 @@ describe('ReactRelayQueryRenderer', () => { renderer.update(); }); expect(fetch.mock.calls.length).toBe(1); - expect({ + expectToBeRenderedFirst(render, { error: null, - props: { + data: { node: { id: '4', @@ -447,10 +495,7 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + }); }); }); describe('when variables change before first result has completed', () => { @@ -458,8 +503,8 @@ describe('ReactRelayQueryRenderer', () => { environment = createMockEnvironment(); await environment.hydrate(); let pendingRequests = []; - jest.spyOn(environment, 'execute').mockImplementation((request) => { - const nextRequest = { request }; + jest.spyOn(environment, 'execute').mockImplementation((request: any) => { + const nextRequest: any = { request }; pendingRequests = pendingRequests.concat([nextRequest]); return Observable.create((sink) => { nextRequest.resolve = (resp) => { @@ -487,9 +532,9 @@ describe('ReactRelayQueryRenderer', () => { const firstRequest = pendingRequests[0]; const firstOwner = firstRequest.request.operation; firstRequest.resolve(response); - expect({ + expectToBeRendered(render, { error: null, - props: { + data: { node: { id: '4', @@ -501,10 +546,7 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + }); render.mockClear(); renderer.getInstance().setProps({ @@ -546,11 +588,11 @@ describe('ReactRelayQueryRenderer', () => { // request thirdRequest.resolve(thirdResponse); secondRequest.resolve(secondResponse); - expect(render.mock.calls.length).toEqual(3); - const lastRender = render.mock.calls[2][0]; + expect(render.mock.calls.length).toEqual(4); + const lastRender = render.mock.calls[3][0]; expect(lastRender).toEqual({ error: null, - props: { + data: { node: { id: '6', @@ -562,8 +604,7 @@ describe('ReactRelayQueryRenderer', () => { __id: '6', }, }, - rehydrated: true, - online: true, + isLoading: false, retry: expect.any(Function), }); }); @@ -593,13 +634,8 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added, - }).toBeRendered(); + + expectToBeLoading(render); }); it('if initial render set from store, skip loading state when data for query is already available', () => { @@ -626,9 +662,10 @@ describe('ReactRelayQueryRenderer', () => { />, ); const owner = createOperationDescriptor(TestQuery, variables); - expect({ + expectToBeRenderedFirst(render, { error: null, - props: { + isLoading: true, + data: { node: { id: '4', @@ -640,18 +677,18 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + }); }); it('skip loading state when request could be resolved synchronously', async () => { const fetch = () => response; - store = new Store(new RecordSource({ storage: createPersistedStorage() }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage() }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = new Environment({ network: Network.create(fetch), store, @@ -667,9 +704,9 @@ describe('ReactRelayQueryRenderer', () => { />, ); const owner = createOperationDescriptor(TestQuery, variables); - expect({ + expectToBeRenderedFirst(render, { error: null, - props: { + data: { node: { id: '4', @@ -681,19 +718,19 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + }); }); it('skip loading state when request failed synchronously', async () => { const error = new Error('Mock Network Error'); - const fetch = () => error; - store = new Store(new RecordSource({ storage: createPersistedStorage() }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + const fetch: any = () => error; + store = new Store( + new RecordSource({ storage: createPersistedStorage() }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = new Environment({ network: Network.create(fetch), store, @@ -708,13 +745,7 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect({ - error: error, - props: null, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + expectToBeError(render, error); }); }); @@ -780,19 +811,13 @@ describe('ReactRelayQueryRenderer', () => { variables, }); // added - const readyState = { - error: null, - props: null, - rehydrated: true, - online: true, - retry: expect.any(Function), - }; - expect(readyState).toBeRendered(); + + expectToBeLoading(render); expect(environment.execute).not.toBeCalled(); }); it('refetches if the `environment` prop changes', async () => { - expect.assertions(4); + expect.assertions(5); environment.mock.resolve(TestQuery, { data: { node: null, @@ -811,17 +836,12 @@ describe('ReactRelayQueryRenderer', () => { variables, }); expect(environment.mock.isLoading(TestQuery, variables, cacheConfig)).toBe(true); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added, - }).toBeRendered(); + + expectToBeLoading(render); }); it('refetches if the `variables` prop changes', () => { - expect.assertions(4); + expect.assertions(5); environment.mock.resolve(TestQuery, { data: { node: null, @@ -839,17 +859,11 @@ describe('ReactRelayQueryRenderer', () => { variables, }); expect(environment.mock.isLoading(TestQuery, variables, cacheConfig)).toBe(true); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added, - }).toBeRendered(); + expectToBeLoading(render); }); it('refetches with default values if the `variables` prop changes', () => { - expect.assertions(4); + expect.assertions(5); environment.mock.resolve(TestQuery, { data: { node: null, @@ -868,17 +882,11 @@ describe('ReactRelayQueryRenderer', () => { variables, }); expect(environment.mock.isLoading(TestQuery, expectedVariables, cacheConfig)).toBe(true); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added, - }).toBeRendered(); + expectToBeLoading(render); }); it('refetches if the `query` prop changes', () => { - expect.assertions(4); + expect.assertions(5); environment.mock.resolve(TestQuery, { data: { node: null, @@ -905,13 +913,7 @@ describe('ReactRelayQueryRenderer', () => { variables, }); expect(environment.mock.isLoading(NextQuery, variables, cacheConfig)).toBe(true); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added, - }).toBeRendered(); + expectToBeLoading(render); }); }); }); @@ -931,21 +933,15 @@ describe('ReactRelayQueryRenderer', () => { }); it('renders the error and retry', () => { - expect.assertions(3); + expect.assertions(4); render.mockClear(); const error = new Error('fail'); environment.mock.reject(TestQuery, error); - expect({ - error, - props: null, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + expectToBeError(render, error); }); it('refetch the query if `retry`', () => { - expect.assertions(4); // changed to 4 + expect.assertions(8); // changed to 4 render.mockClear(); const error = new Error('network fails'); environment.mock.reject(TestQuery, error); @@ -958,9 +954,9 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); environment.mock.resolve(TestQuery, response); const owner = createOperationDescriptor(TestQuery, variables); - expect({ + expectToBeRendered(render, { error: null, - props: { + data: { node: { id: '4', @@ -972,10 +968,7 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + }); }); }); @@ -1008,57 +1001,43 @@ describe('ReactRelayQueryRenderer', () => { } } const renderer = ReactTestRenderer.create(); - expect.assertions(3); + expect.assertions(15); mockA.mockClear(); mockB.mockClear(); environment.mock.resolve(TestQuery, response); const mockACalls = mockA.mock.calls; const mockBCalls = mockB.mock.calls; const owner = createOperationDescriptor(TestQuery, variables); - expect(mockACalls).toEqual([ - [ - { - error: null, - props: { - node: { - id: '4', - - __fragments: { - TestFragment: {}, - }, + expectToBeRendered(mockA, { + error: null, + data: { + node: { + id: '4', - __fragmentOwner: owner.request, - __id: '4', - }, + __fragments: { + TestFragment: {}, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }, - ], - ]); - expect(mockBCalls).toEqual([ - [ - { - error: null, - props: { - node: { - id: '4', - __fragments: { - TestFragment: {}, - }, + __fragmentOwner: owner.request, + __id: '4', + }, + }, + }); + expectToBeRendered(mockB, { + error: null, + data: { + node: { + id: '4', - __fragmentOwner: owner.request, - __id: '4', - }, + __fragments: { + TestFragment: {}, }, - rehydrated: true, - online: true, - retry: expect.any(Function), + + __fragmentOwner: owner.request, + __id: '4', }, - ], - ]); + }, + }); expect(renderer.toJSON()).toEqual(['A', 'B']); }); }); @@ -1086,13 +1065,14 @@ describe('ReactRelayQueryRenderer', () => { }); it('renders the query results', () => { - expect.assertions(3); + expect.assertions(7); render.mockClear(); environment.mock.resolve(TestQuery, response); const owner = createOperationDescriptor(TestQuery, variables); - expect({ + + expectToBeRendered(render, { error: null, - props: { + data: { node: { id: '4', @@ -1104,10 +1084,7 @@ describe('ReactRelayQueryRenderer', () => { __id: '4', }, }, - rehydrated: true, - online: true, - retry: expect.any(Function), - }).toBeRendered(); + }); }); it('subscribes to the root fragment', () => { @@ -1177,13 +1154,7 @@ describe('ReactRelayQueryRenderer', () => { it('renders a pending state', () => { render.mockClear(); renderer.getInstance().setProps(nextProps); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added, - }).toBeRendered(); + expectToBeLoading(render); }); }); @@ -1245,13 +1216,7 @@ describe('ReactRelayQueryRenderer', () => { it('renders the pending state', () => { renderer.getInstance().setProps(nextProps); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added, - }).toBeRendered(); + expectToBeLoading(render); }); it('publishes and notifies the store with changes', () => { @@ -1337,13 +1302,8 @@ describe('ReactRelayQueryRenderer', () => { it('renders the pending and previous state', () => { environment.mockClear(); renderer.getInstance().setProps(nextProps); - expect({ - error: null, - props: null, - retry: expect.any(Function), - rehydrated: true, - online: true, // added, - }).toBeRendered(); + + expectToBeLoading(render); }); it('publishes and notifies the store with changes', () => { @@ -1432,18 +1392,19 @@ describe('ReactRelayQueryRenderer', () => { // @TODO T28041408 Test aborted mount using unstable_flushSync() rather than // throwing once the test renderer exposes such a method. it('should ignore data changes before mount', () => { - class ErrorBoundary extends React.Component { + class ErrorBoundary extends React.Component { state = { error: null }; componentDidCatch(error) { this.setState({ error }); } + render() { return this.state.error === null ? this.props.children : null; } } - render.mockImplementation(({ props }) => { - const error = Error('Make mount fail intentionally'); + render.mockImplementation(() => { + const error: any = Error('Make mount fail intentionally'); // Don't clutter the test console with React's error log error.suppressReactErrorLogging = true; throw error; @@ -1517,7 +1478,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); environment.mock.resolve(TestQuery, response); - expect(render).toBeCalledTimes(1); + expect(render).toBeCalledTimes(2); const readyState = render.mock.calls[0][0]; expect(readyState.retry).not.toBe(null); environment.mockClear(); @@ -1545,7 +1506,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); environment.mock.resolve(TestQuery, response); - expect(render).toBeCalledTimes(1); + expect(render).toBeCalledTimes(2); const readyState = render.mock.calls[0][0]; expect(readyState.retry).not.toBe(null); environment.mockClear(); diff --git a/__tests__/RelayOfflineHydrate-test.tsx b/__tests__/RelayOfflineHydrate-test.tsx index 73f3d3a..e083f37 100644 --- a/__tests__/RelayOfflineHydrate-test.tsx +++ b/__tests__/RelayOfflineHydrate-test.tsx @@ -1,3 +1,8 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable prefer-const */ +/* eslint-disable max-len */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ jest.useFakeTimers(); jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock')); @@ -8,13 +13,14 @@ const Scheduler = require('scheduler'); //import { ReactRelayContext } from "react-relay"; -import { useQuery, RelayEnvironmentProvider, NETWORK_ONLY, Store, RecordSource, Environment, useRestore } from '../src'; +import { useQuery, Store, RecordSource, useRestore } from '../src'; +import { RelayEnvironmentProvider } from 'relay-hooks'; import * as ReactTestRenderer from 'react-test-renderer'; //import readContext from "react-relay/lib/readContext"; -import { createOperationDescriptor, Network, Observable, REF_KEY, ROOT_ID, ROOT_TYPE } from 'relay-runtime'; +import { createOperationDescriptor } from 'relay-runtime'; import { generateAndCompile, simpleClone } from 'relay-test-utils-internal'; import { createMockEnvironment } from './RelayModernEnvironmentMock'; @@ -30,17 +36,17 @@ function expectToBeRendered(renderFn, readyState) { renderFn.mockClear(); }*/ -const QueryRendererHook = (props) => { - const { render, query, variables, cacheConfig, fetchKey } = props; - const { cached, ...relays } = useQuery(query, variables, { +const QueryRendererHook = (props: any) => { + const { render, query, variables, cacheConfig } = props; + const queryData = useQuery(query, variables, { networkCacheConfig: cacheConfig, //fetchKey }); - return {render(relays)}; + return {render(queryData)}; }; -const ReactRelayQueryRenderer = (props) => ( +const ReactRelayQueryRenderer = (props: any) => ( @@ -48,7 +54,7 @@ const ReactRelayQueryRenderer = (props) => ( const NOT_REHYDRATED = 'NOT_REHYDRATED'; -const QueryRendererUseRestore = (props): any => { +const QueryRendererUseRestore = (props: any): any => { const rehydrated = useRestore(props.environment); if (!rehydrated) { return NOT_REHYDRATED; @@ -61,8 +67,89 @@ const QueryRendererUseRestore = (props): any => { ); }; -function sleep(ms) { +/*function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); +}*/ + +function expectToBeRendered( + renderSpy, + readyState: { + data: any; + error: Error | null; + }, +): void { + // Ensure useEffect is called before other timers + + expect(renderSpy).toBeCalledTimes(2); + + expect(renderSpy.mock.calls[0][0].isLoading).toEqual(true); + expect(renderSpy.mock.calls[0][0].error).toEqual(null); + + const actualResult = renderSpy.mock.calls[1][0]; + expect(renderSpy.mock.calls[1][0].isLoading).toEqual(false); + + expect(actualResult.data).toEqual(readyState.data); + expect(actualResult.error).toEqual(readyState.error); + expect(actualResult.retry).toEqual(expect.any(Function)); +} + +function expectToBeLoading(renderSpy): void { + // Ensure useEffect is called before other timers + + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(true); + expect(actualResult.error).toEqual(null); + expect(actualResult.data).toEqual(null); +} + +function expectToBeNotLoading(renderSpy): void { + // Ensure useEffect is called before other timers + + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(false); + expect(actualResult.error).toEqual(null); + expect(actualResult.data).toEqual(null); +} + +function expectToBeError(renderSpy, error): void { + // Ensure useEffect is called before other timers + + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(false); + expect(actualResult.error).toEqual(error); + expect(actualResult.data).toEqual(null); +} + +function expectHydrate(environment, rehydrated, online): void { + expect(environment.isOnline()).toEqual(online); + expect(environment.isRehydrated()).toEqual(rehydrated); +} + +function expectToBeRenderedFirst( + renderSpy, + readyState: { + data: any; + error: Error | null; + isLoading?: boolean; + }, +): void { + // Ensure useEffect is called before other timers + + expect(renderSpy).toBeCalledTimes(1); + const { isLoading = false } = readyState; + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(isLoading); + + expect(actualResult.data).toEqual(readyState.data); + expect(actualResult.error).toEqual(readyState.error); + expect(actualResult.retry).toEqual(expect.any(Function)); } describe('ReactRelayQueryRenderer', () => { @@ -75,6 +162,7 @@ describe('ReactRelayQueryRenderer', () => { let data; let initialData; let owner; + let ownerTTL; let onlineGetter; const variables = { id: '4' }; const restoredState = { @@ -90,10 +178,10 @@ describe('ReactRelayQueryRenderer', () => { 'node(id:"4")': { __ref: '4' }, }, }; - const propsInitialState = (owner, rehydrated, online = rehydrated) => { + const dataInitialState = (owner, isLoading = false, ttl = false) => { return { error: null, - props: { + data: { node: { id: '4', name: 'Zuck', @@ -102,19 +190,18 @@ describe('ReactRelayQueryRenderer', () => { TestFragment: {}, }, - __fragmentOwner: owner.request, + __fragmentOwner: ttl ? ownerTTL.request : owner.request, __id: '4', }, }, - rehydrated, - online - retry: expect.any(Function), + isLoading, }; }; - const propsRestoredState = (owner, online = true) => { + + const dataRestoredState = (owner, isLoading = false, ttl = false) => { return { error: null, - props: { + data: { node: { id: '4', name: 'ZUCK', @@ -123,13 +210,11 @@ describe('ReactRelayQueryRenderer', () => { TestFragment: {}, }, - __fragmentOwner: owner.request, + __fragmentOwner: ttl ? ownerTTL.request : owner.request, __id: '4', }, }, - rehydrated: true, - online, - retry: expect.any(Function), + isLoading, }; }; @@ -139,7 +224,6 @@ describe('ReactRelayQueryRenderer', () => { rehydrated: true, online: true, retry: expect.any(Function), - // @ts-ignore }; const loadingStateRehydratedOffline = { @@ -148,7 +232,6 @@ describe('ReactRelayQueryRenderer', () => { rehydrated: true, online: false, retry: expect.any(Function), - // @ts-ignore }; const loadingStateNotRehydrated = { @@ -157,7 +240,6 @@ describe('ReactRelayQueryRenderer', () => { rehydrated: false, online: false, retry: expect.any(Function), - // @ts-ignore }; ({ TestQuery } = generateAndCompile(` @@ -212,10 +294,13 @@ describe('ReactRelayQueryRenderer', () => { describe('rehydrate the environment when online', () => { describe('no initial state', () => { beforeEach(async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage() }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage() }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = createMockEnvironment({ store }); }); @@ -233,7 +318,7 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(loadingStateRehydrated).toBeRendered(); + expectHydrate(environment, true, true); }); it('without useRestore', () => { @@ -246,19 +331,22 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(loadingStateNotRehydrated).toBeRendered(); + expectHydrate(environment, false, false); render.mockClear(); jest.runAllTimers(); - expect(loadingStateRehydrated).toBeRendered(); + expectHydrate(environment, true, true); }); }); describe('initial state', () => { beforeEach(async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = createMockEnvironment({ store }); }); @@ -276,7 +364,9 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsInitialState(owner, true)).toBeRendered(); + + expectHydrate(environment, true, true); + expectToBeRenderedFirst(render, dataInitialState(owner)); }); it('without useRestore', () => { @@ -289,7 +379,8 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(propsInitialState(owner, false)).toBeRendered(); + expectHydrate(environment, false, false); + expectToBeRenderedFirst(render, dataInitialState(owner)); render.mockClear(); jest.runAllTimers(); @@ -305,7 +396,8 @@ describe('ReactRelayQueryRenderer', () => { storage: createPersistedStorage(restoredState), initialState: { ...data }, }), - { storage: createPersistedStorage(), defaultTTL: -1 }, + { storage: createPersistedStorage() }, + { queryCacheExpirationTime: null }, ); environment = createMockEnvironment({ store }); }); @@ -323,7 +415,9 @@ describe('ReactRelayQueryRenderer', () => { expect(instance.toJSON()).toEqual(NOT_REHYDRATED); render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner)).toBeRendered(); + + expectHydrate(environment, true, true); + expectToBeRenderedFirst(render, dataRestoredState(owner)); }); it('without useRestore', () => { @@ -336,21 +430,27 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(propsInitialState(owner, false)).toBeRendered(); + + expectHydrate(environment, false, false); + expectToBeRenderedFirst(render, dataInitialState(owner)); render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner)).toBeRendered(); + expectHydrate(environment, true, true); + expectToBeRenderedFirst(render, dataRestoredState(owner)); }); }); describe(' no initial state, with restored state', () => { beforeEach(async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage(restoredState) }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage(restoredState) }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = createMockEnvironment({ store }); }); @@ -368,7 +468,9 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner)).toBeRendered(); + + expectHydrate(environment, true, true); + expectToBeRenderedFirst(render, dataRestoredState(owner)); }); it('without useRestore', () => { @@ -381,11 +483,13 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(loadingStateNotRehydrated).toBeRendered(); + expectHydrate(environment, false, false); + expectToBeNotLoading(render); render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner)).toBeRendered(); + expectHydrate(environment, true, true); + expectToBeRenderedFirst(render, dataRestoredState(owner)); }); }); }); @@ -393,10 +497,13 @@ describe('ReactRelayQueryRenderer', () => { describe('rehydrate the environment when offline', () => { describe('no initial state', () => { beforeEach(async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage() }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage() }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = createMockEnvironment({ store }); onlineGetter = jest.spyOn(window.navigator, 'onLine', 'get'); @@ -417,7 +524,9 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(loadingStateRehydratedOffline).toBeRendered(); + + expectToBeNotLoading(render); + expectHydrate(environment, true, false); }); it('without useRestore', () => { @@ -430,19 +539,26 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(loadingStateNotRehydrated).toBeRendered(); + + expectToBeNotLoading(render); + expectHydrate(environment, false, false); render.mockClear(); jest.runAllTimers(); - expect(loadingStateRehydratedOffline).toBeRendered(); + + expectToBeNotLoading(render); + expectHydrate(environment, true, false); }); }); describe('initial state', () => { beforeEach(async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = createMockEnvironment({ store }); }); @@ -460,7 +576,10 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsInitialState(owner, true, false)).toBeRendered(); + + expectHydrate(environment, true, false); + expectToBeRenderedFirst(render, dataInitialState(owner)); + //expect(propsInitialState(owner, true, false)).toBeRendered(); }); it('without useRestore', () => { @@ -473,7 +592,8 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(propsInitialState(owner, false)).toBeRendered(); + expectHydrate(environment, false, false); + expectToBeRenderedFirst(render, dataInitialState(owner)); render.mockClear(); jest.runAllTimers(); @@ -484,10 +604,13 @@ describe('ReactRelayQueryRenderer', () => { describe('initial state is different from restored state', () => { beforeEach(async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage(restoredState), initialState: { ...data } }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage(restoredState), initialState: { ...data } }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = createMockEnvironment({ store }); }); @@ -505,7 +628,9 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner, false)).toBeRendered(); + + expectHydrate(environment, true, false); + expectToBeRenderedFirst(render, dataRestoredState(owner)); }); it('without useRestore', () => { @@ -518,20 +643,27 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(propsInitialState(owner, false)).toBeRendered(); + + expectHydrate(environment, false, false); + expectToBeRenderedFirst(render, dataInitialState(owner)); render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner, false)).toBeRendered(); + + expectHydrate(environment, true, false); + expectToBeRenderedFirst(render, dataRestoredState(owner)); }); }); describe(' no initial state, with restored state', () => { beforeEach(async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage(restoredState) }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage(restoredState) }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = createMockEnvironment({ store }); }); @@ -549,7 +681,9 @@ describe('ReactRelayQueryRenderer', () => { render.mockClear(); jest.runAllTimers(); - expect(propsRestoredState(owner, false)).toBeRendered(); + + expectHydrate(environment, true, false); + expectToBeRenderedFirst(render, dataRestoredState(owner)); }); /* @@ -566,11 +700,15 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(loadingStateNotRehydrated).toBeRendered(); + + expectHydrate(environment, false, false); + expectToBeNotLoading(render); render.mockClear(); jest.runAllTimers(); - expect(loadingStateRehydratedOffline).toBeRendered(); + + expectHydrate(environment, true, false); + expectToBeNotLoading(render); }); }); }); diff --git a/__tests__/RelayOfflineTTL-test.tsx b/__tests__/RelayOfflineTTL-test.tsx index 1690e95..d4a0192 100644 --- a/__tests__/RelayOfflineTTL-test.tsx +++ b/__tests__/RelayOfflineTTL-test.tsx @@ -1,10 +1,15 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable prefer-const */ jest.useFakeTimers(); jest.mock('scheduler', () => require.requireActual('scheduler/unstable_mock')); import * as React from 'react'; const Scheduler = require('scheduler'); -import { useQuery, RelayEnvironmentProvider, Store, RecordSource, useRestore } from '../src'; +import { useQuery, Store, RecordSource, useRestore } from '../src'; +import { RelayEnvironmentProvider } from 'relay-hooks'; import * as ReactTestRenderer from 'react-test-renderer'; import { createOperationDescriptor } from 'relay-runtime'; @@ -12,19 +17,103 @@ import { generateAndCompile, simpleClone } from 'relay-test-utils-internal'; import { createMockEnvironment } from './RelayModernEnvironmentMock'; import { createPersistedStorage } from './Utils'; -const QueryRendererHook = (props) => { +const QueryRendererHook = (props: any) => { const { render, query, variables, cacheConfig, ttl } = props; - const { cached, ...relays } = useQuery(query, variables, { - networkCacheConfig: cacheConfig, - ttl, + let networkCacheConfig = cacheConfig; + if (ttl) { + if (!networkCacheConfig) { + networkCacheConfig = { ttl }; + } else { + networkCacheConfig.ttl = ttl; + } + } + const queryData = useQuery(query, variables, { + networkCacheConfig, }); - return {render(relays)}; + return {render(queryData)}; }; +function expectToBeRendered( + renderSpy, + readyState: { + data: any; + error: Error | null; + }, +): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(2); + + expect(renderSpy.mock.calls[0][0].isLoading).toEqual(true); + expect(renderSpy.mock.calls[0][0].error).toEqual(null); + + const actualResult = renderSpy.mock.calls[1][0]; + expect(renderSpy.mock.calls[1][0].isLoading).toEqual(false); + + expect(actualResult.data).toEqual(readyState.data); + expect(actualResult.error).toEqual(readyState.error); + expect(actualResult.retry).toEqual(expect.any(Function)); +} + +function expectToBeLoading(renderSpy): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(true); + expect(actualResult.error).toEqual(null); + expect(actualResult.data).toEqual(null); +} + +function expectToBeError(renderSpy, error): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(false); + expect(actualResult.error).toEqual(error); + expect(actualResult.data).toEqual(null); +} + +function expectToBeRenderedFirst( + renderSpy, + readyState: { + data: any; + error: Error | null; + isLoading?: boolean; + }, +): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + const { isLoading = false } = readyState; + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(isLoading); + + expect(actualResult.data).toEqual(readyState.data); + expect(actualResult.error).toEqual(readyState.error); + expect(actualResult.retry).toEqual(expect.any(Function)); +} + const NOT_REHYDRATED = 'NOT_REHYDRATED'; -const QueryRendererUseRestore = (props): any => { +const QueryRendererUseRestore = (props: any): any => { const rehydrated = useRestore(props.environment); if (!rehydrated) { return NOT_REHYDRATED; @@ -56,11 +145,12 @@ describe('ReactRelayQueryRenderer', () => { let data; let initialData; let owner; + let ownerTTL; const variables = { id: '4' }; - const propsInitialState = (owner, rehydrated, online = rehydrated) => { + const dataInitialState = (owner, isLoading, ttl = false) => { return { error: null, - props: { + data: { node: { id: '4', name: 'Zuck', @@ -69,25 +159,14 @@ describe('ReactRelayQueryRenderer', () => { TestFragment: {}, }, - __fragmentOwner: owner.request, + __fragmentOwner: ttl ? ownerTTL.request : owner.request, __id: '4', }, }, - rehydrated, - online, - retry: expect.any(Function), + isLoading, }; }; - const loadingStateRehydrated = { - error: null, - props: null, - rehydrated: true, - online: true, - retry: expect.any(Function), - // @ts-ignore - }; - ({ TestQuery } = generateAndCompile(` query TestQuery($id: ID = "") { node(id: $id) { @@ -103,6 +182,7 @@ describe('ReactRelayQueryRenderer', () => { `)); owner = createOperationDescriptor(TestQuery, variables); + ownerTTL = createOperationDescriptor(TestQuery, variables, { ttl: 500 } as any); beforeEach(async () => { Scheduler.unstable_clearYields(); @@ -139,10 +219,13 @@ describe('ReactRelayQueryRenderer', () => { describe('Time To Live', () => { it('without TTL', async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), { - storage: createPersistedStorage(), - defaultTTL: -1, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: null }, + ); environment = createMockEnvironment({ store }); await environment.hydrate(); const instanceA = ReactTestRenderer.create( @@ -178,14 +261,17 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(loadingStateRehydrated).toBeRendered(); + expectToBeLoading(render); }); it('with defaultTTL', async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), { - storage: createPersistedStorage(), - defaultTTL: 100, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: 100 }, + ); environment = createMockEnvironment({ store }); await environment.hydrate(); const instanceA = ReactTestRenderer.create( @@ -221,7 +307,7 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(propsInitialState(owner, true)).toBeRendered(); + expectToBeRenderedFirst(render, dataInitialState(owner, false)); render.mockClear(); ReactTestRenderer.act(() => { @@ -247,14 +333,17 @@ describe('ReactRelayQueryRenderer', () => { variables={variables} />, ); - expect(loadingStateRehydrated).toBeRendered(); + expectToBeLoading(render); }); it('with custom TTL', async () => { - store = new Store(new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), { - storage: createPersistedStorage(), - defaultTTL: 100, - }); + store = new Store( + new RecordSource({ storage: createPersistedStorage(), initialState: { ...data } }), + { + storage: createPersistedStorage(), + }, + { queryCacheExpirationTime: 100 }, + ); environment = createMockEnvironment({ store }); await environment.hydrate(); const instanceA = ReactTestRenderer.create( @@ -293,7 +382,8 @@ describe('ReactRelayQueryRenderer', () => { ttl={500} />, ); - expect(propsInitialState(owner, true)).toBeRendered(); + + expectToBeRenderedFirst(render, dataInitialState(owner, false, true)); render.mockClear(); ReactTestRenderer.act(() => { @@ -320,7 +410,8 @@ describe('ReactRelayQueryRenderer', () => { ttl={500} />, ); - expect(propsInitialState(owner, true)).toBeRendered(); + + expectToBeRenderedFirst(render, dataInitialState(owner, false, true)); render.mockClear(); ReactTestRenderer.act(() => { @@ -347,7 +438,7 @@ describe('ReactRelayQueryRenderer', () => { ttl={500} />, ); - expect(loadingStateRehydrated).toBeRendered(); + expectToBeLoading(render); }); }); }); diff --git a/__tests__/relay.tsx b/__tests__/relay.tsx index 0f2d66d..57733f3 100644 --- a/__tests__/relay.tsx +++ b/__tests__/relay.tsx @@ -1,3 +1,8 @@ +/* eslint-disable max-len */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/camelcase */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /** * Copyright (c) Facebook, Inc. and its affiliates. * @@ -8,74 +13,122 @@ * @emails oncall+relay */ -"use strict"; +'use strict'; -jest.mock("scheduler", () => require("scheduler/unstable_mock")); +jest.mock('scheduler', () => require('scheduler/unstable_mock')); -const React = require("react"); -const Scheduler = require("scheduler"); +import * as React from 'react'; +const Scheduler = require('scheduler'); //import { ReactRelayContext } from "react-relay"; -import { useQuery, RelayEnvironmentProvider, STORE_THEN_NETWORK } from "../lib"; +import { useQuery } from '../lib'; -const ReactTestRenderer = require("react-test-renderer"); +import { RelayEnvironmentProvider, NETWORK_ONLY } from 'relay-hooks'; + +const ReactTestRenderer = require('react-test-renderer'); //import readContext from "react-relay/lib/readContext"; -import { - createOperationDescriptor, - Environment, - Network, - Observable, - RecordSource, - Store, - ROOT_ID -} from "relay-runtime"; -import { ROOT_TYPE } from "relay-runtime/lib/store/RelayStoreUtils"; -import { - createMockEnvironment, - generateAndCompile, - simpleClone -} from "relay-test-utils-internal"; -import { NETWORK_ONLY } from "../lib/RelayHooksType"; -/* -function expectToBeRendered(renderFn, readyState) { - // Ensure useEffect is called before other timers - ReactTestRenderer.act(() => { - jest.runAllImmediates(); - }); - expect(renderFn).toBeCalledTimes(1); - expect(renderFn.mock.calls[0][0]).toEqual(readyState); - renderFn.mockClear(); -}*/ - -const QueryRendererHook = props => { - const { - render, - fetchPolicy = NETWORK_ONLY, - query, - variables, - cacheConfig, - fetchKey, - skip - } = props; - const { cached, ...relays } = useQuery(query, variables, { +import { createOperationDescriptor, Environment, Network, Observable, RecordSource, Store, ROOT_ID } from 'relay-runtime'; +import { ROOT_TYPE } from 'relay-runtime/lib/store/RelayStoreUtils'; +import { createMockEnvironment, generateAndCompile } from 'relay-test-utils-internal'; + +function expectToBeRendered( + renderSpy, + readyState: { + data: any; + error: Error | null; + }, +): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(2); + + expect(renderSpy.mock.calls[0][0].isLoading).toEqual(true); + expect(renderSpy.mock.calls[0][0].error).toEqual(null); + + const actualResult = renderSpy.mock.calls[1][0]; + expect(renderSpy.mock.calls[1][0].isLoading).toEqual(false); + + expect(actualResult.data).toEqual(readyState.data); + expect(actualResult.error).toEqual(readyState.error); + expect(actualResult.retry).toEqual(expect.any(Function)); +} + +function expectToBeLoading(renderSpy): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(true); + expect(actualResult.error).toEqual(null); + expect(actualResult.data).toEqual(null); +} + +function expectToBeError(renderSpy, error): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(false); + expect(actualResult.error).toEqual(error); + expect(actualResult.data).toEqual(null); +} + +function expectToBeRenderedFirst( + renderSpy, + readyState: { + data: any; + error: Error | null; + isLoading?: boolean; + }, +): void { + // Ensure useEffect is called before other timers + + ReactTestRenderer.act(() => { + jest.runAllImmediates(); + }); + expect(renderSpy).toBeCalledTimes(1); + const { isLoading = false } = readyState; + + const actualResult = renderSpy.mock.calls[0][0]; + expect(actualResult.isLoading).toEqual(isLoading); + + expect(actualResult.data).toEqual(readyState.data); + expect(actualResult.error).toEqual(readyState.error); + expect(actualResult.retry).toEqual(expect.any(Function)); +} + +const QueryRendererHook = (props: any) => { + const { render, fetchPolicy = NETWORK_ONLY, query, variables, cacheConfig, fetchKey, skip } = props; + const queryData = useQuery(query, variables, { networkCacheConfig: cacheConfig, fetchPolicy, fetchKey, - skip + skip, }); - return {render(relays)}; + return {render(queryData)}; }; -const ReactRelayQueryRenderer = props => ( +const ReactRelayQueryRenderer = (props: any) => ( ); -describe("ReactRelayQueryRenderer", () => { +describe('ReactRelayQueryRenderer', () => { let TestQuery; let cacheConfig; @@ -87,25 +140,27 @@ describe("ReactRelayQueryRenderer", () => { const response = { data: { node: { - __typename: "User", - id: "4", - name: "Zuck" - } - } + __typename: 'User', + id: '4', + name: 'Zuck', + }, + }, }; - class PropsSetter extends React.Component { + class PropsSetter extends React.Component { constructor(props) { super(props); this.state = { - props: null + props: null, }; } + setProps(props) { this.setState({ props }); } + render() { - const child = React.Children.only(this.props.children); + const child: any = React.Children.only(this.props.children); if (this.state.props) { return React.cloneElement(child, this.state.props); } @@ -115,14 +170,6 @@ describe("ReactRelayQueryRenderer", () => { beforeEach(() => { jest.resetModules(); - expect.extend({ - toBeRendered(readyState) { - const calls = render.mock.calls; - expect(calls.length).toBe(1); - expect(calls[0][0]).toEqual(readyState); - return { pass: true }; - } - }); environment = createMockEnvironment(); store = environment.getStore(); @@ -140,7 +187,7 @@ describe("ReactRelayQueryRenderer", () => { `)); render = jest.fn(() =>
); - variables = { id: "4" }; + variables = { id: '4' }; }); afterEach(async () => { @@ -148,11 +195,8 @@ describe("ReactRelayQueryRenderer", () => { await Promise.resolve(); }); - describe("when initialized", () => { - - - - it("skip", () => { + describe('when initialized', () => { + it('skip', () => { const renderer = ReactTestRenderer.create( { variables={variables} skip={true} /> - + , ); expect(environment.execute.mock.calls.length).toBe(0); environment.mockClear(); @@ -174,16 +218,15 @@ describe("ReactRelayQueryRenderer", () => { query: TestQuery, render, variables, - skip: false + skip: false, }); expect(environment.execute.mock.calls.length).toBe(1); render.mockClear(); environment.mock.resolve(TestQuery, response); const owner = createOperationDescriptor(TestQuery, variables); - expect({ - error: null, - props: { + expectToBeRendered(render, { + data: { node: { id: '4', @@ -195,12 +238,11 @@ describe("ReactRelayQueryRenderer", () => { __id: '4', }, }, - retry: expect.any(Function), - }).toBeRendered(); - + error: null, + }); }); - it("fetches the query", () => { + it('fetches the query', () => { ReactTestRenderer.create( { environment={environment} render={render} variables={variables} - /> + />, ); - expect( - environment.mock.isLoading(TestQuery, variables, cacheConfig) - ).toBe(true); + expect(environment.mock.isLoading(TestQuery, variables, cacheConfig)).toBe(true); }); - describe("when constructor fires multiple times", () => { - describe("when store does not have snapshot and fetch does not return snapshot", () => { - it("fetches the query only once, renders loading state", () => { + describe('when constructor fires multiple times', () => { + describe('when store does not have snapshot and fetch does not return snapshot', () => { + it('fetches the query only once, renders loading state', () => { environment.mockClear(); function Child(props) { // NOTE the unstable_yield method will move to the static renderer. @@ -244,18 +284,15 @@ describe("ReactRelayQueryRenderer", () => { } } const renderer = ReactTestRenderer.create(, { - unstable_isConcurrent: true + unstable_isConcurrent: true, }); // Flush some of the changes, but don't commit Scheduler.unstable_flushNumberOfYields(2); - expect(Scheduler.unstable_clearYields()).toEqual(["A", "B"]); + expect(Scheduler.unstable_clearYields()).toEqual(['A', 'B']); expect(renderer.toJSON()).toEqual(null); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added - }).toBeRendered(); + + expectToBeLoading(render); expect(environment.execute.mock.calls.length).toBe(1); render.mockClear(); @@ -264,28 +301,25 @@ describe("ReactRelayQueryRenderer", () => { renderer.update(); }); expect(environment.execute.mock.calls.length).toBe(1); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added - }).toBeRendered(); + + expectToBeLoading(render); }); }); - describe("when store has a snapshot", () => { - it("fetches the query only once, renders snapshot from store", () => { + describe('when store has a snapshot', () => { + it('fetches the query only once, renders snapshot from store', () => { environment.mockClear(); environment.applyUpdate({ - storeUpdater: _store => { + storeUpdater: (_store) => { let root = _store.get(ROOT_ID); if (!root) { root = _store.create(ROOT_ID, ROOT_TYPE); } - const user = _store.create("4", "User"); - user.setValue("4", "id"); - user.setValue("Zuck", "name"); - root.setLinkedRecord(user, "node", { id: "4" }); - } + const user = _store.create('4', 'User'); + user.setValue('4', 'id'); + user.setValue('Zuck', 'name'); + root.setLinkedRecord(user, 'node', { id: '4' }); + }, }); function Child(props) { @@ -314,30 +348,29 @@ describe("ReactRelayQueryRenderer", () => { } } const renderer = ReactTestRenderer.create(, { - unstable_isConcurrent: true + unstable_isConcurrent: true, }); const owner = createOperationDescriptor(TestQuery, variables); // Flush some of the changes, but don't commit Scheduler.unstable_flushNumberOfYields(2); - expect(Scheduler.unstable_clearYields()).toEqual(["A", "B"]); + expect(Scheduler.unstable_clearYields()).toEqual(['A', 'B']); expect(renderer.toJSON()).toEqual(null); - expect({ - error: null, - props: { + expectToBeRendered(render, { + data: { node: { - id: "4", + id: '4', __fragments: { - TestFragment: {} + TestFragment: {}, }, __fragmentOwner: owner.request, - __id: "4" - } + __id: '4', + }, }, - retry: expect.any(Function) - }).toBeRendered(); + error: null, + }); expect(environment.execute.mock.calls.length).toBe(1); render.mockClear(); @@ -346,31 +379,30 @@ describe("ReactRelayQueryRenderer", () => { renderer.update(); }); expect(environment.execute.mock.calls.length).toBe(1); - expect({ - error: null, - props: { + expectToBeRendered(render, { + data: { node: { - id: "4", + id: '4', __fragments: { - TestFragment: {} + TestFragment: {}, }, __fragmentOwner: owner.request, - __id: "4" - } + __id: '4', + }, }, - retry: expect.any(Function) - }).toBeRendered(); + error: null, + }); }); }); - describe("when fetch returns a response synchronously first time", () => { - it("fetches the query once, always renders snapshot returned by fetch", () => { + describe('when fetch returns a response synchronously first time', () => { + it('fetches the query once, always renders snapshot returned by fetch', () => { const fetch = jest.fn().mockReturnValueOnce(response); store = new Store(new RecordSource()); environment = new Environment({ network: Network.create(fetch), - store + store, }); function Child(props) { @@ -399,30 +431,29 @@ describe("ReactRelayQueryRenderer", () => { } } const renderer = ReactTestRenderer.create(, { - unstable_isConcurrent: true + unstable_isConcurrent: true, }); const owner = createOperationDescriptor(TestQuery, variables); // Flush some of the changes, but don't commit Scheduler.unstable_flushNumberOfYields(2); - expect(Scheduler.unstable_clearYields()).toEqual(["A", "B"]); + expect(Scheduler.unstable_clearYields()).toEqual(['A', 'B']); expect(renderer.toJSON()).toEqual(null); - expect({ - error: null, - props: { + expectToBeRendered(render, { + data: { node: { - id: "4", + id: '4', __fragments: { - TestFragment: {} + TestFragment: {}, }, __fragmentOwner: owner.request, - __id: "4" - } + __id: '4', + }, }, - retry: expect.any(Function) - }).toBeRendered(); + error: null, + }); expect(fetch.mock.calls.length).toBe(1); render.mockClear(); @@ -431,33 +462,33 @@ describe("ReactRelayQueryRenderer", () => { renderer.update(); }); expect(fetch.mock.calls.length).toBe(1); - expect({ - error: null, - props: { + + expectToBeRendered(render, { + data: { node: { - id: "4", + id: '4', __fragments: { - TestFragment: {} + TestFragment: {}, }, __fragmentOwner: owner.request, - __id: "4" - } + __id: '4', + }, }, - retry: expect.any(Function) - }).toBeRendered(); + error: null, + }); }); }); describe('when variables change before first result has completed', () => { it('correctly renders data for new variables', () => { environment = createMockEnvironment(); let pendingRequests = []; - jest.spyOn(environment, 'execute').mockImplementation(request => { - const nextRequest = { request }; + jest.spyOn(environment, 'execute').mockImplementation((request: any) => { + const nextRequest: any = { request }; pendingRequests = pendingRequests.concat([nextRequest]); - return Observable.create(sink => { - nextRequest.resolve = resp => { + return Observable.create((sink) => { + nextRequest.resolve = (resp) => { environment.commitPayload(request.operation, resp.data); sink.next(resp); sink.complete(); @@ -482,9 +513,8 @@ describe("ReactRelayQueryRenderer", () => { const firstRequest = pendingRequests[0]; const firstOwner = firstRequest.request.operation; firstRequest.resolve(response); - expect({ - error: null, - props: { + expectToBeRendered(render, { + data: { node: { id: '4', @@ -496,8 +526,9 @@ describe("ReactRelayQueryRenderer", () => { __id: '4', }, }, - retry: expect.any(Function), - }).toBeRendered(); + error: null, + }); + render.mockClear(); renderer.getInstance().setProps({ @@ -539,11 +570,11 @@ describe("ReactRelayQueryRenderer", () => { // request thirdRequest.resolve(thirdResponse); secondRequest.resolve(secondResponse); - expect(render.mock.calls.length).toEqual(3); - const lastRender = render.mock.calls[2][0]; + expect(render.mock.calls.length).toEqual(4); + const lastRender = render.mock.calls[3][0]; expect(lastRender).toEqual({ error: null, - props: { + data: { node: { id: '6', @@ -555,6 +586,7 @@ describe("ReactRelayQueryRenderer", () => { __id: '6', }, }, + isLoading: false, retry: expect.any(Function), }); }); @@ -571,9 +603,7 @@ describe("ReactRelayQueryRenderer", () => { />, ); variables = { id: '' }; - expect( - environment.mock.isLoading(TestQuery, variables, cacheConfig), - ).toBe(true); + expect(environment.mock.isLoading(TestQuery, variables, cacheConfig)).toBe(true); }); it('renders with a default ready state', () => { @@ -586,16 +616,12 @@ describe("ReactRelayQueryRenderer", () => { variables={variables} />, ); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added, - }).toBeRendered(); + expectToBeLoading(render); }); it('if initial render set from store, skip loading state when data for query is already available', () => { environment.applyUpdate({ - storeUpdater: _store => { + storeUpdater: (_store) => { let root = _store.get(ROOT_ID); if (!root) { root = _store.create(ROOT_ID, ROOT_TYPE); @@ -617,9 +643,8 @@ describe("ReactRelayQueryRenderer", () => { />, ); const owner = createOperationDescriptor(TestQuery, variables); - expect({ - error: null, - props: { + expectToBeRenderedFirst(render, { + data: { node: { id: '4', @@ -631,11 +656,10 @@ describe("ReactRelayQueryRenderer", () => { __id: '4', }, }, - retry: expect.any(Function), - }).toBeRendered(); + error: null, + }); }); - it('skip loading state when request could be resolved synchronously', () => { const fetch = () => response; store = new Store(new RecordSource()); @@ -653,9 +677,8 @@ describe("ReactRelayQueryRenderer", () => { />, ); const owner = createOperationDescriptor(TestQuery, variables); - expect({ - error: null, - props: { + expectToBeRenderedFirst(render, { + data: { node: { id: '4', @@ -667,13 +690,13 @@ describe("ReactRelayQueryRenderer", () => { __id: '4', }, }, - retry: expect.any(Function), - }).toBeRendered(); + error: null, + }); }); it('skip loading state when request failed synchronously', () => { const error = new Error('Mock Network Error'); - const fetch = () => error; + const fetch: any = () => error; store = new Store(new RecordSource()); environment = new Environment({ network: Network.create(fetch), @@ -688,11 +711,10 @@ describe("ReactRelayQueryRenderer", () => { variables={variables} />, ); - expect({ + expectToBeRenderedFirst(render, { + data: null, error: error, - props: null, - retry: expect.any(Function), - }).toBeRendered(); + }); }); }); @@ -879,23 +901,18 @@ describe("ReactRelayQueryRenderer", () => { }); */ - describe("when new props are received", () => { + describe('when new props are received', () => { let renderer; beforeEach(() => { renderer = ReactTestRenderer.create( - - + + , ); }); - it("does not update if all props are ===", () => { + it('does not update if all props are ===', () => { environment.mockClear(); render.mockClear(); @@ -904,23 +921,18 @@ describe("ReactRelayQueryRenderer", () => { environment, query: TestQuery, render, - variables + variables, }); expect(environment.execute).not.toBeCalled(); expect(render).toBeCalled(); // expect(render).not.toBeCalled(); changed hooks renderer with same result }); - it("does not update if variables are equivalent", () => { + it('does not update if variables are equivalent', () => { variables = { foo: [1] }; renderer = ReactTestRenderer.create( - - + + , ); environment.mockClear(); render.mockClear(); @@ -931,13 +943,13 @@ describe("ReactRelayQueryRenderer", () => { environment, query: TestQuery, render, - variables + variables, }); expect(environment.execute).not.toBeCalled(); expect(render).toBeCalled(); // expect(render).not.toBeCalled(); changed hooks renderer with same result }); - it("updates if `render` prop changes", () => { + it('updates if `render` prop changes', () => { render.mock.calls[0][0]; // changed, now retry is not null environment.mockClear(); render.mockClear(); @@ -948,24 +960,19 @@ describe("ReactRelayQueryRenderer", () => { environment, query: TestQuery, render, - variables + variables, }); // added - const readyState = { - error: null, - props: null, - retry: expect.any(Function) - } - expect(readyState).toBeRendered(); + expectToBeLoading(render); expect(environment.execute).not.toBeCalled(); }); - it("refetches if the `environment` prop changes", () => { + it('refetches if the `environment` prop changes', () => { expect.assertions(4); environment.mock.resolve(TestQuery, { data: { - node: null - } + node: null, + }, }); render.mockClear(); @@ -976,81 +983,63 @@ describe("ReactRelayQueryRenderer", () => { environment, query: TestQuery, render, - variables + variables, }); - expect( - environment.mock.isLoading(TestQuery, variables, cacheConfig) - ).toBe(true); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added, - }).toBeRendered(); + expect(environment.mock.isLoading(TestQuery, variables, cacheConfig)).toBe(true); + expectToBeLoading(render); }); - it("refetches if the `variables` prop changes", () => { + it('refetches if the `variables` prop changes', () => { expect.assertions(4); environment.mock.resolve(TestQuery, { data: { - node: null - } + node: null, + }, }); environment.mockClear(); render.mockClear(); // Update with different variables - variables = { id: "beast" }; + variables = { id: 'beast' }; renderer.getInstance().setProps({ environment, query: TestQuery, render, - variables + variables, }); - expect( - environment.mock.isLoading(TestQuery, variables, cacheConfig) - ).toBe(true); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added, - }).toBeRendered(); + expect(environment.mock.isLoading(TestQuery, variables, cacheConfig)).toBe(true); + expectToBeLoading(render); }); - it("refetches with default values if the `variables` prop changes", () => { + it('refetches with default values if the `variables` prop changes', () => { expect.assertions(4); environment.mock.resolve(TestQuery, { data: { - node: null - } + node: null, + }, }); environment.mockClear(); render.mockClear(); // Update with different variables variables = {}; // no `id` - const expectedVariables = { id: "" }; + const expectedVariables = { id: '' }; renderer.getInstance().setProps({ environment, query: TestQuery, render, - variables + variables, }); - expect( - environment.mock.isLoading(TestQuery, expectedVariables, cacheConfig) - ).toBe(true); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added, - }).toBeRendered(); + expect(environment.mock.isLoading(TestQuery, expectedVariables, cacheConfig)).toBe(true); + expectToBeLoading(render); }); - it("refetches if the `query` prop changes", () => { + it('refetches if the `query` prop changes', () => { expect.assertions(4); environment.mock.resolve(TestQuery, { data: { - node: null - } + node: null, + }, }); environment.mockClear(); render.mockClear(); @@ -1070,16 +1059,10 @@ describe("ReactRelayQueryRenderer", () => { environment, query: NextQuery, render, - variables + variables, }); - expect( - environment.mock.isLoading(NextQuery, variables, cacheConfig) - ).toBe(true); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added, - }).toBeRendered(); + expect(environment.mock.isLoading(NextQuery, variables, cacheConfig)).toBe(true); + expectToBeLoading(render); }); /*it("renders if the `query` prop changes to null", () => { // removed, now query is required @@ -1117,41 +1100,35 @@ describe("ReactRelayQueryRenderer", () => { }); }); - describe("when the fetch fails", () => { + describe('when the fetch fails', () => { beforeEach(() => { ReactTestRenderer.create( - + , ); }); - it("retains immediately", () => { + it('retains immediately', () => { expect.assertions(1); render.mockClear(); - environment.mock.reject(TestQuery, new Error("fail")); + environment.mock.reject(TestQuery, new Error('fail')); expect(environment.retain.mock.calls.length).toBe(1); }); - it("renders the error and retry", () => { + it('renders the error and retry', () => { expect.assertions(3); render.mockClear(); - const error = new Error("fail"); + const error = new Error('fail'); environment.mock.reject(TestQuery, error); - expect({ - error, - props: null, - retry: expect.any(Function) - }).toBeRendered(); + expectToBeRenderedFirst(render, { + data: null, + error: error, + }); }); - it("refetch the query if `retry`", () => { + it('refetch the query if `retry`', () => { expect.assertions(4); // changed to 4 render.mockClear(); - const error = new Error("network fails"); + const error = new Error('network fails'); environment.mock.reject(TestQuery, error); const readyState = render.mock.calls[0][0]; expect(readyState.retry).not.toBe(null); @@ -1167,22 +1144,21 @@ describe("ReactRelayQueryRenderer", () => { render.mockClear(); environment.mock.resolve(TestQuery, response); const owner = createOperationDescriptor(TestQuery, variables); - expect({ - error: null, - props: { + expectToBeRendered(render, { + data: { node: { - id: "4", + id: '4', __fragments: { - TestFragment: {} + TestFragment: {}, }, __fragmentOwner: owner.request, - __id: "4" - } + __id: '4', + }, }, - retry: expect.any(Function) - }).toBeRendered(); + error: null, + }); }); }); @@ -1215,53 +1191,43 @@ describe("ReactRelayQueryRenderer", () => { } } const renderer = ReactTestRenderer.create(); - expect.assertions(3); + expect.assertions(15); mockA.mockClear(); mockB.mockClear(); environment.mock.resolve(TestQuery, response); const mockACalls = mockA.mock.calls; const mockBCalls = mockB.mock.calls; const owner = createOperationDescriptor(TestQuery, variables); - expect(mockACalls).toEqual([ - [ - { - error: null, - props: { - node: { - id: '4', - - __fragments: { - TestFragment: {}, - }, - - __fragmentOwner: owner.request, - __id: '4', - }, + expectToBeRendered(mockA, { + error: null, + data: { + node: { + id: '4', + + __fragments: { + TestFragment: {}, }, - retry: expect.any(Function), + + __fragmentOwner: owner.request, + __id: '4', }, - ], - ]); - expect(mockBCalls).toEqual([ - [ - { - error: null, - props: { - node: { - id: '4', - - __fragments: { - TestFragment: {}, - }, - - __fragmentOwner: owner.request, - __id: '4', - }, + }, + }); + expectToBeRendered(mockB, { + error: null, + data: { + node: { + id: '4', + + __fragments: { + TestFragment: {}, }, - retry: expect.any(Function), + + __fragmentOwner: owner.request, + __id: '4', }, - ], - ]); + }, + }); expect(renderer.toJSON()).toEqual(['A', 'B']); }); }); @@ -1270,12 +1236,7 @@ describe("ReactRelayQueryRenderer", () => { describe('when the fetch succeeds', () => { beforeEach(() => { ReactTestRenderer.create( - , + , ); }); @@ -1294,13 +1255,13 @@ describe("ReactRelayQueryRenderer", () => { }); it('renders the query results', () => { - expect.assertions(3); + expect.assertions(7); render.mockClear(); environment.mock.resolve(TestQuery, response); const owner = createOperationDescriptor(TestQuery, variables); - expect({ + expectToBeRendered(render, { error: null, - props: { + data: { node: { id: '4', @@ -1312,26 +1273,19 @@ describe("ReactRelayQueryRenderer", () => { __id: '4', }, }, - retry: expect.any(Function), - }).toBeRendered(); + }); }); it('subscribes to the root fragment', () => { expect.assertions(4); environment.mock.resolve(TestQuery, response); expect(environment.subscribe).toBeCalled(); - expect(environment.subscribe.mock.calls[0][0].selector.dataID).toBe( - 'client:root', - ); - expect(environment.subscribe.mock.calls[0][0].selector.node).toBe( - TestQuery.fragment, - ); - expect(environment.subscribe.mock.calls[0][0].selector.variables).toEqual( - variables, - ); + expect(environment.subscribe.mock.calls[0][0].selector.dataID).toBe('client:root'); + expect(environment.subscribe.mock.calls[0][0].selector.node).toBe(TestQuery.fragment); + expect(environment.subscribe.mock.calls[0][0].selector.variables).toEqual(variables); }); }); - describe("when props change during a fetch", () => { + describe('when props change during a fetch', () => { let NextQuery; let renderer; let nextProps; @@ -1347,33 +1301,28 @@ describe("ReactRelayQueryRenderer", () => { } `)); - variables = { id: "4" }; + variables = { id: '4' }; renderer = ReactTestRenderer.create( - - + + , ); nextProps = { environment, query: NextQuery, render, - variables + variables, }; }); - it("cancels the pending fetch", () => { + it('cancels the pending fetch', () => { const subscription = environment.execute.mock.subscriptions[0]; expect(subscription.closed).toBe(false); renderer.getInstance().setProps(nextProps); expect(subscription.closed).toBe(true); }); - it("releases the pending selection", () => { + it('releases the pending selection', () => { environment.mock.resolve(TestQuery, response); const disposeHold = environment.retain.mock.dispose; expect(disposeHold).not.toBeCalled(); @@ -1382,25 +1331,19 @@ describe("ReactRelayQueryRenderer", () => { expect(disposeHold).toBeCalled(); }); - it("retains the new selection", () => { + it('retains the new selection', () => { environment.mockClear(); renderer.getInstance().setProps(nextProps); environment.mock.resolve(NextQuery, response); - expect(environment.retain.mock.calls[0][0].root.dataID).toBe("client:root"); - expect(environment.retain.mock.calls[0][0].root.node).toBe( - NextQuery.operation - ); + expect(environment.retain.mock.calls[0][0].root.dataID).toBe('client:root'); + expect(environment.retain.mock.calls[0][0].root.node).toBe(NextQuery.operation); expect(environment.retain.mock.calls[0][0].root.variables).toEqual(variables); }); - it("renders a pending state", () => { + it('renders a pending state', () => { render.mockClear(); renderer.getInstance().setProps(nextProps); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added, - }).toBeRendered(); + expectToBeLoading(render); }); /*it("renders if the `query` prop changes to null", () => { removed, query is required @@ -1432,9 +1375,7 @@ describe("ReactRelayQueryRenderer", () => { });*/ }); - - - describe("when props change after a fetch fails", () => { + describe('when props change after a fetch fails', () => { let NextQuery; let error; let renderer; @@ -1451,64 +1392,51 @@ describe("ReactRelayQueryRenderer", () => { } `)); - variables = { id: "4" }; + variables = { id: '4' }; renderer = ReactTestRenderer.create( - - + + , ); - error = new Error("fail"); + error = new Error('fail'); environment.mock.reject(TestQuery, error); render.mockClear(); nextProps = { environment, query: NextQuery, render, - variables + variables, }; }); - it("fetches the new query", () => { + it('fetches the new query', () => { environment.mockClear(); renderer.getInstance().setProps(nextProps); - expect( - environment.mock.isLoading(NextQuery, variables, cacheConfig) - ).toBe(true); + expect(environment.mock.isLoading(NextQuery, variables, cacheConfig)).toBe(true); }); - it("retains the new selection", () => { + it('retains the new selection', () => { expect.assertions(5); environment.mockClear(); renderer.getInstance().setProps(nextProps); environment.mock.resolve(NextQuery, { data: { - node: null - } + node: null, + }, }); expect(environment.retain.mock.calls.length).toBe(1); - expect(environment.retain.mock.calls[0][0].root.dataID).toBe("client:root"); - expect(environment.retain.mock.calls[0][0].root.node).toBe( - NextQuery.operation - ); + expect(environment.retain.mock.calls[0][0].root.dataID).toBe('client:root'); + expect(environment.retain.mock.calls[0][0].root.node).toBe(NextQuery.operation); expect(environment.retain.mock.calls[0][0].root.variables).toEqual(variables); expect(environment.retain.mock.dispose).not.toBeCalled(); }); - it("renders the pending state", () => { + it('renders the pending state', () => { renderer.getInstance().setProps(nextProps); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added, - }).toBeRendered(); + expectToBeLoading(render); }); - it("publishes and notifies the store with changes", () => { + it('publishes and notifies the store with changes', () => { expect.assertions(2); environment.mockClear(); renderer.getInstance().setProps(nextProps); @@ -1518,7 +1446,7 @@ describe("ReactRelayQueryRenderer", () => { }); }); - describe("when props change after a fetch succeeds", () => { + describe('when props change after a fetch succeeds', () => { let NextQuery; let renderer; let nextProps; @@ -1536,78 +1464,65 @@ describe("ReactRelayQueryRenderer", () => { renderer = ReactTestRenderer.create( - - + + , ); environment.mock.resolve(TestQuery, { data: { node: { - __typename: "User", - id: "4", - name: "Zuck" - } - } + __typename: 'User', + id: '4', + name: 'Zuck', + }, + }, }); render.mockClear(); nextProps = { environment, query: NextQuery, render, - variables + variables, }; }); - it("disposes the root fragment subscription", () => { + it('disposes the root fragment subscription', () => { const disposeUpdate = environment.subscribe.mock.dispose; expect(disposeUpdate).not.toBeCalled(); renderer.getInstance().setProps(nextProps); expect(disposeUpdate).toBeCalled(); }); - it("fetches the new query", () => { + it('fetches the new query', () => { environment.mockClear(); renderer.getInstance().setProps(nextProps); - expect( - environment.mock.isLoading(NextQuery, variables, cacheConfig) - ).toBe(true); + expect(environment.mock.isLoading(NextQuery, variables, cacheConfig)).toBe(true); }); - it("disposes the previous selection and retains the new one", () => { + it('disposes the previous selection and retains the new one', () => { expect.assertions(6); const prevDispose = environment.retain.mock.dispose; environment.mockClear(); renderer.getInstance().setProps(nextProps); environment.mock.resolve(NextQuery, { data: { - node: null - } + node: null, + }, }); expect(environment.retain).toBeCalled(); - expect(environment.retain.mock.calls[0][0].root.dataID).toBe("client:root"); - expect(environment.retain.mock.calls[0][0].root.node).toBe( - NextQuery.operation - ); + expect(environment.retain.mock.calls[0][0].root.dataID).toBe('client:root'); + expect(environment.retain.mock.calls[0][0].root.node).toBe(NextQuery.operation); expect(environment.retain.mock.calls[0][0].root.variables).toEqual(variables); expect(prevDispose).toBeCalled(); expect(environment.retain.mock.dispose).not.toBeCalled(); }); - it("renders the pending and previous state", () => { + it('renders the pending and previous state', () => { environment.mockClear(); renderer.getInstance().setProps(nextProps); - expect({ - error: null, - props: null, - retry: expect.any(Function) // added, - }).toBeRendered(); + expectToBeLoading(render); }); - it("publishes and notifies the store with changes", () => { + it('publishes and notifies the store with changes', () => { expect.assertions(2); environment.mockClear(); renderer.getInstance().setProps(nextProps); @@ -1617,15 +1532,10 @@ describe("ReactRelayQueryRenderer", () => { }); }); - describe("when unmounted", () => { - it("releases its reference if unmounted before fetch completes", () => { + describe('when unmounted', () => { + it('releases its reference if unmounted before fetch completes', () => { const renderer = ReactTestRenderer.create( - + , ); ReactTestRenderer.act(() => { // added for execute useEffect retain @@ -1639,14 +1549,9 @@ describe("ReactRelayQueryRenderer", () => { expect(dispose).toBeCalled(); }); - it("releases its reference if unmounted after fetch completes", () => { + it('releases its reference if unmounted after fetch completes', () => { const renderer = ReactTestRenderer.create( - + , ); environment.mock.resolve(TestQuery, response); expect(environment.retain).toBeCalled(); @@ -1657,14 +1562,9 @@ describe("ReactRelayQueryRenderer", () => { expect(dispose).toBeCalled(); }); - it("aborts a pending fetch", () => { + it('aborts a pending fetch', () => { const renderer = ReactTestRenderer.create( - + , ); const subscription = environment.execute.mock.subscriptions[0]; expect(subscription.closed).toBe(false); @@ -1673,7 +1573,7 @@ describe("ReactRelayQueryRenderer", () => { }); }); - describe("multiple payloads", () => { + describe('multiple payloads', () => { let NextQuery; let renderer; let nextProps; @@ -1691,19 +1591,14 @@ describe("ReactRelayQueryRenderer", () => { renderer = ReactTestRenderer.create( - - + + , ); nextProps = { environment, query: NextQuery, render, - variables + variables, }; }); @@ -1721,23 +1616,24 @@ describe("ReactRelayQueryRenderer", () => { });*/ }); - describe("async", () => { + describe('async', () => { // Verify the component doesn't leak references if it doesn't finish mount. // @TODO T28041408 Test aborted mount using unstable_flushSync() rather than // throwing once the test renderer exposes such a method. - it("should ignore data changes before mount", () => { - class ErrorBoundary extends React.Component { + it('should ignore data changes before mount', () => { + class ErrorBoundary extends React.Component { state = { error: null }; componentDidCatch(error) { this.setState({ error }); } + render() { return this.state.error === null ? this.props.children : null; } } render.mockImplementation(({ props }) => { - const error = Error("Make mount fail intentionally"); + const error: any = Error('Make mount fail intentionally'); // Don't clutter the test console with React's error log error.suppressReactErrorLogging = true; throw error; @@ -1745,40 +1641,30 @@ describe("ReactRelayQueryRenderer", () => { ReactTestRenderer.create( - - + + , ); environment.mock.resolve(TestQuery, { data: { node: { - __typename: "User", - id: "4", - name: "Zuck" - } - } + __typename: 'User', + id: '4', + name: 'Zuck', + }, + }, }); expect(render.mock.calls).toHaveLength(1); }); }); - describe("When retry", () => { - it("uses the latest variables after initial render", () => { + describe('When retry', () => { + it('uses the latest variables after initial render', () => { const renderer = ReactTestRenderer.create( - - + + , ); environment.mock.resolve(TestQuery, response); environment.mockClear(); @@ -1788,17 +1674,17 @@ describe("ReactRelayQueryRenderer", () => { environment, query: TestQuery, render, - variables: { id: "5" } + variables: { id: '5' }, }); render.mockClear(); environment.mock.resolve(TestQuery, { data: { node: { - __typename: "User", - id: "5", - name: "Zuck" - } - } + __typename: 'User', + id: '5', + name: 'Zuck', + }, + }, }); const readyState = render.mock.calls[0][0]; expect(readyState.retry).not.toBe(null); @@ -1807,23 +1693,16 @@ describe("ReactRelayQueryRenderer", () => { readyState.retry(); expect(environment.execute).toBeCalledTimes(1); - expect( - environment.mock.getMostRecentOperation().request.variables - ).toEqual({ - id: "5" + expect(environment.mock.getMostRecentOperation().request.variables).toEqual({ + id: '5', }); }); - it("skips cache if `force` is set to true", () => { + it('skips cache if `force` is set to true', () => { ReactTestRenderer.create( - - + + , ); render.mockClear(); environment.mock.resolve(TestQuery, response); @@ -1834,18 +1713,14 @@ describe("ReactRelayQueryRenderer", () => { environment.mockClear(); readyState.retry({ force: true }); - expect( - environment.mock.isLoading(TestQuery, variables, { force: true }) - ).toBe(true); + expect(environment.mock.isLoading(TestQuery, variables, { force: true })).toBe(true); jest.runAllTimers(); environment.mockClear(); readyState.retry(); - expect( - environment.mock.isLoading(TestQuery, variables, { force: true }) - ).toBe(false); + expect(environment.mock.isLoading(TestQuery, variables, { force: true })).toBe(false); }); - it("uses cache if `force` is set to false", () => { + it('uses cache if `force` is set to false', () => { ReactTestRenderer.create( { variables={variables} cacheConfig={{ force: true }} /> - + , ); render.mockClear(); environment.mock.resolve(TestQuery, response); @@ -1866,20 +1741,15 @@ describe("ReactRelayQueryRenderer", () => { environment.mockClear(); readyState.retry({ force: false }); - expect( - environment.mock.isLoading(TestQuery, variables, { force: true }) - ).toBe(false); + expect(environment.mock.isLoading(TestQuery, variables, { force: true })).toBe(false); jest.runAllTimers(); environment.mockClear(); readyState.retry(); - expect( - environment.mock.isLoading(TestQuery, variables, { force: true }) - ).toBe(true); + expect(environment.mock.isLoading(TestQuery, variables, { force: true })).toBe(true); }); }); - - describe("fetch key", () => { + describe('fetch key', () => { let renderer; const fetchKey = 'fetchKey'; @@ -1894,12 +1764,11 @@ describe("ReactRelayQueryRenderer", () => { fetchKey={fetchKey} fetchPolicy="network-only" /> - + , ); }); - - it("does not refetches if the `fetchKey` prop not changes", () => { + it('does not refetches if the `fetchKey` prop not changes', () => { expect.assertions(2); expect(environment.execute.mock.calls.length).toBe(1); render.mockClear(); @@ -1910,12 +1779,12 @@ describe("ReactRelayQueryRenderer", () => { query: TestQuery, render, variables, - fetchKey + fetchKey, }); expect(environment.execute.mock.calls.length).toBe(0); }); - it("refetches if the `fetchKey` prop changes", () => { + it('refetches if the `fetchKey` prop changes', () => { expect.assertions(2); expect(environment.execute.mock.calls.length).toBe(1); environment.mockClear(); @@ -1926,10 +1795,9 @@ describe("ReactRelayQueryRenderer", () => { query: TestQuery, render, variables, - fetchKey: "refetchKey" + fetchKey: 'refetchKey', }); expect(environment.execute.mock.calls.length).toBe(1); }); }); }); - diff --git a/docs/ReactRelayOffline-Introduction.md b/docs/ReactRelayOffline-Introduction.md index a142028..c5ac871 100644 --- a/docs/ReactRelayOffline-Introduction.md +++ b/docs/ReactRelayOffline-Introduction.md @@ -209,45 +209,46 @@ const environment = new Environment({ network, store }, persistOfflineOptions); ```ts import { Store } from "react-relay-offline"; import { CacheOptions } from "@wora/cache-persist"; -import { CacheOptionsStore } from "@wora/relay-store"; +import { StoreOptions } from "@wora/relay-store"; -const persistOptionsStore: CacheOptionsStore = { defaultTTL: 10 * 60 * 1000 }; // default -const persistOptionsRecords: CacheOptions = {}; // default +const persistOptionsStore: CacheOptions = { }; +const persistOptionsRecords: CacheOptions = {}; +const relayStoreOptions: StoreOptions = { queryCacheExpirationTime: 10 * 60 * 1000 }; // default const recordSource = new RecordSource(persistOptionsRecords); -const store = new Store(recordSource, persistOptionsStore); +const store = new Store(recordSource, persistOptionsStore, relayStoreOptions); const environment = new Environment({ network, store }); ``` -## QueryRenderer -- Add "cached" property in render function -- Add "ttl" property in order to change default ttl in store -- `fetchPolicy` determine whether it should use data cached in the Relay store and whether to send a network request. The options are: - - `store-or-network` (default): Reuse data cached in the store; if the whole query is cached, skip the network request - - `store-and-network`: Reuse data cached in the store; always send a network request. - - `network-only`: Don't reuse data cached in the store; always send a network request. (This is the default behavior of Relay's existing `QueryRenderer`.) - - `store-only`: Reuse data cached in the store; never send a network request. +## useQuery -```ts -import { QueryRenderer } from 'react-relay-offline'; - - { -``` +`useQuery` does not take an environment as an argument. Instead, it reads the environment set in the context; this also implies that it does not set any React context. +In addition to `query` (first argument) and `variables` (second argument), `useQuery` accepts a third argument `options`. -## useQuery +**options** + +`fetchPolicy`: determine whether it should use data cached in the Relay store and whether to send a network request. The options are: + * `store-or-network` (default): Reuse data cached in the store; if the whole query is cached, skip the network request + * `store-and-network`: Reuse data cached in the store; always send a network request. + * `network-only`: Don't reuse data cached in the store; always send a network request. (This is the default behavior of Relay's existing `QueryRenderer`.) + * `store-only`: Reuse data cached in the store; never send a network request. + +`fetchKey`: [Optional] A fetchKey can be passed to force a refetch of the current query and variables when the component re-renders, even if the variables didn't change, or even if the component isn't remounted (similarly to how passing a different key to a React component will cause it to remount). If the fetchKey is different from the one used in the previous render, the current query and variables will be refetched. + +`networkCacheConfig`: [Optional] Object containing cache config options for the network layer. Note the the network layer may contain an additional query response cache which will reuse network responses for identical queries. If you want to bypass this cache completely, pass {force: true} as the value for this option. **Added the TTL property to configure a specific ttl for the query.** + +`skip`: [Optional] If skip is true, the query will be skipped entirely. + +`onComplete`: [Optional] Function that will be called whenever the fetch request has completed ```ts import { useQuery } from "react-relay-offline"; +const networkCacheConfig = { + ttl: 1000 +} const hooksProps = useQuery(query, variables, { - networkCacheConfig: cacheConfig, + networkCacheConfig, fetchPolicy, - ttl }); ``` @@ -255,10 +256,12 @@ const hooksProps = useQuery(query, variables, { ```ts import { useQuery } from "react-relay-offline"; +const networkCacheConfig = { + ttl: 1000 +} const hooksProps = useLazyLoadQuery(query, variables, { - networkCacheConfig: cacheConfig, + networkCacheConfig, fetchPolicy, - ttl }); ``` @@ -283,11 +286,6 @@ const isRehydrated = useRestore(environment); import { fetchQuery } from "react-relay-offline"; ``` -## Mutation - -```ts -import { commitMutation, graphql } from "react-relay-offline"; -``` ## Detect Network @@ -366,9 +364,10 @@ to notify any updated data in the store. ## Requirement -- Version >=8.0.0 of the react-relay library +- Version >=10.1.0 of the relay-runtime library - When a new node is created by mutation the id must be generated in the browser to use it in the optimistic response ## License React Relay Offline is [MIT licensed](./LICENSE). + diff --git a/package-lock.json b/package-lock.json index 364e49e..dd9a6ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-relay-offline", - "version": "2.3.0", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -783,11 +783,11 @@ } }, "@babel/runtime": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.5.5.tgz", - "integrity": "sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==", + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", "requires": { - "regenerator-runtime": "^0.13.2" + "regenerator-runtime": "^0.13.4" } }, "@babel/runtime-corejs3": { @@ -6327,12 +6327,19 @@ "dev": true }, "@restart/hooks": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.25.tgz", - "integrity": "sha512-m2v3N5pxTsIiSH74/sb1yW8D9RxkJidGW+5Mfwn/lHb2QzhZNlaU1su7abSyT9EGf0xS/0waLjrf7/XxQHUk7w==", + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.3.26.tgz", + "integrity": "sha512-7Hwk2ZMYm+JLWcb7R9qIXk1OoUg1Z+saKWqZXlrvFwT3w6UArVNWgxYOzf+PJoK9zZejp8okPAKTctthhXLt5g==", "requires": { - "lodash": "^4.17.15", - "lodash-es": "^4.17.15" + "lodash": "^4.17.20", + "lodash-es": "^4.17.20" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + } } }, "@types/babel__core": { @@ -6504,9 +6511,9 @@ } }, "@types/relay-runtime": { - "version": "9.1.6", - "resolved": "https://registry.npmjs.org/@types/relay-runtime/-/relay-runtime-9.1.6.tgz", - "integrity": "sha512-rdylQOGAoqjDjtzC2uKdjIqeDRcAvUsjONQB+FZld3cAP9qU4eEwTfMKJ7MdZSw2YbWRcTphczT4OpKKlLxY3Q==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/@types/relay-runtime/-/relay-runtime-10.1.5.tgz", + "integrity": "sha512-Sjm1Z3WZsDY3CeS2ypq7bz8X4U7h7tup3Gh1zPFaF2n4nKW+zzH9YKu+GgYC/IxwOJcdv1g3GPKsRovRf+OdPw==", "dev": true }, "@types/stack-utils": { @@ -6650,40 +6657,40 @@ } }, "@wora/netinfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@wora/netinfo/-/netinfo-2.0.0.tgz", - "integrity": "sha512-Yw+ELfv6k8VMAV+NxwgzCvHewpaQbLLhZfBSDwklDcpvdwvZC+0lvvtzHYNtXTMINKML6wtvcaP5WOk1sKOPxQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@wora/netinfo/-/netinfo-2.1.0.tgz", + "integrity": "sha512-ZcCdlCjsXe9XL1AtYvc+0ers3VfyR2xYlMXAiz1zCFfkbtavwHLyQn3y7UhVDVXTSiIz32AVTlD3ipe+ttzGrA==", "requires": { - "fbjs": "^1.0.0" + "fbjs": "^3.0.0" } }, "@wora/offline-first": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@wora/offline-first/-/offline-first-2.1.0.tgz", - "integrity": "sha512-6LaAHEfCPmH8Kyp9T3EmwyarXmHrzisgy2nbWU5VzO9E6P671oUkcb8wU4tq/qeB5arsZdq+subeLCMm+UE8cw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@wora/offline-first/-/offline-first-2.3.0.tgz", + "integrity": "sha512-CKwDYw0Mz67IhjQh2QXd+NO0Bzd60gvt4IeUWTdeo6iVxnnfHDTPfkg1YxD+X7QVti2OiZMtfIcxvExnA3KmHw==", "requires": { "@wora/cache-persist": "^2.1.0", - "@wora/netinfo": "^2.0.0", - "fbjs": "^1.0.0" + "@wora/netinfo": "^2.1.0", + "fbjs": "^3.0.0" } }, "@wora/relay-offline": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@wora/relay-offline/-/relay-offline-3.2.0.tgz", - "integrity": "sha512-Dwgp9M625HR8UWAyB160ZiFbxak44LzZPZ5Vc+PAFdxMq+Cc4AYMgt/UOXWJJYCXy3RX/nJjwRrD0cJ5YzOrCg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@wora/relay-offline/-/relay-offline-4.0.0.tgz", + "integrity": "sha512-qiHRl7s35dJIDw2IbClipgmcwYUmys/tgaNcp/qujazCF04O9dF23HwQ6VDOSTD8l796heBAo9IcyBqgY3Hcng==", "requires": { "@wora/cache-persist": "^2.1.0", - "@wora/offline-first": "^2.1.0", - "@wora/relay-store": "^3.2.0", - "fbjs": "^1.0.0", + "@wora/offline-first": "^2.3.0", + "@wora/relay-store": "^4.0.0", + "fbjs": "^3.0.0", "tslib": "^1.11.1", "uuid": "3.3.2" } }, "@wora/relay-store": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@wora/relay-store/-/relay-store-3.2.0.tgz", - "integrity": "sha512-WZlG8iGvunbd34cRs6YatHBv6DBW8rxa5h6pVEw3LQGMkNzDQNiCMlIKWQon+h8Ta8zv2s3AMYrlFLUl0dR17A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@wora/relay-store/-/relay-store-4.0.0.tgz", + "integrity": "sha512-i9hpQLUWx6aEHtAMI+QqrCz8cIUQadnkpiOvAiKHiJoNv/5g2eHdriNyyhn/t/OTRwAtZ9abINi2jKezHtFydQ==", "requires": { "@wora/cache-persist": "^2.1.0", "tslib": "^1.11.1" @@ -8581,7 +8588,8 @@ "core-js": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", - "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", + "dev": true }, "core-js-pure": { "version": "3.6.4", @@ -8641,6 +8649,21 @@ } } }, + "cross-fetch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.6.tgz", + "integrity": "sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==", + "requires": { + "node-fetch": "2.6.1" + }, + "dependencies": { + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + } + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -9045,6 +9068,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, "requires": { "iconv-lite": "~0.4.13" } @@ -10422,13 +10446,12 @@ } }, "fbjs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-1.0.0.tgz", - "integrity": "sha512-MUgcMEJaFhCaF1QtWGnmq9ZDRAzECTCRAF7O6UZIlAlkTs1SasiX9aP0Iw7wfD2mJ7wDTNfg2w7u5fSCwJk1OA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.0.tgz", + "integrity": "sha512-dJd4PiDOFuhe7vk4F80Mba83Vr2QuK86FoxtgPmzBqEJahncp+13YCmfoa53KHCo6OnlXLG7eeMWPfB5CrpVKg==", "requires": { - "core-js": "^2.4.1", + "cross-fetch": "^3.0.4", "fbjs-css-vars": "^1.0.0", - "isomorphic-fetch": "^2.1.1", "loose-envify": "^1.0.0", "object-assign": "^4.1.0", "promise": "^7.1.1", @@ -11774,13 +11797,10 @@ "dev": true }, "graphql": { - "version": "14.4.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.4.2.tgz", - "integrity": "sha512-6uQadiRgnpnSS56hdZUSvFrVcQ6OF9y6wkxJfKquFtHlnl7+KSuWwSJsdwiK1vybm1HgcdbpGkCpvhvsVQ0UZQ==", - "dev": true, - "requires": { - "iterall": "^1.2.2" - } + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.4.0.tgz", + "integrity": "sha512-EB3zgGchcabbsU9cFe1j+yxdzKQKAbGUWRb13DsrsMN1yyfmmIq+2+L5MqVWcDCE4V89R5AyUOi7sMOGxdsYtA==", + "dev": true }, "growly": { "version": "1.3.0", @@ -12050,6 +12070,7 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -12446,7 +12467,8 @@ "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true }, "is-string": { "version": "1.0.5", @@ -12529,6 +12551,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "dev": true, "requires": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" @@ -12636,12 +12659,6 @@ "html-escaper": "^2.0.0" } }, - "iterall": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", - "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", - "dev": true - }, "jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", @@ -15815,12 +15832,13 @@ "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true }, "lodash-es": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", - "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.20.tgz", + "integrity": "sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==" }, "lodash._reinterpolate": { "version": "3.0.0", @@ -16208,6 +16226,22 @@ "yargs": "^9.0.0" }, "dependencies": { + "fbjs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-1.0.0.tgz", + "integrity": "sha512-MUgcMEJaFhCaF1QtWGnmq9ZDRAzECTCRAF7O6UZIlAlkTs1SasiX9aP0Iw7wfD2mJ7wDTNfg2w7u5fSCwJk1OA==", + "dev": true, + "requires": { + "core-js": "^2.4.1", + "fbjs-css-vars": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, "mime-db": { "version": "1.23.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz", @@ -16781,6 +16815,7 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "dev": true, "requires": { "encoding": "^0.1.11", "is-stream": "^1.0.1" @@ -18010,9 +18045,9 @@ "dev": true }, "react": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz", - "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==", + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.11.0.tgz", + "integrity": "sha512-M5Y8yITaLmU0ynd0r1Yvfq98Rmll6q8AxaEe88c8e7LxO8fZ2cNgmFt0aGAS9wzf1Ao32NKXtCl+/tVVtkxq6g==", "dev": true, "requires": { "loose-envify": "^1.1.0", @@ -18125,6 +18160,22 @@ "yargs": "^9.0.0" }, "dependencies": { + "fbjs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-1.0.0.tgz", + "integrity": "sha512-MUgcMEJaFhCaF1QtWGnmq9ZDRAzECTCRAF7O6UZIlAlkTs1SasiX9aP0Iw7wfD2mJ7wDTNfg2w7u5fSCwJk1OA==", + "dev": true, + "requires": { + "core-js": "^2.4.1", + "fbjs-css-vars": "^1.0.0", + "isomorphic-fetch": "^2.1.1", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^0.7.18" + } + }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -18149,28 +18200,16 @@ "react-deep-force-update": "^1.0.0" } }, - "react-relay": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/react-relay/-/react-relay-9.0.0.tgz", - "integrity": "sha512-5K0SGkeGBCscS+7d9x0D5czI6ZZtWbmeBu2C6nBFEX4qPKvh0poIn8oJNN1cyXPCmdQPzQBaAbxLbI7v4Vpxiw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.0.0", - "fbjs": "^1.0.0", - "nullthrows": "^1.1.1", - "relay-runtime": "9.0.0" - } - }, "react-test-renderer": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.10.2.tgz", - "integrity": "sha512-k9Qzyev6cTIcIfrhgrFlYQAFxh5EEDO6ALNqYqmKsWVA7Q/rUMTay5nD3nthi6COmYsd4ghVYyi8U86aoeMqYQ==", + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.11.0.tgz", + "integrity": "sha512-nh9gDl8R4ut+ZNNb2EeKO5VMvTKxwzurbSMuGBoKtjpjbg8JK/u3eVPVNi1h1Ue+eYK9oSzJjb+K3lzLxyA4ag==", "dev": true, "requires": { "object-assign": "^4.1.1", "prop-types": "^15.6.2", "react-is": "^16.8.6", - "scheduler": "^0.16.2" + "scheduler": "^0.17.0" } }, "react-transform-hmr": { @@ -18308,9 +18347,9 @@ } }, "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" }, "regenerator-transform": { "version": "0.14.1", @@ -18394,9 +18433,9 @@ } }, "relay-compiler": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/relay-compiler/-/relay-compiler-9.0.0.tgz", - "integrity": "sha512-V509bPZMqVvITUcgXFgvO9pcnAKzF7pJXObnxfglOl3JsfJeDwTW1fu/CzPtvk5suxVMK6Cn9fMxZK2GdRXqYQ==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/relay-compiler/-/relay-compiler-10.1.2.tgz", + "integrity": "sha512-MRZWMIyl7f+YvtD9BVsxFcn/5pUlb5p5+YZ7dFzlWVS6ox0wtAvs+CFMSm6zYbxfR+E39E1PWdsGQ2zQxedKBQ==", "dev": true, "requires": { "@babel/core": "^7.0.0", @@ -18406,30 +18445,30 @@ "@babel/traverse": "^7.0.0", "@babel/types": "^7.0.0", "babel-preset-fbjs": "^3.3.0", - "chalk": "^2.4.1", - "fast-glob": "^2.2.2", + "chalk": "^4.0.0", "fb-watchman": "^2.0.0", - "fbjs": "^1.0.0", + "fbjs": "^3.0.0", + "glob": "^7.1.1", "immutable": "~3.7.6", "nullthrows": "^1.1.1", - "relay-runtime": "9.0.0", + "relay-runtime": "10.1.2", "signedsource": "^1.0.0", - "yargs": "^14.2.0" + "yargs": "^15.3.1" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "babel-preset-fbjs": { @@ -18473,30 +18512,50 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, "get-caller-file": { @@ -18505,32 +18564,43 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "locate-path": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "^4.1.0" } }, "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "^2.2.0" } }, "p-try": { @@ -18539,6 +18609,12 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -18546,65 +18622,74 @@ "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yargs": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", - "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "requires": { - "cliui": "^5.0.0", + "cliui": "^6.0.0", "decamelize": "^1.2.0", - "find-up": "^3.0.0", + "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^3.0.0", + "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" + "yargs-parser": "^18.1.2" } }, "yargs-parser": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", - "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -18614,45 +18699,45 @@ } }, "relay-hooks": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/relay-hooks/-/relay-hooks-3.7.0.tgz", - "integrity": "sha512-tZnS2rjeOvWpMAKIGS0w09InX//yigtyNg2ic43aAAZaLnjnp6iCJyn7HXwPx3SO/5tCymjgF/hMBicipZJnTw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/relay-hooks/-/relay-hooks-4.0.0.tgz", + "integrity": "sha512-KY4k7mOtelICeulzS16D0yzP+YqMFE14yeevRmKii9OCekdze8k00KY84i4WCAtHjIyp4AnrBux//6f8tKgzOA==", "requires": { "@restart/hooks": "^0.3.1", - "fbjs": "^1.0.0" + "fbjs": "^3.0.0" } }, "relay-runtime": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-9.0.0.tgz", - "integrity": "sha512-bBNeNmwMOnqtCvuPnWdgflFSVwuUbGV7m7os8qHCUCSJ52DT5B/m6K4wisVh3eZ0QWYr7hheRDfmR/3UEdUe5A==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/relay-runtime/-/relay-runtime-10.1.2.tgz", + "integrity": "sha512-nAMji6xaQ4bPlBZdXwVEDBDmmPZMfmgHHskUpnDVVldhtuu9zUb4bTDYM5Sk4MMBO8uOR5MJqDkEfBkpV02w+A==", "dev": true, "requires": { "@babel/runtime": "^7.0.0", - "fbjs": "^1.0.0" + "fbjs": "^3.0.0" } }, "relay-test-utils": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/relay-test-utils/-/relay-test-utils-9.0.0.tgz", - "integrity": "sha512-ivOpAihkSb0fp9e+oOtHYE1LpDyXWBQK7ByopjSyMW0UTK/LD17Xw1RJfjC2OGys3fvcxlH3wdqawjdeSSCSdw==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/relay-test-utils/-/relay-test-utils-10.1.2.tgz", + "integrity": "sha512-aiVyYZu9A4KaRrCSlOHNlU1P7ZizvE0YECQ1hTYshS/MoaSBTbw3vG8qnysnw9AvliNSgCaF8QQjjSJgUZSaiw==", "dev": true, "requires": { "@babel/runtime": "^7.0.0", - "fbjs": "^1.0.0", - "relay-runtime": "9.0.0" + "fbjs": "^3.0.0", + "relay-runtime": "10.1.2" } }, "relay-test-utils-internal": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/relay-test-utils-internal/-/relay-test-utils-internal-9.0.0.tgz", - "integrity": "sha512-xum0Bp/uDOjgbd08MDXJAFi1iOxafhe/B1PM7VU2BZchdzsV8m3z37IT2qm7P2dZVSp4Vuac6QOrJ+s+20roSw==", + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/relay-test-utils-internal/-/relay-test-utils-internal-10.1.2.tgz", + "integrity": "sha512-FvuVaJqrUxdeiU4PHhcOhX/MGcnzv91MGWOj6lVjEBvZ5jSmyBb8xbyd5Gks+7XVIRwqZr7hN3Q3Hr8Vm3Pk5g==", "dev": true, "requires": { "@babel/runtime": "^7.0.0", - "fbjs": "^1.0.0", - "relay-compiler": "9.0.0", - "relay-runtime": "9.0.0" + "fbjs": "^3.0.0", + "relay-compiler": "10.1.2", + "relay-runtime": "10.1.2" } }, "remove-trailing-separator": { @@ -18869,7 +18954,8 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, "sane": { "version": "3.1.0", @@ -19172,9 +19258,9 @@ "dev": true }, "scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-BqYVWqwz6s1wZMhjFvLfVR5WXP7ZY32M/wYPo04CcuPM7XZEbV2TBNW7Z0UkguPTl0dWMA59VbNXxK6q+pHItg==", + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.17.0.tgz", + "integrity": "sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==", "dev": true, "requires": { "loose-envify": "^1.1.0", @@ -20812,7 +20898,8 @@ "whatwg-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==" + "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", + "dev": true }, "whatwg-mimetype": { "version": "2.3.0", diff --git a/package.json b/package.json index e999bf9..a428d5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-relay-offline", - "version": "2.3.0", + "version": "3.0.0", "keywords": [ "graphql", "relay", @@ -28,23 +28,22 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@babel/runtime": "^7.0.0", - "@wora/relay-offline": "^3.2.0", - "@wora/netinfo": "^2.0.0", + "@babel/runtime": "^7.7.2", + "@wora/relay-offline": "^4.0.0", + "@wora/netinfo": "^2.1.0", "@wora/detect-network": "^2.0.0", "@wora/cache-persist": "^2.1.0", - "@wora/offline-first": "^2.1.0", - "@wora/relay-store": "^3.2.0", - "fbjs": "^1.0.0", + "@wora/offline-first": "^2.3.0", + "@wora/relay-store": "^4.0.0", + "fbjs": "^3.0.0", "nullthrows": "^1.1.0", "uuid": "3.3.2", - "relay-hooks": "^3.7.0", + "relay-hooks": "^4.0.0", "tslib": "^1.11.1" }, "peerDependencies": { - "react": ">=16.9.0", - "react-relay": ">=8.0.0", - "relay-runtime": ">=8.0.0" + "react": "^16.9.0 || ^17.0.0", + "relay-runtime": "^10.1.0" }, "devDependencies": { "@react-native-community/netinfo": "3.2.1", @@ -53,7 +52,7 @@ "@types/promise-polyfill": "^6.0.3", "@types/react": "16.8.14", "@types/react-dom": "16.8.4", - "@types/relay-runtime": "^9.0.0", + "@types/relay-runtime": "^10.0.0", "@typescript-eslint/eslint-plugin": "2.24.0", "@typescript-eslint/parser": "2.24.0", "babel-jest": "24.9.0", @@ -73,16 +72,15 @@ "lerna": "^3.16.4", "prettier": "2.0.1", "promise-polyfill": "6.1.0", - "react": "16.10.2", + "react": "16.11.0", + "react-test-renderer": "16.11.0", "react-native": "0.59.9", - "react-relay": "^9.0.0", - "relay-runtime": "^9.0.0", - "react-test-renderer": "16.10.2", - "relay-test-utils-internal": "9.0.0", - "relay-test-utils": "9.0.0", + "relay-runtime": "^10.0.0", + "relay-test-utils-internal": "^10.0.0", + "relay-test-utils": "^10.0.0", "rimraf": "2.6.3", "ts-jest": "24.1.0", "typescript": "3.8.3", - "graphql": "14.4.2" + "graphql": "^15.0.0" } } diff --git a/src/QueryRendererHook.tsx b/src/QueryRendererHook.tsx deleted file mode 100644 index 77781c9..0000000 --- a/src/QueryRendererHook.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react'; -import { QueryRendererProps } from './RelayOfflineTypes'; -import { useQueryOffline } from './hooks/useQueryOffline'; -import { OperationType } from 'relay-runtime'; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const QueryRendererHook = (props: QueryRendererProps) => { - const { render, fetchPolicy, query, variables, cacheConfig, ttl, fetchKey, fetchObserver } = props; - const hooksProps = useQueryOffline(query, variables, { - networkCacheConfig: cacheConfig, - fetchPolicy, - ttl, - fetchKey, - fetchObserver, - }); - - return {render(hooksProps)}; -}; diff --git a/src/QueryRendererOffline.tsx b/src/QueryRendererOffline.tsx deleted file mode 100644 index 5f10eeb..0000000 --- a/src/QueryRendererOffline.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; -import { QueryRendererHook } from './QueryRendererHook'; -import { RelayEnvironmentProvider } from 'relay-hooks'; -import { QueryRendererOfflineProps } from './RelayOfflineTypes'; -import { OperationType } from 'relay-runtime'; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const QueryRendererOffline = function (props: QueryRendererOfflineProps) { - const { environment } = props; - - return ( - - - - ); -}; diff --git a/src/RelayOfflineTypes.ts b/src/RelayOfflineTypes.ts index 5ce0ca5..98d318d 100644 --- a/src/RelayOfflineTypes.ts +++ b/src/RelayOfflineTypes.ts @@ -1,43 +1,28 @@ -import { OperationType, CacheConfig, GraphQLTaggedNode, Observer, Snapshot } from 'relay-runtime'; -import { FetchPolicy, RenderProps, QueryOptions, LoadQuery } from 'relay-hooks'; +import { OperationType, CacheConfig, GraphQLTaggedNode } from 'relay-runtime'; +import { RenderProps, QueryOptions, LoadQuery } from 'relay-hooks'; import { Environment } from '@wora/relay-offline'; -export interface QueryRendererProps extends QueryProps { - render: (renderProps: OfflineRenderProps) => React.ReactNode; -} - -export interface QueryRendererOfflineProps extends QueryRendererProps { - environment: Environment; -} - -export interface OfflineRenderProps extends RenderProps { - rehydrated: boolean; - online: boolean; +export interface CacheConfigTTL extends CacheConfig { + ttl?: number; } export interface OfflineLoadQuery extends LoadQuery { getValue: ( environment?: Environment, - ) => OfflineRenderProps | Promise; + ) => RenderProps | Promise; next: ( environment: Environment, gqlQuery: GraphQLTaggedNode, variables?: TOperationType['variables'], - options?: QueryOptions, + options?: QueryOptionsOffline, ) => Promise; } -export interface QueryProps { - cacheConfig?: CacheConfig; - fetchPolicy?: FetchPolicy; - fetchKey?: string | number; +export interface QueryProps extends QueryOptionsOffline { query: GraphQLTaggedNode; variables: T['variables']; - ttl?: number; - skip?: boolean; - fetchObserver?: Observer; } export type QueryOptionsOffline = QueryOptions & { - ttl?: number; + networkCacheConfig?: CacheConfigTTL; }; diff --git a/src/hooks/useLazyLoadQueryOffline.ts b/src/hooks/useLazyLoadQueryOffline.ts index 03aeef5..b7dd17e 100644 --- a/src/hooks/useLazyLoadQueryOffline.ts +++ b/src/hooks/useLazyLoadQueryOffline.ts @@ -1,45 +1,24 @@ -import { useRef } from 'react'; -import { useRelayEnvironment, useQueryFetcher, STORE_ONLY } from 'relay-hooks'; +import { useRelayEnvironment, useLazyLoadQuery, STORE_ONLY, RenderProps } from 'relay-hooks'; import { OperationType, GraphQLTaggedNode } from 'relay-runtime'; -import { OfflineRenderProps, QueryOptionsOffline } from '../RelayOfflineTypes'; -import { useMemoOperationDescriptor } from 'relay-hooks/lib/useQuery'; +import { QueryOptionsOffline } from '../RelayOfflineTypes'; import { Environment } from '@wora/relay-offline'; export const useLazyLoadQueryOffline = function ( gqlQuery: GraphQLTaggedNode, variables: TOperationType['variables'], options: QueryOptionsOffline = {}, -): OfflineRenderProps { +): RenderProps { const environment = useRelayEnvironment(); - const ref = useRef<{ haveProps: boolean }>({ - haveProps: false, - }); const rehydrated = environment.isRehydrated(); - const query = useMemoOperationDescriptor(gqlQuery, variables); - - const queryFetcher = useQueryFetcher(query); - - const { fetchPolicy, networkCacheConfig, ttl, skip, fetchKey, fetchObserver } = options; - const online = environment.isOnline(); - const { props, error, ...others } = queryFetcher.execute( - environment, - query, - { - networkCacheConfig, - fetchPolicy: rehydrated && online ? fetchPolicy : STORE_ONLY, - skip, - fetchKey, - fetchObserver, - }, - (environment, query) => environment.retain(query, { ttl }), // TODO new directive - ); - ref.current = { - haveProps: !!props, - }; + if (!rehydrated || !online) { + options.fetchPolicy = STORE_ONLY; + } + + const queryResult = useLazyLoadQuery(gqlQuery, variables, options); if (!rehydrated) { const promise = environment @@ -48,16 +27,9 @@ export const useLazyLoadQueryOffline = function { throw error; // }); - const { haveProps } = ref.current; - if (!haveProps) { + if (!queryResult.data) { throw promise; } } - return { - ...others, - props, - rehydrated, - error: error, - online, - }; + return queryResult; }; diff --git a/src/hooks/usePreloadedQueryOffline.ts b/src/hooks/usePreloadedQueryOffline.ts index 3934180..a460bda 100644 --- a/src/hooks/usePreloadedQueryOffline.ts +++ b/src/hooks/usePreloadedQueryOffline.ts @@ -1,9 +1,9 @@ import { OperationType } from 'relay-runtime'; -import { OfflineLoadQuery, OfflineRenderProps } from '../RelayOfflineTypes'; -import { usePreloadedQuery } from 'relay-hooks'; +import { OfflineLoadQuery } from '../RelayOfflineTypes'; +import { usePreloadedQuery, RenderProps } from 'relay-hooks'; export const usePreloadedQueryOffline = ( loadQuery: OfflineLoadQuery, -): OfflineRenderProps => { - return usePreloadedQuery(loadQuery) as OfflineRenderProps; +): RenderProps => { + return usePreloadedQuery(loadQuery); }; diff --git a/src/hooks/useQueryOffline.ts b/src/hooks/useQueryOffline.ts index 6fc2968..1e2e410 100644 --- a/src/hooks/useQueryOffline.ts +++ b/src/hooks/useQueryOffline.ts @@ -1,66 +1,39 @@ -import { useState, useRef } from 'react'; -import { useRelayEnvironment, useQueryFetcher, STORE_ONLY } from 'relay-hooks'; +import { useQuery, useRelayEnvironment, STORE_ONLY, RenderProps } from 'relay-hooks'; import { OperationType, GraphQLTaggedNode } from 'relay-runtime'; -import { OfflineRenderProps, QueryOptionsOffline } from '../RelayOfflineTypes'; -import { useMemoOperationDescriptor } from 'relay-hooks/lib/useQuery'; +import { QueryOptionsOffline } from '../RelayOfflineTypes'; +import { useForceUpdate } from 'relay-hooks/lib/useForceUpdate'; import { Environment } from '@wora/relay-offline'; export const useQueryOffline = function ( gqlQuery: GraphQLTaggedNode, variables: TOperationType['variables'], options: QueryOptionsOffline = {}, -): OfflineRenderProps { +): RenderProps { const environment = useRelayEnvironment(); - const ref = useRef<{ haveProps: boolean }>({ - haveProps: false, - }); const rehydrated = environment.isRehydrated(); - const [, forceUpdate] = useState(null); + const forceUpdate = useForceUpdate(); + + const online = environment.isOnline(); + + if (!rehydrated || !online) { + options.fetchPolicy = STORE_ONLY; + } + + const queryResult = useQuery(gqlQuery, variables, options); if (!rehydrated) { environment .hydrate() .then(() => { - const { haveProps } = ref.current; - if (!haveProps) { - forceUpdate(ref.current); + if (!queryResult.data) { + forceUpdate(); } }) .catch((error) => { throw error; // }); } - - const query = useMemoOperationDescriptor(gqlQuery, variables); - - const queryFetcher = useQueryFetcher(); - - const { fetchPolicy, networkCacheConfig, ttl, skip, fetchKey, fetchObserver } = options; - - const online = environment.isOnline(); - - const { props, error, ...others } = queryFetcher.execute( - environment, - query, - { - networkCacheConfig, - fetchPolicy: rehydrated && online ? fetchPolicy : STORE_ONLY, - skip, - fetchKey, - fetchObserver, - }, - (environment, query) => environment.retain(query, { ttl }), // TODO new directive - ); - ref.current = { - haveProps: !!props, - }; - return { - ...others, - props, - rehydrated, - error: error, - online, - }; + return queryResult; }; diff --git a/src/index.ts b/src/index.ts index c962fd7..12e0245 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,39 +1,14 @@ -export { - ReactRelayContext, - applyOptimisticMutation, - commitMutation, - commitLocalUpdate, - createFragmentContainer, - createPaginationContainer, - createRefetchContainer, - graphql, - requestSubscription, -} from 'react-relay'; -export { - $FragmentRef, - RelayFragmentContainer, - RelayPaginationContainer, - RelayPaginationProp, - RelayProp, - RelayRefetchContainer, - RelayRefetchProp, -} from 'react-relay/lib/ReactRelayTypes'; - -export { - DataID, - DeclarativeMutationConfig, - Disposable, - GraphQLTaggedNode, - MutationType, - NormalizationSelector, - OperationDescriptor, - RangeOperation, - ReaderSelector, - RelayContext, - Snapshot, - Variables, -} from 'relay-runtime'; - +export { applyOptimisticMutation, commitMutation, commitLocalUpdate, graphql, requestSubscription } from 'relay-runtime'; +export { useRestore } from './hooks/useRestore'; +export { loadQuery, loadLazyQuery } from './runtime/loadQuery'; +export { useQueryOffline as useQuery } from './hooks/useQueryOffline'; +export { usePreloadedQueryOffline as usePreloadedQuery } from './hooks/usePreloadedQueryOffline'; +export { useLazyLoadQueryOffline as useLazyLoadQuery } from './hooks/useLazyLoadQueryOffline'; +export { Environment, fetchQuery } from '@wora/relay-offline'; +export { Store, RecordSource } from '@wora/relay-store'; +export { NetInfo } from '@wora/netinfo'; +export { useNetInfo, useIsConnected } from '@wora/detect-network'; +export * from './RelayOfflineTypes'; export { NETWORK_ONLY, STORE_THEN_NETWORK, @@ -42,20 +17,10 @@ export { FetchPolicy, useFragment, useMutation, - useOssFragment, usePagination, - useRefetch, + useRefetchable, + useRefetchableFragment, + usePaginationFragment, + useSuspenseFragment, RelayEnvironmentProvider, } from 'relay-hooks'; - -export { QueryRendererOffline as QueryRenderer } from './QueryRendererOffline'; -export { useRestore } from './hooks/useRestore'; -export { loadQuery, loadLazyQuery } from './runtime/loadQuery'; -export { useQueryOffline as useQuery } from './hooks/useQueryOffline'; -export { usePreloadedQueryOffline as usePreloadedQuery } from './hooks/usePreloadedQueryOffline'; -export { useLazyLoadQueryOffline as useLazyLoadQuery } from './hooks/useLazyLoadQueryOffline'; -export { Environment, fetchQuery } from '@wora/relay-offline'; -export { Store, RecordSource } from '@wora/relay-store'; -export { NetInfo } from '@wora/netinfo'; -export { useNetInfo, useIsConnected } from '@wora/detect-network'; -export * from './RelayOfflineTypes'; diff --git a/src/runtime/loadQuery.ts b/src/runtime/loadQuery.ts index b42c50e..4ef9db4 100644 --- a/src/runtime/loadQuery.ts +++ b/src/runtime/loadQuery.ts @@ -1,32 +1,57 @@ -import { internalLoadQuery } from 'relay-hooks/lib/loadQuery'; +import { GraphQLTaggedNode, OperationType, IEnvironment } from 'relay-runtime'; +import { QueryOptionsOffline } from '../RelayOfflineTypes'; import { QueryFetcher } from 'relay-hooks/lib/QueryFetcher'; -import { OperationType, OperationDescriptor } from 'relay-runtime'; -import { OfflineLoadQuery, OfflineRenderProps, QueryOptionsOffline } from '../RelayOfflineTypes'; -import { Environment } from '@wora/relay-offline'; +import { RenderProps, LoadQuery } from 'relay-hooks'; -const queryExecute = ( - queryFetcher: QueryFetcher, - environment: Environment, - query: OperationDescriptor, - options: QueryOptionsOffline, -): OfflineRenderProps => { - const online = environment.isOnline(); - const rehydrated = environment.isRehydrated(); - if (!online) { - options.fetchPolicy = 'store-only'; - } - const data = queryFetcher.execute(environment, query, options, (environment, query) => environment.retain(query, { ttl: options.ttl })); +export const internalLoadQuery = (promise = false): LoadQuery => { + let queryFetcher = new QueryFetcher(); + + const dispose = (): void => { + queryFetcher.dispose(); + queryFetcher = new QueryFetcher(); + }; + + const next = ( + environment, + gqlQuery: GraphQLTaggedNode, + variables: TOperationType['variables'] = {}, + options: QueryOptionsOffline = {}, + ): Promise => { + const online = environment.isOnline(); + const rehydrated = environment.isRehydrated(); + if (!online || !rehydrated) { + options.fetchPolicy = 'store-only'; + } + options.networkCacheConfig = options.networkCacheConfig ?? { force: true }; + queryFetcher.resolve(environment, gqlQuery, variables, options); + const toThrow = queryFetcher.checkAndSuspense(); + return toThrow ? (toThrow instanceof Error ? Promise.reject(toThrow) : toThrow) : Promise.resolve(); + }; + + const getValue = (environment?: IEnvironment): RenderProps | null | Promise => { + queryFetcher.resolveEnvironment(environment); + queryFetcher.checkAndSuspense(promise); + return queryFetcher.getData(); + }; + + const subscribe = (callback: () => any): (() => void) => { + queryFetcher.setForceUpdate(callback); + return (): void => { + queryFetcher.setForceUpdate(() => undefined); + }; + }; return { - ...data, - online, - rehydrated, + next, + subscribe, + getValue, + dispose, }; }; -export const loadLazyQuery = (): OfflineLoadQuery => { - return internalLoadQuery(true, queryExecute) as OfflineLoadQuery; +export const loadLazyQuery = (): LoadQuery => { + return internalLoadQuery(true); }; -export const loadQuery = (): OfflineLoadQuery => { - return internalLoadQuery(false, queryExecute) as OfflineLoadQuery; +export const loadQuery = (): LoadQuery => { + return internalLoadQuery(false); };