relay-runtime 9.0.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/handlers/RelayDefaultHandlerProvider.js.flow +34 -0
- package/handlers/connection/ConnectionHandler.js.flow +549 -0
- package/handlers/connection/ConnectionInterface.js.flow +92 -0
- package/index.js +1 -1
- package/index.js.flow +314 -0
- package/lib/handlers/connection/ConnectionHandler.js +1 -3
- package/lib/index.js +1 -2
- package/lib/mutations/RelayDeclarativeMutationConfig.js +22 -45
- package/lib/mutations/RelayRecordProxy.js +1 -3
- package/lib/mutations/RelayRecordSourceMutator.js +1 -3
- package/lib/mutations/RelayRecordSourceProxy.js +1 -3
- package/lib/mutations/RelayRecordSourceSelectorProxy.js +1 -3
- package/lib/mutations/commitMutation.js +2 -0
- package/lib/mutations/validateMutation.js +13 -4
- package/lib/network/RelayObservable.js +9 -9
- package/lib/network/RelayQueryResponseCache.js +8 -6
- package/lib/query/fetchQueryInternal.js +1 -8
- package/lib/store/DataChecker.js +23 -51
- package/lib/store/RelayConcreteVariables.js +6 -2
- package/lib/store/RelayModernEnvironment.js +30 -12
- package/lib/store/RelayModernFragmentSpecResolver.js +9 -13
- package/lib/store/RelayModernQueryExecutor.js +73 -37
- package/lib/store/RelayModernRecord.js +14 -9
- package/lib/store/RelayModernStore.js +107 -70
- package/lib/store/RelayOperationTracker.js +35 -78
- package/lib/store/RelayOptimisticRecordSource.js +7 -5
- package/lib/store/RelayPublishQueue.js +1 -3
- package/lib/store/RelayReader.js +1 -3
- package/lib/store/RelayRecordSource.js +1 -3
- package/lib/store/RelayRecordSourceMapImpl.js +13 -18
- package/lib/store/RelayReferenceMarker.js +2 -6
- package/lib/store/RelayResponseNormalizer.js +9 -10
- package/lib/store/StoreInspector.js +7 -5
- package/lib/store/normalizeRelayPayload.js +6 -2
- package/lib/subscription/requestSubscription.js +4 -2
- package/lib/util/RelayFeatureFlags.js +1 -1
- package/lib/util/RelayReplaySubject.js +1 -3
- package/lib/util/createPayloadFor3DField.js +7 -2
- package/mutations/RelayDeclarativeMutationConfig.js.flow +380 -0
- package/mutations/RelayRecordProxy.js.flow +165 -0
- package/mutations/RelayRecordSourceMutator.js.flow +238 -0
- package/mutations/RelayRecordSourceProxy.js.flow +164 -0
- package/mutations/RelayRecordSourceSelectorProxy.js.flow +119 -0
- package/mutations/applyOptimisticMutation.js.flow +76 -0
- package/mutations/commitLocalUpdate.js.flow +24 -0
- package/mutations/commitMutation.js.flow +184 -0
- package/mutations/validateMutation.js.flow +211 -0
- package/network/ConvertToExecuteFunction.js.flow +49 -0
- package/network/RelayNetwork.js.flow +84 -0
- package/network/RelayNetworkTypes.js.flow +123 -0
- package/network/RelayObservable.js.flow +634 -0
- package/network/RelayQueryResponseCache.js.flow +111 -0
- package/package.json +1 -1
- package/query/GraphQLTag.js.flow +166 -0
- package/query/fetchQuery.js.flow +47 -0
- package/query/fetchQueryInternal.js.flow +349 -0
- package/relay-runtime.js +2 -2
- package/relay-runtime.min.js +2 -2
- package/store/ClientID.js.flow +43 -0
- package/store/DataChecker.js.flow +426 -0
- package/store/RelayConcreteVariables.js.flow +96 -0
- package/store/RelayModernEnvironment.js.flow +526 -0
- package/store/RelayModernFragmentSpecResolver.js.flow +426 -0
- package/store/RelayModernOperationDescriptor.js.flow +88 -0
- package/store/RelayModernQueryExecutor.js.flow +1327 -0
- package/store/RelayModernRecord.js.flow +403 -0
- package/store/RelayModernSelector.js.flow +444 -0
- package/store/RelayModernStore.js.flow +757 -0
- package/store/RelayOperationTracker.js.flow +164 -0
- package/store/RelayOptimisticRecordSource.js.flow +119 -0
- package/store/RelayPublishQueue.js.flow +401 -0
- package/store/RelayReader.js.flow +376 -0
- package/store/RelayRecordSource.js.flow +29 -0
- package/store/RelayRecordSourceMapImpl.js.flow +87 -0
- package/store/RelayRecordState.js.flow +37 -0
- package/store/RelayReferenceMarker.js.flow +236 -0
- package/store/RelayResponseNormalizer.js.flow +556 -0
- package/store/RelayStoreTypes.js.flow +873 -0
- package/store/RelayStoreUtils.js.flow +218 -0
- package/store/StoreInspector.js.flow +173 -0
- package/store/ViewerPattern.js.flow +26 -0
- package/store/cloneRelayHandleSourceField.js.flow +66 -0
- package/store/createFragmentSpecResolver.js.flow +55 -0
- package/store/createRelayContext.js.flow +44 -0
- package/store/defaultGetDataID.js.flow +27 -0
- package/store/hasOverlappingIDs.js.flow +34 -0
- package/store/isRelayModernEnvironment.js.flow +27 -0
- package/store/normalizeRelayPayload.js.flow +51 -0
- package/store/readInlineData.js.flow +75 -0
- package/subscription/requestSubscription.js.flow +100 -0
- package/util/JSResourceTypes.flow.js.flow +20 -0
- package/util/NormalizationNode.js.flow +191 -0
- package/util/ReaderNode.js.flow +208 -0
- package/util/RelayConcreteNode.js.flow +80 -0
- package/util/RelayDefaultHandleKey.js.flow +17 -0
- package/util/RelayError.js.flow +33 -0
- package/util/RelayFeatureFlags.js.flow +30 -0
- package/util/RelayProfiler.js.flow +284 -0
- package/util/RelayReplaySubject.js.flow +134 -0
- package/util/RelayRuntimeTypes.js.flow +70 -0
- package/util/createPayloadFor3DField.js.flow +43 -0
- package/util/deepFreeze.js.flow +36 -0
- package/util/generateID.js.flow +21 -0
- package/util/getFragmentIdentifier.js.flow +52 -0
- package/util/getRelayHandleKey.js.flow +41 -0
- package/util/getRequestIdentifier.js.flow +41 -0
- package/util/isPromise.js.flow +21 -0
- package/util/isScalarAndEqual.js.flow +26 -0
- package/util/recycleNodesInto.js.flow +80 -0
- package/util/resolveImmediate.js.flow +30 -0
- package/util/stableCopy.js.flow +35 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @flow strict-local
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// flowlint ambiguous-object-type:error
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const invariant = require('invariant');
|
|
16
|
+
const stableCopy = require('../util/stableCopy');
|
|
17
|
+
|
|
18
|
+
import type {Variables} from '../util/RelayRuntimeTypes';
|
|
19
|
+
import type {GraphQLSingularResponse} from './RelayNetworkTypes';
|
|
20
|
+
|
|
21
|
+
type Response = {
|
|
22
|
+
fetchTime: number,
|
|
23
|
+
payload: GraphQLSingularResponse,
|
|
24
|
+
...
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A cache for storing query responses, featuring:
|
|
29
|
+
* - `get` with TTL
|
|
30
|
+
* - cache size limiting, with least-recently *updated* entries purged first
|
|
31
|
+
*/
|
|
32
|
+
class RelayQueryResponseCache {
|
|
33
|
+
_responses: Map<string, Response>;
|
|
34
|
+
_size: number;
|
|
35
|
+
_ttl: number;
|
|
36
|
+
|
|
37
|
+
constructor({size, ttl}: {size: number, ttl: number, ...}) {
|
|
38
|
+
invariant(
|
|
39
|
+
size > 0,
|
|
40
|
+
'RelayQueryResponseCache: Expected the max cache size to be > 0, got ' +
|
|
41
|
+
'`%s`.',
|
|
42
|
+
size,
|
|
43
|
+
);
|
|
44
|
+
invariant(
|
|
45
|
+
ttl > 0,
|
|
46
|
+
'RelayQueryResponseCache: Expected the max ttl to be > 0, got `%s`.',
|
|
47
|
+
ttl,
|
|
48
|
+
);
|
|
49
|
+
this._responses = new Map();
|
|
50
|
+
this._size = size;
|
|
51
|
+
this._ttl = ttl;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
clear(): void {
|
|
55
|
+
this._responses.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get(queryID: string, variables: Variables): ?GraphQLSingularResponse {
|
|
59
|
+
const cacheKey = getCacheKey(queryID, variables);
|
|
60
|
+
this._responses.forEach((response, key) => {
|
|
61
|
+
if (!isCurrent(response.fetchTime, this._ttl)) {
|
|
62
|
+
this._responses.delete(key);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const response = this._responses.get(cacheKey);
|
|
66
|
+
return response != null
|
|
67
|
+
? ({
|
|
68
|
+
...response.payload,
|
|
69
|
+
extensions: {
|
|
70
|
+
...response.payload.extensions,
|
|
71
|
+
cacheTimestamp: response.fetchTime,
|
|
72
|
+
},
|
|
73
|
+
}: GraphQLSingularResponse)
|
|
74
|
+
: null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
set(
|
|
78
|
+
queryID: string,
|
|
79
|
+
variables: Variables,
|
|
80
|
+
payload: GraphQLSingularResponse,
|
|
81
|
+
): void {
|
|
82
|
+
const fetchTime = Date.now();
|
|
83
|
+
const cacheKey = getCacheKey(queryID, variables);
|
|
84
|
+
this._responses.delete(cacheKey); // deletion resets key ordering
|
|
85
|
+
this._responses.set(cacheKey, {
|
|
86
|
+
fetchTime,
|
|
87
|
+
payload,
|
|
88
|
+
});
|
|
89
|
+
// Purge least-recently updated key when max size reached
|
|
90
|
+
if (this._responses.size > this._size) {
|
|
91
|
+
const firstKey = this._responses.keys().next();
|
|
92
|
+
if (!firstKey.done) {
|
|
93
|
+
this._responses.delete(firstKey.value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getCacheKey(queryID: string, variables: Variables): string {
|
|
100
|
+
return JSON.stringify(stableCopy({queryID, variables}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Determine whether a response fetched at `fetchTime` is still valid given
|
|
105
|
+
* some `ttl`.
|
|
106
|
+
*/
|
|
107
|
+
function isCurrent(fetchTime: number, ttl: number): boolean {
|
|
108
|
+
return fetchTime + ttl >= Date.now();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = RelayQueryResponseCache;
|
package/package.json
CHANGED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @flow
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// flowlint ambiguous-object-type:error
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const RelayConcreteNode = require('../util/RelayConcreteNode');
|
|
16
|
+
|
|
17
|
+
const invariant = require('invariant');
|
|
18
|
+
const warning = require('warning');
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
ReaderFragment,
|
|
22
|
+
ReaderRefetchableFragment,
|
|
23
|
+
ReaderPaginationFragment,
|
|
24
|
+
ReaderInlineDataFragment,
|
|
25
|
+
} from '../util/ReaderNode';
|
|
26
|
+
import type {ConcreteRequest} from '../util/RelayConcreteNode';
|
|
27
|
+
|
|
28
|
+
// The type of a graphql`...` tagged template expression.
|
|
29
|
+
export type GraphQLTaggedNode =
|
|
30
|
+
| ReaderFragment
|
|
31
|
+
| ConcreteRequest
|
|
32
|
+
| {
|
|
33
|
+
// This is this case when we `require()` a generated ES6 module
|
|
34
|
+
default: ReaderFragment | ConcreteRequest,
|
|
35
|
+
...
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Runtime function to correspond to the `graphql` tagged template function.
|
|
40
|
+
* All calls to this function should be transformed by the plugin.
|
|
41
|
+
*/
|
|
42
|
+
function graphql(strings: Array<string>): GraphQLTaggedNode {
|
|
43
|
+
invariant(
|
|
44
|
+
false,
|
|
45
|
+
'graphql: Unexpected invocation at runtime. Either the Babel transform ' +
|
|
46
|
+
'was not set up, or it failed to identify this call site. Make sure it ' +
|
|
47
|
+
'is being used verbatim as `graphql`.',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getNode(
|
|
52
|
+
taggedNode: GraphQLTaggedNode,
|
|
53
|
+
): ReaderFragment | ConcreteRequest {
|
|
54
|
+
let node = taggedNode;
|
|
55
|
+
if (typeof node === 'function') {
|
|
56
|
+
node = (node(): ReaderFragment | ConcreteRequest);
|
|
57
|
+
warning(
|
|
58
|
+
false,
|
|
59
|
+
'RelayGraphQLTag: node `%s` unexpectedly wrapped in a function.',
|
|
60
|
+
node.kind === 'Fragment' ? node.name : node.operation.name,
|
|
61
|
+
);
|
|
62
|
+
} else if (node.default) {
|
|
63
|
+
// Support for languages that work (best) with ES6 modules, such as TypeScript.
|
|
64
|
+
node = node.default;
|
|
65
|
+
}
|
|
66
|
+
return node;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isFragment(node: GraphQLTaggedNode): boolean {
|
|
70
|
+
const fragment = getNode(node);
|
|
71
|
+
return (
|
|
72
|
+
typeof fragment === 'object' &&
|
|
73
|
+
fragment !== null &&
|
|
74
|
+
fragment.kind === RelayConcreteNode.FRAGMENT
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isRequest(node: GraphQLTaggedNode): boolean {
|
|
79
|
+
const request = getNode(node);
|
|
80
|
+
return (
|
|
81
|
+
typeof request === 'object' &&
|
|
82
|
+
request !== null &&
|
|
83
|
+
request.kind === RelayConcreteNode.REQUEST
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isInlineDataFragment(node: GraphQLTaggedNode): boolean {
|
|
88
|
+
const fragment = getNode(node);
|
|
89
|
+
return (
|
|
90
|
+
typeof fragment === 'object' &&
|
|
91
|
+
fragment !== null &&
|
|
92
|
+
fragment.kind === RelayConcreteNode.INLINE_DATA_FRAGMENT
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getFragment(taggedNode: GraphQLTaggedNode): ReaderFragment {
|
|
97
|
+
const fragment = getNode(taggedNode);
|
|
98
|
+
invariant(
|
|
99
|
+
isFragment(fragment),
|
|
100
|
+
'GraphQLTag: Expected a fragment, got `%s`.',
|
|
101
|
+
JSON.stringify(fragment),
|
|
102
|
+
);
|
|
103
|
+
return (fragment: any);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getPaginationFragment(
|
|
107
|
+
taggedNode: GraphQLTaggedNode,
|
|
108
|
+
): ReaderPaginationFragment | null {
|
|
109
|
+
const fragment = getFragment(taggedNode);
|
|
110
|
+
const refetch = fragment.metadata?.refetch;
|
|
111
|
+
const connection = refetch?.connection;
|
|
112
|
+
if (
|
|
113
|
+
refetch === null ||
|
|
114
|
+
typeof refetch !== 'object' ||
|
|
115
|
+
connection === null ||
|
|
116
|
+
typeof connection !== 'object'
|
|
117
|
+
) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return (fragment: any);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getRefetchableFragment(
|
|
124
|
+
taggedNode: GraphQLTaggedNode,
|
|
125
|
+
): ReaderRefetchableFragment | null {
|
|
126
|
+
const fragment = getFragment(taggedNode);
|
|
127
|
+
const refetch = fragment.metadata?.refetch;
|
|
128
|
+
if (refetch === null || typeof refetch !== 'object') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return (fragment: any);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getRequest(taggedNode: GraphQLTaggedNode): ConcreteRequest {
|
|
135
|
+
const request = getNode(taggedNode);
|
|
136
|
+
invariant(
|
|
137
|
+
isRequest(request),
|
|
138
|
+
'GraphQLTag: Expected a request, got `%s`.',
|
|
139
|
+
JSON.stringify(request),
|
|
140
|
+
);
|
|
141
|
+
return (request: any);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getInlineDataFragment(
|
|
145
|
+
taggedNode: GraphQLTaggedNode,
|
|
146
|
+
): ReaderInlineDataFragment {
|
|
147
|
+
const fragment = getNode(taggedNode);
|
|
148
|
+
invariant(
|
|
149
|
+
isInlineDataFragment(fragment),
|
|
150
|
+
'GraphQLTag: Expected an inline data fragment, got `%s`.',
|
|
151
|
+
JSON.stringify(fragment),
|
|
152
|
+
);
|
|
153
|
+
return (fragment: any);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
getFragment,
|
|
158
|
+
getPaginationFragment,
|
|
159
|
+
getRefetchableFragment,
|
|
160
|
+
getRequest,
|
|
161
|
+
getInlineDataFragment,
|
|
162
|
+
graphql,
|
|
163
|
+
isFragment,
|
|
164
|
+
isRequest,
|
|
165
|
+
isInlineDataFragment,
|
|
166
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @flow strict-local
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// flowlint ambiguous-object-type:error
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
createOperationDescriptor,
|
|
17
|
+
} = require('../store/RelayModernOperationDescriptor');
|
|
18
|
+
const {getRequest} = require('./GraphQLTag');
|
|
19
|
+
|
|
20
|
+
import type {IEnvironment} from '../store/RelayStoreTypes';
|
|
21
|
+
import type {CacheConfig, OperationType} from '../util/RelayRuntimeTypes';
|
|
22
|
+
import type {GraphQLTaggedNode} from './GraphQLTag';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A helper function to fetch the results of a query. Note that results for
|
|
26
|
+
* fragment spreads are masked: fields must be explicitly listed in the query in
|
|
27
|
+
* order to be accessible in the result object.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
function fetchQuery<T: OperationType>(
|
|
31
|
+
environment: IEnvironment,
|
|
32
|
+
taggedNode: GraphQLTaggedNode,
|
|
33
|
+
variables: $PropertyType<T, 'variables'>,
|
|
34
|
+
cacheConfig?: ?CacheConfig,
|
|
35
|
+
): Promise<$PropertyType<T, 'response'>> {
|
|
36
|
+
const query = getRequest(taggedNode);
|
|
37
|
+
if (query.params.operationKind !== 'query') {
|
|
38
|
+
throw new Error('fetchQuery: Expected query operation');
|
|
39
|
+
}
|
|
40
|
+
const operation = createOperationDescriptor(query, variables);
|
|
41
|
+
return environment
|
|
42
|
+
.execute({operation, cacheConfig})
|
|
43
|
+
.map(() => environment.lookup(operation.fragment).data)
|
|
44
|
+
.toPromise();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = fetchQuery;
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* @flow strict-local
|
|
8
|
+
* @format
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// flowlint ambiguous-object-type:error
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const Observable = require('../network/RelayObservable');
|
|
16
|
+
const RelayReplaySubject = require('../util/RelayReplaySubject');
|
|
17
|
+
|
|
18
|
+
const invariant = require('invariant');
|
|
19
|
+
|
|
20
|
+
import type {GraphQLResponse} from '../network/RelayNetworkTypes';
|
|
21
|
+
import type {Subscription} from '../network/RelayObservable';
|
|
22
|
+
import type {
|
|
23
|
+
IEnvironment,
|
|
24
|
+
OperationDescriptor,
|
|
25
|
+
RequestDescriptor,
|
|
26
|
+
} from '../store/RelayStoreTypes';
|
|
27
|
+
import type {CacheConfig} from '../util/RelayRuntimeTypes';
|
|
28
|
+
import type {RequestIdentifier} from '../util/getRequestIdentifier';
|
|
29
|
+
|
|
30
|
+
type RequestCacheEntry = {|
|
|
31
|
+
+identifier: RequestIdentifier,
|
|
32
|
+
+subject: RelayReplaySubject<GraphQLResponse>,
|
|
33
|
+
+subjectForInFlightStatus: RelayReplaySubject<GraphQLResponse>,
|
|
34
|
+
+subscription: Subscription,
|
|
35
|
+
|};
|
|
36
|
+
|
|
37
|
+
const WEAKMAP_SUPPORTED = typeof WeakMap === 'function';
|
|
38
|
+
|
|
39
|
+
const requestCachesByEnvironment = WEAKMAP_SUPPORTED
|
|
40
|
+
? new WeakMap()
|
|
41
|
+
: new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetches the given query and variables on the provided environment,
|
|
45
|
+
* and de-dupes identical in-flight requests.
|
|
46
|
+
*
|
|
47
|
+
* Observing a request:
|
|
48
|
+
* ====================
|
|
49
|
+
* fetchQuery returns an Observable which you can call .subscribe()
|
|
50
|
+
* on. subscribe() takes an Observer, which you can provide to
|
|
51
|
+
* observe network events:
|
|
52
|
+
*
|
|
53
|
+
* ```
|
|
54
|
+
* fetchQuery(environment, query, variables).subscribe({
|
|
55
|
+
* // Called when network requests starts
|
|
56
|
+
* start: (subscription) => {},
|
|
57
|
+
*
|
|
58
|
+
* // Called after a payload is received and written to the local store
|
|
59
|
+
* next: (payload) => {},
|
|
60
|
+
*
|
|
61
|
+
* // Called when network requests errors
|
|
62
|
+
* error: (error) => {},
|
|
63
|
+
*
|
|
64
|
+
* // Called when network requests fully completes
|
|
65
|
+
* complete: () => {},
|
|
66
|
+
*
|
|
67
|
+
* // Called when network request is unsubscribed
|
|
68
|
+
* unsubscribe: (subscription) => {},
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* In-flight request de-duping:
|
|
73
|
+
* ============================
|
|
74
|
+
* By default, calling fetchQuery multiple times with the same
|
|
75
|
+
* environment, query and variables will not initiate a new request if a request
|
|
76
|
+
* for those same parameters is already in flight.
|
|
77
|
+
*
|
|
78
|
+
* A request is marked in-flight from the moment it starts until the moment it
|
|
79
|
+
* fully completes, regardless of error or successful completion.
|
|
80
|
+
*
|
|
81
|
+
* NOTE: If the request completes _synchronously_, calling fetchQuery
|
|
82
|
+
* a second time with the same arguments in the same tick will _NOT_ de-dupe
|
|
83
|
+
* the request given that it will no longer be in-flight.
|
|
84
|
+
*
|
|
85
|
+
*
|
|
86
|
+
* Data Retention:
|
|
87
|
+
* ===============
|
|
88
|
+
* This function will not retain any query data outside the scope of the
|
|
89
|
+
* request, which means it is not guaranteed that it won't be garbage
|
|
90
|
+
* collected after the request completes.
|
|
91
|
+
* If you need to retain data, you can do so manually with environment.retain().
|
|
92
|
+
*
|
|
93
|
+
* Cancelling requests:
|
|
94
|
+
* ====================
|
|
95
|
+
* If the subscription returned by subscribe is called while the
|
|
96
|
+
* request is in-flight, apart from releasing retained data, the request will
|
|
97
|
+
* also be cancelled.
|
|
98
|
+
*
|
|
99
|
+
* ```
|
|
100
|
+
* const subscription = fetchQuery(...).subscribe(...);
|
|
101
|
+
*
|
|
102
|
+
* // This will cancel the request if it is in-flight.
|
|
103
|
+
* subscription.unsubscribe();
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
function fetchQuery(
|
|
107
|
+
environment: IEnvironment,
|
|
108
|
+
operation: OperationDescriptor,
|
|
109
|
+
options?: {|
|
|
110
|
+
networkCacheConfig?: CacheConfig,
|
|
111
|
+
|},
|
|
112
|
+
): Observable<GraphQLResponse> {
|
|
113
|
+
return fetchQueryDeduped(environment, operation.request, () =>
|
|
114
|
+
environment.execute({
|
|
115
|
+
operation,
|
|
116
|
+
cacheConfig: options?.networkCacheConfig,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Low-level implementation details of `fetchQuery`.
|
|
123
|
+
*
|
|
124
|
+
* `fetchQueryDeduped` can also be used to share a single cache for
|
|
125
|
+
* requests that aren't using `fetchQuery` directly (e.g. because they don't
|
|
126
|
+
* have an `OperationDescriptor` when they are called).
|
|
127
|
+
*/
|
|
128
|
+
function fetchQueryDeduped(
|
|
129
|
+
environment: IEnvironment,
|
|
130
|
+
request: RequestDescriptor,
|
|
131
|
+
fetchFn: () => Observable<GraphQLResponse>,
|
|
132
|
+
): Observable<GraphQLResponse> {
|
|
133
|
+
return Observable.create(sink => {
|
|
134
|
+
const requestCache = getRequestCache(environment);
|
|
135
|
+
const identifier = request.identifier;
|
|
136
|
+
let cachedRequest = requestCache.get(identifier);
|
|
137
|
+
|
|
138
|
+
if (!cachedRequest) {
|
|
139
|
+
fetchFn()
|
|
140
|
+
.finally(() => requestCache.delete(identifier))
|
|
141
|
+
.subscribe({
|
|
142
|
+
start: subscription => {
|
|
143
|
+
cachedRequest = {
|
|
144
|
+
identifier,
|
|
145
|
+
subject: new RelayReplaySubject(),
|
|
146
|
+
subjectForInFlightStatus: new RelayReplaySubject(),
|
|
147
|
+
subscription: subscription,
|
|
148
|
+
};
|
|
149
|
+
requestCache.set(identifier, cachedRequest);
|
|
150
|
+
},
|
|
151
|
+
next: response => {
|
|
152
|
+
const cachedReq = getCachedRequest(requestCache, identifier);
|
|
153
|
+
cachedReq.subject.next(response);
|
|
154
|
+
cachedReq.subjectForInFlightStatus.next(response);
|
|
155
|
+
},
|
|
156
|
+
error: error => {
|
|
157
|
+
const cachedReq = getCachedRequest(requestCache, identifier);
|
|
158
|
+
cachedReq.subject.error(error);
|
|
159
|
+
cachedReq.subjectForInFlightStatus.error(error);
|
|
160
|
+
},
|
|
161
|
+
complete: () => {
|
|
162
|
+
const cachedReq = getCachedRequest(requestCache, identifier);
|
|
163
|
+
cachedReq.subject.complete();
|
|
164
|
+
cachedReq.subjectForInFlightStatus.complete();
|
|
165
|
+
},
|
|
166
|
+
unsubscribe: subscription => {
|
|
167
|
+
const cachedReq = getCachedRequest(requestCache, identifier);
|
|
168
|
+
cachedReq.subject.unsubscribe();
|
|
169
|
+
cachedReq.subjectForInFlightStatus.unsubscribe();
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
invariant(
|
|
175
|
+
cachedRequest != null,
|
|
176
|
+
'[fetchQueryInternal] fetchQueryDeduped: Expected `start` to be ' +
|
|
177
|
+
'called synchronously',
|
|
178
|
+
);
|
|
179
|
+
return getObservableForCachedRequest(requestCache, cachedRequest).subscribe(
|
|
180
|
+
sink,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
function getObservableForCachedRequest(
|
|
189
|
+
requestCache: Map<RequestIdentifier, RequestCacheEntry>,
|
|
190
|
+
cachedRequest: RequestCacheEntry,
|
|
191
|
+
): Observable<GraphQLResponse> {
|
|
192
|
+
return Observable.create(sink => {
|
|
193
|
+
const subscription = cachedRequest.subject.subscribe(sink);
|
|
194
|
+
|
|
195
|
+
return () => {
|
|
196
|
+
subscription.unsubscribe();
|
|
197
|
+
const cachedRequestInstance = requestCache.get(cachedRequest.identifier);
|
|
198
|
+
if (cachedRequestInstance) {
|
|
199
|
+
const requestSubscription = cachedRequestInstance.subscription;
|
|
200
|
+
if (
|
|
201
|
+
requestSubscription != null &&
|
|
202
|
+
cachedRequestInstance.subject.getObserverCount() === 0
|
|
203
|
+
) {
|
|
204
|
+
requestSubscription.unsubscribe();
|
|
205
|
+
requestCache.delete(cachedRequest.identifier);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @private
|
|
214
|
+
*/
|
|
215
|
+
function getActiveStatusObservableForCachedRequest(
|
|
216
|
+
environment: IEnvironment,
|
|
217
|
+
requestCache: Map<RequestIdentifier, RequestCacheEntry>,
|
|
218
|
+
cachedRequest: RequestCacheEntry,
|
|
219
|
+
): Observable<void> {
|
|
220
|
+
return Observable.create(sink => {
|
|
221
|
+
const subscription = cachedRequest.subjectForInFlightStatus.subscribe({
|
|
222
|
+
error: sink.error,
|
|
223
|
+
next: response => {
|
|
224
|
+
if (!environment.isRequestActive(cachedRequest.identifier)) {
|
|
225
|
+
sink.complete();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
sink.next();
|
|
229
|
+
},
|
|
230
|
+
complete: sink.complete,
|
|
231
|
+
unsubscribe: sink.complete,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return () => {
|
|
235
|
+
subscription.unsubscribe();
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* If a request is active for the given query, variables and environment,
|
|
242
|
+
* this function will return a Promise that will resolve when that request has
|
|
243
|
+
* stops being active (receives a final payload), and the data has been saved
|
|
244
|
+
* to the store.
|
|
245
|
+
* If no request is active, null will be returned
|
|
246
|
+
*/
|
|
247
|
+
function getPromiseForActiveRequest(
|
|
248
|
+
environment: IEnvironment,
|
|
249
|
+
request: RequestDescriptor,
|
|
250
|
+
): Promise<void> | null {
|
|
251
|
+
const requestCache = getRequestCache(environment);
|
|
252
|
+
const cachedRequest = requestCache.get(request.identifier);
|
|
253
|
+
if (!cachedRequest) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (!environment.isRequestActive(cachedRequest.identifier)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
let resolveOnNext = false;
|
|
262
|
+
getActiveStatusObservableForCachedRequest(
|
|
263
|
+
environment,
|
|
264
|
+
requestCache,
|
|
265
|
+
cachedRequest,
|
|
266
|
+
).subscribe({
|
|
267
|
+
complete: resolve,
|
|
268
|
+
error: reject,
|
|
269
|
+
next: response => {
|
|
270
|
+
/*
|
|
271
|
+
* The underlying `RelayReplaySubject` will synchronously replay events
|
|
272
|
+
* as soon as we subscribe, but since we want the *next* asynchronous
|
|
273
|
+
* one, we'll ignore them until the replay finishes.
|
|
274
|
+
*/
|
|
275
|
+
if (resolveOnNext) {
|
|
276
|
+
resolve(response);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
resolveOnNext = true;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* If there is a pending request for the given query, returns an Observable of
|
|
286
|
+
* *all* its responses. Existing responses are published synchronously and
|
|
287
|
+
* subsequent responses are published asynchronously. Returns null if there is
|
|
288
|
+
* no pending request. This is similar to fetchQuery() except that it will not
|
|
289
|
+
* issue a fetch if there isn't already one pending.
|
|
290
|
+
*/
|
|
291
|
+
function getObservableForActiveRequest(
|
|
292
|
+
environment: IEnvironment,
|
|
293
|
+
request: RequestDescriptor,
|
|
294
|
+
): Observable<void> | null {
|
|
295
|
+
const requestCache = getRequestCache(environment);
|
|
296
|
+
const cachedRequest = requestCache.get(request.identifier);
|
|
297
|
+
if (!cachedRequest) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
if (!environment.isRequestActive(cachedRequest.identifier)) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return getActiveStatusObservableForCachedRequest(
|
|
305
|
+
environment,
|
|
306
|
+
requestCache,
|
|
307
|
+
cachedRequest,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @private
|
|
313
|
+
*/
|
|
314
|
+
function getRequestCache(
|
|
315
|
+
environment: IEnvironment,
|
|
316
|
+
): Map<RequestIdentifier, RequestCacheEntry> {
|
|
317
|
+
const cached: ?Map<
|
|
318
|
+
RequestIdentifier,
|
|
319
|
+
RequestCacheEntry,
|
|
320
|
+
> = requestCachesByEnvironment.get(environment);
|
|
321
|
+
if (cached != null) {
|
|
322
|
+
return cached;
|
|
323
|
+
}
|
|
324
|
+
const requestCache: Map<RequestIdentifier, RequestCacheEntry> = new Map();
|
|
325
|
+
requestCachesByEnvironment.set(environment, requestCache);
|
|
326
|
+
return requestCache;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
function getCachedRequest(
|
|
333
|
+
requestCache: Map<RequestIdentifier, RequestCacheEntry>,
|
|
334
|
+
identifier: RequestIdentifier,
|
|
335
|
+
) {
|
|
336
|
+
const cached = requestCache.get(identifier);
|
|
337
|
+
invariant(
|
|
338
|
+
cached != null,
|
|
339
|
+
'[fetchQueryInternal] getCachedRequest: Expected request to be cached',
|
|
340
|
+
);
|
|
341
|
+
return cached;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = {
|
|
345
|
+
fetchQuery,
|
|
346
|
+
fetchQueryDeduped,
|
|
347
|
+
getPromiseForActiveRequest,
|
|
348
|
+
getObservableForActiveRequest,
|
|
349
|
+
};
|