PluginProbe
BeyondWords – AI audio for publishers / 7.0.0
BeyondWords – AI audio for publishers v7.0.0
7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.7.0 All 43 releases
speechkit / src / editor / lib / poll-content-status.js

poll-content-status.js in BeyondWords – AI audio for publishers 7.0.0, at src/editor/lib/poll-content-status.js

162 lines 4.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Poll a BeyondWords content object until it reaches a terminal status.
3 *
4 * Embedding a player while the backend is still processing CDN-caches a 404 for
5 * its first asset request, so callers poll and embed only once `processed`.
6 * No React/@wordpress imports so the block editor, classic IIFE and jest share it.
7 */
8
9 /**
10 * Statuses that mean "not finished yet" — keep polling.
11 *
12 * Everything else (`processed`, `error`, `skipped`, or an unknown/absent value)
13 * is terminal so the loop always ends.
14 *
15 * @type {string[]}
16 */
17 export const NON_TERMINAL_STATUSES = [ 'draft', 'queued', 'processing' ];
18
19 /**
20 * The success status — the only one for which a player should be embedded.
21 *
22 * @type {string}
23 */
24 export const PROCESSED_STATUS = 'processed';
25
26 /**
27 * Default poll interval, in milliseconds.
28 *
29 * @type {number}
30 */
31 export const DEFAULT_INTERVAL_MS = 3000;
32
33 /**
34 * Default overall time budget, in milliseconds, before giving up.
35 *
36 * @type {number}
37 */
38 export const DEFAULT_TIMEOUT_MS = 120000;
39
40 /**
41 * Build an `AbortError` that callers can recognise and swallow.
42 *
43 * @return {Error} An error whose `name` is `'AbortError'`.
44 */
45 function abortError() {
46 const error = new Error( 'Aborted' );
47 error.name = 'AbortError';
48 return error;
49 }
50
51 /**
52 * Resolve after `ms`, or reject with an `AbortError` if `signal` aborts first.
53 *
54 * Abort-aware so unmount/cleanup doesn't have to wait out the interval.
55 *
56 * @param {number} ms Delay in milliseconds.
57 * @param {AbortSignal=} signal Optional abort signal.
58 *
59 * @return {Promise<void>} Resolves after the delay.
60 */
61 function sleep( ms, signal ) {
62 return new Promise( ( resolve, reject ) => {
63 if ( signal?.aborted ) {
64 reject( abortError() );
65 return;
66 }
67
68 const onAbort = () => {
69 clearTimeout( timeoutId );
70 reject( abortError() );
71 };
72
73 const timeoutId = setTimeout( () => {
74 signal?.removeEventListener( 'abort', onAbort );
75 resolve();
76 }, ms );
77
78 signal?.addEventListener( 'abort', onAbort, { once: true } );
79 } );
80 }
81
82 /**
83 * Poll `fetchStatus` until the content reaches a terminal status.
84 *
85 * Chained `setTimeout` (not `setInterval`) so a slow request never overlaps the
86 * next tick; one transient fetch failure counts as a non-terminal tick.
87 *
88 * @param {Object} options Options.
89 * @param {Function} options.fetchStatus `() => Promise<{ status: string }>`.
90 * Fetches the content object.
91 * @param {Function=} options.onTick Called with the current status on
92 * each non-terminal poll.
93 * @param {Function=} options.isHidden `() => boolean`. When it returns
94 * true the upstream call is skipped
95 * for that cycle (throttle background
96 * tabs — each poll is an uncached
97 * upstream API call).
98 * @param {AbortSignal=} options.signal Abort signal to stop polling.
99 * @param {number=} options.intervalMs Delay between polls.
100 * @param {number=} options.timeoutMs Overall time budget.
101 *
102 * @return {Promise<{status: (string|undefined), timedOut: boolean}>} Last-seen
103 * status; `timedOut` is true when the budget elapsed while non-terminal.
104 */
105 export async function pollContentStatus( {
106 fetchStatus,
107 onTick,
108 isHidden,
109 signal,
110 intervalMs = DEFAULT_INTERVAL_MS,
111 timeoutMs = DEFAULT_TIMEOUT_MS,
112 } ) {
113 const start = Date.now();
114 let lastStatus;
115 let hiddenMs = 0;
116
117 // Loop terminates via: terminal status (return), budget elapsed (return),
118 // or abort (throw). The `while ( true )` body always awaits, so it can't spin.
119 while ( true ) {
120 if ( signal?.aborted ) {
121 throw abortError();
122 }
123
124 // The budget measures *visible* time: a backgrounded tab must resume
125 // polling on return rather than time out having never checked.
126 if ( Date.now() - start - hiddenMs >= timeoutMs ) {
127 return { status: lastStatus, timedOut: true };
128 }
129
130 if ( typeof isHidden === 'function' && isHidden() ) {
131 const hiddenAt = Date.now();
132 await sleep( intervalMs, signal );
133 hiddenMs += Date.now() - hiddenAt;
134 continue;
135 }
136
137 let status;
138 try {
139 const response = await fetchStatus();
140 status = response?.status;
141 } catch {
142 // Transient failure — keep polling.
143 await sleep( intervalMs, signal );
144 continue;
145 }
146
147 lastStatus = status;
148
149 if ( ! NON_TERMINAL_STATUSES.includes( status ) ) {
150 return { status, timedOut: false };
151 }
152
153 if ( typeof onTick === 'function' ) {
154 onTick( status );
155 }
156
157 await sleep( intervalMs, signal );
158 }
159 }
160
161 export default pollContentStatus;
162