PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / assets / js / sync / index.js

index.js in ElasticPress 5.3.5, at assets/js/sync/index.js

621 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * External dependencies.
3 */
4 import { v4 as uuid } from 'uuid';
5
6 /**
7 * WordPress dependencies.
8 */
9 import { dateI18n } from '@wordpress/date';
10 import {
11 createContext,
12 useCallback,
13 useContext,
14 useEffect,
15 useRef,
16 useState,
17 WPElement,
18 } from '@wordpress/element';
19 import { __, sprintf } from '@wordpress/i18n';
20
21 /**
22 * Internal dependencies.
23 */
24 import { useIndex } from './src/hooks';
25 import {
26 clearSyncParam,
27 getItemsProcessedFromIndexMeta,
28 getItemsTotalFromIndexMeta,
29 } from './src/utilities';
30
31 /**
32 * Sync context.
33 */
34 const Context = createContext();
35
36 /**
37 * App component.
38 *
39 * @param {object} props Component props.
40 * @param {string} props.apiUrl API endpoint URL.
41 * @param {Function} props.children Component children
42 * @param {Array} props.defaultSyncHistory Sync history.
43 * @param {Array} props.defaultSyncTrigger Sync trigger.
44 * @param {object|null} props.indexMeta Details of a sync in progress.
45 * @param {boolean} props.isEpio Whether ElasticPress.io is in use.
46 * @param {string} props.nonce WordPress nonce.
47 * @returns {WPElement} App component.
48 */
49 export const SyncProvider = ({
50 apiUrl,
51 children,
52 defaultSyncHistory,
53 defaultSyncTrigger,
54 indexMeta,
55 isEpio,
56 nonce,
57 }) => {
58 /**
59 * Indexing methods.
60 */
61 const { cancelIndex, index, indexStatus } = useIndex(apiUrl, nonce);
62
63 /**
64 * Message log state.
65 */
66 const [log, setLog] = useState([]);
67
68 /**
69 * Error types state.
70 */
71 const [errorCounts, setErrorCounts] = useState([]);
72
73 /**
74 * Sync state.
75 */
76 const [state, setState] = useState({
77 isCli: false,
78 isComplete: false,
79 isDeleting: false,
80 isFailed: false,
81 isPaused: false,
82 isSyncing: false,
83 itemsProcessed: 0,
84 itemsTotal: 100,
85 syncStartDateTime: null,
86 syncHistory: defaultSyncHistory,
87 syncTrigger: defaultSyncTrigger,
88 });
89
90 /**
91 * Current state reference.
92 */
93 const stateRef = useRef(state);
94
95 /**
96 * Update state, and current state ref.
97 *
98 * @param {object} newState New state properties.
99 * @returns {void}
100 */
101 const updateState = (newState) => {
102 stateRef.current = { ...stateRef.current, ...newState };
103 setState((state) => ({ ...state, ...newState }));
104 };
105
106 const countErrors = useCallback(
107 /**
108 * Add up the counts for each error type.
109 *
110 * @param {object} errors Errors returned by the sync request.
111 */
112 (errors) => {
113 setErrorCounts((errorCounts) => {
114 const newErrorCounts = [...errorCounts];
115
116 Object.keys(errors).forEach((e) => {
117 if (!errors[e].solution) {
118 return;
119 }
120
121 const i = newErrorCounts.findIndex((t) => e === t.type);
122
123 if (i !== -1) {
124 newErrorCounts[i].count += errors[e].count;
125 } else {
126 newErrorCounts.push({
127 ...errors[e],
128 type: e,
129 });
130 }
131 });
132
133 return newErrorCounts;
134 });
135 },
136 [],
137 );
138
139 const logMessage = useCallback(
140 /**
141 * Log a message.
142 *
143 * @param {Array|string} message Message/s to log.
144 * @param {string} status Message status.
145 * @returns {void}
146 */
147 (message, status) => {
148 const { isDeleting } = stateRef.current;
149
150 const messages = Array.isArray(message) ? message : [message];
151
152 for (const message of messages) {
153 setLog((log) => [
154 ...log,
155 {
156 message,
157 status,
158 dateTime: dateI18n('Y-m-d H:i:s', new Date()),
159 isDeleting,
160 id: uuid(),
161 },
162 ]);
163 }
164 },
165 [],
166 );
167
168 const clearLog = useCallback(
169 /**
170 * Clear the log.
171 *
172 * @returns {void}
173 */
174 () => {
175 setLog([]);
176 setErrorCounts([]);
177 },
178 [setLog],
179 );
180
181 const syncCompleted = useCallback(
182 /**
183 * Set sync state to completed, with success based on the number of
184 * failures in the index totals.
185 *
186 * @param {object} indexTotals Index totals.
187 * @returns {void}
188 */
189 (indexTotals) => {
190 updateState({
191 isComplete: true,
192 isPaused: false,
193 isSyncing: false,
194 syncHistory: [indexTotals, ...stateRef.current.syncHistory],
195 });
196
197 /**
198 * Hide the "just need to sync" notice, if it's present.
199 */
200 document.querySelector('[data-ep-notice="no_sync"]')?.remove();
201 },
202 [],
203 );
204
205 const syncFailed = useCallback(
206 /**
207 * Handle an error in the sync request.
208 *
209 * @param {object|Error} response Request response.
210 * @returns {void}
211 */
212 (response) => {
213 /**
214 * Any running requests are cancelled when a new request is made.
215 * We can handle this silently.
216 */
217 if (response.name === 'AbortError') {
218 return;
219 }
220
221 /**
222 * Log any error messages.
223 */
224 if (response.message) {
225 logMessage(response.message, 'error');
226 }
227
228 /**
229 * If the error has totals, add to the sync history.
230 */
231 const syncHistory = response.totals
232 ? [response.totals, ...stateRef.current.syncHistory]
233 : stateRef.current.syncHistory;
234
235 /**
236 * Log a final message and update the sync state.
237 */
238 logMessage(__('Sync failed', 'elasticpress'), 'error');
239
240 updateState({
241 isFailed: true,
242 isSyncing: false,
243 syncHistory,
244 });
245 },
246 [logMessage],
247 );
248
249 const syncInterrupted = useCallback(
250 /**
251 * Set sync state to interrupted.
252 *
253 * Logs an appropriate message based on the sync method and
254 * Elasticsearch hosting.
255 *
256 * @returns {void}
257 */
258 () => {
259 const { isDeleting } = stateRef.current;
260
261 const message = isDeleting
262 ? sprintf(
263 /* translators: %s: Index type. ElasticPress.io or Elasticsearch. */
264 __(
265 'Your indexing process has been stopped by WP-CLI and your %s index could be missing content. To restart indexing, please click the Start button or use WP-CLI commands to perform the reindex. Please note that search results could be incorrect or incomplete until the reindex finishes.',
266 'elasticpress',
267 ),
268 isEpio
269 ? __('ElasticPress.io', 'elasticpress')
270 : __('Elasticsearch', 'elasticpress'),
271 )
272 : __('Sync interrupted by WP-CLI command.', 'elasticpress');
273
274 logMessage(message, 'info');
275 updateState({ isSyncing: false });
276 },
277 [isEpio, logMessage],
278 );
279
280 const syncInProgress = useCallback(
281 /**
282 * Set state for a sync in progress from its index meta.
283 *
284 * @param {object} indexMeta Index meta.
285 * @returns {void}
286 */
287 (indexMeta) => {
288 updateState({
289 isCli: indexMeta.method === 'cli',
290 isSyncing: true,
291 itemsProcessed: getItemsProcessedFromIndexMeta(indexMeta),
292 itemsTotal: getItemsTotalFromIndexMeta(indexMeta),
293 syncStartDateTime: indexMeta.start_date_time,
294 syncTrigger: indexMeta.trigger || null,
295 });
296 },
297 [],
298 );
299
300 const syncStopped = useCallback(
301 /**
302 * Set state for a stopped sync.
303 *
304 * @param {object} response Cancel request response.
305 * @returns {void}
306 */
307 (response) => {
308 const syncHistory = response.data
309 ? [response.data, ...stateRef.current.syncHistory]
310 : stateRef.current.syncHistory;
311
312 updateState({ syncHistory });
313 },
314 [],
315 );
316
317 const updateSyncState = useCallback(
318 /**
319 * Handle the response to a request to index.
320 *
321 * Updates the application state from the response data and logs any
322 * messages. Returns a Promise that resolves if syncing should
323 * continue.
324 *
325 * @param {object} response API response.
326 * @returns {Promise} Promise that resolves if sync is to continue.
327 */
328 (response) => {
329 const { isPaused, isSyncing } = stateRef.current;
330 const { errors, message, status, totals = [], index_meta: indexMeta } = response.data;
331
332 return new Promise((resolve) => {
333 /**
334 * Don't continue if syncing has been stopped.
335 */
336 if (!isSyncing) {
337 return;
338 }
339
340 if (errors) {
341 countErrors(errors);
342 }
343
344 /**
345 * Stop sync if there is an error.
346 */
347 if (status === 'error') {
348 syncFailed(response.data);
349 return;
350 }
351
352 /**
353 * Log any messages.
354 */
355 if (message) {
356 logMessage(message, status);
357 }
358
359 /**
360 * If totals are available the index is complete.
361 */
362 if (!Array.isArray(totals)) {
363 syncCompleted(totals);
364 return;
365 }
366
367 /**
368 * Update sync progress from index meta.
369 */
370 syncInProgress(indexMeta);
371
372 /**
373 * Don't continue if the sync was interrupted externally.
374 */
375 if (indexMeta.should_interrupt_sync) {
376 syncInterrupted();
377 return;
378 }
379
380 /**
381 * Don't continue if syncing has been paused.
382 */
383 if (isPaused) {
384 logMessage(__('Sync paused', 'elasticpress'), 'info');
385 return;
386 }
387
388 /**
389 * Syncing should continue.
390 */
391 resolve(indexMeta.method);
392 });
393 },
394 [syncCompleted, syncFailed, syncInProgress, syncInterrupted, countErrors, logMessage],
395 );
396
397 const doCancelIndex = useCallback(
398 /**
399 * Cancel a sync.
400 *
401 * @returns {void}
402 */
403 () => {
404 cancelIndex()
405 .then(syncStopped)
406 .catch((error) => {
407 if (error?.name !== 'AbortError') {
408 throw error;
409 }
410 });
411 },
412 [cancelIndex, syncStopped],
413 );
414
415 const doIndexStatus = useCallback(
416 /**
417 * Check the status of a sync.
418 *
419 * Used to get the status of an external sync already in progress, such
420 * as a WP CLI index.
421 *
422 * @returns {void}
423 */
424 () => {
425 indexStatus().then(updateSyncState).then(doIndexStatus).catch(syncFailed);
426 },
427 [indexStatus, syncFailed, updateSyncState],
428 );
429
430 const doIndex = useCallback(
431 /**
432 * Start or continue a sync.
433 *
434 * @param {object} args Sync args.
435 * @returns {void}
436 */
437 (args) => {
438 index(args)
439 .then(updateSyncState)
440 .then(
441 /**
442 * If an existing sync has been found just check its status,
443 * otherwise continue syncing.
444 *
445 * @param {string} method Sync method.
446 */
447 (method) => {
448 if (method === 'cli') {
449 doIndexStatus();
450 } else {
451 doIndex(args);
452 }
453 },
454 )
455 .catch(syncFailed);
456 },
457 [doIndexStatus, index, syncFailed, updateSyncState],
458 );
459
460 const pauseSync = useCallback(
461 /**
462 * Stop syncing.
463 *
464 * @returns {void}
465 */
466 () => {
467 updateState({ isPaused: true, isSyncing: true });
468 },
469 [],
470 );
471
472 const resumeSync = useCallback(
473 /**
474 * Resume syncing.
475 *
476 * @param {object} args Sync args.
477 * @returns {void}
478 */
479 (args) => {
480 updateState({ isPaused: false, isSyncing: true });
481 doIndex(args);
482 },
483 [doIndex],
484 );
485
486 const startSync = useCallback(
487 /**
488 * Start syncing.
489 *
490 * @param {object} args Sync args.
491 * @returns {void}
492 */
493 (args) => {
494 const { syncHistory } = stateRef.current;
495 const isInitialSync = !syncHistory.length;
496
497 /**
498 * We should not appear to be deleting if this is the first sync.
499 */
500 const isDeleting = !!(isInitialSync || args.put_mapping);
501
502 updateState({
503 isComplete: false,
504 isFailed: false,
505 isDeleting,
506 isPaused: false,
507 isSyncing: true,
508 });
509
510 updateState({
511 itemsProcessed: 0,
512 syncStartDateTime: Date.now(),
513 syncTrigger: args.trigger || null,
514 });
515
516 doIndex(args);
517 },
518 [doIndex],
519 );
520
521 const stopSync = useCallback(
522 /**
523 * Stop syncing.
524 *
525 * @returns {void}
526 */
527 () => {
528 updateState({ isPaused: false, isSyncing: false });
529 doCancelIndex();
530 },
531 [doCancelIndex],
532 );
533
534 /**
535 * Initialize.
536 *
537 * @returns {void}
538 */
539 const init = () => {
540 /**
541 * Clear sync parameter from the URL to prevent a refresh triggering a new
542 * sync.
543 */
544 clearSyncParam();
545
546 /**
547 * If a sync is in progress, update state to reflect its progress.
548 */
549 if (indexMeta) {
550 syncInProgress(indexMeta);
551
552 /**
553 * If the sync is a CLI sync, start getting its status.
554 */
555 if (indexMeta.method === 'cli') {
556 doIndexStatus();
557 logMessage(__('WP CLI sync in progress', 'elasticpress'), 'info');
558 } else {
559 pauseSync();
560 logMessage(__('Sync paused', 'elasticpress'), 'info');
561 }
562 }
563 };
564
565 /**
566 * Effects.
567 */
568 useEffect(init, [doIndexStatus, syncInProgress, indexMeta, logMessage, pauseSync, startSync]);
569
570 /**
571 * Provide state to context.
572 */
573 const {
574 isCli,
575 isComplete,
576 isDeleting,
577 isFailed,
578 isPaused,
579 isSyncing,
580 itemsProcessed,
581 itemsTotal,
582 syncHistory,
583 syncStartDateTime,
584 syncTrigger,
585 } = stateRef.current;
586
587 // eslint-disable-next-line react/jsx-no-constructed-context-values
588 const contextValue = {
589 clearLog,
590 errorCounts,
591 isCli,
592 isComplete,
593 isDeleting,
594 isFailed,
595 isPaused,
596 isSyncing,
597 itemsProcessed,
598 itemsTotal,
599 syncHistory,
600 log,
601 logMessage,
602 pauseSync,
603 resumeSync,
604 startSync,
605 stopSync,
606 syncStartDateTime,
607 syncTrigger,
608 };
609
610 return <Context.Provider value={contextValue}>{children}</Context.Provider>;
611 };
612
613 /**
614 * Use the API Search context.
615 *
616 * @returns {object} API Search Context.
617 */
618 export const useSync = () => {
619 return useContext(Context);
620 };
621