shared-controls.ts
5.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import triggerFetch, { APIFetchOptions } from '@wordpress/api-fetch';
import DataLoader from 'dataloader';
import { isWpVersion } from '@woocommerce/settings';
/**
* Internal dependencies
*/
import {
assertBatchResponseIsValid,
assertResponseIsValid,
ApiResponse,
} from './types';
/**
* Dispatched a control action for triggering an api fetch call with no parsing.
* Typically this would be used in scenarios where headers are needed.
*
* @param {APIFetchOptions} options The options for the API request.
*/
export const apiFetchWithHeaders = ( options: APIFetchOptions ) =>
( {
type: 'API_FETCH_WITH_HEADERS',
options,
} as const );
const EMPTY_OBJECT = {};
/**
* Error thrown when JSON cannot be parsed.
*/
const invalidJsonError = {
code: 'invalid_json',
message: __(
'The response is not a valid JSON response.',
'woo-gutenberg-products-block'
),
};
const setNonceOnFetch = ( headers: Headers ): void => {
if (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- this does exist because it's monkey patched in
// middleware/store-api-nonce.
triggerFetch.setNonce &&
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- this does exist because it's monkey patched in
// middleware/store-api-nonce.
typeof triggerFetch.setNonce === 'function'
) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore -- this does exist because it's monkey patched in
// middleware/store-api-nonce.
triggerFetch.setNonce( headers );
} else {
// eslint-disable-next-line no-console
console.error(
'The monkey patched function on APIFetch, "setNonce", is not present, likely another plugin or some other code has removed this augmentation'
);
}
};
/**
* Trigger a fetch from the API using the batch endpoint.
*/
const triggerBatchFetch = ( keys: readonly APIFetchOptions[] ) => {
return triggerFetch( {
path: `/wc/store/v1/batch`,
method: 'POST',
data: {
requests: keys.map( ( request: APIFetchOptions ) => {
return {
...request,
body: request?.data,
};
} ),
},
} ).then( ( response: unknown ) => {
assertBatchResponseIsValid( response );
return keys.map(
( key, index: number ) =>
response.responses[ index ] || EMPTY_OBJECT
);
} );
};
/**
* In ms, how long we should wait for requests to batch.
*
* DataLoader collects all requests over this window of time (and as a consequence, adds this amount of latency).
*/
const triggerBatchFetchDelay = 300;
/**
* DataLoader instance for triggerBatchFetch.
*/
const triggerBatchFetchLoader = new DataLoader( triggerBatchFetch, {
batchScheduleFn: ( callback: () => void ) =>
setTimeout( callback, triggerBatchFetchDelay ),
cache: false,
maxBatchSize: 25,
} );
/**
* Trigger a fetch from the API using the batch endpoint.
*
* @param {APIFetchOptions} request Request object containing API request.
*/
const batchFetch = async ( request: APIFetchOptions ) => {
return await triggerBatchFetchLoader.load( request );
};
/**
* Default export for registering the controls with the store.
*
* @return {Object} An object with the controls to register with the store on
* the controls property of the registration object.
*/
export const controls = {
API_FETCH_WITH_HEADERS: ( {
options,
}: ReturnType< typeof apiFetchWithHeaders > ): Promise< unknown > => {
return new Promise( ( resolve, reject ) => {
// GET Requests cannot be batched.
if (
! options.method ||
options.method === 'GET' ||
isWpVersion( '5.6', '<' )
) {
// Parse is disabled here to avoid returning just the body--we also need headers.
triggerFetch( {
...options,
parse: false,
} )
.then( ( fetchResponse ) => {
fetchResponse
.json()
.then( ( response ) => {
resolve( {
response,
headers: fetchResponse.headers,
} );
setNonceOnFetch( fetchResponse.headers );
} )
.catch( () => {
reject( invalidJsonError );
} );
} )
.catch( ( errorResponse ) => {
setNonceOnFetch( errorResponse.headers );
if ( typeof errorResponse.json === 'function' ) {
// Parse error response before rejecting it.
errorResponse
.json()
.then( ( error: unknown ) => {
reject( error );
} )
.catch( () => {
reject( invalidJsonError );
} );
} else {
reject( errorResponse.message );
}
} );
} else {
batchFetch( options )
.then( ( response: ApiResponse ) => {
assertResponseIsValid( response );
if ( response.status >= 200 && response.status < 300 ) {
resolve( {
response: response.body,
headers: response.headers,
} );
setNonceOnFetch( response.headers );
}
// Status code indicates error.
throw response;
} )
.catch( ( errorResponse: ApiResponse ) => {
if ( errorResponse.headers ) {
setNonceOnFetch( errorResponse.headers );
}
if ( errorResponse.body ) {
reject( errorResponse.body );
} else {
reject( errorResponse );
}
} );
}
} );
},
};