Setting the file. One moment.
Wp HTTP · Rp Source Wordpress · wix/skills · Skills Docs
ContentsBack to the top of the page
Number 16.2
Position 2 of 4
Type JavaScript
Size 8 KB
Lines 249 lib/ wp-http.js
JavaScript · 249 lines · 8 KB
15
// vendored copy needs no install step.
16
17 const DEFAULT_TIMEOUT_MS = 60000 ;
18 const DEFAULT_RATE_LIMIT_RPM = 120 ;
19 const DEFAULT_MAX_RETRIES = 3 ;
20 const MAX_BACKOFF_MS = 30000 ;
21
22 // Shared throttle state. Every request routes through fetchJson, so a single
23 // module-level throttle enforces the rate limit across the whole run.
24 const rateState = { minIntervalMs: 0 , lastRequestAt: 0 , maxRetries: DEFAULT_MAX_RETRIES };
25
26 function sleep ( ms ) {
27 return new Promise (( resolve ) => setTimeout (resolve, ms));
28 }
29
30 // Configure the shared throttle. Call once before issuing requests.
31 function configureRateLimit ({ rateLimitRpm = DEFAULT_RATE_LIMIT_RPM , maxRetries = DEFAULT_MAX_RETRIES } = {}) {
32 const rpm = Number. isFinite (rateLimitRpm) && rateLimitRpm >= 1 ? rateLimitRpm : DEFAULT_RATE_LIMIT_RPM ;
33 rateState.minIntervalMs = Math. ceil ( 60000 / rpm);
34 rateState.maxRetries = Number. isFinite (maxRetries) && maxRetries >= 0 ? maxRetries : DEFAULT_MAX_RETRIES ;
35 }
36
37 async function applyThrottle ( progress ) {
38 if (rateState.minIntervalMs <= 0 ) {
39 return ;
40 }
41 const elapsed = Date. now () - rateState.lastRequestAt;
42 const wait = rateState.minIntervalMs - elapsed;
43 if (wait > 0 ) {
44 if (progress) {
45 await progress. withHeartbeat ({ phase: 'discovery' , step: 'rate-limit' , message: 'Still waiting for WordPress rate limit' }, () => sleep (wait));
46 } else {
47 await sleep (wait);
48 }
49 }
50 rateState.lastRequestAt = Date. now ();
51 }
52
53 function retryAfterMs ( responseHeaders ) {
54 const raw = responseHeaders?.[ 'retry-after' ];
55 if ( ! raw) {
56 return null ;
57 }
58 const seconds = Number. parseInt (raw, 10 );
59 return Number. isFinite (seconds) ? seconds * 1000 : null ;
60 }
61
62 function buildHeaders ( args ) {
63 const headers = new Headers ();
64 headers. set ( 'accept' , 'application/json' );
65
66 for ( const rawHeader of args.authHeaders || []) {
67 const splitIndex = rawHeader. indexOf ( ':' );
68 if (splitIndex === - 1 ) {
69 throw new Error ( `Invalid --auth-header value: ${ rawHeader }` );
70 }
71 const name = rawHeader. slice ( 0 , splitIndex). trim ();
72 const value = rawHeader. slice (splitIndex + 1 ). trim ();
73 headers. set (name, value);
74 }
75
76 if (args.username && args.applicationPassword) {
77 const credentials = Buffer. from ( `${ args . username }:${ args . applicationPassword }` ). toString ( 'base64' );
78 headers. set ( 'authorization' , `Basic ${ credentials }` );
79 } else if (args.apiKey) {
80 const headerName = args.apiKeyHeader || 'Authorization' ;
81 const headerValue = headerName. toLowerCase () === 'authorization' && ! / ^ bearer \s + / i . test (args.apiKey)
82 ? `Bearer ${ args . apiKey }`
83 : args.apiKey;
84 headers. set (headerName, headerValue);
85 }
86
87 return headers;
88 }
89
90 function normalizeBaseUrl ( input ) {
91 return input. replace ( / \/ +$ / , '' );
92 }
93
94 function buildApiUrl ( baseUrl , routePath , query = {}) {
95 const url = new URL ( `${ normalizeBaseUrl ( baseUrl ) }/wp-json${ routePath }` );
96 for ( const [ key , value ] of Object. entries (query)) {
97 if (value === undefined || value === null || value === '' ) {
98 continue ;
99 }
100 url.searchParams. set (key, String (value));
101 }
102 return url;
103 }
104
105 async function fetchJson ( baseUrl , routePath , { headers , method = 'GET' , query , timeoutMs = DEFAULT_TIMEOUT_MS , progress = null , progressContext = {} }) {
106 const url = buildApiUrl (baseUrl, routePath, query);
107
108 for ( let attempt = 0 ; ; attempt += 1 ) {
109 await applyThrottle (progress);
110 const controller = new AbortController ();
111 const timeout = setTimeout (() => controller. abort (), timeoutMs);
112
113 try {
114 const response = progress
115 ? await progress. withHeartbeat ({
116 phase: 'discovery' ,
117 step: 'wordpress-request' ,
118 ... progressContext,
119 message: `Still waiting on WordPress ${ method } ${ routePath || '/'}` ,
120 }, () => fetch (url, {
121 method,
122 headers,
123 signal: controller.signal,
124 }))
125 : await fetch (url, {
126 method,
127 headers,
128 signal: controller.signal,
129 });
130 const text = await response. text ();
131 const responseHeaders = Object. fromEntries (response.headers. entries ());
132
133 // Back off and retry on throttling / transient unavailability,
134 // honoring Retry-After when the server provides it.
135 if ((response.status === 429 || response.status === 503 ) && attempt < rateState.maxRetries) {
136 const backoff = retryAfterMs (responseHeaders) ?? Math. min ( MAX_BACKOFF_MS , 1000 * 2 ** attempt);
137 if (progress) {
138 progress. warn ( `WordPress ${ response . status } response; backing off before retry` , {
139 phase: 'discovery' ,
140 step: 'wordpress-retry' ,
141 ... progressContext,
142 count: attempt + 1 ,
143 total: rateState.maxRetries,
144 unit: 'retries' ,
145 });
146 await progress. withHeartbeat ({
147 phase: 'discovery' ,
148 step: 'wordpress-retry' ,
149 ... progressContext,
150 message: `Still backing off after WordPress ${ response . status }` ,
151 }, () => sleep (backoff));
152 } else {
153 await sleep (backoff);
154 }
155 continue ;
156 }
157
158 let json;
159 if (text) {
160 try {
161 json = JSON . parse (text);
162 } catch {
163 json = undefined ;
164 }
165 }
166
167 return {
168 ok: response.ok,
169 status: response.status,
170 statusText: response.statusText,
171 url: url. toString (),
172 headers: responseHeaders,
173 json,
174 text,
175 retries: attempt,
176 };
177 } catch (error) {
178 return {
179 ok: false ,
180 status: 0 ,
181 statusText: error.name === 'AbortError' ? 'Request Timeout' : error.message,
182 url: url. toString (),
183 headers: {},
184 json: undefined ,
185 text: '' ,
186 error,
187 retries: attempt,
188 };
189 } finally {
190 clearTimeout (timeout);
191 }
192 }
193 }
194
195 // Total record count for a collection. WordPress returns it in X-WP-Total;
196 // X-WP-TotalPages carries the page count. Header names are lowercased by fetch's
197 // Headers iterator, but accept the canonical casing too for resilience.
198 function parseTotalHeader ( responseHeaders , name = 'x-wp-total' ) {
199 if ( ! responseHeaders) {
200 return null ;
201 }
202 // fetch's Headers iterator lowercases names, but match case-insensitively so a
203 // plain object built with WordPress's canonical casing (X-WP-Total) also works.
204 const target = name. toLowerCase ();
205 let raw;
206 for ( const [ key , value ] of Object. entries (responseHeaders)) {
207 if (key. toLowerCase () === target) {
208 raw = value;
209 break ;
210 }
211 }
212 if (raw === undefined || raw === null || raw === '' ) {
213 return null ;
214 }
215 const total = Number. parseInt (raw, 10 );
216 return Number. isFinite (total) ? total : null ;
217 }
218
219 function parseTotalPagesHeader ( responseHeaders ) {
220 return parseTotalHeader (responseHeaders, 'x-wp-totalpages' );
221 }
222
223 function shouldContinueCollectionPaging ({ responseHeaders , page , perPage , itemCount }) {
224 const totalPages = parseTotalPagesHeader (responseHeaders);
225 if (totalPages !== null ) {
226 return Number (page) < totalPages;
227 }
228 if (Number. isFinite (perPage) && perPage > 0 ) {
229 return Number (itemCount) >= perPage;
230 }
231 return false ;
232 }
233
234 module . exports = {
235 DEFAULT_TIMEOUT_MS,
236 DEFAULT_RATE_LIMIT_RPM,
237 DEFAULT_MAX_RETRIES,
238 MAX_BACKOFF_MS,
239 sleep,
240 configureRateLimit,
241 retryAfterMs,
242 buildHeaders,
243 normalizeBaseUrl,
244 buildApiUrl,
245 fetchJson,
246 parseTotalHeader,
247 parseTotalPagesHeader,
248 shouldContinueCollectionPaging,
249 };