PluginProbe
ElasticPress / 5.0.0
ElasticPress v5.0.0
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.0.0, at assets/js/sync/index.js

577 lines 11.5 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 * Sync state.
70 */
71 const [state, setState] = useState({
72 isCli: false,
73 isComplete: false,
74 isDeleting: false,
75 isFailed: false,
76 isPaused: false,
77 isSyncing: false,
78 itemsProcessed: 0,
79 itemsTotal: 100,
80 syncStartDateTime: null,
81 syncHistory: defaultSyncHistory,
82 syncTrigger: defaultSyncTrigger,
83 });
84
85 /**
86 * Current state reference.
87 */
88 const stateRef = useRef(state);
89
90 /**
91 * Update state, and current state ref.
92 *
93 * @param {object} newState New state properties.
94 * @returns {void}
95 */
96 const updateState = (newState) => {
97 stateRef.current = { ...stateRef.current, ...newState };
98 setState((state) => ({ ...state, ...newState }));
99 };
100
101 const logMessage = useCallback(
102 /**
103 * Log a message.
104 *
105 * @param {Array|string} message Message/s to log.
106 * @param {string} status Message status.
107 * @returns {void}
108 */
109 (message, status) => {
110 const { isDeleting } = stateRef.current;
111
112 const messages = Array.isArray(message) ? message : [message];
113
114 for (const message of messages) {
115 setLog((log) => [
116 ...log,
117 {
118 message,
119 status,
120 dateTime: dateI18n('Y-m-d H:i:s', new Date()),
121 isDeleting,
122 id: uuid(),
123 },
124 ]);
125 }
126 },
127 [],
128 );
129
130 const clearLog = useCallback(
131 /**
132 * Clear the log.
133 *
134 * @returns {void}
135 */
136 () => {
137 setLog([]);
138 },
139 [setLog],
140 );
141
142 const syncCompleted = useCallback(
143 /**
144 * Set sync state to completed, with success based on the number of
145 * failures in the index totals.
146 *
147 * @param {object} indexTotals Index totals.
148 * @returns {void}
149 */
150 (indexTotals) => {
151 updateState({
152 isComplete: true,
153 isPaused: false,
154 isSyncing: false,
155 syncHistory: [indexTotals, ...stateRef.current.syncHistory],
156 });
157
158 /**
159 * Hide the "just need to sync" notice, if it's present.
160 */
161 document.querySelector('[data-ep-notice="no_sync"]')?.remove();
162 },
163 [],
164 );
165
166 const syncFailed = useCallback(
167 /**
168 * Handle an error in the sync request.
169 *
170 * @param {object|Error} response Request response.
171 * @returns {void}
172 */
173 (response) => {
174 /**
175 * Any running requests are cancelled when a new request is made.
176 * We can handle this silently.
177 */
178 if (response.name === 'AbortError') {
179 return;
180 }
181
182 /**
183 * Log any error messages.
184 */
185 if (response.message) {
186 logMessage(response.message, 'error');
187 }
188
189 /**
190 * If the error has totals, add to the sync history.
191 */
192 const syncHistory = response.totals
193 ? [response.totals, ...stateRef.current.syncHistory]
194 : stateRef.current.syncHistory;
195
196 /**
197 * Log a final message and update the sync state.
198 */
199 logMessage(__('Sync failed', 'elasticpress'), 'error');
200
201 updateState({
202 isFailed: true,
203 isSyncing: false,
204 syncHistory,
205 });
206 },
207 [logMessage],
208 );
209
210 const syncInterrupted = useCallback(
211 /**
212 * Set sync state to interrupted.
213 *
214 * Logs an appropriate message based on the sync method and
215 * Elasticsearch hosting.
216 *
217 * @returns {void}
218 */
219 () => {
220 const { isDeleting } = stateRef.current;
221
222 const message = isDeleting
223 ? sprintf(
224 /* translators: %s: Index type. ElasticPress.io or Elasticsearch. */
225 __(
226 '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.',
227 'elasticpress',
228 ),
229 isEpio
230 ? __('ElasticPress.io', 'elasticpress')
231 : __('Elasticsearch', 'elasticpress'),
232 )
233 : __('Sync interrupted by WP-CLI command.', 'elasticpress');
234
235 logMessage(message, 'info');
236 updateState({ isSyncing: false });
237 },
238 [isEpio, logMessage],
239 );
240
241 const syncInProgress = useCallback(
242 /**
243 * Set state for a sync in progress from its index meta.
244 *
245 * @param {object} indexMeta Index meta.
246 * @returns {void}
247 */
248 (indexMeta) => {
249 updateState({
250 isCli: indexMeta.method === 'cli',
251 isSyncing: true,
252 itemsProcessed: getItemsProcessedFromIndexMeta(indexMeta),
253 itemsTotal: getItemsTotalFromIndexMeta(indexMeta),
254 syncStartDateTime: indexMeta.start_date_time,
255 syncTrigger: indexMeta.trigger || null,
256 });
257 },
258 [],
259 );
260
261 const syncStopped = useCallback(
262 /**
263 * Set state for a stopped sync.
264 *
265 * @param {object} response Cancel request response.
266 * @returns {void}
267 */
268 (response) => {
269 const syncHistory = response.data
270 ? [response.data, ...stateRef.current.syncHistory]
271 : stateRef.current.syncHistory;
272
273 updateState({ syncHistory });
274 },
275 [],
276 );
277
278 const updateSyncState = useCallback(
279 /**
280 * Handle the response to a request to index.
281 *
282 * Updates the application state from the response data and logs any
283 * messages. Returns a Promise that resolves if syncing should
284 * continue.
285 *
286 * @param {object} response API response.
287 * @returns {Promise} Promise that resolves if sync is to continue.
288 */
289 (response) => {
290 const { isPaused, isSyncing } = stateRef.current;
291 const { message, status, totals = [], index_meta: indexMeta } = response.data;
292
293 return new Promise((resolve) => {
294 /**
295 * Don't continue if syncing has been stopped.
296 */
297 if (!isSyncing) {
298 return;
299 }
300
301 /**
302 * Stop sync if there is an error.
303 */
304 if (status === 'error') {
305 syncFailed(response.data);
306 return;
307 }
308
309 /**
310 * Log any messages.
311 */
312 if (message) {
313 logMessage(message, status);
314 }
315
316 /**
317 * If totals are available the index is complete.
318 */
319 if (!Array.isArray(totals)) {
320 syncCompleted(totals);
321 return;
322 }
323
324 /**
325 * Update sync progress from index meta.
326 */
327 syncInProgress(indexMeta);
328
329 /**
330 * Don't continue if the sync was interrupted externally.
331 */
332 if (indexMeta.should_interrupt_sync) {
333 syncInterrupted();
334 return;
335 }
336
337 /**
338 * Don't continue if syncing has been paused.
339 */
340 if (isPaused) {
341 logMessage(__('Sync paused', 'elasticpress'), 'info');
342 return;
343 }
344
345 /**
346 * Syncing should continue.
347 */
348 resolve(indexMeta.method);
349 });
350 },
351 [syncCompleted, syncFailed, syncInProgress, syncInterrupted, logMessage],
352 );
353
354 const doCancelIndex = useCallback(
355 /**
356 * Cancel a sync.
357 *
358 * @returns {void}
359 */
360 () => {
361 cancelIndex()
362 .then(syncStopped)
363 .catch((error) => {
364 if (error?.name !== 'AbortError') {
365 throw error;
366 }
367 });
368 },
369 [cancelIndex, syncStopped],
370 );
371
372 const doIndexStatus = useCallback(
373 /**
374 * Check the status of a sync.
375 *
376 * Used to get the status of an external sync already in progress, such
377 * as a WP CLI index.
378 *
379 * @returns {void}
380 */
381 () => {
382 indexStatus().then(updateSyncState).then(doIndexStatus).catch(syncFailed);
383 },
384 [indexStatus, syncFailed, updateSyncState],
385 );
386
387 const doIndex = useCallback(
388 /**
389 * Start or continue a sync.
390 *
391 * @param {object} args Sync args.
392 * @returns {void}
393 */
394 (args) => {
395 index(args)
396 .then(updateSyncState)
397 .then(
398 /**
399 * If an existing sync has been found just check its status,
400 * otherwise continue syncing.
401 *
402 * @param {string} method Sync method.
403 */
404 (method) => {
405 if (method === 'cli') {
406 doIndexStatus();
407 } else {
408 doIndex(args);
409 }
410 },
411 )
412 .catch(syncFailed);
413 },
414 [doIndexStatus, index, syncFailed, updateSyncState],
415 );
416
417 const pauseSync = useCallback(
418 /**
419 * Stop syncing.
420 *
421 * @returns {void}
422 */
423 () => {
424 updateState({ isPaused: true, isSyncing: true });
425 },
426 [],
427 );
428
429 const resumeSync = useCallback(
430 /**
431 * Resume syncing.
432 *
433 * @param {object} args Sync args.
434 * @returns {void}
435 */
436 (args) => {
437 updateState({ isPaused: false, isSyncing: true });
438 doIndex(args);
439 },
440 [doIndex],
441 );
442
443 const startSync = useCallback(
444 /**
445 * Start syncing.
446 *
447 * @param {object} args Sync args.
448 * @returns {void}
449 */
450 (args) => {
451 const { syncHistory } = stateRef.current;
452 const isInitialSync = !syncHistory.length;
453
454 /**
455 * We should not appear to be deleting if this is the first sync.
456 */
457 const isDeleting = !!(isInitialSync || args.put_mapping);
458
459 updateState({
460 isComplete: false,
461 isFailed: false,
462 isDeleting,
463 isPaused: false,
464 isSyncing: true,
465 });
466
467 updateState({
468 itemsProcessed: 0,
469 syncStartDateTime: Date.now(),
470 syncTrigger: args.trigger || null,
471 });
472
473 doIndex(args);
474 },
475 [doIndex],
476 );
477
478 const stopSync = useCallback(
479 /**
480 * Stop syncing.
481 *
482 * @returns {void}
483 */
484 () => {
485 updateState({ isPaused: false, isSyncing: false });
486 doCancelIndex();
487 },
488 [doCancelIndex],
489 );
490
491 /**
492 * Initialize.
493 *
494 * @returns {void}
495 */
496 const init = () => {
497 /**
498 * Clear sync parameter from the URL to prevent a refresh triggering a new
499 * sync.
500 */
501 clearSyncParam();
502
503 /**
504 * If a sync is in progress, update state to reflect its progress.
505 */
506 if (indexMeta) {
507 syncInProgress(indexMeta);
508
509 /**
510 * If the sync is a CLI sync, start getting its status.
511 */
512 if (indexMeta.method === 'cli') {
513 doIndexStatus();
514 logMessage(__('WP CLI sync in progress', 'elasticpress'), 'info');
515 } else {
516 pauseSync();
517 logMessage(__('Sync paused', 'elasticpress'), 'info');
518 }
519 }
520 };
521
522 /**
523 * Effects.
524 */
525 useEffect(init, [doIndexStatus, syncInProgress, indexMeta, logMessage, pauseSync, startSync]);
526
527 /**
528 * Provide state to context.
529 */
530 const {
531 isCli,
532 isComplete,
533 isDeleting,
534 isFailed,
535 isPaused,
536 isSyncing,
537 itemsProcessed,
538 itemsTotal,
539 syncHistory,
540 syncStartDateTime,
541 syncTrigger,
542 } = stateRef.current;
543
544 // eslint-disable-next-line react/jsx-no-constructed-context-values
545 const contextValue = {
546 clearLog,
547 isCli,
548 isComplete,
549 isDeleting,
550 isFailed,
551 isPaused,
552 isSyncing,
553 itemsProcessed,
554 itemsTotal,
555 syncHistory,
556 log,
557 logMessage,
558 pauseSync,
559 resumeSync,
560 startSync,
561 stopSync,
562 syncStartDateTime,
563 syncTrigger,
564 };
565
566 return <Context.Provider value={contextValue}>{children}</Context.Provider>;
567 };
568
569 /**
570 * Use the API Search context.
571 *
572 * @returns {object} API Search Context.
573 */
574 export const useSync = () => {
575 return useContext(Context);
576 };
577